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