##// END OF EJS Templates
subrepo: svn abort now depends on exit code (issue2833)
Regis Desgroppes -
r14505:90ef40bf stable
parent child Browse files
Show More
@@ -1,1054 +1,1056 b''
1 # subrepo.py - sub-repository handling for Mercurial
1 # subrepo.py - sub-repository handling for Mercurial
2 #
2 #
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import errno, os, re, xml.dom.minidom, shutil, urlparse, posixpath
8 import errno, os, re, xml.dom.minidom, shutil, urlparse, posixpath
9 import stat, subprocess, tarfile
9 import stat, subprocess, tarfile
10 from i18n import _
10 from i18n import _
11 import config, util, node, error, cmdutil
11 import config, util, node, error, cmdutil
12 hg = None
12 hg = None
13 propertycache = util.propertycache
13 propertycache = util.propertycache
14
14
15 nullstate = ('', '', 'empty')
15 nullstate = ('', '', 'empty')
16
16
17 def state(ctx, ui):
17 def state(ctx, ui):
18 """return a state dict, mapping subrepo paths configured in .hgsub
18 """return a state dict, mapping subrepo paths configured in .hgsub
19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
20 (key in types dict))
20 (key in types dict))
21 """
21 """
22 p = config.config()
22 p = config.config()
23 def read(f, sections=None, remap=None):
23 def read(f, sections=None, remap=None):
24 if f in ctx:
24 if f in ctx:
25 try:
25 try:
26 data = ctx[f].data()
26 data = ctx[f].data()
27 except IOError, err:
27 except IOError, err:
28 if err.errno != errno.ENOENT:
28 if err.errno != errno.ENOENT:
29 raise
29 raise
30 # handle missing subrepo spec files as removed
30 # handle missing subrepo spec files as removed
31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
32 return
32 return
33 p.parse(f, data, sections, remap, read)
33 p.parse(f, data, sections, remap, read)
34 else:
34 else:
35 raise util.Abort(_("subrepo spec file %s not found") % f)
35 raise util.Abort(_("subrepo spec file %s not found") % f)
36
36
37 if '.hgsub' in ctx:
37 if '.hgsub' in ctx:
38 read('.hgsub')
38 read('.hgsub')
39
39
40 for path, src in ui.configitems('subpaths'):
40 for path, src in ui.configitems('subpaths'):
41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
42
42
43 rev = {}
43 rev = {}
44 if '.hgsubstate' in ctx:
44 if '.hgsubstate' in ctx:
45 try:
45 try:
46 for l in ctx['.hgsubstate'].data().splitlines():
46 for l in ctx['.hgsubstate'].data().splitlines():
47 revision, path = l.split(" ", 1)
47 revision, path = l.split(" ", 1)
48 rev[path] = revision
48 rev[path] = revision
49 except IOError, err:
49 except IOError, err:
50 if err.errno != errno.ENOENT:
50 if err.errno != errno.ENOENT:
51 raise
51 raise
52
52
53 state = {}
53 state = {}
54 for path, src in p[''].items():
54 for path, src in p[''].items():
55 kind = 'hg'
55 kind = 'hg'
56 if src.startswith('['):
56 if src.startswith('['):
57 if ']' not in src:
57 if ']' not in src:
58 raise util.Abort(_('missing ] in subrepo source'))
58 raise util.Abort(_('missing ] in subrepo source'))
59 kind, src = src.split(']', 1)
59 kind, src = src.split(']', 1)
60 kind = kind[1:]
60 kind = kind[1:]
61
61
62 for pattern, repl in p.items('subpaths'):
62 for pattern, repl in p.items('subpaths'):
63 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
63 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
64 # does a string decode.
64 # does a string decode.
65 repl = repl.encode('string-escape')
65 repl = repl.encode('string-escape')
66 # However, we still want to allow back references to go
66 # However, we still want to allow back references to go
67 # through unharmed, so we turn r'\\1' into r'\1'. Again,
67 # through unharmed, so we turn r'\\1' into r'\1'. Again,
68 # extra escapes are needed because re.sub string decodes.
68 # extra escapes are needed because re.sub string decodes.
69 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
69 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
70 try:
70 try:
71 src = re.sub(pattern, repl, src, 1)
71 src = re.sub(pattern, repl, src, 1)
72 except re.error, e:
72 except re.error, e:
73 raise util.Abort(_("bad subrepository pattern in %s: %s")
73 raise util.Abort(_("bad subrepository pattern in %s: %s")
74 % (p.source('subpaths', pattern), e))
74 % (p.source('subpaths', pattern), e))
75
75
76 state[path] = (src.strip(), rev.get(path, ''), kind)
76 state[path] = (src.strip(), rev.get(path, ''), kind)
77
77
78 return state
78 return state
79
79
80 def writestate(repo, state):
80 def writestate(repo, state):
81 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
81 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
82 repo.wwrite('.hgsubstate',
82 repo.wwrite('.hgsubstate',
83 ''.join(['%s %s\n' % (state[s][1], s)
83 ''.join(['%s %s\n' % (state[s][1], s)
84 for s in sorted(state)]), '')
84 for s in sorted(state)]), '')
85
85
86 def submerge(repo, wctx, mctx, actx, overwrite):
86 def submerge(repo, wctx, mctx, actx, overwrite):
87 """delegated from merge.applyupdates: merging of .hgsubstate file
87 """delegated from merge.applyupdates: merging of .hgsubstate file
88 in working context, merging context and ancestor context"""
88 in working context, merging context and ancestor context"""
89 if mctx == actx: # backwards?
89 if mctx == actx: # backwards?
90 actx = wctx.p1()
90 actx = wctx.p1()
91 s1 = wctx.substate
91 s1 = wctx.substate
92 s2 = mctx.substate
92 s2 = mctx.substate
93 sa = actx.substate
93 sa = actx.substate
94 sm = {}
94 sm = {}
95
95
96 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
96 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
97
97
98 def debug(s, msg, r=""):
98 def debug(s, msg, r=""):
99 if r:
99 if r:
100 r = "%s:%s:%s" % r
100 r = "%s:%s:%s" % r
101 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
101 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
102
102
103 for s, l in s1.items():
103 for s, l in s1.items():
104 a = sa.get(s, nullstate)
104 a = sa.get(s, nullstate)
105 ld = l # local state with possible dirty flag for compares
105 ld = l # local state with possible dirty flag for compares
106 if wctx.sub(s).dirty():
106 if wctx.sub(s).dirty():
107 ld = (l[0], l[1] + "+")
107 ld = (l[0], l[1] + "+")
108 if wctx == actx: # overwrite
108 if wctx == actx: # overwrite
109 a = ld
109 a = ld
110
110
111 if s in s2:
111 if s in s2:
112 r = s2[s]
112 r = s2[s]
113 if ld == r or r == a: # no change or local is newer
113 if ld == r or r == a: # no change or local is newer
114 sm[s] = l
114 sm[s] = l
115 continue
115 continue
116 elif ld == a: # other side changed
116 elif ld == a: # other side changed
117 debug(s, "other changed, get", r)
117 debug(s, "other changed, get", r)
118 wctx.sub(s).get(r, overwrite)
118 wctx.sub(s).get(r, overwrite)
119 sm[s] = r
119 sm[s] = r
120 elif ld[0] != r[0]: # sources differ
120 elif ld[0] != r[0]: # sources differ
121 if repo.ui.promptchoice(
121 if repo.ui.promptchoice(
122 _(' subrepository sources for %s differ\n'
122 _(' subrepository sources for %s differ\n'
123 'use (l)ocal source (%s) or (r)emote source (%s)?')
123 'use (l)ocal source (%s) or (r)emote source (%s)?')
124 % (s, l[0], r[0]),
124 % (s, l[0], r[0]),
125 (_('&Local'), _('&Remote')), 0):
125 (_('&Local'), _('&Remote')), 0):
126 debug(s, "prompt changed, get", r)
126 debug(s, "prompt changed, get", r)
127 wctx.sub(s).get(r, overwrite)
127 wctx.sub(s).get(r, overwrite)
128 sm[s] = r
128 sm[s] = r
129 elif ld[1] == a[1]: # local side is unchanged
129 elif ld[1] == a[1]: # local side is unchanged
130 debug(s, "other side changed, get", r)
130 debug(s, "other side changed, get", r)
131 wctx.sub(s).get(r, overwrite)
131 wctx.sub(s).get(r, overwrite)
132 sm[s] = r
132 sm[s] = r
133 else:
133 else:
134 debug(s, "both sides changed, merge with", r)
134 debug(s, "both sides changed, merge with", r)
135 wctx.sub(s).merge(r)
135 wctx.sub(s).merge(r)
136 sm[s] = l
136 sm[s] = l
137 elif ld == a: # remote removed, local unchanged
137 elif ld == a: # remote removed, local unchanged
138 debug(s, "remote removed, remove")
138 debug(s, "remote removed, remove")
139 wctx.sub(s).remove()
139 wctx.sub(s).remove()
140 else:
140 else:
141 if repo.ui.promptchoice(
141 if repo.ui.promptchoice(
142 _(' local changed subrepository %s which remote removed\n'
142 _(' local changed subrepository %s which remote removed\n'
143 'use (c)hanged version or (d)elete?') % s,
143 'use (c)hanged version or (d)elete?') % s,
144 (_('&Changed'), _('&Delete')), 0):
144 (_('&Changed'), _('&Delete')), 0):
145 debug(s, "prompt remove")
145 debug(s, "prompt remove")
146 wctx.sub(s).remove()
146 wctx.sub(s).remove()
147
147
148 for s, r in s2.items():
148 for s, r in s2.items():
149 if s in s1:
149 if s in s1:
150 continue
150 continue
151 elif s not in sa:
151 elif s not in sa:
152 debug(s, "remote added, get", r)
152 debug(s, "remote added, get", r)
153 mctx.sub(s).get(r)
153 mctx.sub(s).get(r)
154 sm[s] = r
154 sm[s] = r
155 elif r != sa[s]:
155 elif r != sa[s]:
156 if repo.ui.promptchoice(
156 if repo.ui.promptchoice(
157 _(' remote changed subrepository %s which local removed\n'
157 _(' remote changed subrepository %s which local removed\n'
158 'use (c)hanged version or (d)elete?') % s,
158 'use (c)hanged version or (d)elete?') % s,
159 (_('&Changed'), _('&Delete')), 0) == 0:
159 (_('&Changed'), _('&Delete')), 0) == 0:
160 debug(s, "prompt recreate", r)
160 debug(s, "prompt recreate", r)
161 wctx.sub(s).get(r)
161 wctx.sub(s).get(r)
162 sm[s] = r
162 sm[s] = r
163
163
164 # record merged .hgsubstate
164 # record merged .hgsubstate
165 writestate(repo, sm)
165 writestate(repo, sm)
166
166
167 def _updateprompt(ui, sub, dirty, local, remote):
167 def _updateprompt(ui, sub, dirty, local, remote):
168 if dirty:
168 if dirty:
169 msg = (_(' subrepository sources for %s differ\n'
169 msg = (_(' subrepository sources for %s differ\n'
170 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
170 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
171 % (subrelpath(sub), local, remote))
171 % (subrelpath(sub), local, remote))
172 else:
172 else:
173 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
173 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
174 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
174 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
175 % (subrelpath(sub), local, remote))
175 % (subrelpath(sub), local, remote))
176 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
176 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
177
177
178 def reporelpath(repo):
178 def reporelpath(repo):
179 """return path to this (sub)repo as seen from outermost repo"""
179 """return path to this (sub)repo as seen from outermost repo"""
180 parent = repo
180 parent = repo
181 while hasattr(parent, '_subparent'):
181 while hasattr(parent, '_subparent'):
182 parent = parent._subparent
182 parent = parent._subparent
183 return repo.root[len(parent.root)+1:]
183 return repo.root[len(parent.root)+1:]
184
184
185 def subrelpath(sub):
185 def subrelpath(sub):
186 """return path to this subrepo as seen from outermost repo"""
186 """return path to this subrepo as seen from outermost repo"""
187 if hasattr(sub, '_relpath'):
187 if hasattr(sub, '_relpath'):
188 return sub._relpath
188 return sub._relpath
189 if not hasattr(sub, '_repo'):
189 if not hasattr(sub, '_repo'):
190 return sub._path
190 return sub._path
191 return reporelpath(sub._repo)
191 return reporelpath(sub._repo)
192
192
193 def _abssource(repo, push=False, abort=True):
193 def _abssource(repo, push=False, abort=True):
194 """return pull/push path of repo - either based on parent repo .hgsub info
194 """return pull/push path of repo - either based on parent repo .hgsub info
195 or on the top repo config. Abort or return None if no source found."""
195 or on the top repo config. Abort or return None if no source found."""
196 if hasattr(repo, '_subparent'):
196 if hasattr(repo, '_subparent'):
197 source = repo._subsource
197 source = repo._subsource
198 if source.startswith('/') or '://' in source:
198 if source.startswith('/') or '://' in source:
199 return source
199 return source
200 parent = _abssource(repo._subparent, push, abort=False)
200 parent = _abssource(repo._subparent, push, abort=False)
201 if parent:
201 if parent:
202 if '://' in parent:
202 if '://' in parent:
203 if parent[-1] == '/':
203 if parent[-1] == '/':
204 parent = parent[:-1]
204 parent = parent[:-1]
205 r = urlparse.urlparse(parent + '/' + source)
205 r = urlparse.urlparse(parent + '/' + source)
206 if parent.startswith('ssh://') and not r[1]:
206 if parent.startswith('ssh://') and not r[1]:
207 # Python before 2.6 parses ssh:// URLs wrong
207 # Python before 2.6 parses ssh:// URLs wrong
208 host, path = r[2][2:].split('/', 1)
208 host, path = r[2][2:].split('/', 1)
209 r2 = '//%s/%s' % (host, posixpath.normpath(path))
209 r2 = '//%s/%s' % (host, posixpath.normpath(path))
210 else:
210 else:
211 r2 = posixpath.normpath(r[2])
211 r2 = posixpath.normpath(r[2])
212 r = urlparse.urlunparse((r[0], r[1], r2,
212 r = urlparse.urlunparse((r[0], r[1], r2,
213 r[3], r[4], r[5]))
213 r[3], r[4], r[5]))
214 return r
214 return r
215 else: # plain file system path
215 else: # plain file system path
216 return posixpath.normpath(os.path.join(parent, repo._subsource))
216 return posixpath.normpath(os.path.join(parent, repo._subsource))
217 else: # recursion reached top repo
217 else: # recursion reached top repo
218 if hasattr(repo, '_subtoppath'):
218 if hasattr(repo, '_subtoppath'):
219 return repo._subtoppath
219 return repo._subtoppath
220 if push and repo.ui.config('paths', 'default-push'):
220 if push and repo.ui.config('paths', 'default-push'):
221 return repo.ui.config('paths', 'default-push')
221 return repo.ui.config('paths', 'default-push')
222 if repo.ui.config('paths', 'default'):
222 if repo.ui.config('paths', 'default'):
223 return repo.ui.config('paths', 'default')
223 return repo.ui.config('paths', 'default')
224 if abort:
224 if abort:
225 raise util.Abort(_("default path for subrepository %s not found") %
225 raise util.Abort(_("default path for subrepository %s not found") %
226 reporelpath(repo))
226 reporelpath(repo))
227
227
228 def itersubrepos(ctx1, ctx2):
228 def itersubrepos(ctx1, ctx2):
229 """find subrepos in ctx1 or ctx2"""
229 """find subrepos in ctx1 or ctx2"""
230 # Create a (subpath, ctx) mapping where we prefer subpaths from
230 # Create a (subpath, ctx) mapping where we prefer subpaths from
231 # ctx1. The subpaths from ctx2 are important when the .hgsub file
231 # ctx1. The subpaths from ctx2 are important when the .hgsub file
232 # has been modified (in ctx2) but not yet committed (in ctx1).
232 # has been modified (in ctx2) but not yet committed (in ctx1).
233 subpaths = dict.fromkeys(ctx2.substate, ctx2)
233 subpaths = dict.fromkeys(ctx2.substate, ctx2)
234 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
234 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
235 for subpath, ctx in sorted(subpaths.iteritems()):
235 for subpath, ctx in sorted(subpaths.iteritems()):
236 yield subpath, ctx.sub(subpath)
236 yield subpath, ctx.sub(subpath)
237
237
238 def subrepo(ctx, path):
238 def subrepo(ctx, path):
239 """return instance of the right subrepo class for subrepo in path"""
239 """return instance of the right subrepo class for subrepo in path"""
240 # subrepo inherently violates our import layering rules
240 # subrepo inherently violates our import layering rules
241 # because it wants to make repo objects from deep inside the stack
241 # because it wants to make repo objects from deep inside the stack
242 # so we manually delay the circular imports to not break
242 # so we manually delay the circular imports to not break
243 # scripts that don't use our demand-loading
243 # scripts that don't use our demand-loading
244 global hg
244 global hg
245 import hg as h
245 import hg as h
246 hg = h
246 hg = h
247
247
248 util.path_auditor(ctx._repo.root)(path)
248 util.path_auditor(ctx._repo.root)(path)
249 state = ctx.substate.get(path, nullstate)
249 state = ctx.substate.get(path, nullstate)
250 if state[2] not in types:
250 if state[2] not in types:
251 raise util.Abort(_('unknown subrepo type %s') % state[2])
251 raise util.Abort(_('unknown subrepo type %s') % state[2])
252 return types[state[2]](ctx, path, state[:2])
252 return types[state[2]](ctx, path, state[:2])
253
253
254 # subrepo classes need to implement the following abstract class:
254 # subrepo classes need to implement the following abstract class:
255
255
256 class abstractsubrepo(object):
256 class abstractsubrepo(object):
257
257
258 def dirty(self, ignoreupdate=False):
258 def dirty(self, ignoreupdate=False):
259 """returns true if the dirstate of the subrepo is dirty or does not
259 """returns true if the dirstate of the subrepo is dirty or does not
260 match current stored state. If ignoreupdate is true, only check
260 match current stored state. If ignoreupdate is true, only check
261 whether the subrepo has uncommitted changes in its dirstate.
261 whether the subrepo has uncommitted changes in its dirstate.
262 """
262 """
263 raise NotImplementedError
263 raise NotImplementedError
264
264
265 def checknested(self, path):
265 def checknested(self, path):
266 """check if path is a subrepository within this repository"""
266 """check if path is a subrepository within this repository"""
267 return False
267 return False
268
268
269 def commit(self, text, user, date):
269 def commit(self, text, user, date):
270 """commit the current changes to the subrepo with the given
270 """commit the current changes to the subrepo with the given
271 log message. Use given user and date if possible. Return the
271 log message. Use given user and date if possible. Return the
272 new state of the subrepo.
272 new state of the subrepo.
273 """
273 """
274 raise NotImplementedError
274 raise NotImplementedError
275
275
276 def remove(self):
276 def remove(self):
277 """remove the subrepo
277 """remove the subrepo
278
278
279 (should verify the dirstate is not dirty first)
279 (should verify the dirstate is not dirty first)
280 """
280 """
281 raise NotImplementedError
281 raise NotImplementedError
282
282
283 def get(self, state, overwrite=False):
283 def get(self, state, overwrite=False):
284 """run whatever commands are needed to put the subrepo into
284 """run whatever commands are needed to put the subrepo into
285 this state
285 this state
286 """
286 """
287 raise NotImplementedError
287 raise NotImplementedError
288
288
289 def merge(self, state):
289 def merge(self, state):
290 """merge currently-saved state with the new state."""
290 """merge currently-saved state with the new state."""
291 raise NotImplementedError
291 raise NotImplementedError
292
292
293 def push(self, force):
293 def push(self, force):
294 """perform whatever action is analogous to 'hg push'
294 """perform whatever action is analogous to 'hg push'
295
295
296 This may be a no-op on some systems.
296 This may be a no-op on some systems.
297 """
297 """
298 raise NotImplementedError
298 raise NotImplementedError
299
299
300 def add(self, ui, match, dryrun, prefix):
300 def add(self, ui, match, dryrun, prefix):
301 return []
301 return []
302
302
303 def status(self, rev2, **opts):
303 def status(self, rev2, **opts):
304 return [], [], [], [], [], [], []
304 return [], [], [], [], [], [], []
305
305
306 def diff(self, diffopts, node2, match, prefix, **opts):
306 def diff(self, diffopts, node2, match, prefix, **opts):
307 pass
307 pass
308
308
309 def outgoing(self, ui, dest, opts):
309 def outgoing(self, ui, dest, opts):
310 return 1
310 return 1
311
311
312 def incoming(self, ui, source, opts):
312 def incoming(self, ui, source, opts):
313 return 1
313 return 1
314
314
315 def files(self):
315 def files(self):
316 """return filename iterator"""
316 """return filename iterator"""
317 raise NotImplementedError
317 raise NotImplementedError
318
318
319 def filedata(self, name):
319 def filedata(self, name):
320 """return file data"""
320 """return file data"""
321 raise NotImplementedError
321 raise NotImplementedError
322
322
323 def fileflags(self, name):
323 def fileflags(self, name):
324 """return file flags"""
324 """return file flags"""
325 return ''
325 return ''
326
326
327 def archive(self, ui, archiver, prefix):
327 def archive(self, ui, archiver, prefix):
328 files = self.files()
328 files = self.files()
329 total = len(files)
329 total = len(files)
330 relpath = subrelpath(self)
330 relpath = subrelpath(self)
331 ui.progress(_('archiving (%s)') % relpath, 0,
331 ui.progress(_('archiving (%s)') % relpath, 0,
332 unit=_('files'), total=total)
332 unit=_('files'), total=total)
333 for i, name in enumerate(files):
333 for i, name in enumerate(files):
334 flags = self.fileflags(name)
334 flags = self.fileflags(name)
335 mode = 'x' in flags and 0755 or 0644
335 mode = 'x' in flags and 0755 or 0644
336 symlink = 'l' in flags
336 symlink = 'l' in flags
337 archiver.addfile(os.path.join(prefix, self._path, name),
337 archiver.addfile(os.path.join(prefix, self._path, name),
338 mode, symlink, self.filedata(name))
338 mode, symlink, self.filedata(name))
339 ui.progress(_('archiving (%s)') % relpath, i + 1,
339 ui.progress(_('archiving (%s)') % relpath, i + 1,
340 unit=_('files'), total=total)
340 unit=_('files'), total=total)
341 ui.progress(_('archiving (%s)') % relpath, None)
341 ui.progress(_('archiving (%s)') % relpath, None)
342
342
343
343
344 class hgsubrepo(abstractsubrepo):
344 class hgsubrepo(abstractsubrepo):
345 def __init__(self, ctx, path, state):
345 def __init__(self, ctx, path, state):
346 self._path = path
346 self._path = path
347 self._state = state
347 self._state = state
348 r = ctx._repo
348 r = ctx._repo
349 root = r.wjoin(path)
349 root = r.wjoin(path)
350 create = False
350 create = False
351 if not os.path.exists(os.path.join(root, '.hg')):
351 if not os.path.exists(os.path.join(root, '.hg')):
352 create = True
352 create = True
353 util.makedirs(root)
353 util.makedirs(root)
354 self._repo = hg.repository(r.ui, root, create=create)
354 self._repo = hg.repository(r.ui, root, create=create)
355 self._repo._subparent = r
355 self._repo._subparent = r
356 self._repo._subsource = state[0]
356 self._repo._subsource = state[0]
357
357
358 if create:
358 if create:
359 fp = self._repo.opener("hgrc", "w", text=True)
359 fp = self._repo.opener("hgrc", "w", text=True)
360 fp.write('[paths]\n')
360 fp.write('[paths]\n')
361
361
362 def addpathconfig(key, value):
362 def addpathconfig(key, value):
363 if value:
363 if value:
364 fp.write('%s = %s\n' % (key, value))
364 fp.write('%s = %s\n' % (key, value))
365 self._repo.ui.setconfig('paths', key, value)
365 self._repo.ui.setconfig('paths', key, value)
366
366
367 defpath = _abssource(self._repo, abort=False)
367 defpath = _abssource(self._repo, abort=False)
368 defpushpath = _abssource(self._repo, True, abort=False)
368 defpushpath = _abssource(self._repo, True, abort=False)
369 addpathconfig('default', defpath)
369 addpathconfig('default', defpath)
370 if defpath != defpushpath:
370 if defpath != defpushpath:
371 addpathconfig('default-push', defpushpath)
371 addpathconfig('default-push', defpushpath)
372 fp.close()
372 fp.close()
373
373
374 def add(self, ui, match, dryrun, prefix):
374 def add(self, ui, match, dryrun, prefix):
375 return cmdutil.add(ui, self._repo, match, dryrun, True,
375 return cmdutil.add(ui, self._repo, match, dryrun, True,
376 os.path.join(prefix, self._path))
376 os.path.join(prefix, self._path))
377
377
378 def status(self, rev2, **opts):
378 def status(self, rev2, **opts):
379 try:
379 try:
380 rev1 = self._state[1]
380 rev1 = self._state[1]
381 ctx1 = self._repo[rev1]
381 ctx1 = self._repo[rev1]
382 ctx2 = self._repo[rev2]
382 ctx2 = self._repo[rev2]
383 return self._repo.status(ctx1, ctx2, **opts)
383 return self._repo.status(ctx1, ctx2, **opts)
384 except error.RepoLookupError, inst:
384 except error.RepoLookupError, inst:
385 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
385 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
386 % (inst, subrelpath(self)))
386 % (inst, subrelpath(self)))
387 return [], [], [], [], [], [], []
387 return [], [], [], [], [], [], []
388
388
389 def diff(self, diffopts, node2, match, prefix, **opts):
389 def diff(self, diffopts, node2, match, prefix, **opts):
390 try:
390 try:
391 node1 = node.bin(self._state[1])
391 node1 = node.bin(self._state[1])
392 # We currently expect node2 to come from substate and be
392 # We currently expect node2 to come from substate and be
393 # in hex format
393 # in hex format
394 if node2 is not None:
394 if node2 is not None:
395 node2 = node.bin(node2)
395 node2 = node.bin(node2)
396 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
396 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
397 node1, node2, match,
397 node1, node2, match,
398 prefix=os.path.join(prefix, self._path),
398 prefix=os.path.join(prefix, self._path),
399 listsubrepos=True, **opts)
399 listsubrepos=True, **opts)
400 except error.RepoLookupError, inst:
400 except error.RepoLookupError, inst:
401 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
401 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
402 % (inst, subrelpath(self)))
402 % (inst, subrelpath(self)))
403
403
404 def archive(self, ui, archiver, prefix):
404 def archive(self, ui, archiver, prefix):
405 abstractsubrepo.archive(self, ui, archiver, prefix)
405 abstractsubrepo.archive(self, ui, archiver, prefix)
406
406
407 rev = self._state[1]
407 rev = self._state[1]
408 ctx = self._repo[rev]
408 ctx = self._repo[rev]
409 for subpath in ctx.substate:
409 for subpath in ctx.substate:
410 s = subrepo(ctx, subpath)
410 s = subrepo(ctx, subpath)
411 s.archive(ui, archiver, os.path.join(prefix, self._path))
411 s.archive(ui, archiver, os.path.join(prefix, self._path))
412
412
413 def dirty(self, ignoreupdate=False):
413 def dirty(self, ignoreupdate=False):
414 r = self._state[1]
414 r = self._state[1]
415 if r == '' and not ignoreupdate: # no state recorded
415 if r == '' and not ignoreupdate: # no state recorded
416 return True
416 return True
417 w = self._repo[None]
417 w = self._repo[None]
418 if w.p1() != self._repo[r] and not ignoreupdate:
418 if w.p1() != self._repo[r] and not ignoreupdate:
419 # different version checked out
419 # different version checked out
420 return True
420 return True
421 return w.dirty() # working directory changed
421 return w.dirty() # working directory changed
422
422
423 def checknested(self, path):
423 def checknested(self, path):
424 return self._repo._checknested(self._repo.wjoin(path))
424 return self._repo._checknested(self._repo.wjoin(path))
425
425
426 def commit(self, text, user, date):
426 def commit(self, text, user, date):
427 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
427 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
428 n = self._repo.commit(text, user, date)
428 n = self._repo.commit(text, user, date)
429 if not n:
429 if not n:
430 return self._repo['.'].hex() # different version checked out
430 return self._repo['.'].hex() # different version checked out
431 return node.hex(n)
431 return node.hex(n)
432
432
433 def remove(self):
433 def remove(self):
434 # we can't fully delete the repository as it may contain
434 # we can't fully delete the repository as it may contain
435 # local-only history
435 # local-only history
436 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
436 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
437 hg.clean(self._repo, node.nullid, False)
437 hg.clean(self._repo, node.nullid, False)
438
438
439 def _get(self, state):
439 def _get(self, state):
440 source, revision, kind = state
440 source, revision, kind = state
441 try:
441 try:
442 self._repo.lookup(revision)
442 self._repo.lookup(revision)
443 except error.RepoError:
443 except error.RepoError:
444 self._repo._subsource = source
444 self._repo._subsource = source
445 srcurl = _abssource(self._repo)
445 srcurl = _abssource(self._repo)
446 self._repo.ui.status(_('pulling subrepo %s from %s\n')
446 self._repo.ui.status(_('pulling subrepo %s from %s\n')
447 % (subrelpath(self), srcurl))
447 % (subrelpath(self), srcurl))
448 other = hg.repository(self._repo.ui, srcurl)
448 other = hg.repository(self._repo.ui, srcurl)
449 self._repo.pull(other)
449 self._repo.pull(other)
450
450
451 def get(self, state, overwrite=False):
451 def get(self, state, overwrite=False):
452 self._get(state)
452 self._get(state)
453 source, revision, kind = state
453 source, revision, kind = state
454 self._repo.ui.debug("getting subrepo %s\n" % self._path)
454 self._repo.ui.debug("getting subrepo %s\n" % self._path)
455 hg.clean(self._repo, revision, False)
455 hg.clean(self._repo, revision, False)
456
456
457 def merge(self, state):
457 def merge(self, state):
458 self._get(state)
458 self._get(state)
459 cur = self._repo['.']
459 cur = self._repo['.']
460 dst = self._repo[state[1]]
460 dst = self._repo[state[1]]
461 anc = dst.ancestor(cur)
461 anc = dst.ancestor(cur)
462
462
463 def mergefunc():
463 def mergefunc():
464 if anc == cur:
464 if anc == cur:
465 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
465 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
466 hg.update(self._repo, state[1])
466 hg.update(self._repo, state[1])
467 elif anc == dst:
467 elif anc == dst:
468 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
468 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
469 else:
469 else:
470 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
470 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
471 hg.merge(self._repo, state[1], remind=False)
471 hg.merge(self._repo, state[1], remind=False)
472
472
473 wctx = self._repo[None]
473 wctx = self._repo[None]
474 if self.dirty():
474 if self.dirty():
475 if anc != dst:
475 if anc != dst:
476 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
476 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
477 mergefunc()
477 mergefunc()
478 else:
478 else:
479 mergefunc()
479 mergefunc()
480 else:
480 else:
481 mergefunc()
481 mergefunc()
482
482
483 def push(self, force):
483 def push(self, force):
484 # push subrepos depth-first for coherent ordering
484 # push subrepos depth-first for coherent ordering
485 c = self._repo['']
485 c = self._repo['']
486 subs = c.substate # only repos that are committed
486 subs = c.substate # only repos that are committed
487 for s in sorted(subs):
487 for s in sorted(subs):
488 if not c.sub(s).push(force):
488 if not c.sub(s).push(force):
489 return False
489 return False
490
490
491 dsturl = _abssource(self._repo, True)
491 dsturl = _abssource(self._repo, True)
492 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
492 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
493 (subrelpath(self), dsturl))
493 (subrelpath(self), dsturl))
494 other = hg.repository(self._repo.ui, dsturl)
494 other = hg.repository(self._repo.ui, dsturl)
495 return self._repo.push(other, force)
495 return self._repo.push(other, force)
496
496
497 def outgoing(self, ui, dest, opts):
497 def outgoing(self, ui, dest, opts):
498 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
498 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
499
499
500 def incoming(self, ui, source, opts):
500 def incoming(self, ui, source, opts):
501 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
501 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
502
502
503 def files(self):
503 def files(self):
504 rev = self._state[1]
504 rev = self._state[1]
505 ctx = self._repo[rev]
505 ctx = self._repo[rev]
506 return ctx.manifest()
506 return ctx.manifest()
507
507
508 def filedata(self, name):
508 def filedata(self, name):
509 rev = self._state[1]
509 rev = self._state[1]
510 return self._repo[rev][name].data()
510 return self._repo[rev][name].data()
511
511
512 def fileflags(self, name):
512 def fileflags(self, name):
513 rev = self._state[1]
513 rev = self._state[1]
514 ctx = self._repo[rev]
514 ctx = self._repo[rev]
515 return ctx.flags(name)
515 return ctx.flags(name)
516
516
517
517
518 class svnsubrepo(abstractsubrepo):
518 class svnsubrepo(abstractsubrepo):
519 def __init__(self, ctx, path, state):
519 def __init__(self, ctx, path, state):
520 self._path = path
520 self._path = path
521 self._state = state
521 self._state = state
522 self._ctx = ctx
522 self._ctx = ctx
523 self._ui = ctx._repo.ui
523 self._ui = ctx._repo.ui
524
524
525 def _svncommand(self, commands, filename=''):
525 def _svncommand(self, commands, filename=''):
526 cmd = ['svn']
526 cmd = ['svn']
527 # Starting in svn 1.5 --non-interactive is a global flag
527 # Starting in svn 1.5 --non-interactive is a global flag
528 # instead of being per-command, but we need to support 1.4 so
528 # instead of being per-command, but we need to support 1.4 so
529 # we have to be intelligent about what commands take
529 # we have to be intelligent about what commands take
530 # --non-interactive.
530 # --non-interactive.
531 if (not self._ui.interactive() and
531 if (not self._ui.interactive() and
532 commands[0] in ('update', 'checkout', 'commit')):
532 commands[0] in ('update', 'checkout', 'commit')):
533 cmd.append('--non-interactive')
533 cmd.append('--non-interactive')
534 cmd.extend(commands)
534 cmd.extend(commands)
535 if filename is not None:
535 if filename is not None:
536 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
536 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
537 cmd.append(path)
537 cmd.append(path)
538 env = dict(os.environ)
538 env = dict(os.environ)
539 # Avoid localized output, preserve current locale for everything else.
539 # Avoid localized output, preserve current locale for everything else.
540 env['LC_MESSAGES'] = 'C'
540 env['LC_MESSAGES'] = 'C'
541 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
541 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
542 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
542 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
543 universal_newlines=True, env=env)
543 universal_newlines=True, env=env)
544 stdout, stderr = p.communicate()
544 stdout, stderr = p.communicate()
545 stderr = stderr.strip()
545 stderr = stderr.strip()
546 if p.returncode:
547 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
546 if stderr:
548 if stderr:
547 raise util.Abort(stderr)
549 self._ui.warn(stderr + '\n')
548 return stdout
550 return stdout
549
551
550 @propertycache
552 @propertycache
551 def _svnversion(self):
553 def _svnversion(self):
552 output = self._svncommand(['--version'], filename=None)
554 output = self._svncommand(['--version'], filename=None)
553 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
555 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
554 if not m:
556 if not m:
555 raise util.Abort(_('cannot retrieve svn tool version'))
557 raise util.Abort(_('cannot retrieve svn tool version'))
556 return (int(m.group(1)), int(m.group(2)))
558 return (int(m.group(1)), int(m.group(2)))
557
559
558 def _wcrevs(self):
560 def _wcrevs(self):
559 # Get the working directory revision as well as the last
561 # Get the working directory revision as well as the last
560 # commit revision so we can compare the subrepo state with
562 # commit revision so we can compare the subrepo state with
561 # both. We used to store the working directory one.
563 # both. We used to store the working directory one.
562 output = self._svncommand(['info', '--xml'])
564 output = self._svncommand(['info', '--xml'])
563 doc = xml.dom.minidom.parseString(output)
565 doc = xml.dom.minidom.parseString(output)
564 entries = doc.getElementsByTagName('entry')
566 entries = doc.getElementsByTagName('entry')
565 lastrev, rev = '0', '0'
567 lastrev, rev = '0', '0'
566 if entries:
568 if entries:
567 rev = str(entries[0].getAttribute('revision')) or '0'
569 rev = str(entries[0].getAttribute('revision')) or '0'
568 commits = entries[0].getElementsByTagName('commit')
570 commits = entries[0].getElementsByTagName('commit')
569 if commits:
571 if commits:
570 lastrev = str(commits[0].getAttribute('revision')) or '0'
572 lastrev = str(commits[0].getAttribute('revision')) or '0'
571 return (lastrev, rev)
573 return (lastrev, rev)
572
574
573 def _wcrev(self):
575 def _wcrev(self):
574 return self._wcrevs()[0]
576 return self._wcrevs()[0]
575
577
576 def _wcchanged(self):
578 def _wcchanged(self):
577 """Return (changes, extchanges) where changes is True
579 """Return (changes, extchanges) where changes is True
578 if the working directory was changed, and extchanges is
580 if the working directory was changed, and extchanges is
579 True if any of these changes concern an external entry.
581 True if any of these changes concern an external entry.
580 """
582 """
581 output = self._svncommand(['status', '--xml'])
583 output = self._svncommand(['status', '--xml'])
582 externals, changes = [], []
584 externals, changes = [], []
583 doc = xml.dom.minidom.parseString(output)
585 doc = xml.dom.minidom.parseString(output)
584 for e in doc.getElementsByTagName('entry'):
586 for e in doc.getElementsByTagName('entry'):
585 s = e.getElementsByTagName('wc-status')
587 s = e.getElementsByTagName('wc-status')
586 if not s:
588 if not s:
587 continue
589 continue
588 item = s[0].getAttribute('item')
590 item = s[0].getAttribute('item')
589 props = s[0].getAttribute('props')
591 props = s[0].getAttribute('props')
590 path = e.getAttribute('path')
592 path = e.getAttribute('path')
591 if item == 'external':
593 if item == 'external':
592 externals.append(path)
594 externals.append(path)
593 if (item not in ('', 'normal', 'unversioned', 'external')
595 if (item not in ('', 'normal', 'unversioned', 'external')
594 or props not in ('', 'none')):
596 or props not in ('', 'none')):
595 changes.append(path)
597 changes.append(path)
596 for path in changes:
598 for path in changes:
597 for ext in externals:
599 for ext in externals:
598 if path == ext or path.startswith(ext + os.sep):
600 if path == ext or path.startswith(ext + os.sep):
599 return True, True
601 return True, True
600 return bool(changes), False
602 return bool(changes), False
601
603
602 def dirty(self, ignoreupdate=False):
604 def dirty(self, ignoreupdate=False):
603 if not self._wcchanged()[0]:
605 if not self._wcchanged()[0]:
604 if self._state[1] in self._wcrevs() or ignoreupdate:
606 if self._state[1] in self._wcrevs() or ignoreupdate:
605 return False
607 return False
606 return True
608 return True
607
609
608 def commit(self, text, user, date):
610 def commit(self, text, user, date):
609 # user and date are out of our hands since svn is centralized
611 # user and date are out of our hands since svn is centralized
610 changed, extchanged = self._wcchanged()
612 changed, extchanged = self._wcchanged()
611 if not changed:
613 if not changed:
612 return self._wcrev()
614 return self._wcrev()
613 if extchanged:
615 if extchanged:
614 # Do not try to commit externals
616 # Do not try to commit externals
615 raise util.Abort(_('cannot commit svn externals'))
617 raise util.Abort(_('cannot commit svn externals'))
616 commitinfo = self._svncommand(['commit', '-m', text])
618 commitinfo = self._svncommand(['commit', '-m', text])
617 self._ui.status(commitinfo)
619 self._ui.status(commitinfo)
618 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
620 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
619 if not newrev:
621 if not newrev:
620 raise util.Abort(commitinfo.splitlines()[-1])
622 raise util.Abort(commitinfo.splitlines()[-1])
621 newrev = newrev.groups()[0]
623 newrev = newrev.groups()[0]
622 self._ui.status(self._svncommand(['update', '-r', newrev]))
624 self._ui.status(self._svncommand(['update', '-r', newrev]))
623 return newrev
625 return newrev
624
626
625 def remove(self):
627 def remove(self):
626 if self.dirty():
628 if self.dirty():
627 self._ui.warn(_('not removing repo %s because '
629 self._ui.warn(_('not removing repo %s because '
628 'it has changes.\n' % self._path))
630 'it has changes.\n' % self._path))
629 return
631 return
630 self._ui.note(_('removing subrepo %s\n') % self._path)
632 self._ui.note(_('removing subrepo %s\n') % self._path)
631
633
632 def onerror(function, path, excinfo):
634 def onerror(function, path, excinfo):
633 if function is not os.remove:
635 if function is not os.remove:
634 raise
636 raise
635 # read-only files cannot be unlinked under Windows
637 # read-only files cannot be unlinked under Windows
636 s = os.stat(path)
638 s = os.stat(path)
637 if (s.st_mode & stat.S_IWRITE) != 0:
639 if (s.st_mode & stat.S_IWRITE) != 0:
638 raise
640 raise
639 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
641 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
640 os.remove(path)
642 os.remove(path)
641
643
642 path = self._ctx._repo.wjoin(self._path)
644 path = self._ctx._repo.wjoin(self._path)
643 shutil.rmtree(path, onerror=onerror)
645 shutil.rmtree(path, onerror=onerror)
644 try:
646 try:
645 os.removedirs(os.path.dirname(path))
647 os.removedirs(os.path.dirname(path))
646 except OSError:
648 except OSError:
647 pass
649 pass
648
650
649 def get(self, state, overwrite=False):
651 def get(self, state, overwrite=False):
650 if overwrite:
652 if overwrite:
651 self._svncommand(['revert', '--recursive'])
653 self._svncommand(['revert', '--recursive'])
652 args = ['checkout']
654 args = ['checkout']
653 if self._svnversion >= (1, 5):
655 if self._svnversion >= (1, 5):
654 args.append('--force')
656 args.append('--force')
655 args.extend([state[0], '--revision', state[1]])
657 args.extend([state[0], '--revision', state[1]])
656 status = self._svncommand(args)
658 status = self._svncommand(args)
657 if not re.search('Checked out revision [0-9]+.', status):
659 if not re.search('Checked out revision [0-9]+.', status):
658 raise util.Abort(status.splitlines()[-1])
660 raise util.Abort(status.splitlines()[-1])
659 self._ui.status(status)
661 self._ui.status(status)
660
662
661 def merge(self, state):
663 def merge(self, state):
662 old = self._state[1]
664 old = self._state[1]
663 new = state[1]
665 new = state[1]
664 if new != self._wcrev():
666 if new != self._wcrev():
665 dirty = old == self._wcrev() or self._wcchanged()[0]
667 dirty = old == self._wcrev() or self._wcchanged()[0]
666 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
668 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
667 self.get(state, False)
669 self.get(state, False)
668
670
669 def push(self, force):
671 def push(self, force):
670 # push is a no-op for SVN
672 # push is a no-op for SVN
671 return True
673 return True
672
674
673 def files(self):
675 def files(self):
674 output = self._svncommand(['list'])
676 output = self._svncommand(['list'])
675 # This works because svn forbids \n in filenames.
677 # This works because svn forbids \n in filenames.
676 return output.splitlines()
678 return output.splitlines()
677
679
678 def filedata(self, name):
680 def filedata(self, name):
679 return self._svncommand(['cat'], name)
681 return self._svncommand(['cat'], name)
680
682
681
683
682 class gitsubrepo(abstractsubrepo):
684 class gitsubrepo(abstractsubrepo):
683 def __init__(self, ctx, path, state):
685 def __init__(self, ctx, path, state):
684 # TODO add git version check.
686 # TODO add git version check.
685 self._state = state
687 self._state = state
686 self._ctx = ctx
688 self._ctx = ctx
687 self._path = path
689 self._path = path
688 self._relpath = os.path.join(reporelpath(ctx._repo), path)
690 self._relpath = os.path.join(reporelpath(ctx._repo), path)
689 self._abspath = ctx._repo.wjoin(path)
691 self._abspath = ctx._repo.wjoin(path)
690 self._subparent = ctx._repo
692 self._subparent = ctx._repo
691 self._ui = ctx._repo.ui
693 self._ui = ctx._repo.ui
692
694
693 def _gitcommand(self, commands, env=None, stream=False):
695 def _gitcommand(self, commands, env=None, stream=False):
694 return self._gitdir(commands, env=env, stream=stream)[0]
696 return self._gitdir(commands, env=env, stream=stream)[0]
695
697
696 def _gitdir(self, commands, env=None, stream=False):
698 def _gitdir(self, commands, env=None, stream=False):
697 return self._gitnodir(commands, env=env, stream=stream,
699 return self._gitnodir(commands, env=env, stream=stream,
698 cwd=self._abspath)
700 cwd=self._abspath)
699
701
700 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
702 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
701 """Calls the git command
703 """Calls the git command
702
704
703 The methods tries to call the git command. versions previor to 1.6.0
705 The methods tries to call the git command. versions previor to 1.6.0
704 are not supported and very probably fail.
706 are not supported and very probably fail.
705 """
707 """
706 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
708 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
707 # unless ui.quiet is set, print git's stderr,
709 # unless ui.quiet is set, print git's stderr,
708 # which is mostly progress and useful info
710 # which is mostly progress and useful info
709 errpipe = None
711 errpipe = None
710 if self._ui.quiet:
712 if self._ui.quiet:
711 errpipe = open(os.devnull, 'w')
713 errpipe = open(os.devnull, 'w')
712 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
714 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
713 close_fds=util.closefds,
715 close_fds=util.closefds,
714 stdout=subprocess.PIPE, stderr=errpipe)
716 stdout=subprocess.PIPE, stderr=errpipe)
715 if stream:
717 if stream:
716 return p.stdout, None
718 return p.stdout, None
717
719
718 retdata = p.stdout.read().strip()
720 retdata = p.stdout.read().strip()
719 # wait for the child to exit to avoid race condition.
721 # wait for the child to exit to avoid race condition.
720 p.wait()
722 p.wait()
721
723
722 if p.returncode != 0 and p.returncode != 1:
724 if p.returncode != 0 and p.returncode != 1:
723 # there are certain error codes that are ok
725 # there are certain error codes that are ok
724 command = commands[0]
726 command = commands[0]
725 if command in ('cat-file', 'symbolic-ref'):
727 if command in ('cat-file', 'symbolic-ref'):
726 return retdata, p.returncode
728 return retdata, p.returncode
727 # for all others, abort
729 # for all others, abort
728 raise util.Abort('git %s error %d in %s' %
730 raise util.Abort('git %s error %d in %s' %
729 (command, p.returncode, self._relpath))
731 (command, p.returncode, self._relpath))
730
732
731 return retdata, p.returncode
733 return retdata, p.returncode
732
734
733 def _gitmissing(self):
735 def _gitmissing(self):
734 return not os.path.exists(os.path.join(self._abspath, '.git'))
736 return not os.path.exists(os.path.join(self._abspath, '.git'))
735
737
736 def _gitstate(self):
738 def _gitstate(self):
737 return self._gitcommand(['rev-parse', 'HEAD'])
739 return self._gitcommand(['rev-parse', 'HEAD'])
738
740
739 def _gitcurrentbranch(self):
741 def _gitcurrentbranch(self):
740 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
742 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
741 if err:
743 if err:
742 current = None
744 current = None
743 return current
745 return current
744
746
745 def _githavelocally(self, revision):
747 def _githavelocally(self, revision):
746 out, code = self._gitdir(['cat-file', '-e', revision])
748 out, code = self._gitdir(['cat-file', '-e', revision])
747 return code == 0
749 return code == 0
748
750
749 def _gitisancestor(self, r1, r2):
751 def _gitisancestor(self, r1, r2):
750 base = self._gitcommand(['merge-base', r1, r2])
752 base = self._gitcommand(['merge-base', r1, r2])
751 return base == r1
753 return base == r1
752
754
753 def _gitbranchmap(self):
755 def _gitbranchmap(self):
754 '''returns 2 things:
756 '''returns 2 things:
755 a map from git branch to revision
757 a map from git branch to revision
756 a map from revision to branches'''
758 a map from revision to branches'''
757 branch2rev = {}
759 branch2rev = {}
758 rev2branch = {}
760 rev2branch = {}
759
761
760 out = self._gitcommand(['for-each-ref', '--format',
762 out = self._gitcommand(['for-each-ref', '--format',
761 '%(objectname) %(refname)'])
763 '%(objectname) %(refname)'])
762 for line in out.split('\n'):
764 for line in out.split('\n'):
763 revision, ref = line.split(' ')
765 revision, ref = line.split(' ')
764 if (not ref.startswith('refs/heads/') and
766 if (not ref.startswith('refs/heads/') and
765 not ref.startswith('refs/remotes/')):
767 not ref.startswith('refs/remotes/')):
766 continue
768 continue
767 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
769 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
768 continue # ignore remote/HEAD redirects
770 continue # ignore remote/HEAD redirects
769 branch2rev[ref] = revision
771 branch2rev[ref] = revision
770 rev2branch.setdefault(revision, []).append(ref)
772 rev2branch.setdefault(revision, []).append(ref)
771 return branch2rev, rev2branch
773 return branch2rev, rev2branch
772
774
773 def _gittracking(self, branches):
775 def _gittracking(self, branches):
774 'return map of remote branch to local tracking branch'
776 'return map of remote branch to local tracking branch'
775 # assumes no more than one local tracking branch for each remote
777 # assumes no more than one local tracking branch for each remote
776 tracking = {}
778 tracking = {}
777 for b in branches:
779 for b in branches:
778 if b.startswith('refs/remotes/'):
780 if b.startswith('refs/remotes/'):
779 continue
781 continue
780 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
782 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
781 if remote:
783 if remote:
782 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
784 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
783 tracking['refs/remotes/%s/%s' %
785 tracking['refs/remotes/%s/%s' %
784 (remote, ref.split('/', 2)[2])] = b
786 (remote, ref.split('/', 2)[2])] = b
785 return tracking
787 return tracking
786
788
787 def _abssource(self, source):
789 def _abssource(self, source):
788 if '://' not in source:
790 if '://' not in source:
789 # recognize the scp syntax as an absolute source
791 # recognize the scp syntax as an absolute source
790 colon = source.find(':')
792 colon = source.find(':')
791 if colon != -1 and '/' not in source[:colon]:
793 if colon != -1 and '/' not in source[:colon]:
792 return source
794 return source
793 self._subsource = source
795 self._subsource = source
794 return _abssource(self)
796 return _abssource(self)
795
797
796 def _fetch(self, source, revision):
798 def _fetch(self, source, revision):
797 if self._gitmissing():
799 if self._gitmissing():
798 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
800 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
799 self._gitnodir(['clone', self._abssource(source), self._abspath])
801 self._gitnodir(['clone', self._abssource(source), self._abspath])
800 if self._githavelocally(revision):
802 if self._githavelocally(revision):
801 return
803 return
802 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
804 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
803 # try only origin: the originally cloned repo
805 # try only origin: the originally cloned repo
804 self._gitcommand(['fetch'])
806 self._gitcommand(['fetch'])
805 if not self._githavelocally(revision):
807 if not self._githavelocally(revision):
806 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
808 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
807 (revision, self._relpath))
809 (revision, self._relpath))
808
810
809 def dirty(self, ignoreupdate=False):
811 def dirty(self, ignoreupdate=False):
810 if self._gitmissing():
812 if self._gitmissing():
811 return self._state[1] != ''
813 return self._state[1] != ''
812 if not ignoreupdate and self._state[1] != self._gitstate():
814 if not ignoreupdate and self._state[1] != self._gitstate():
813 # different version checked out
815 # different version checked out
814 return True
816 return True
815 # check for staged changes or modified files; ignore untracked files
817 # check for staged changes or modified files; ignore untracked files
816 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
818 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
817 return code == 1
819 return code == 1
818
820
819 def get(self, state, overwrite=False):
821 def get(self, state, overwrite=False):
820 source, revision, kind = state
822 source, revision, kind = state
821 if not revision:
823 if not revision:
822 self.remove()
824 self.remove()
823 return
825 return
824 self._fetch(source, revision)
826 self._fetch(source, revision)
825 # if the repo was set to be bare, unbare it
827 # if the repo was set to be bare, unbare it
826 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
828 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
827 self._gitcommand(['config', 'core.bare', 'false'])
829 self._gitcommand(['config', 'core.bare', 'false'])
828 if self._gitstate() == revision:
830 if self._gitstate() == revision:
829 self._gitcommand(['reset', '--hard', 'HEAD'])
831 self._gitcommand(['reset', '--hard', 'HEAD'])
830 return
832 return
831 elif self._gitstate() == revision:
833 elif self._gitstate() == revision:
832 if overwrite:
834 if overwrite:
833 # first reset the index to unmark new files for commit, because
835 # first reset the index to unmark new files for commit, because
834 # reset --hard will otherwise throw away files added for commit,
836 # reset --hard will otherwise throw away files added for commit,
835 # not just unmark them.
837 # not just unmark them.
836 self._gitcommand(['reset', 'HEAD'])
838 self._gitcommand(['reset', 'HEAD'])
837 self._gitcommand(['reset', '--hard', 'HEAD'])
839 self._gitcommand(['reset', '--hard', 'HEAD'])
838 return
840 return
839 branch2rev, rev2branch = self._gitbranchmap()
841 branch2rev, rev2branch = self._gitbranchmap()
840
842
841 def checkout(args):
843 def checkout(args):
842 cmd = ['checkout']
844 cmd = ['checkout']
843 if overwrite:
845 if overwrite:
844 # first reset the index to unmark new files for commit, because
846 # first reset the index to unmark new files for commit, because
845 # the -f option will otherwise throw away files added for
847 # the -f option will otherwise throw away files added for
846 # commit, not just unmark them.
848 # commit, not just unmark them.
847 self._gitcommand(['reset', 'HEAD'])
849 self._gitcommand(['reset', 'HEAD'])
848 cmd.append('-f')
850 cmd.append('-f')
849 self._gitcommand(cmd + args)
851 self._gitcommand(cmd + args)
850
852
851 def rawcheckout():
853 def rawcheckout():
852 # no branch to checkout, check it out with no branch
854 # no branch to checkout, check it out with no branch
853 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
855 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
854 self._relpath)
856 self._relpath)
855 self._ui.warn(_('check out a git branch if you intend '
857 self._ui.warn(_('check out a git branch if you intend '
856 'to make changes\n'))
858 'to make changes\n'))
857 checkout(['-q', revision])
859 checkout(['-q', revision])
858
860
859 if revision not in rev2branch:
861 if revision not in rev2branch:
860 rawcheckout()
862 rawcheckout()
861 return
863 return
862 branches = rev2branch[revision]
864 branches = rev2branch[revision]
863 firstlocalbranch = None
865 firstlocalbranch = None
864 for b in branches:
866 for b in branches:
865 if b == 'refs/heads/master':
867 if b == 'refs/heads/master':
866 # master trumps all other branches
868 # master trumps all other branches
867 checkout(['refs/heads/master'])
869 checkout(['refs/heads/master'])
868 return
870 return
869 if not firstlocalbranch and not b.startswith('refs/remotes/'):
871 if not firstlocalbranch and not b.startswith('refs/remotes/'):
870 firstlocalbranch = b
872 firstlocalbranch = b
871 if firstlocalbranch:
873 if firstlocalbranch:
872 checkout([firstlocalbranch])
874 checkout([firstlocalbranch])
873 return
875 return
874
876
875 tracking = self._gittracking(branch2rev.keys())
877 tracking = self._gittracking(branch2rev.keys())
876 # choose a remote branch already tracked if possible
878 # choose a remote branch already tracked if possible
877 remote = branches[0]
879 remote = branches[0]
878 if remote not in tracking:
880 if remote not in tracking:
879 for b in branches:
881 for b in branches:
880 if b in tracking:
882 if b in tracking:
881 remote = b
883 remote = b
882 break
884 break
883
885
884 if remote not in tracking:
886 if remote not in tracking:
885 # create a new local tracking branch
887 # create a new local tracking branch
886 local = remote.split('/', 2)[2]
888 local = remote.split('/', 2)[2]
887 checkout(['-b', local, remote])
889 checkout(['-b', local, remote])
888 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
890 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
889 # When updating to a tracked remote branch,
891 # When updating to a tracked remote branch,
890 # if the local tracking branch is downstream of it,
892 # if the local tracking branch is downstream of it,
891 # a normal `git pull` would have performed a "fast-forward merge"
893 # a normal `git pull` would have performed a "fast-forward merge"
892 # which is equivalent to updating the local branch to the remote.
894 # which is equivalent to updating the local branch to the remote.
893 # Since we are only looking at branching at update, we need to
895 # Since we are only looking at branching at update, we need to
894 # detect this situation and perform this action lazily.
896 # detect this situation and perform this action lazily.
895 if tracking[remote] != self._gitcurrentbranch():
897 if tracking[remote] != self._gitcurrentbranch():
896 checkout([tracking[remote]])
898 checkout([tracking[remote]])
897 self._gitcommand(['merge', '--ff', remote])
899 self._gitcommand(['merge', '--ff', remote])
898 else:
900 else:
899 # a real merge would be required, just checkout the revision
901 # a real merge would be required, just checkout the revision
900 rawcheckout()
902 rawcheckout()
901
903
902 def commit(self, text, user, date):
904 def commit(self, text, user, date):
903 if self._gitmissing():
905 if self._gitmissing():
904 raise util.Abort(_("subrepo %s is missing") % self._relpath)
906 raise util.Abort(_("subrepo %s is missing") % self._relpath)
905 cmd = ['commit', '-a', '-m', text]
907 cmd = ['commit', '-a', '-m', text]
906 env = os.environ.copy()
908 env = os.environ.copy()
907 if user:
909 if user:
908 cmd += ['--author', user]
910 cmd += ['--author', user]
909 if date:
911 if date:
910 # git's date parser silently ignores when seconds < 1e9
912 # git's date parser silently ignores when seconds < 1e9
911 # convert to ISO8601
913 # convert to ISO8601
912 env['GIT_AUTHOR_DATE'] = util.datestr(date,
914 env['GIT_AUTHOR_DATE'] = util.datestr(date,
913 '%Y-%m-%dT%H:%M:%S %1%2')
915 '%Y-%m-%dT%H:%M:%S %1%2')
914 self._gitcommand(cmd, env=env)
916 self._gitcommand(cmd, env=env)
915 # make sure commit works otherwise HEAD might not exist under certain
917 # make sure commit works otherwise HEAD might not exist under certain
916 # circumstances
918 # circumstances
917 return self._gitstate()
919 return self._gitstate()
918
920
919 def merge(self, state):
921 def merge(self, state):
920 source, revision, kind = state
922 source, revision, kind = state
921 self._fetch(source, revision)
923 self._fetch(source, revision)
922 base = self._gitcommand(['merge-base', revision, self._state[1]])
924 base = self._gitcommand(['merge-base', revision, self._state[1]])
923 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
925 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
924
926
925 def mergefunc():
927 def mergefunc():
926 if base == revision:
928 if base == revision:
927 self.get(state) # fast forward merge
929 self.get(state) # fast forward merge
928 elif base != self._state[1]:
930 elif base != self._state[1]:
929 self._gitcommand(['merge', '--no-commit', revision])
931 self._gitcommand(['merge', '--no-commit', revision])
930
932
931 if self.dirty():
933 if self.dirty():
932 if self._gitstate() != revision:
934 if self._gitstate() != revision:
933 dirty = self._gitstate() == self._state[1] or code != 0
935 dirty = self._gitstate() == self._state[1] or code != 0
934 if _updateprompt(self._ui, self, dirty,
936 if _updateprompt(self._ui, self, dirty,
935 self._state[1][:7], revision[:7]):
937 self._state[1][:7], revision[:7]):
936 mergefunc()
938 mergefunc()
937 else:
939 else:
938 mergefunc()
940 mergefunc()
939
941
940 def push(self, force):
942 def push(self, force):
941 if not self._state[1]:
943 if not self._state[1]:
942 return True
944 return True
943 if self._gitmissing():
945 if self._gitmissing():
944 raise util.Abort(_("subrepo %s is missing") % self._relpath)
946 raise util.Abort(_("subrepo %s is missing") % self._relpath)
945 # if a branch in origin contains the revision, nothing to do
947 # if a branch in origin contains the revision, nothing to do
946 branch2rev, rev2branch = self._gitbranchmap()
948 branch2rev, rev2branch = self._gitbranchmap()
947 if self._state[1] in rev2branch:
949 if self._state[1] in rev2branch:
948 for b in rev2branch[self._state[1]]:
950 for b in rev2branch[self._state[1]]:
949 if b.startswith('refs/remotes/origin/'):
951 if b.startswith('refs/remotes/origin/'):
950 return True
952 return True
951 for b, revision in branch2rev.iteritems():
953 for b, revision in branch2rev.iteritems():
952 if b.startswith('refs/remotes/origin/'):
954 if b.startswith('refs/remotes/origin/'):
953 if self._gitisancestor(self._state[1], revision):
955 if self._gitisancestor(self._state[1], revision):
954 return True
956 return True
955 # otherwise, try to push the currently checked out branch
957 # otherwise, try to push the currently checked out branch
956 cmd = ['push']
958 cmd = ['push']
957 if force:
959 if force:
958 cmd.append('--force')
960 cmd.append('--force')
959
961
960 current = self._gitcurrentbranch()
962 current = self._gitcurrentbranch()
961 if current:
963 if current:
962 # determine if the current branch is even useful
964 # determine if the current branch is even useful
963 if not self._gitisancestor(self._state[1], current):
965 if not self._gitisancestor(self._state[1], current):
964 self._ui.warn(_('unrelated git branch checked out '
966 self._ui.warn(_('unrelated git branch checked out '
965 'in subrepo %s\n') % self._relpath)
967 'in subrepo %s\n') % self._relpath)
966 return False
968 return False
967 self._ui.status(_('pushing branch %s of subrepo %s\n') %
969 self._ui.status(_('pushing branch %s of subrepo %s\n') %
968 (current.split('/', 2)[2], self._relpath))
970 (current.split('/', 2)[2], self._relpath))
969 self._gitcommand(cmd + ['origin', current])
971 self._gitcommand(cmd + ['origin', current])
970 return True
972 return True
971 else:
973 else:
972 self._ui.warn(_('no branch checked out in subrepo %s\n'
974 self._ui.warn(_('no branch checked out in subrepo %s\n'
973 'cannot push revision %s') %
975 'cannot push revision %s') %
974 (self._relpath, self._state[1]))
976 (self._relpath, self._state[1]))
975 return False
977 return False
976
978
977 def remove(self):
979 def remove(self):
978 if self._gitmissing():
980 if self._gitmissing():
979 return
981 return
980 if self.dirty():
982 if self.dirty():
981 self._ui.warn(_('not removing repo %s because '
983 self._ui.warn(_('not removing repo %s because '
982 'it has changes.\n') % self._relpath)
984 'it has changes.\n') % self._relpath)
983 return
985 return
984 # we can't fully delete the repository as it may contain
986 # we can't fully delete the repository as it may contain
985 # local-only history
987 # local-only history
986 self._ui.note(_('removing subrepo %s\n') % self._relpath)
988 self._ui.note(_('removing subrepo %s\n') % self._relpath)
987 self._gitcommand(['config', 'core.bare', 'true'])
989 self._gitcommand(['config', 'core.bare', 'true'])
988 for f in os.listdir(self._abspath):
990 for f in os.listdir(self._abspath):
989 if f == '.git':
991 if f == '.git':
990 continue
992 continue
991 path = os.path.join(self._abspath, f)
993 path = os.path.join(self._abspath, f)
992 if os.path.isdir(path) and not os.path.islink(path):
994 if os.path.isdir(path) and not os.path.islink(path):
993 shutil.rmtree(path)
995 shutil.rmtree(path)
994 else:
996 else:
995 os.remove(path)
997 os.remove(path)
996
998
997 def archive(self, ui, archiver, prefix):
999 def archive(self, ui, archiver, prefix):
998 source, revision = self._state
1000 source, revision = self._state
999 if not revision:
1001 if not revision:
1000 return
1002 return
1001 self._fetch(source, revision)
1003 self._fetch(source, revision)
1002
1004
1003 # Parse git's native archive command.
1005 # Parse git's native archive command.
1004 # This should be much faster than manually traversing the trees
1006 # This should be much faster than manually traversing the trees
1005 # and objects with many subprocess calls.
1007 # and objects with many subprocess calls.
1006 tarstream = self._gitcommand(['archive', revision], stream=True)
1008 tarstream = self._gitcommand(['archive', revision], stream=True)
1007 tar = tarfile.open(fileobj=tarstream, mode='r|')
1009 tar = tarfile.open(fileobj=tarstream, mode='r|')
1008 relpath = subrelpath(self)
1010 relpath = subrelpath(self)
1009 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1011 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1010 for i, info in enumerate(tar):
1012 for i, info in enumerate(tar):
1011 if info.isdir():
1013 if info.isdir():
1012 continue
1014 continue
1013 if info.issym():
1015 if info.issym():
1014 data = info.linkname
1016 data = info.linkname
1015 else:
1017 else:
1016 data = tar.extractfile(info).read()
1018 data = tar.extractfile(info).read()
1017 archiver.addfile(os.path.join(prefix, self._path, info.name),
1019 archiver.addfile(os.path.join(prefix, self._path, info.name),
1018 info.mode, info.issym(), data)
1020 info.mode, info.issym(), data)
1019 ui.progress(_('archiving (%s)') % relpath, i + 1,
1021 ui.progress(_('archiving (%s)') % relpath, i + 1,
1020 unit=_('files'))
1022 unit=_('files'))
1021 ui.progress(_('archiving (%s)') % relpath, None)
1023 ui.progress(_('archiving (%s)') % relpath, None)
1022
1024
1023
1025
1024 def status(self, rev2, **opts):
1026 def status(self, rev2, **opts):
1025 rev1 = self._state[1]
1027 rev1 = self._state[1]
1026 if self._gitmissing() or not rev1:
1028 if self._gitmissing() or not rev1:
1027 # if the repo is missing, return no results
1029 # if the repo is missing, return no results
1028 return [], [], [], [], [], [], []
1030 return [], [], [], [], [], [], []
1029 modified, added, removed = [], [], []
1031 modified, added, removed = [], [], []
1030 if rev2:
1032 if rev2:
1031 command = ['diff-tree', rev1, rev2]
1033 command = ['diff-tree', rev1, rev2]
1032 else:
1034 else:
1033 command = ['diff-index', rev1]
1035 command = ['diff-index', rev1]
1034 out = self._gitcommand(command)
1036 out = self._gitcommand(command)
1035 for line in out.split('\n'):
1037 for line in out.split('\n'):
1036 tab = line.find('\t')
1038 tab = line.find('\t')
1037 if tab == -1:
1039 if tab == -1:
1038 continue
1040 continue
1039 status, f = line[tab - 1], line[tab + 1:]
1041 status, f = line[tab - 1], line[tab + 1:]
1040 if status == 'M':
1042 if status == 'M':
1041 modified.append(f)
1043 modified.append(f)
1042 elif status == 'A':
1044 elif status == 'A':
1043 added.append(f)
1045 added.append(f)
1044 elif status == 'D':
1046 elif status == 'D':
1045 removed.append(f)
1047 removed.append(f)
1046
1048
1047 deleted = unknown = ignored = clean = []
1049 deleted = unknown = ignored = clean = []
1048 return modified, added, removed, deleted, unknown, ignored, clean
1050 return modified, added, removed, deleted, unknown, ignored, clean
1049
1051
1050 types = {
1052 types = {
1051 'hg': hgsubrepo,
1053 'hg': hgsubrepo,
1052 'svn': svnsubrepo,
1054 'svn': svnsubrepo,
1053 'git': gitsubrepo,
1055 'git': gitsubrepo,
1054 }
1056 }
General Comments 0
You need to be logged in to leave comments. Login now