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