##// END OF EJS Templates
subrepo: make stdin for svn a pipe for non-interactive use (issue2759)...
Augie Fackler -
r14506:733af5d9 1.8.4 stable
parent child Browse files
Show More
@@ -1,1056 +1,1060 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 extrakw = {}
528 # instead of being per-command, but we need to support 1.4 so
528 if not self._ui.interactive():
529 # we have to be intelligent about what commands take
529 # Making stdin be a pipe should prevent svn from behaving
530 # --non-interactive.
530 # interactively even if we can't pass --non-interactive.
531 if (not self._ui.interactive() and
531 extrakw['stdin'] = subprocess.PIPE
532 commands[0] in ('update', 'checkout', 'commit')):
532 # Starting in svn 1.5 --non-interactive is a global flag
533 cmd.append('--non-interactive')
533 # instead of being per-command, but we need to support 1.4 so
534 # we have to be intelligent about what commands take
535 # --non-interactive.
536 if commands[0] in ('update', 'checkout', 'commit'):
537 cmd.append('--non-interactive')
534 cmd.extend(commands)
538 cmd.extend(commands)
535 if filename is not None:
539 if filename is not None:
536 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
540 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
537 cmd.append(path)
541 cmd.append(path)
538 env = dict(os.environ)
542 env = dict(os.environ)
539 # Avoid localized output, preserve current locale for everything else.
543 # Avoid localized output, preserve current locale for everything else.
540 env['LC_MESSAGES'] = 'C'
544 env['LC_MESSAGES'] = 'C'
541 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
545 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
542 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
546 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
543 universal_newlines=True, env=env)
547 universal_newlines=True, env=env, **extrakw)
544 stdout, stderr = p.communicate()
548 stdout, stderr = p.communicate()
545 stderr = stderr.strip()
549 stderr = stderr.strip()
546 if p.returncode:
550 if p.returncode:
547 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
551 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
548 if stderr:
552 if stderr:
549 self._ui.warn(stderr + '\n')
553 self._ui.warn(stderr + '\n')
550 return stdout
554 return stdout
551
555
552 @propertycache
556 @propertycache
553 def _svnversion(self):
557 def _svnversion(self):
554 output = self._svncommand(['--version'], filename=None)
558 output = self._svncommand(['--version'], filename=None)
555 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
559 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
556 if not m:
560 if not m:
557 raise util.Abort(_('cannot retrieve svn tool version'))
561 raise util.Abort(_('cannot retrieve svn tool version'))
558 return (int(m.group(1)), int(m.group(2)))
562 return (int(m.group(1)), int(m.group(2)))
559
563
560 def _wcrevs(self):
564 def _wcrevs(self):
561 # Get the working directory revision as well as the last
565 # Get the working directory revision as well as the last
562 # commit revision so we can compare the subrepo state with
566 # commit revision so we can compare the subrepo state with
563 # both. We used to store the working directory one.
567 # both. We used to store the working directory one.
564 output = self._svncommand(['info', '--xml'])
568 output = self._svncommand(['info', '--xml'])
565 doc = xml.dom.minidom.parseString(output)
569 doc = xml.dom.minidom.parseString(output)
566 entries = doc.getElementsByTagName('entry')
570 entries = doc.getElementsByTagName('entry')
567 lastrev, rev = '0', '0'
571 lastrev, rev = '0', '0'
568 if entries:
572 if entries:
569 rev = str(entries[0].getAttribute('revision')) or '0'
573 rev = str(entries[0].getAttribute('revision')) or '0'
570 commits = entries[0].getElementsByTagName('commit')
574 commits = entries[0].getElementsByTagName('commit')
571 if commits:
575 if commits:
572 lastrev = str(commits[0].getAttribute('revision')) or '0'
576 lastrev = str(commits[0].getAttribute('revision')) or '0'
573 return (lastrev, rev)
577 return (lastrev, rev)
574
578
575 def _wcrev(self):
579 def _wcrev(self):
576 return self._wcrevs()[0]
580 return self._wcrevs()[0]
577
581
578 def _wcchanged(self):
582 def _wcchanged(self):
579 """Return (changes, extchanges) where changes is True
583 """Return (changes, extchanges) where changes is True
580 if the working directory was changed, and extchanges is
584 if the working directory was changed, and extchanges is
581 True if any of these changes concern an external entry.
585 True if any of these changes concern an external entry.
582 """
586 """
583 output = self._svncommand(['status', '--xml'])
587 output = self._svncommand(['status', '--xml'])
584 externals, changes = [], []
588 externals, changes = [], []
585 doc = xml.dom.minidom.parseString(output)
589 doc = xml.dom.minidom.parseString(output)
586 for e in doc.getElementsByTagName('entry'):
590 for e in doc.getElementsByTagName('entry'):
587 s = e.getElementsByTagName('wc-status')
591 s = e.getElementsByTagName('wc-status')
588 if not s:
592 if not s:
589 continue
593 continue
590 item = s[0].getAttribute('item')
594 item = s[0].getAttribute('item')
591 props = s[0].getAttribute('props')
595 props = s[0].getAttribute('props')
592 path = e.getAttribute('path')
596 path = e.getAttribute('path')
593 if item == 'external':
597 if item == 'external':
594 externals.append(path)
598 externals.append(path)
595 if (item not in ('', 'normal', 'unversioned', 'external')
599 if (item not in ('', 'normal', 'unversioned', 'external')
596 or props not in ('', 'none')):
600 or props not in ('', 'none')):
597 changes.append(path)
601 changes.append(path)
598 for path in changes:
602 for path in changes:
599 for ext in externals:
603 for ext in externals:
600 if path == ext or path.startswith(ext + os.sep):
604 if path == ext or path.startswith(ext + os.sep):
601 return True, True
605 return True, True
602 return bool(changes), False
606 return bool(changes), False
603
607
604 def dirty(self, ignoreupdate=False):
608 def dirty(self, ignoreupdate=False):
605 if not self._wcchanged()[0]:
609 if not self._wcchanged()[0]:
606 if self._state[1] in self._wcrevs() or ignoreupdate:
610 if self._state[1] in self._wcrevs() or ignoreupdate:
607 return False
611 return False
608 return True
612 return True
609
613
610 def commit(self, text, user, date):
614 def commit(self, text, user, date):
611 # user and date are out of our hands since svn is centralized
615 # user and date are out of our hands since svn is centralized
612 changed, extchanged = self._wcchanged()
616 changed, extchanged = self._wcchanged()
613 if not changed:
617 if not changed:
614 return self._wcrev()
618 return self._wcrev()
615 if extchanged:
619 if extchanged:
616 # Do not try to commit externals
620 # Do not try to commit externals
617 raise util.Abort(_('cannot commit svn externals'))
621 raise util.Abort(_('cannot commit svn externals'))
618 commitinfo = self._svncommand(['commit', '-m', text])
622 commitinfo = self._svncommand(['commit', '-m', text])
619 self._ui.status(commitinfo)
623 self._ui.status(commitinfo)
620 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
624 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
621 if not newrev:
625 if not newrev:
622 raise util.Abort(commitinfo.splitlines()[-1])
626 raise util.Abort(commitinfo.splitlines()[-1])
623 newrev = newrev.groups()[0]
627 newrev = newrev.groups()[0]
624 self._ui.status(self._svncommand(['update', '-r', newrev]))
628 self._ui.status(self._svncommand(['update', '-r', newrev]))
625 return newrev
629 return newrev
626
630
627 def remove(self):
631 def remove(self):
628 if self.dirty():
632 if self.dirty():
629 self._ui.warn(_('not removing repo %s because '
633 self._ui.warn(_('not removing repo %s because '
630 'it has changes.\n' % self._path))
634 'it has changes.\n' % self._path))
631 return
635 return
632 self._ui.note(_('removing subrepo %s\n') % self._path)
636 self._ui.note(_('removing subrepo %s\n') % self._path)
633
637
634 def onerror(function, path, excinfo):
638 def onerror(function, path, excinfo):
635 if function is not os.remove:
639 if function is not os.remove:
636 raise
640 raise
637 # read-only files cannot be unlinked under Windows
641 # read-only files cannot be unlinked under Windows
638 s = os.stat(path)
642 s = os.stat(path)
639 if (s.st_mode & stat.S_IWRITE) != 0:
643 if (s.st_mode & stat.S_IWRITE) != 0:
640 raise
644 raise
641 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
645 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
642 os.remove(path)
646 os.remove(path)
643
647
644 path = self._ctx._repo.wjoin(self._path)
648 path = self._ctx._repo.wjoin(self._path)
645 shutil.rmtree(path, onerror=onerror)
649 shutil.rmtree(path, onerror=onerror)
646 try:
650 try:
647 os.removedirs(os.path.dirname(path))
651 os.removedirs(os.path.dirname(path))
648 except OSError:
652 except OSError:
649 pass
653 pass
650
654
651 def get(self, state, overwrite=False):
655 def get(self, state, overwrite=False):
652 if overwrite:
656 if overwrite:
653 self._svncommand(['revert', '--recursive'])
657 self._svncommand(['revert', '--recursive'])
654 args = ['checkout']
658 args = ['checkout']
655 if self._svnversion >= (1, 5):
659 if self._svnversion >= (1, 5):
656 args.append('--force')
660 args.append('--force')
657 args.extend([state[0], '--revision', state[1]])
661 args.extend([state[0], '--revision', state[1]])
658 status = self._svncommand(args)
662 status = self._svncommand(args)
659 if not re.search('Checked out revision [0-9]+.', status):
663 if not re.search('Checked out revision [0-9]+.', status):
660 raise util.Abort(status.splitlines()[-1])
664 raise util.Abort(status.splitlines()[-1])
661 self._ui.status(status)
665 self._ui.status(status)
662
666
663 def merge(self, state):
667 def merge(self, state):
664 old = self._state[1]
668 old = self._state[1]
665 new = state[1]
669 new = state[1]
666 if new != self._wcrev():
670 if new != self._wcrev():
667 dirty = old == self._wcrev() or self._wcchanged()[0]
671 dirty = old == self._wcrev() or self._wcchanged()[0]
668 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
672 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
669 self.get(state, False)
673 self.get(state, False)
670
674
671 def push(self, force):
675 def push(self, force):
672 # push is a no-op for SVN
676 # push is a no-op for SVN
673 return True
677 return True
674
678
675 def files(self):
679 def files(self):
676 output = self._svncommand(['list'])
680 output = self._svncommand(['list'])
677 # This works because svn forbids \n in filenames.
681 # This works because svn forbids \n in filenames.
678 return output.splitlines()
682 return output.splitlines()
679
683
680 def filedata(self, name):
684 def filedata(self, name):
681 return self._svncommand(['cat'], name)
685 return self._svncommand(['cat'], name)
682
686
683
687
684 class gitsubrepo(abstractsubrepo):
688 class gitsubrepo(abstractsubrepo):
685 def __init__(self, ctx, path, state):
689 def __init__(self, ctx, path, state):
686 # TODO add git version check.
690 # TODO add git version check.
687 self._state = state
691 self._state = state
688 self._ctx = ctx
692 self._ctx = ctx
689 self._path = path
693 self._path = path
690 self._relpath = os.path.join(reporelpath(ctx._repo), path)
694 self._relpath = os.path.join(reporelpath(ctx._repo), path)
691 self._abspath = ctx._repo.wjoin(path)
695 self._abspath = ctx._repo.wjoin(path)
692 self._subparent = ctx._repo
696 self._subparent = ctx._repo
693 self._ui = ctx._repo.ui
697 self._ui = ctx._repo.ui
694
698
695 def _gitcommand(self, commands, env=None, stream=False):
699 def _gitcommand(self, commands, env=None, stream=False):
696 return self._gitdir(commands, env=env, stream=stream)[0]
700 return self._gitdir(commands, env=env, stream=stream)[0]
697
701
698 def _gitdir(self, commands, env=None, stream=False):
702 def _gitdir(self, commands, env=None, stream=False):
699 return self._gitnodir(commands, env=env, stream=stream,
703 return self._gitnodir(commands, env=env, stream=stream,
700 cwd=self._abspath)
704 cwd=self._abspath)
701
705
702 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
706 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
703 """Calls the git command
707 """Calls the git command
704
708
705 The methods tries to call the git command. versions previor to 1.6.0
709 The methods tries to call the git command. versions previor to 1.6.0
706 are not supported and very probably fail.
710 are not supported and very probably fail.
707 """
711 """
708 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
712 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
709 # unless ui.quiet is set, print git's stderr,
713 # unless ui.quiet is set, print git's stderr,
710 # which is mostly progress and useful info
714 # which is mostly progress and useful info
711 errpipe = None
715 errpipe = None
712 if self._ui.quiet:
716 if self._ui.quiet:
713 errpipe = open(os.devnull, 'w')
717 errpipe = open(os.devnull, 'w')
714 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
718 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
715 close_fds=util.closefds,
719 close_fds=util.closefds,
716 stdout=subprocess.PIPE, stderr=errpipe)
720 stdout=subprocess.PIPE, stderr=errpipe)
717 if stream:
721 if stream:
718 return p.stdout, None
722 return p.stdout, None
719
723
720 retdata = p.stdout.read().strip()
724 retdata = p.stdout.read().strip()
721 # wait for the child to exit to avoid race condition.
725 # wait for the child to exit to avoid race condition.
722 p.wait()
726 p.wait()
723
727
724 if p.returncode != 0 and p.returncode != 1:
728 if p.returncode != 0 and p.returncode != 1:
725 # there are certain error codes that are ok
729 # there are certain error codes that are ok
726 command = commands[0]
730 command = commands[0]
727 if command in ('cat-file', 'symbolic-ref'):
731 if command in ('cat-file', 'symbolic-ref'):
728 return retdata, p.returncode
732 return retdata, p.returncode
729 # for all others, abort
733 # for all others, abort
730 raise util.Abort('git %s error %d in %s' %
734 raise util.Abort('git %s error %d in %s' %
731 (command, p.returncode, self._relpath))
735 (command, p.returncode, self._relpath))
732
736
733 return retdata, p.returncode
737 return retdata, p.returncode
734
738
735 def _gitmissing(self):
739 def _gitmissing(self):
736 return not os.path.exists(os.path.join(self._abspath, '.git'))
740 return not os.path.exists(os.path.join(self._abspath, '.git'))
737
741
738 def _gitstate(self):
742 def _gitstate(self):
739 return self._gitcommand(['rev-parse', 'HEAD'])
743 return self._gitcommand(['rev-parse', 'HEAD'])
740
744
741 def _gitcurrentbranch(self):
745 def _gitcurrentbranch(self):
742 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
746 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
743 if err:
747 if err:
744 current = None
748 current = None
745 return current
749 return current
746
750
747 def _githavelocally(self, revision):
751 def _githavelocally(self, revision):
748 out, code = self._gitdir(['cat-file', '-e', revision])
752 out, code = self._gitdir(['cat-file', '-e', revision])
749 return code == 0
753 return code == 0
750
754
751 def _gitisancestor(self, r1, r2):
755 def _gitisancestor(self, r1, r2):
752 base = self._gitcommand(['merge-base', r1, r2])
756 base = self._gitcommand(['merge-base', r1, r2])
753 return base == r1
757 return base == r1
754
758
755 def _gitbranchmap(self):
759 def _gitbranchmap(self):
756 '''returns 2 things:
760 '''returns 2 things:
757 a map from git branch to revision
761 a map from git branch to revision
758 a map from revision to branches'''
762 a map from revision to branches'''
759 branch2rev = {}
763 branch2rev = {}
760 rev2branch = {}
764 rev2branch = {}
761
765
762 out = self._gitcommand(['for-each-ref', '--format',
766 out = self._gitcommand(['for-each-ref', '--format',
763 '%(objectname) %(refname)'])
767 '%(objectname) %(refname)'])
764 for line in out.split('\n'):
768 for line in out.split('\n'):
765 revision, ref = line.split(' ')
769 revision, ref = line.split(' ')
766 if (not ref.startswith('refs/heads/') and
770 if (not ref.startswith('refs/heads/') and
767 not ref.startswith('refs/remotes/')):
771 not ref.startswith('refs/remotes/')):
768 continue
772 continue
769 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
773 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
770 continue # ignore remote/HEAD redirects
774 continue # ignore remote/HEAD redirects
771 branch2rev[ref] = revision
775 branch2rev[ref] = revision
772 rev2branch.setdefault(revision, []).append(ref)
776 rev2branch.setdefault(revision, []).append(ref)
773 return branch2rev, rev2branch
777 return branch2rev, rev2branch
774
778
775 def _gittracking(self, branches):
779 def _gittracking(self, branches):
776 'return map of remote branch to local tracking branch'
780 'return map of remote branch to local tracking branch'
777 # assumes no more than one local tracking branch for each remote
781 # assumes no more than one local tracking branch for each remote
778 tracking = {}
782 tracking = {}
779 for b in branches:
783 for b in branches:
780 if b.startswith('refs/remotes/'):
784 if b.startswith('refs/remotes/'):
781 continue
785 continue
782 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
786 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
783 if remote:
787 if remote:
784 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
788 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
785 tracking['refs/remotes/%s/%s' %
789 tracking['refs/remotes/%s/%s' %
786 (remote, ref.split('/', 2)[2])] = b
790 (remote, ref.split('/', 2)[2])] = b
787 return tracking
791 return tracking
788
792
789 def _abssource(self, source):
793 def _abssource(self, source):
790 if '://' not in source:
794 if '://' not in source:
791 # recognize the scp syntax as an absolute source
795 # recognize the scp syntax as an absolute source
792 colon = source.find(':')
796 colon = source.find(':')
793 if colon != -1 and '/' not in source[:colon]:
797 if colon != -1 and '/' not in source[:colon]:
794 return source
798 return source
795 self._subsource = source
799 self._subsource = source
796 return _abssource(self)
800 return _abssource(self)
797
801
798 def _fetch(self, source, revision):
802 def _fetch(self, source, revision):
799 if self._gitmissing():
803 if self._gitmissing():
800 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
804 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
801 self._gitnodir(['clone', self._abssource(source), self._abspath])
805 self._gitnodir(['clone', self._abssource(source), self._abspath])
802 if self._githavelocally(revision):
806 if self._githavelocally(revision):
803 return
807 return
804 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
808 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
805 # try only origin: the originally cloned repo
809 # try only origin: the originally cloned repo
806 self._gitcommand(['fetch'])
810 self._gitcommand(['fetch'])
807 if not self._githavelocally(revision):
811 if not self._githavelocally(revision):
808 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
812 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
809 (revision, self._relpath))
813 (revision, self._relpath))
810
814
811 def dirty(self, ignoreupdate=False):
815 def dirty(self, ignoreupdate=False):
812 if self._gitmissing():
816 if self._gitmissing():
813 return self._state[1] != ''
817 return self._state[1] != ''
814 if not ignoreupdate and self._state[1] != self._gitstate():
818 if not ignoreupdate and self._state[1] != self._gitstate():
815 # different version checked out
819 # different version checked out
816 return True
820 return True
817 # check for staged changes or modified files; ignore untracked files
821 # check for staged changes or modified files; ignore untracked files
818 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
822 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
819 return code == 1
823 return code == 1
820
824
821 def get(self, state, overwrite=False):
825 def get(self, state, overwrite=False):
822 source, revision, kind = state
826 source, revision, kind = state
823 if not revision:
827 if not revision:
824 self.remove()
828 self.remove()
825 return
829 return
826 self._fetch(source, revision)
830 self._fetch(source, revision)
827 # if the repo was set to be bare, unbare it
831 # if the repo was set to be bare, unbare it
828 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
832 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
829 self._gitcommand(['config', 'core.bare', 'false'])
833 self._gitcommand(['config', 'core.bare', 'false'])
830 if self._gitstate() == revision:
834 if self._gitstate() == revision:
831 self._gitcommand(['reset', '--hard', 'HEAD'])
835 self._gitcommand(['reset', '--hard', 'HEAD'])
832 return
836 return
833 elif self._gitstate() == revision:
837 elif self._gitstate() == revision:
834 if overwrite:
838 if overwrite:
835 # first reset the index to unmark new files for commit, because
839 # first reset the index to unmark new files for commit, because
836 # reset --hard will otherwise throw away files added for commit,
840 # reset --hard will otherwise throw away files added for commit,
837 # not just unmark them.
841 # not just unmark them.
838 self._gitcommand(['reset', 'HEAD'])
842 self._gitcommand(['reset', 'HEAD'])
839 self._gitcommand(['reset', '--hard', 'HEAD'])
843 self._gitcommand(['reset', '--hard', 'HEAD'])
840 return
844 return
841 branch2rev, rev2branch = self._gitbranchmap()
845 branch2rev, rev2branch = self._gitbranchmap()
842
846
843 def checkout(args):
847 def checkout(args):
844 cmd = ['checkout']
848 cmd = ['checkout']
845 if overwrite:
849 if overwrite:
846 # first reset the index to unmark new files for commit, because
850 # first reset the index to unmark new files for commit, because
847 # the -f option will otherwise throw away files added for
851 # the -f option will otherwise throw away files added for
848 # commit, not just unmark them.
852 # commit, not just unmark them.
849 self._gitcommand(['reset', 'HEAD'])
853 self._gitcommand(['reset', 'HEAD'])
850 cmd.append('-f')
854 cmd.append('-f')
851 self._gitcommand(cmd + args)
855 self._gitcommand(cmd + args)
852
856
853 def rawcheckout():
857 def rawcheckout():
854 # no branch to checkout, check it out with no branch
858 # no branch to checkout, check it out with no branch
855 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
859 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
856 self._relpath)
860 self._relpath)
857 self._ui.warn(_('check out a git branch if you intend '
861 self._ui.warn(_('check out a git branch if you intend '
858 'to make changes\n'))
862 'to make changes\n'))
859 checkout(['-q', revision])
863 checkout(['-q', revision])
860
864
861 if revision not in rev2branch:
865 if revision not in rev2branch:
862 rawcheckout()
866 rawcheckout()
863 return
867 return
864 branches = rev2branch[revision]
868 branches = rev2branch[revision]
865 firstlocalbranch = None
869 firstlocalbranch = None
866 for b in branches:
870 for b in branches:
867 if b == 'refs/heads/master':
871 if b == 'refs/heads/master':
868 # master trumps all other branches
872 # master trumps all other branches
869 checkout(['refs/heads/master'])
873 checkout(['refs/heads/master'])
870 return
874 return
871 if not firstlocalbranch and not b.startswith('refs/remotes/'):
875 if not firstlocalbranch and not b.startswith('refs/remotes/'):
872 firstlocalbranch = b
876 firstlocalbranch = b
873 if firstlocalbranch:
877 if firstlocalbranch:
874 checkout([firstlocalbranch])
878 checkout([firstlocalbranch])
875 return
879 return
876
880
877 tracking = self._gittracking(branch2rev.keys())
881 tracking = self._gittracking(branch2rev.keys())
878 # choose a remote branch already tracked if possible
882 # choose a remote branch already tracked if possible
879 remote = branches[0]
883 remote = branches[0]
880 if remote not in tracking:
884 if remote not in tracking:
881 for b in branches:
885 for b in branches:
882 if b in tracking:
886 if b in tracking:
883 remote = b
887 remote = b
884 break
888 break
885
889
886 if remote not in tracking:
890 if remote not in tracking:
887 # create a new local tracking branch
891 # create a new local tracking branch
888 local = remote.split('/', 2)[2]
892 local = remote.split('/', 2)[2]
889 checkout(['-b', local, remote])
893 checkout(['-b', local, remote])
890 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
894 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
891 # When updating to a tracked remote branch,
895 # When updating to a tracked remote branch,
892 # if the local tracking branch is downstream of it,
896 # if the local tracking branch is downstream of it,
893 # a normal `git pull` would have performed a "fast-forward merge"
897 # a normal `git pull` would have performed a "fast-forward merge"
894 # which is equivalent to updating the local branch to the remote.
898 # which is equivalent to updating the local branch to the remote.
895 # Since we are only looking at branching at update, we need to
899 # Since we are only looking at branching at update, we need to
896 # detect this situation and perform this action lazily.
900 # detect this situation and perform this action lazily.
897 if tracking[remote] != self._gitcurrentbranch():
901 if tracking[remote] != self._gitcurrentbranch():
898 checkout([tracking[remote]])
902 checkout([tracking[remote]])
899 self._gitcommand(['merge', '--ff', remote])
903 self._gitcommand(['merge', '--ff', remote])
900 else:
904 else:
901 # a real merge would be required, just checkout the revision
905 # a real merge would be required, just checkout the revision
902 rawcheckout()
906 rawcheckout()
903
907
904 def commit(self, text, user, date):
908 def commit(self, text, user, date):
905 if self._gitmissing():
909 if self._gitmissing():
906 raise util.Abort(_("subrepo %s is missing") % self._relpath)
910 raise util.Abort(_("subrepo %s is missing") % self._relpath)
907 cmd = ['commit', '-a', '-m', text]
911 cmd = ['commit', '-a', '-m', text]
908 env = os.environ.copy()
912 env = os.environ.copy()
909 if user:
913 if user:
910 cmd += ['--author', user]
914 cmd += ['--author', user]
911 if date:
915 if date:
912 # git's date parser silently ignores when seconds < 1e9
916 # git's date parser silently ignores when seconds < 1e9
913 # convert to ISO8601
917 # convert to ISO8601
914 env['GIT_AUTHOR_DATE'] = util.datestr(date,
918 env['GIT_AUTHOR_DATE'] = util.datestr(date,
915 '%Y-%m-%dT%H:%M:%S %1%2')
919 '%Y-%m-%dT%H:%M:%S %1%2')
916 self._gitcommand(cmd, env=env)
920 self._gitcommand(cmd, env=env)
917 # make sure commit works otherwise HEAD might not exist under certain
921 # make sure commit works otherwise HEAD might not exist under certain
918 # circumstances
922 # circumstances
919 return self._gitstate()
923 return self._gitstate()
920
924
921 def merge(self, state):
925 def merge(self, state):
922 source, revision, kind = state
926 source, revision, kind = state
923 self._fetch(source, revision)
927 self._fetch(source, revision)
924 base = self._gitcommand(['merge-base', revision, self._state[1]])
928 base = self._gitcommand(['merge-base', revision, self._state[1]])
925 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
929 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
926
930
927 def mergefunc():
931 def mergefunc():
928 if base == revision:
932 if base == revision:
929 self.get(state) # fast forward merge
933 self.get(state) # fast forward merge
930 elif base != self._state[1]:
934 elif base != self._state[1]:
931 self._gitcommand(['merge', '--no-commit', revision])
935 self._gitcommand(['merge', '--no-commit', revision])
932
936
933 if self.dirty():
937 if self.dirty():
934 if self._gitstate() != revision:
938 if self._gitstate() != revision:
935 dirty = self._gitstate() == self._state[1] or code != 0
939 dirty = self._gitstate() == self._state[1] or code != 0
936 if _updateprompt(self._ui, self, dirty,
940 if _updateprompt(self._ui, self, dirty,
937 self._state[1][:7], revision[:7]):
941 self._state[1][:7], revision[:7]):
938 mergefunc()
942 mergefunc()
939 else:
943 else:
940 mergefunc()
944 mergefunc()
941
945
942 def push(self, force):
946 def push(self, force):
943 if not self._state[1]:
947 if not self._state[1]:
944 return True
948 return True
945 if self._gitmissing():
949 if self._gitmissing():
946 raise util.Abort(_("subrepo %s is missing") % self._relpath)
950 raise util.Abort(_("subrepo %s is missing") % self._relpath)
947 # if a branch in origin contains the revision, nothing to do
951 # if a branch in origin contains the revision, nothing to do
948 branch2rev, rev2branch = self._gitbranchmap()
952 branch2rev, rev2branch = self._gitbranchmap()
949 if self._state[1] in rev2branch:
953 if self._state[1] in rev2branch:
950 for b in rev2branch[self._state[1]]:
954 for b in rev2branch[self._state[1]]:
951 if b.startswith('refs/remotes/origin/'):
955 if b.startswith('refs/remotes/origin/'):
952 return True
956 return True
953 for b, revision in branch2rev.iteritems():
957 for b, revision in branch2rev.iteritems():
954 if b.startswith('refs/remotes/origin/'):
958 if b.startswith('refs/remotes/origin/'):
955 if self._gitisancestor(self._state[1], revision):
959 if self._gitisancestor(self._state[1], revision):
956 return True
960 return True
957 # otherwise, try to push the currently checked out branch
961 # otherwise, try to push the currently checked out branch
958 cmd = ['push']
962 cmd = ['push']
959 if force:
963 if force:
960 cmd.append('--force')
964 cmd.append('--force')
961
965
962 current = self._gitcurrentbranch()
966 current = self._gitcurrentbranch()
963 if current:
967 if current:
964 # determine if the current branch is even useful
968 # determine if the current branch is even useful
965 if not self._gitisancestor(self._state[1], current):
969 if not self._gitisancestor(self._state[1], current):
966 self._ui.warn(_('unrelated git branch checked out '
970 self._ui.warn(_('unrelated git branch checked out '
967 'in subrepo %s\n') % self._relpath)
971 'in subrepo %s\n') % self._relpath)
968 return False
972 return False
969 self._ui.status(_('pushing branch %s of subrepo %s\n') %
973 self._ui.status(_('pushing branch %s of subrepo %s\n') %
970 (current.split('/', 2)[2], self._relpath))
974 (current.split('/', 2)[2], self._relpath))
971 self._gitcommand(cmd + ['origin', current])
975 self._gitcommand(cmd + ['origin', current])
972 return True
976 return True
973 else:
977 else:
974 self._ui.warn(_('no branch checked out in subrepo %s\n'
978 self._ui.warn(_('no branch checked out in subrepo %s\n'
975 'cannot push revision %s') %
979 'cannot push revision %s') %
976 (self._relpath, self._state[1]))
980 (self._relpath, self._state[1]))
977 return False
981 return False
978
982
979 def remove(self):
983 def remove(self):
980 if self._gitmissing():
984 if self._gitmissing():
981 return
985 return
982 if self.dirty():
986 if self.dirty():
983 self._ui.warn(_('not removing repo %s because '
987 self._ui.warn(_('not removing repo %s because '
984 'it has changes.\n') % self._relpath)
988 'it has changes.\n') % self._relpath)
985 return
989 return
986 # we can't fully delete the repository as it may contain
990 # we can't fully delete the repository as it may contain
987 # local-only history
991 # local-only history
988 self._ui.note(_('removing subrepo %s\n') % self._relpath)
992 self._ui.note(_('removing subrepo %s\n') % self._relpath)
989 self._gitcommand(['config', 'core.bare', 'true'])
993 self._gitcommand(['config', 'core.bare', 'true'])
990 for f in os.listdir(self._abspath):
994 for f in os.listdir(self._abspath):
991 if f == '.git':
995 if f == '.git':
992 continue
996 continue
993 path = os.path.join(self._abspath, f)
997 path = os.path.join(self._abspath, f)
994 if os.path.isdir(path) and not os.path.islink(path):
998 if os.path.isdir(path) and not os.path.islink(path):
995 shutil.rmtree(path)
999 shutil.rmtree(path)
996 else:
1000 else:
997 os.remove(path)
1001 os.remove(path)
998
1002
999 def archive(self, ui, archiver, prefix):
1003 def archive(self, ui, archiver, prefix):
1000 source, revision = self._state
1004 source, revision = self._state
1001 if not revision:
1005 if not revision:
1002 return
1006 return
1003 self._fetch(source, revision)
1007 self._fetch(source, revision)
1004
1008
1005 # Parse git's native archive command.
1009 # Parse git's native archive command.
1006 # This should be much faster than manually traversing the trees
1010 # This should be much faster than manually traversing the trees
1007 # and objects with many subprocess calls.
1011 # and objects with many subprocess calls.
1008 tarstream = self._gitcommand(['archive', revision], stream=True)
1012 tarstream = self._gitcommand(['archive', revision], stream=True)
1009 tar = tarfile.open(fileobj=tarstream, mode='r|')
1013 tar = tarfile.open(fileobj=tarstream, mode='r|')
1010 relpath = subrelpath(self)
1014 relpath = subrelpath(self)
1011 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1015 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1012 for i, info in enumerate(tar):
1016 for i, info in enumerate(tar):
1013 if info.isdir():
1017 if info.isdir():
1014 continue
1018 continue
1015 if info.issym():
1019 if info.issym():
1016 data = info.linkname
1020 data = info.linkname
1017 else:
1021 else:
1018 data = tar.extractfile(info).read()
1022 data = tar.extractfile(info).read()
1019 archiver.addfile(os.path.join(prefix, self._path, info.name),
1023 archiver.addfile(os.path.join(prefix, self._path, info.name),
1020 info.mode, info.issym(), data)
1024 info.mode, info.issym(), data)
1021 ui.progress(_('archiving (%s)') % relpath, i + 1,
1025 ui.progress(_('archiving (%s)') % relpath, i + 1,
1022 unit=_('files'))
1026 unit=_('files'))
1023 ui.progress(_('archiving (%s)') % relpath, None)
1027 ui.progress(_('archiving (%s)') % relpath, None)
1024
1028
1025
1029
1026 def status(self, rev2, **opts):
1030 def status(self, rev2, **opts):
1027 rev1 = self._state[1]
1031 rev1 = self._state[1]
1028 if self._gitmissing() or not rev1:
1032 if self._gitmissing() or not rev1:
1029 # if the repo is missing, return no results
1033 # if the repo is missing, return no results
1030 return [], [], [], [], [], [], []
1034 return [], [], [], [], [], [], []
1031 modified, added, removed = [], [], []
1035 modified, added, removed = [], [], []
1032 if rev2:
1036 if rev2:
1033 command = ['diff-tree', rev1, rev2]
1037 command = ['diff-tree', rev1, rev2]
1034 else:
1038 else:
1035 command = ['diff-index', rev1]
1039 command = ['diff-index', rev1]
1036 out = self._gitcommand(command)
1040 out = self._gitcommand(command)
1037 for line in out.split('\n'):
1041 for line in out.split('\n'):
1038 tab = line.find('\t')
1042 tab = line.find('\t')
1039 if tab == -1:
1043 if tab == -1:
1040 continue
1044 continue
1041 status, f = line[tab - 1], line[tab + 1:]
1045 status, f = line[tab - 1], line[tab + 1:]
1042 if status == 'M':
1046 if status == 'M':
1043 modified.append(f)
1047 modified.append(f)
1044 elif status == 'A':
1048 elif status == 'A':
1045 added.append(f)
1049 added.append(f)
1046 elif status == 'D':
1050 elif status == 'D':
1047 removed.append(f)
1051 removed.append(f)
1048
1052
1049 deleted = unknown = ignored = clean = []
1053 deleted = unknown = ignored = clean = []
1050 return modified, added, removed, deleted, unknown, ignored, clean
1054 return modified, added, removed, deleted, unknown, ignored, clean
1051
1055
1052 types = {
1056 types = {
1053 'hg': hgsubrepo,
1057 'hg': hgsubrepo,
1054 'svn': svnsubrepo,
1058 'svn': svnsubrepo,
1055 'git': gitsubrepo,
1059 'git': gitsubrepo,
1056 }
1060 }
General Comments 0
You need to be logged in to leave comments. Login now