##// END OF EJS Templates
subrepo: implement "storeclean" method for mercurial subrepos...
Angel Ezquerra -
r18939:aab0c14c default
parent child Browse files
Show More
@@ -1,1354 +1,1429 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, 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, match as matchmod
11 import config, scmutil, util, node, error, cmdutil, bookmarks, match as matchmod
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
17
18 def _getstorehashcachename(remotepath):
18 def _getstorehashcachename(remotepath):
19 '''get a unique filename for the store hash cache of a remote repository'''
19 '''get a unique filename for the store hash cache of a remote repository'''
20 return util.sha1(remotepath).hexdigest()[0:12]
20 return util.sha1(remotepath).hexdigest()[0:12]
21
21
22 def _calcfilehash(filename):
22 def _calcfilehash(filename):
23 data = ''
23 data = ''
24 if os.path.exists(filename):
24 if os.path.exists(filename):
25 fd = open(filename)
25 fd = open(filename)
26 data = fd.read()
26 data = fd.read()
27 fd.close()
27 fd.close()
28 return util.sha1(data).hexdigest()
28 return util.sha1(data).hexdigest()
29
29
30 class SubrepoAbort(error.Abort):
30 class SubrepoAbort(error.Abort):
31 """Exception class used to avoid handling a subrepo error more than once"""
31 """Exception class used to avoid handling a subrepo error more than once"""
32 def __init__(self, *args, **kw):
32 def __init__(self, *args, **kw):
33 error.Abort.__init__(self, *args, **kw)
33 error.Abort.__init__(self, *args, **kw)
34 self.subrepo = kw.get('subrepo')
34 self.subrepo = kw.get('subrepo')
35
35
36 def annotatesubrepoerror(func):
36 def annotatesubrepoerror(func):
37 def decoratedmethod(self, *args, **kargs):
37 def decoratedmethod(self, *args, **kargs):
38 try:
38 try:
39 res = func(self, *args, **kargs)
39 res = func(self, *args, **kargs)
40 except SubrepoAbort, ex:
40 except SubrepoAbort, ex:
41 # This exception has already been handled
41 # This exception has already been handled
42 raise ex
42 raise ex
43 except error.Abort, ex:
43 except error.Abort, ex:
44 subrepo = subrelpath(self)
44 subrepo = subrelpath(self)
45 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
45 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
46 # avoid handling this exception by raising a SubrepoAbort exception
46 # avoid handling this exception by raising a SubrepoAbort exception
47 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo)
47 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo)
48 return res
48 return res
49 return decoratedmethod
49 return decoratedmethod
50
50
51 def state(ctx, ui):
51 def state(ctx, ui):
52 """return a state dict, mapping subrepo paths configured in .hgsub
52 """return a state dict, mapping subrepo paths configured in .hgsub
53 to tuple: (source from .hgsub, revision from .hgsubstate, kind
53 to tuple: (source from .hgsub, revision from .hgsubstate, kind
54 (key in types dict))
54 (key in types dict))
55 """
55 """
56 p = config.config()
56 p = config.config()
57 def read(f, sections=None, remap=None):
57 def read(f, sections=None, remap=None):
58 if f in ctx:
58 if f in ctx:
59 try:
59 try:
60 data = ctx[f].data()
60 data = ctx[f].data()
61 except IOError, err:
61 except IOError, err:
62 if err.errno != errno.ENOENT:
62 if err.errno != errno.ENOENT:
63 raise
63 raise
64 # handle missing subrepo spec files as removed
64 # handle missing subrepo spec files as removed
65 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
65 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
66 return
66 return
67 p.parse(f, data, sections, remap, read)
67 p.parse(f, data, sections, remap, read)
68 else:
68 else:
69 raise util.Abort(_("subrepo spec file %s not found") % f)
69 raise util.Abort(_("subrepo spec file %s not found") % f)
70
70
71 if '.hgsub' in ctx:
71 if '.hgsub' in ctx:
72 read('.hgsub')
72 read('.hgsub')
73
73
74 for path, src in ui.configitems('subpaths'):
74 for path, src in ui.configitems('subpaths'):
75 p.set('subpaths', path, src, ui.configsource('subpaths', path))
75 p.set('subpaths', path, src, ui.configsource('subpaths', path))
76
76
77 rev = {}
77 rev = {}
78 if '.hgsubstate' in ctx:
78 if '.hgsubstate' in ctx:
79 try:
79 try:
80 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
80 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
81 l = l.lstrip()
81 l = l.lstrip()
82 if not l:
82 if not l:
83 continue
83 continue
84 try:
84 try:
85 revision, path = l.split(" ", 1)
85 revision, path = l.split(" ", 1)
86 except ValueError:
86 except ValueError:
87 raise util.Abort(_("invalid subrepository revision "
87 raise util.Abort(_("invalid subrepository revision "
88 "specifier in .hgsubstate line %d")
88 "specifier in .hgsubstate line %d")
89 % (i + 1))
89 % (i + 1))
90 rev[path] = revision
90 rev[path] = revision
91 except IOError, err:
91 except IOError, err:
92 if err.errno != errno.ENOENT:
92 if err.errno != errno.ENOENT:
93 raise
93 raise
94
94
95 def remap(src):
95 def remap(src):
96 for pattern, repl in p.items('subpaths'):
96 for pattern, repl in p.items('subpaths'):
97 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
97 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
98 # does a string decode.
98 # does a string decode.
99 repl = repl.encode('string-escape')
99 repl = repl.encode('string-escape')
100 # However, we still want to allow back references to go
100 # However, we still want to allow back references to go
101 # through unharmed, so we turn r'\\1' into r'\1'. Again,
101 # through unharmed, so we turn r'\\1' into r'\1'. Again,
102 # extra escapes are needed because re.sub string decodes.
102 # extra escapes are needed because re.sub string decodes.
103 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
103 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
104 try:
104 try:
105 src = re.sub(pattern, repl, src, 1)
105 src = re.sub(pattern, repl, src, 1)
106 except re.error, e:
106 except re.error, e:
107 raise util.Abort(_("bad subrepository pattern in %s: %s")
107 raise util.Abort(_("bad subrepository pattern in %s: %s")
108 % (p.source('subpaths', pattern), e))
108 % (p.source('subpaths', pattern), e))
109 return src
109 return src
110
110
111 state = {}
111 state = {}
112 for path, src in p[''].items():
112 for path, src in p[''].items():
113 kind = 'hg'
113 kind = 'hg'
114 if src.startswith('['):
114 if src.startswith('['):
115 if ']' not in src:
115 if ']' not in src:
116 raise util.Abort(_('missing ] in subrepo source'))
116 raise util.Abort(_('missing ] in subrepo source'))
117 kind, src = src.split(']', 1)
117 kind, src = src.split(']', 1)
118 kind = kind[1:]
118 kind = kind[1:]
119 src = src.lstrip() # strip any extra whitespace after ']'
119 src = src.lstrip() # strip any extra whitespace after ']'
120
120
121 if not util.url(src).isabs():
121 if not util.url(src).isabs():
122 parent = _abssource(ctx._repo, abort=False)
122 parent = _abssource(ctx._repo, abort=False)
123 if parent:
123 if parent:
124 parent = util.url(parent)
124 parent = util.url(parent)
125 parent.path = posixpath.join(parent.path or '', src)
125 parent.path = posixpath.join(parent.path or '', src)
126 parent.path = posixpath.normpath(parent.path)
126 parent.path = posixpath.normpath(parent.path)
127 joined = str(parent)
127 joined = str(parent)
128 # Remap the full joined path and use it if it changes,
128 # Remap the full joined path and use it if it changes,
129 # else remap the original source.
129 # else remap the original source.
130 remapped = remap(joined)
130 remapped = remap(joined)
131 if remapped == joined:
131 if remapped == joined:
132 src = remap(src)
132 src = remap(src)
133 else:
133 else:
134 src = remapped
134 src = remapped
135
135
136 src = remap(src)
136 src = remap(src)
137 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
137 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
138
138
139 return state
139 return state
140
140
141 def writestate(repo, state):
141 def writestate(repo, state):
142 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
142 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
143 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
143 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
144 repo.wwrite('.hgsubstate', ''.join(lines), '')
144 repo.wwrite('.hgsubstate', ''.join(lines), '')
145
145
146 def submerge(repo, wctx, mctx, actx, overwrite):
146 def submerge(repo, wctx, mctx, actx, overwrite):
147 """delegated from merge.applyupdates: merging of .hgsubstate file
147 """delegated from merge.applyupdates: merging of .hgsubstate file
148 in working context, merging context and ancestor context"""
148 in working context, merging context and ancestor context"""
149 if mctx == actx: # backwards?
149 if mctx == actx: # backwards?
150 actx = wctx.p1()
150 actx = wctx.p1()
151 s1 = wctx.substate
151 s1 = wctx.substate
152 s2 = mctx.substate
152 s2 = mctx.substate
153 sa = actx.substate
153 sa = actx.substate
154 sm = {}
154 sm = {}
155
155
156 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
156 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
157
157
158 def debug(s, msg, r=""):
158 def debug(s, msg, r=""):
159 if r:
159 if r:
160 r = "%s:%s:%s" % r
160 r = "%s:%s:%s" % r
161 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
161 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
162
162
163 for s, l in sorted(s1.iteritems()):
163 for s, l in sorted(s1.iteritems()):
164 a = sa.get(s, nullstate)
164 a = sa.get(s, nullstate)
165 ld = l # local state with possible dirty flag for compares
165 ld = l # local state with possible dirty flag for compares
166 if wctx.sub(s).dirty():
166 if wctx.sub(s).dirty():
167 ld = (l[0], l[1] + "+")
167 ld = (l[0], l[1] + "+")
168 if wctx == actx: # overwrite
168 if wctx == actx: # overwrite
169 a = ld
169 a = ld
170
170
171 if s in s2:
171 if s in s2:
172 r = s2[s]
172 r = s2[s]
173 if ld == r or r == a: # no change or local is newer
173 if ld == r or r == a: # no change or local is newer
174 sm[s] = l
174 sm[s] = l
175 continue
175 continue
176 elif ld == a: # other side changed
176 elif ld == a: # other side changed
177 debug(s, "other changed, get", r)
177 debug(s, "other changed, get", r)
178 wctx.sub(s).get(r, overwrite)
178 wctx.sub(s).get(r, overwrite)
179 sm[s] = r
179 sm[s] = r
180 elif ld[0] != r[0]: # sources differ
180 elif ld[0] != r[0]: # sources differ
181 if repo.ui.promptchoice(
181 if repo.ui.promptchoice(
182 _(' subrepository sources for %s differ\n'
182 _(' subrepository sources for %s differ\n'
183 'use (l)ocal source (%s) or (r)emote source (%s)?')
183 'use (l)ocal source (%s) or (r)emote source (%s)?')
184 % (s, l[0], r[0]),
184 % (s, l[0], r[0]),
185 (_('&Local'), _('&Remote')), 0):
185 (_('&Local'), _('&Remote')), 0):
186 debug(s, "prompt changed, get", r)
186 debug(s, "prompt changed, get", r)
187 wctx.sub(s).get(r, overwrite)
187 wctx.sub(s).get(r, overwrite)
188 sm[s] = r
188 sm[s] = r
189 elif ld[1] == a[1]: # local side is unchanged
189 elif ld[1] == a[1]: # local side is unchanged
190 debug(s, "other side changed, get", r)
190 debug(s, "other side changed, get", r)
191 wctx.sub(s).get(r, overwrite)
191 wctx.sub(s).get(r, overwrite)
192 sm[s] = r
192 sm[s] = r
193 else:
193 else:
194 debug(s, "both sides changed, merge with", r)
194 debug(s, "both sides changed, merge with", r)
195 wctx.sub(s).merge(r)
195 wctx.sub(s).merge(r)
196 sm[s] = l
196 sm[s] = l
197 elif ld == a: # remote removed, local unchanged
197 elif ld == a: # remote removed, local unchanged
198 debug(s, "remote removed, remove")
198 debug(s, "remote removed, remove")
199 wctx.sub(s).remove()
199 wctx.sub(s).remove()
200 elif a == nullstate: # not present in remote or ancestor
200 elif a == nullstate: # not present in remote or ancestor
201 debug(s, "local added, keep")
201 debug(s, "local added, keep")
202 sm[s] = l
202 sm[s] = l
203 continue
203 continue
204 else:
204 else:
205 if repo.ui.promptchoice(
205 if repo.ui.promptchoice(
206 _(' local changed subrepository %s which remote removed\n'
206 _(' local changed subrepository %s which remote removed\n'
207 'use (c)hanged version or (d)elete?') % s,
207 'use (c)hanged version or (d)elete?') % s,
208 (_('&Changed'), _('&Delete')), 0):
208 (_('&Changed'), _('&Delete')), 0):
209 debug(s, "prompt remove")
209 debug(s, "prompt remove")
210 wctx.sub(s).remove()
210 wctx.sub(s).remove()
211
211
212 for s, r in sorted(s2.items()):
212 for s, r in sorted(s2.items()):
213 if s in s1:
213 if s in s1:
214 continue
214 continue
215 elif s not in sa:
215 elif s not in sa:
216 debug(s, "remote added, get", r)
216 debug(s, "remote added, get", r)
217 mctx.sub(s).get(r)
217 mctx.sub(s).get(r)
218 sm[s] = r
218 sm[s] = r
219 elif r != sa[s]:
219 elif r != sa[s]:
220 if repo.ui.promptchoice(
220 if repo.ui.promptchoice(
221 _(' remote changed subrepository %s which local removed\n'
221 _(' remote changed subrepository %s which local removed\n'
222 'use (c)hanged version or (d)elete?') % s,
222 'use (c)hanged version or (d)elete?') % s,
223 (_('&Changed'), _('&Delete')), 0) == 0:
223 (_('&Changed'), _('&Delete')), 0) == 0:
224 debug(s, "prompt recreate", r)
224 debug(s, "prompt recreate", r)
225 wctx.sub(s).get(r)
225 wctx.sub(s).get(r)
226 sm[s] = r
226 sm[s] = r
227
227
228 # record merged .hgsubstate
228 # record merged .hgsubstate
229 writestate(repo, sm)
229 writestate(repo, sm)
230
230
231 def _updateprompt(ui, sub, dirty, local, remote):
231 def _updateprompt(ui, sub, dirty, local, remote):
232 if dirty:
232 if dirty:
233 msg = (_(' subrepository sources for %s differ\n'
233 msg = (_(' subrepository sources for %s differ\n'
234 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
234 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
235 % (subrelpath(sub), local, remote))
235 % (subrelpath(sub), local, remote))
236 else:
236 else:
237 msg = (_(' subrepository sources for %s differ (in checked out '
237 msg = (_(' subrepository sources for %s differ (in checked out '
238 'version)\n'
238 'version)\n'
239 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
239 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
240 % (subrelpath(sub), local, remote))
240 % (subrelpath(sub), local, remote))
241 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
241 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
242
242
243 def reporelpath(repo):
243 def reporelpath(repo):
244 """return path to this (sub)repo as seen from outermost repo"""
244 """return path to this (sub)repo as seen from outermost repo"""
245 parent = repo
245 parent = repo
246 while util.safehasattr(parent, '_subparent'):
246 while util.safehasattr(parent, '_subparent'):
247 parent = parent._subparent
247 parent = parent._subparent
248 p = parent.root.rstrip(os.sep)
248 p = parent.root.rstrip(os.sep)
249 return repo.root[len(p) + 1:]
249 return repo.root[len(p) + 1:]
250
250
251 def subrelpath(sub):
251 def subrelpath(sub):
252 """return path to this subrepo as seen from outermost repo"""
252 """return path to this subrepo as seen from outermost repo"""
253 if util.safehasattr(sub, '_relpath'):
253 if util.safehasattr(sub, '_relpath'):
254 return sub._relpath
254 return sub._relpath
255 if not util.safehasattr(sub, '_repo'):
255 if not util.safehasattr(sub, '_repo'):
256 return sub._path
256 return sub._path
257 return reporelpath(sub._repo)
257 return reporelpath(sub._repo)
258
258
259 def _abssource(repo, push=False, abort=True):
259 def _abssource(repo, push=False, abort=True):
260 """return pull/push path of repo - either based on parent repo .hgsub info
260 """return pull/push path of repo - either based on parent repo .hgsub info
261 or on the top repo config. Abort or return None if no source found."""
261 or on the top repo config. Abort or return None if no source found."""
262 if util.safehasattr(repo, '_subparent'):
262 if util.safehasattr(repo, '_subparent'):
263 source = util.url(repo._subsource)
263 source = util.url(repo._subsource)
264 if source.isabs():
264 if source.isabs():
265 return str(source)
265 return str(source)
266 source.path = posixpath.normpath(source.path)
266 source.path = posixpath.normpath(source.path)
267 parent = _abssource(repo._subparent, push, abort=False)
267 parent = _abssource(repo._subparent, push, abort=False)
268 if parent:
268 if parent:
269 parent = util.url(util.pconvert(parent))
269 parent = util.url(util.pconvert(parent))
270 parent.path = posixpath.join(parent.path or '', source.path)
270 parent.path = posixpath.join(parent.path or '', source.path)
271 parent.path = posixpath.normpath(parent.path)
271 parent.path = posixpath.normpath(parent.path)
272 return str(parent)
272 return str(parent)
273 else: # recursion reached top repo
273 else: # recursion reached top repo
274 if util.safehasattr(repo, '_subtoppath'):
274 if util.safehasattr(repo, '_subtoppath'):
275 return repo._subtoppath
275 return repo._subtoppath
276 if push and repo.ui.config('paths', 'default-push'):
276 if push and repo.ui.config('paths', 'default-push'):
277 return repo.ui.config('paths', 'default-push')
277 return repo.ui.config('paths', 'default-push')
278 if repo.ui.config('paths', 'default'):
278 if repo.ui.config('paths', 'default'):
279 return repo.ui.config('paths', 'default')
279 return repo.ui.config('paths', 'default')
280 if repo.sharedpath != repo.path:
280 if repo.sharedpath != repo.path:
281 # chop off the .hg component to get the default path form
281 # chop off the .hg component to get the default path form
282 return os.path.dirname(repo.sharedpath)
282 return os.path.dirname(repo.sharedpath)
283 if abort:
283 if abort:
284 raise util.Abort(_("default path for subrepository not found"))
284 raise util.Abort(_("default path for subrepository not found"))
285
285
286 def itersubrepos(ctx1, ctx2):
286 def itersubrepos(ctx1, ctx2):
287 """find subrepos in ctx1 or ctx2"""
287 """find subrepos in ctx1 or ctx2"""
288 # Create a (subpath, ctx) mapping where we prefer subpaths from
288 # Create a (subpath, ctx) mapping where we prefer subpaths from
289 # ctx1. The subpaths from ctx2 are important when the .hgsub file
289 # ctx1. The subpaths from ctx2 are important when the .hgsub file
290 # has been modified (in ctx2) but not yet committed (in ctx1).
290 # has been modified (in ctx2) but not yet committed (in ctx1).
291 subpaths = dict.fromkeys(ctx2.substate, ctx2)
291 subpaths = dict.fromkeys(ctx2.substate, ctx2)
292 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
292 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
293 for subpath, ctx in sorted(subpaths.iteritems()):
293 for subpath, ctx in sorted(subpaths.iteritems()):
294 yield subpath, ctx.sub(subpath)
294 yield subpath, ctx.sub(subpath)
295
295
296 def subrepo(ctx, path):
296 def subrepo(ctx, path):
297 """return instance of the right subrepo class for subrepo in path"""
297 """return instance of the right subrepo class for subrepo in path"""
298 # subrepo inherently violates our import layering rules
298 # subrepo inherently violates our import layering rules
299 # because it wants to make repo objects from deep inside the stack
299 # because it wants to make repo objects from deep inside the stack
300 # so we manually delay the circular imports to not break
300 # so we manually delay the circular imports to not break
301 # scripts that don't use our demand-loading
301 # scripts that don't use our demand-loading
302 global hg
302 global hg
303 import hg as h
303 import hg as h
304 hg = h
304 hg = h
305
305
306 scmutil.pathauditor(ctx._repo.root)(path)
306 scmutil.pathauditor(ctx._repo.root)(path)
307 state = ctx.substate[path]
307 state = ctx.substate[path]
308 if state[2] not in types:
308 if state[2] not in types:
309 raise util.Abort(_('unknown subrepo type %s') % state[2])
309 raise util.Abort(_('unknown subrepo type %s') % state[2])
310 return types[state[2]](ctx, path, state[:2])
310 return types[state[2]](ctx, path, state[:2])
311
311
312 # subrepo classes need to implement the following abstract class:
312 # subrepo classes need to implement the following abstract class:
313
313
314 class abstractsubrepo(object):
314 class abstractsubrepo(object):
315
315
316 def storeclean(self, path):
316 def storeclean(self, path):
317 """
317 """
318 returns true if the repository has not changed since it was last
318 returns true if the repository has not changed since it was last
319 cloned from or pushed to a given repository.
319 cloned from or pushed to a given repository.
320 """
320 """
321 return False
321 return False
322
322
323 def dirty(self, ignoreupdate=False):
323 def dirty(self, ignoreupdate=False):
324 """returns true if the dirstate of the subrepo is dirty or does not
324 """returns true if the dirstate of the subrepo is dirty or does not
325 match current stored state. If ignoreupdate is true, only check
325 match current stored state. If ignoreupdate is true, only check
326 whether the subrepo has uncommitted changes in its dirstate.
326 whether the subrepo has uncommitted changes in its dirstate.
327 """
327 """
328 raise NotImplementedError
328 raise NotImplementedError
329
329
330 def basestate(self):
330 def basestate(self):
331 """current working directory base state, disregarding .hgsubstate
331 """current working directory base state, disregarding .hgsubstate
332 state and working directory modifications"""
332 state and working directory modifications"""
333 raise NotImplementedError
333 raise NotImplementedError
334
334
335 def checknested(self, path):
335 def checknested(self, path):
336 """check if path is a subrepository within this repository"""
336 """check if path is a subrepository within this repository"""
337 return False
337 return False
338
338
339 def commit(self, text, user, date):
339 def commit(self, text, user, date):
340 """commit the current changes to the subrepo with the given
340 """commit the current changes to the subrepo with the given
341 log message. Use given user and date if possible. Return the
341 log message. Use given user and date if possible. Return the
342 new state of the subrepo.
342 new state of the subrepo.
343 """
343 """
344 raise NotImplementedError
344 raise NotImplementedError
345
345
346 def remove(self):
346 def remove(self):
347 """remove the subrepo
347 """remove the subrepo
348
348
349 (should verify the dirstate is not dirty first)
349 (should verify the dirstate is not dirty first)
350 """
350 """
351 raise NotImplementedError
351 raise NotImplementedError
352
352
353 def get(self, state, overwrite=False):
353 def get(self, state, overwrite=False):
354 """run whatever commands are needed to put the subrepo into
354 """run whatever commands are needed to put the subrepo into
355 this state
355 this state
356 """
356 """
357 raise NotImplementedError
357 raise NotImplementedError
358
358
359 def merge(self, state):
359 def merge(self, state):
360 """merge currently-saved state with the new state."""
360 """merge currently-saved state with the new state."""
361 raise NotImplementedError
361 raise NotImplementedError
362
362
363 def push(self, opts):
363 def push(self, opts):
364 """perform whatever action is analogous to 'hg push'
364 """perform whatever action is analogous to 'hg push'
365
365
366 This may be a no-op on some systems.
366 This may be a no-op on some systems.
367 """
367 """
368 raise NotImplementedError
368 raise NotImplementedError
369
369
370 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
370 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
371 return []
371 return []
372
372
373 def status(self, rev2, **opts):
373 def status(self, rev2, **opts):
374 return [], [], [], [], [], [], []
374 return [], [], [], [], [], [], []
375
375
376 def diff(self, ui, diffopts, node2, match, prefix, **opts):
376 def diff(self, ui, diffopts, node2, match, prefix, **opts):
377 pass
377 pass
378
378
379 def outgoing(self, ui, dest, opts):
379 def outgoing(self, ui, dest, opts):
380 return 1
380 return 1
381
381
382 def incoming(self, ui, source, opts):
382 def incoming(self, ui, source, opts):
383 return 1
383 return 1
384
384
385 def files(self):
385 def files(self):
386 """return filename iterator"""
386 """return filename iterator"""
387 raise NotImplementedError
387 raise NotImplementedError
388
388
389 def filedata(self, name):
389 def filedata(self, name):
390 """return file data"""
390 """return file data"""
391 raise NotImplementedError
391 raise NotImplementedError
392
392
393 def fileflags(self, name):
393 def fileflags(self, name):
394 """return file flags"""
394 """return file flags"""
395 return ''
395 return ''
396
396
397 def archive(self, ui, archiver, prefix, match=None):
397 def archive(self, ui, archiver, prefix, match=None):
398 if match is not None:
398 if match is not None:
399 files = [f for f in self.files() if match(f)]
399 files = [f for f in self.files() if match(f)]
400 else:
400 else:
401 files = self.files()
401 files = self.files()
402 total = len(files)
402 total = len(files)
403 relpath = subrelpath(self)
403 relpath = subrelpath(self)
404 ui.progress(_('archiving (%s)') % relpath, 0,
404 ui.progress(_('archiving (%s)') % relpath, 0,
405 unit=_('files'), total=total)
405 unit=_('files'), total=total)
406 for i, name in enumerate(files):
406 for i, name in enumerate(files):
407 flags = self.fileflags(name)
407 flags = self.fileflags(name)
408 mode = 'x' in flags and 0755 or 0644
408 mode = 'x' in flags and 0755 or 0644
409 symlink = 'l' in flags
409 symlink = 'l' in flags
410 archiver.addfile(os.path.join(prefix, self._path, name),
410 archiver.addfile(os.path.join(prefix, self._path, name),
411 mode, symlink, self.filedata(name))
411 mode, symlink, self.filedata(name))
412 ui.progress(_('archiving (%s)') % relpath, i + 1,
412 ui.progress(_('archiving (%s)') % relpath, i + 1,
413 unit=_('files'), total=total)
413 unit=_('files'), total=total)
414 ui.progress(_('archiving (%s)') % relpath, None)
414 ui.progress(_('archiving (%s)') % relpath, None)
415
415
416 def walk(self, match):
416 def walk(self, match):
417 '''
417 '''
418 walk recursively through the directory tree, finding all files
418 walk recursively through the directory tree, finding all files
419 matched by the match function
419 matched by the match function
420 '''
420 '''
421 pass
421 pass
422
422
423 def forget(self, ui, match, prefix):
423 def forget(self, ui, match, prefix):
424 return ([], [])
424 return ([], [])
425
425
426 def revert(self, ui, substate, *pats, **opts):
426 def revert(self, ui, substate, *pats, **opts):
427 ui.warn('%s: reverting %s subrepos is unsupported\n' \
427 ui.warn('%s: reverting %s subrepos is unsupported\n' \
428 % (substate[0], substate[2]))
428 % (substate[0], substate[2]))
429 return []
429 return []
430
430
431 class hgsubrepo(abstractsubrepo):
431 class hgsubrepo(abstractsubrepo):
432 def __init__(self, ctx, path, state):
432 def __init__(self, ctx, path, state):
433 self._path = path
433 self._path = path
434 self._state = state
434 self._state = state
435 r = ctx._repo
435 r = ctx._repo
436 root = r.wjoin(path)
436 root = r.wjoin(path)
437 create = False
437 create = False
438 if not os.path.exists(os.path.join(root, '.hg')):
438 if not os.path.exists(os.path.join(root, '.hg')):
439 create = True
439 create = True
440 util.makedirs(root)
440 util.makedirs(root)
441 self._repo = hg.repository(r.baseui, root, create=create)
441 self._repo = hg.repository(r.baseui, root, create=create)
442 for s, k in [('ui', 'commitsubrepos')]:
442 for s, k in [('ui', 'commitsubrepos')]:
443 v = r.ui.config(s, k)
443 v = r.ui.config(s, k)
444 if v:
444 if v:
445 self._repo.ui.setconfig(s, k, v)
445 self._repo.ui.setconfig(s, k, v)
446 self._repo.ui.setconfig('ui', '_usedassubrepo', 'True')
446 self._repo.ui.setconfig('ui', '_usedassubrepo', 'True')
447 self._initrepo(r, state[0], create)
447 self._initrepo(r, state[0], create)
448
448
449 def storeclean(self, path):
450 clean = True
451 lock = self._repo.lock()
452 itercache = self._calcstorehash(path)
453 try:
454 for filehash in self._readstorehashcache(path):
455 if filehash != itercache.next():
456 clean = False
457 break
458 except StopIteration:
459 # the cached and current pull states have a different size
460 clean = False
461 if clean:
462 try:
463 itercache.next()
464 # the cached and current pull states have a different size
465 clean = False
466 except StopIteration:
467 pass
468 lock.release()
469 return clean
470
471 def _calcstorehash(self, remotepath):
472 '''calculate a unique "store hash"
473
474 This method is used to to detect when there are changes that may
475 require a push to a given remote path.'''
476 # sort the files that will be hashed in increasing (likely) file size
477 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
478 yield '# %s\n' % remotepath
479 for relname in filelist:
480 absname = os.path.normpath(self._repo.join(relname))
481 yield '%s = %s\n' % (absname, _calcfilehash(absname))
482
483 def _getstorehashcachepath(self, remotepath):
484 '''get a unique path for the store hash cache'''
485 return self._repo.join(os.path.join(
486 'cache', 'storehash', _getstorehashcachename(remotepath)))
487
488 def _readstorehashcache(self, remotepath):
489 '''read the store hash cache for a given remote repository'''
490 cachefile = self._getstorehashcachepath(remotepath)
491 if not os.path.exists(cachefile):
492 return ''
493 fd = open(cachefile, 'r')
494 pullstate = fd.readlines()
495 fd.close()
496 return pullstate
497
498 def _cachestorehash(self, remotepath):
499 '''cache the current store hash
500
501 Each remote repo requires its own store hash cache, because a subrepo
502 store may be "clean" versus a given remote repo, but not versus another
503 '''
504 cachefile = self._getstorehashcachepath(remotepath)
505 lock = self._repo.lock()
506 storehash = list(self._calcstorehash(remotepath))
507 cachedir = os.path.dirname(cachefile)
508 if not os.path.exists(cachedir):
509 util.makedirs(cachedir, notindexed=True)
510 fd = open(cachefile, 'w')
511 fd.writelines(storehash)
512 fd.close()
513 lock.release()
514
449 @annotatesubrepoerror
515 @annotatesubrepoerror
450 def _initrepo(self, parentrepo, source, create):
516 def _initrepo(self, parentrepo, source, create):
451 self._repo._subparent = parentrepo
517 self._repo._subparent = parentrepo
452 self._repo._subsource = source
518 self._repo._subsource = source
453
519
454 if create:
520 if create:
455 fp = self._repo.opener("hgrc", "w", text=True)
521 fp = self._repo.opener("hgrc", "w", text=True)
456 fp.write('[paths]\n')
522 fp.write('[paths]\n')
457
523
458 def addpathconfig(key, value):
524 def addpathconfig(key, value):
459 if value:
525 if value:
460 fp.write('%s = %s\n' % (key, value))
526 fp.write('%s = %s\n' % (key, value))
461 self._repo.ui.setconfig('paths', key, value)
527 self._repo.ui.setconfig('paths', key, value)
462
528
463 defpath = _abssource(self._repo, abort=False)
529 defpath = _abssource(self._repo, abort=False)
464 defpushpath = _abssource(self._repo, True, abort=False)
530 defpushpath = _abssource(self._repo, True, abort=False)
465 addpathconfig('default', defpath)
531 addpathconfig('default', defpath)
466 if defpath != defpushpath:
532 if defpath != defpushpath:
467 addpathconfig('default-push', defpushpath)
533 addpathconfig('default-push', defpushpath)
468 fp.close()
534 fp.close()
469
535
470 @annotatesubrepoerror
536 @annotatesubrepoerror
471 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
537 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
472 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
538 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
473 os.path.join(prefix, self._path), explicitonly)
539 os.path.join(prefix, self._path), explicitonly)
474
540
475 @annotatesubrepoerror
541 @annotatesubrepoerror
476 def status(self, rev2, **opts):
542 def status(self, rev2, **opts):
477 try:
543 try:
478 rev1 = self._state[1]
544 rev1 = self._state[1]
479 ctx1 = self._repo[rev1]
545 ctx1 = self._repo[rev1]
480 ctx2 = self._repo[rev2]
546 ctx2 = self._repo[rev2]
481 return self._repo.status(ctx1, ctx2, **opts)
547 return self._repo.status(ctx1, ctx2, **opts)
482 except error.RepoLookupError, inst:
548 except error.RepoLookupError, inst:
483 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
549 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
484 % (inst, subrelpath(self)))
550 % (inst, subrelpath(self)))
485 return [], [], [], [], [], [], []
551 return [], [], [], [], [], [], []
486
552
487 @annotatesubrepoerror
553 @annotatesubrepoerror
488 def diff(self, ui, diffopts, node2, match, prefix, **opts):
554 def diff(self, ui, diffopts, node2, match, prefix, **opts):
489 try:
555 try:
490 node1 = node.bin(self._state[1])
556 node1 = node.bin(self._state[1])
491 # We currently expect node2 to come from substate and be
557 # We currently expect node2 to come from substate and be
492 # in hex format
558 # in hex format
493 if node2 is not None:
559 if node2 is not None:
494 node2 = node.bin(node2)
560 node2 = node.bin(node2)
495 cmdutil.diffordiffstat(ui, self._repo, diffopts,
561 cmdutil.diffordiffstat(ui, self._repo, diffopts,
496 node1, node2, match,
562 node1, node2, match,
497 prefix=posixpath.join(prefix, self._path),
563 prefix=posixpath.join(prefix, self._path),
498 listsubrepos=True, **opts)
564 listsubrepos=True, **opts)
499 except error.RepoLookupError, inst:
565 except error.RepoLookupError, inst:
500 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
566 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
501 % (inst, subrelpath(self)))
567 % (inst, subrelpath(self)))
502
568
503 @annotatesubrepoerror
569 @annotatesubrepoerror
504 def archive(self, ui, archiver, prefix, match=None):
570 def archive(self, ui, archiver, prefix, match=None):
505 self._get(self._state + ('hg',))
571 self._get(self._state + ('hg',))
506 abstractsubrepo.archive(self, ui, archiver, prefix, match)
572 abstractsubrepo.archive(self, ui, archiver, prefix, match)
507
573
508 rev = self._state[1]
574 rev = self._state[1]
509 ctx = self._repo[rev]
575 ctx = self._repo[rev]
510 for subpath in ctx.substate:
576 for subpath in ctx.substate:
511 s = subrepo(ctx, subpath)
577 s = subrepo(ctx, subpath)
512 submatch = matchmod.narrowmatcher(subpath, match)
578 submatch = matchmod.narrowmatcher(subpath, match)
513 s.archive(ui, archiver, os.path.join(prefix, self._path), submatch)
579 s.archive(ui, archiver, os.path.join(prefix, self._path), submatch)
514
580
515 @annotatesubrepoerror
581 @annotatesubrepoerror
516 def dirty(self, ignoreupdate=False):
582 def dirty(self, ignoreupdate=False):
517 r = self._state[1]
583 r = self._state[1]
518 if r == '' and not ignoreupdate: # no state recorded
584 if r == '' and not ignoreupdate: # no state recorded
519 return True
585 return True
520 w = self._repo[None]
586 w = self._repo[None]
521 if r != w.p1().hex() and not ignoreupdate:
587 if r != w.p1().hex() and not ignoreupdate:
522 # different version checked out
588 # different version checked out
523 return True
589 return True
524 return w.dirty() # working directory changed
590 return w.dirty() # working directory changed
525
591
526 def basestate(self):
592 def basestate(self):
527 return self._repo['.'].hex()
593 return self._repo['.'].hex()
528
594
529 def checknested(self, path):
595 def checknested(self, path):
530 return self._repo._checknested(self._repo.wjoin(path))
596 return self._repo._checknested(self._repo.wjoin(path))
531
597
532 @annotatesubrepoerror
598 @annotatesubrepoerror
533 def commit(self, text, user, date):
599 def commit(self, text, user, date):
534 # don't bother committing in the subrepo if it's only been
600 # don't bother committing in the subrepo if it's only been
535 # updated
601 # updated
536 if not self.dirty(True):
602 if not self.dirty(True):
537 return self._repo['.'].hex()
603 return self._repo['.'].hex()
538 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
604 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
539 n = self._repo.commit(text, user, date)
605 n = self._repo.commit(text, user, date)
540 if not n:
606 if not n:
541 return self._repo['.'].hex() # different version checked out
607 return self._repo['.'].hex() # different version checked out
542 return node.hex(n)
608 return node.hex(n)
543
609
544 @annotatesubrepoerror
610 @annotatesubrepoerror
545 def remove(self):
611 def remove(self):
546 # we can't fully delete the repository as it may contain
612 # we can't fully delete the repository as it may contain
547 # local-only history
613 # local-only history
548 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
614 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
549 hg.clean(self._repo, node.nullid, False)
615 hg.clean(self._repo, node.nullid, False)
550
616
551 def _get(self, state):
617 def _get(self, state):
552 source, revision, kind = state
618 source, revision, kind = state
553 if revision not in self._repo:
619 if revision not in self._repo:
554 self._repo._subsource = source
620 self._repo._subsource = source
555 srcurl = _abssource(self._repo)
621 srcurl = _abssource(self._repo)
556 other = hg.peer(self._repo, {}, srcurl)
622 other = hg.peer(self._repo, {}, srcurl)
557 if len(self._repo) == 0:
623 if len(self._repo) == 0:
558 self._repo.ui.status(_('cloning subrepo %s from %s\n')
624 self._repo.ui.status(_('cloning subrepo %s from %s\n')
559 % (subrelpath(self), srcurl))
625 % (subrelpath(self), srcurl))
560 parentrepo = self._repo._subparent
626 parentrepo = self._repo._subparent
561 shutil.rmtree(self._repo.path)
627 shutil.rmtree(self._repo.path)
562 other, cloned = hg.clone(self._repo._subparent.baseui, {},
628 other, cloned = hg.clone(self._repo._subparent.baseui, {},
563 other, self._repo.root,
629 other, self._repo.root,
564 update=False)
630 update=False)
565 self._repo = cloned.local()
631 self._repo = cloned.local()
566 self._initrepo(parentrepo, source, create=True)
632 self._initrepo(parentrepo, source, create=True)
633 self._cachestorehash(srcurl)
567 else:
634 else:
568 self._repo.ui.status(_('pulling subrepo %s from %s\n')
635 self._repo.ui.status(_('pulling subrepo %s from %s\n')
569 % (subrelpath(self), srcurl))
636 % (subrelpath(self), srcurl))
637 cleansub = self.storeclean(srcurl)
570 remotebookmarks = other.listkeys('bookmarks')
638 remotebookmarks = other.listkeys('bookmarks')
571 self._repo.pull(other)
639 self._repo.pull(other)
572 bookmarks.updatefromremote(self._repo.ui, self._repo,
640 bookmarks.updatefromremote(self._repo.ui, self._repo,
573 remotebookmarks, srcurl)
641 remotebookmarks, srcurl)
642 if cleansub:
643 # keep the repo clean after pull
644 self._cachestorehash(srcurl)
574
645
575 @annotatesubrepoerror
646 @annotatesubrepoerror
576 def get(self, state, overwrite=False):
647 def get(self, state, overwrite=False):
577 self._get(state)
648 self._get(state)
578 source, revision, kind = state
649 source, revision, kind = state
579 self._repo.ui.debug("getting subrepo %s\n" % self._path)
650 self._repo.ui.debug("getting subrepo %s\n" % self._path)
580 hg.updaterepo(self._repo, revision, overwrite)
651 hg.updaterepo(self._repo, revision, overwrite)
581
652
582 @annotatesubrepoerror
653 @annotatesubrepoerror
583 def merge(self, state):
654 def merge(self, state):
584 self._get(state)
655 self._get(state)
585 cur = self._repo['.']
656 cur = self._repo['.']
586 dst = self._repo[state[1]]
657 dst = self._repo[state[1]]
587 anc = dst.ancestor(cur)
658 anc = dst.ancestor(cur)
588
659
589 def mergefunc():
660 def mergefunc():
590 if anc == cur and dst.branch() == cur.branch():
661 if anc == cur and dst.branch() == cur.branch():
591 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
662 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
592 hg.update(self._repo, state[1])
663 hg.update(self._repo, state[1])
593 elif anc == dst:
664 elif anc == dst:
594 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
665 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
595 else:
666 else:
596 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
667 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
597 hg.merge(self._repo, state[1], remind=False)
668 hg.merge(self._repo, state[1], remind=False)
598
669
599 wctx = self._repo[None]
670 wctx = self._repo[None]
600 if self.dirty():
671 if self.dirty():
601 if anc != dst:
672 if anc != dst:
602 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
673 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
603 mergefunc()
674 mergefunc()
604 else:
675 else:
605 mergefunc()
676 mergefunc()
606 else:
677 else:
607 mergefunc()
678 mergefunc()
608
679
609 @annotatesubrepoerror
680 @annotatesubrepoerror
610 def push(self, opts):
681 def push(self, opts):
611 force = opts.get('force')
682 force = opts.get('force')
612 newbranch = opts.get('new_branch')
683 newbranch = opts.get('new_branch')
613 ssh = opts.get('ssh')
684 ssh = opts.get('ssh')
614
685
615 # push subrepos depth-first for coherent ordering
686 # push subrepos depth-first for coherent ordering
616 c = self._repo['']
687 c = self._repo['']
617 subs = c.substate # only repos that are committed
688 subs = c.substate # only repos that are committed
618 for s in sorted(subs):
689 for s in sorted(subs):
619 if c.sub(s).push(opts) == 0:
690 if c.sub(s).push(opts) == 0:
620 return False
691 return False
621
692
622 dsturl = _abssource(self._repo, True)
693 dsturl = _abssource(self._repo, True)
623 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
694 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
624 (subrelpath(self), dsturl))
695 (subrelpath(self), dsturl))
625 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
696 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
626 return self._repo.push(other, force, newbranch=newbranch)
697 res = self._repo.push(other, force, newbranch=newbranch)
698
699 # the repo is now clean
700 self._cachestorehash(dsturl)
701 return res
627
702
628 @annotatesubrepoerror
703 @annotatesubrepoerror
629 def outgoing(self, ui, dest, opts):
704 def outgoing(self, ui, dest, opts):
630 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
705 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
631
706
632 @annotatesubrepoerror
707 @annotatesubrepoerror
633 def incoming(self, ui, source, opts):
708 def incoming(self, ui, source, opts):
634 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
709 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
635
710
636 @annotatesubrepoerror
711 @annotatesubrepoerror
637 def files(self):
712 def files(self):
638 rev = self._state[1]
713 rev = self._state[1]
639 ctx = self._repo[rev]
714 ctx = self._repo[rev]
640 return ctx.manifest()
715 return ctx.manifest()
641
716
642 def filedata(self, name):
717 def filedata(self, name):
643 rev = self._state[1]
718 rev = self._state[1]
644 return self._repo[rev][name].data()
719 return self._repo[rev][name].data()
645
720
646 def fileflags(self, name):
721 def fileflags(self, name):
647 rev = self._state[1]
722 rev = self._state[1]
648 ctx = self._repo[rev]
723 ctx = self._repo[rev]
649 return ctx.flags(name)
724 return ctx.flags(name)
650
725
651 def walk(self, match):
726 def walk(self, match):
652 ctx = self._repo[None]
727 ctx = self._repo[None]
653 return ctx.walk(match)
728 return ctx.walk(match)
654
729
655 @annotatesubrepoerror
730 @annotatesubrepoerror
656 def forget(self, ui, match, prefix):
731 def forget(self, ui, match, prefix):
657 return cmdutil.forget(ui, self._repo, match,
732 return cmdutil.forget(ui, self._repo, match,
658 os.path.join(prefix, self._path), True)
733 os.path.join(prefix, self._path), True)
659
734
660 @annotatesubrepoerror
735 @annotatesubrepoerror
661 def revert(self, ui, substate, *pats, **opts):
736 def revert(self, ui, substate, *pats, **opts):
662 # reverting a subrepo is a 2 step process:
737 # reverting a subrepo is a 2 step process:
663 # 1. if the no_backup is not set, revert all modified
738 # 1. if the no_backup is not set, revert all modified
664 # files inside the subrepo
739 # files inside the subrepo
665 # 2. update the subrepo to the revision specified in
740 # 2. update the subrepo to the revision specified in
666 # the corresponding substate dictionary
741 # the corresponding substate dictionary
667 ui.status(_('reverting subrepo %s\n') % substate[0])
742 ui.status(_('reverting subrepo %s\n') % substate[0])
668 if not opts.get('no_backup'):
743 if not opts.get('no_backup'):
669 # Revert all files on the subrepo, creating backups
744 # Revert all files on the subrepo, creating backups
670 # Note that this will not recursively revert subrepos
745 # Note that this will not recursively revert subrepos
671 # We could do it if there was a set:subrepos() predicate
746 # We could do it if there was a set:subrepos() predicate
672 opts = opts.copy()
747 opts = opts.copy()
673 opts['date'] = None
748 opts['date'] = None
674 opts['rev'] = substate[1]
749 opts['rev'] = substate[1]
675
750
676 pats = []
751 pats = []
677 if not opts['all']:
752 if not opts['all']:
678 pats = ['set:modified()']
753 pats = ['set:modified()']
679 self.filerevert(ui, *pats, **opts)
754 self.filerevert(ui, *pats, **opts)
680
755
681 # Update the repo to the revision specified in the given substate
756 # Update the repo to the revision specified in the given substate
682 self.get(substate, overwrite=True)
757 self.get(substate, overwrite=True)
683
758
684 def filerevert(self, ui, *pats, **opts):
759 def filerevert(self, ui, *pats, **opts):
685 ctx = self._repo[opts['rev']]
760 ctx = self._repo[opts['rev']]
686 parents = self._repo.dirstate.parents()
761 parents = self._repo.dirstate.parents()
687 if opts['all']:
762 if opts['all']:
688 pats = ['set:modified()']
763 pats = ['set:modified()']
689 else:
764 else:
690 pats = []
765 pats = []
691 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
766 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
692
767
693 class svnsubrepo(abstractsubrepo):
768 class svnsubrepo(abstractsubrepo):
694 def __init__(self, ctx, path, state):
769 def __init__(self, ctx, path, state):
695 self._path = path
770 self._path = path
696 self._state = state
771 self._state = state
697 self._ctx = ctx
772 self._ctx = ctx
698 self._ui = ctx._repo.ui
773 self._ui = ctx._repo.ui
699 self._exe = util.findexe('svn')
774 self._exe = util.findexe('svn')
700 if not self._exe:
775 if not self._exe:
701 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
776 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
702 % self._path)
777 % self._path)
703
778
704 def _svncommand(self, commands, filename='', failok=False):
779 def _svncommand(self, commands, filename='', failok=False):
705 cmd = [self._exe]
780 cmd = [self._exe]
706 extrakw = {}
781 extrakw = {}
707 if not self._ui.interactive():
782 if not self._ui.interactive():
708 # Making stdin be a pipe should prevent svn from behaving
783 # Making stdin be a pipe should prevent svn from behaving
709 # interactively even if we can't pass --non-interactive.
784 # interactively even if we can't pass --non-interactive.
710 extrakw['stdin'] = subprocess.PIPE
785 extrakw['stdin'] = subprocess.PIPE
711 # Starting in svn 1.5 --non-interactive is a global flag
786 # Starting in svn 1.5 --non-interactive is a global flag
712 # instead of being per-command, but we need to support 1.4 so
787 # instead of being per-command, but we need to support 1.4 so
713 # we have to be intelligent about what commands take
788 # we have to be intelligent about what commands take
714 # --non-interactive.
789 # --non-interactive.
715 if commands[0] in ('update', 'checkout', 'commit'):
790 if commands[0] in ('update', 'checkout', 'commit'):
716 cmd.append('--non-interactive')
791 cmd.append('--non-interactive')
717 cmd.extend(commands)
792 cmd.extend(commands)
718 if filename is not None:
793 if filename is not None:
719 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
794 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
720 cmd.append(path)
795 cmd.append(path)
721 env = dict(os.environ)
796 env = dict(os.environ)
722 # Avoid localized output, preserve current locale for everything else.
797 # Avoid localized output, preserve current locale for everything else.
723 lc_all = env.get('LC_ALL')
798 lc_all = env.get('LC_ALL')
724 if lc_all:
799 if lc_all:
725 env['LANG'] = lc_all
800 env['LANG'] = lc_all
726 del env['LC_ALL']
801 del env['LC_ALL']
727 env['LC_MESSAGES'] = 'C'
802 env['LC_MESSAGES'] = 'C'
728 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
803 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
729 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
804 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
730 universal_newlines=True, env=env, **extrakw)
805 universal_newlines=True, env=env, **extrakw)
731 stdout, stderr = p.communicate()
806 stdout, stderr = p.communicate()
732 stderr = stderr.strip()
807 stderr = stderr.strip()
733 if not failok:
808 if not failok:
734 if p.returncode:
809 if p.returncode:
735 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
810 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
736 if stderr:
811 if stderr:
737 self._ui.warn(stderr + '\n')
812 self._ui.warn(stderr + '\n')
738 return stdout, stderr
813 return stdout, stderr
739
814
740 @propertycache
815 @propertycache
741 def _svnversion(self):
816 def _svnversion(self):
742 output, err = self._svncommand(['--version', '--quiet'], filename=None)
817 output, err = self._svncommand(['--version', '--quiet'], filename=None)
743 m = re.search(r'^(\d+)\.(\d+)', output)
818 m = re.search(r'^(\d+)\.(\d+)', output)
744 if not m:
819 if not m:
745 raise util.Abort(_('cannot retrieve svn tool version'))
820 raise util.Abort(_('cannot retrieve svn tool version'))
746 return (int(m.group(1)), int(m.group(2)))
821 return (int(m.group(1)), int(m.group(2)))
747
822
748 def _wcrevs(self):
823 def _wcrevs(self):
749 # Get the working directory revision as well as the last
824 # Get the working directory revision as well as the last
750 # commit revision so we can compare the subrepo state with
825 # commit revision so we can compare the subrepo state with
751 # both. We used to store the working directory one.
826 # both. We used to store the working directory one.
752 output, err = self._svncommand(['info', '--xml'])
827 output, err = self._svncommand(['info', '--xml'])
753 doc = xml.dom.minidom.parseString(output)
828 doc = xml.dom.minidom.parseString(output)
754 entries = doc.getElementsByTagName('entry')
829 entries = doc.getElementsByTagName('entry')
755 lastrev, rev = '0', '0'
830 lastrev, rev = '0', '0'
756 if entries:
831 if entries:
757 rev = str(entries[0].getAttribute('revision')) or '0'
832 rev = str(entries[0].getAttribute('revision')) or '0'
758 commits = entries[0].getElementsByTagName('commit')
833 commits = entries[0].getElementsByTagName('commit')
759 if commits:
834 if commits:
760 lastrev = str(commits[0].getAttribute('revision')) or '0'
835 lastrev = str(commits[0].getAttribute('revision')) or '0'
761 return (lastrev, rev)
836 return (lastrev, rev)
762
837
763 def _wcrev(self):
838 def _wcrev(self):
764 return self._wcrevs()[0]
839 return self._wcrevs()[0]
765
840
766 def _wcchanged(self):
841 def _wcchanged(self):
767 """Return (changes, extchanges, missing) where changes is True
842 """Return (changes, extchanges, missing) where changes is True
768 if the working directory was changed, extchanges is
843 if the working directory was changed, extchanges is
769 True if any of these changes concern an external entry and missing
844 True if any of these changes concern an external entry and missing
770 is True if any change is a missing entry.
845 is True if any change is a missing entry.
771 """
846 """
772 output, err = self._svncommand(['status', '--xml'])
847 output, err = self._svncommand(['status', '--xml'])
773 externals, changes, missing = [], [], []
848 externals, changes, missing = [], [], []
774 doc = xml.dom.minidom.parseString(output)
849 doc = xml.dom.minidom.parseString(output)
775 for e in doc.getElementsByTagName('entry'):
850 for e in doc.getElementsByTagName('entry'):
776 s = e.getElementsByTagName('wc-status')
851 s = e.getElementsByTagName('wc-status')
777 if not s:
852 if not s:
778 continue
853 continue
779 item = s[0].getAttribute('item')
854 item = s[0].getAttribute('item')
780 props = s[0].getAttribute('props')
855 props = s[0].getAttribute('props')
781 path = e.getAttribute('path')
856 path = e.getAttribute('path')
782 if item == 'external':
857 if item == 'external':
783 externals.append(path)
858 externals.append(path)
784 elif item == 'missing':
859 elif item == 'missing':
785 missing.append(path)
860 missing.append(path)
786 if (item not in ('', 'normal', 'unversioned', 'external')
861 if (item not in ('', 'normal', 'unversioned', 'external')
787 or props not in ('', 'none', 'normal')):
862 or props not in ('', 'none', 'normal')):
788 changes.append(path)
863 changes.append(path)
789 for path in changes:
864 for path in changes:
790 for ext in externals:
865 for ext in externals:
791 if path == ext or path.startswith(ext + os.sep):
866 if path == ext or path.startswith(ext + os.sep):
792 return True, True, bool(missing)
867 return True, True, bool(missing)
793 return bool(changes), False, bool(missing)
868 return bool(changes), False, bool(missing)
794
869
795 def dirty(self, ignoreupdate=False):
870 def dirty(self, ignoreupdate=False):
796 if not self._wcchanged()[0]:
871 if not self._wcchanged()[0]:
797 if self._state[1] in self._wcrevs() or ignoreupdate:
872 if self._state[1] in self._wcrevs() or ignoreupdate:
798 return False
873 return False
799 return True
874 return True
800
875
801 def basestate(self):
876 def basestate(self):
802 lastrev, rev = self._wcrevs()
877 lastrev, rev = self._wcrevs()
803 if lastrev != rev:
878 if lastrev != rev:
804 # Last committed rev is not the same than rev. We would
879 # Last committed rev is not the same than rev. We would
805 # like to take lastrev but we do not know if the subrepo
880 # like to take lastrev but we do not know if the subrepo
806 # URL exists at lastrev. Test it and fallback to rev it
881 # URL exists at lastrev. Test it and fallback to rev it
807 # is not there.
882 # is not there.
808 try:
883 try:
809 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
884 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
810 return lastrev
885 return lastrev
811 except error.Abort:
886 except error.Abort:
812 pass
887 pass
813 return rev
888 return rev
814
889
815 @annotatesubrepoerror
890 @annotatesubrepoerror
816 def commit(self, text, user, date):
891 def commit(self, text, user, date):
817 # user and date are out of our hands since svn is centralized
892 # user and date are out of our hands since svn is centralized
818 changed, extchanged, missing = self._wcchanged()
893 changed, extchanged, missing = self._wcchanged()
819 if not changed:
894 if not changed:
820 return self.basestate()
895 return self.basestate()
821 if extchanged:
896 if extchanged:
822 # Do not try to commit externals
897 # Do not try to commit externals
823 raise util.Abort(_('cannot commit svn externals'))
898 raise util.Abort(_('cannot commit svn externals'))
824 if missing:
899 if missing:
825 # svn can commit with missing entries but aborting like hg
900 # svn can commit with missing entries but aborting like hg
826 # seems a better approach.
901 # seems a better approach.
827 raise util.Abort(_('cannot commit missing svn entries'))
902 raise util.Abort(_('cannot commit missing svn entries'))
828 commitinfo, err = self._svncommand(['commit', '-m', text])
903 commitinfo, err = self._svncommand(['commit', '-m', text])
829 self._ui.status(commitinfo)
904 self._ui.status(commitinfo)
830 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
905 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
831 if not newrev:
906 if not newrev:
832 if not commitinfo.strip():
907 if not commitinfo.strip():
833 # Sometimes, our definition of "changed" differs from
908 # Sometimes, our definition of "changed" differs from
834 # svn one. For instance, svn ignores missing files
909 # svn one. For instance, svn ignores missing files
835 # when committing. If there are only missing files, no
910 # when committing. If there are only missing files, no
836 # commit is made, no output and no error code.
911 # commit is made, no output and no error code.
837 raise util.Abort(_('failed to commit svn changes'))
912 raise util.Abort(_('failed to commit svn changes'))
838 raise util.Abort(commitinfo.splitlines()[-1])
913 raise util.Abort(commitinfo.splitlines()[-1])
839 newrev = newrev.groups()[0]
914 newrev = newrev.groups()[0]
840 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
915 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
841 return newrev
916 return newrev
842
917
843 @annotatesubrepoerror
918 @annotatesubrepoerror
844 def remove(self):
919 def remove(self):
845 if self.dirty():
920 if self.dirty():
846 self._ui.warn(_('not removing repo %s because '
921 self._ui.warn(_('not removing repo %s because '
847 'it has changes.\n' % self._path))
922 'it has changes.\n' % self._path))
848 return
923 return
849 self._ui.note(_('removing subrepo %s\n') % self._path)
924 self._ui.note(_('removing subrepo %s\n') % self._path)
850
925
851 def onerror(function, path, excinfo):
926 def onerror(function, path, excinfo):
852 if function is not os.remove:
927 if function is not os.remove:
853 raise
928 raise
854 # read-only files cannot be unlinked under Windows
929 # read-only files cannot be unlinked under Windows
855 s = os.stat(path)
930 s = os.stat(path)
856 if (s.st_mode & stat.S_IWRITE) != 0:
931 if (s.st_mode & stat.S_IWRITE) != 0:
857 raise
932 raise
858 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
933 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
859 os.remove(path)
934 os.remove(path)
860
935
861 path = self._ctx._repo.wjoin(self._path)
936 path = self._ctx._repo.wjoin(self._path)
862 shutil.rmtree(path, onerror=onerror)
937 shutil.rmtree(path, onerror=onerror)
863 try:
938 try:
864 os.removedirs(os.path.dirname(path))
939 os.removedirs(os.path.dirname(path))
865 except OSError:
940 except OSError:
866 pass
941 pass
867
942
868 @annotatesubrepoerror
943 @annotatesubrepoerror
869 def get(self, state, overwrite=False):
944 def get(self, state, overwrite=False):
870 if overwrite:
945 if overwrite:
871 self._svncommand(['revert', '--recursive'])
946 self._svncommand(['revert', '--recursive'])
872 args = ['checkout']
947 args = ['checkout']
873 if self._svnversion >= (1, 5):
948 if self._svnversion >= (1, 5):
874 args.append('--force')
949 args.append('--force')
875 # The revision must be specified at the end of the URL to properly
950 # The revision must be specified at the end of the URL to properly
876 # update to a directory which has since been deleted and recreated.
951 # update to a directory which has since been deleted and recreated.
877 args.append('%s@%s' % (state[0], state[1]))
952 args.append('%s@%s' % (state[0], state[1]))
878 status, err = self._svncommand(args, failok=True)
953 status, err = self._svncommand(args, failok=True)
879 if not re.search('Checked out revision [0-9]+.', status):
954 if not re.search('Checked out revision [0-9]+.', status):
880 if ('is already a working copy for a different URL' in err
955 if ('is already a working copy for a different URL' in err
881 and (self._wcchanged()[:2] == (False, False))):
956 and (self._wcchanged()[:2] == (False, False))):
882 # obstructed but clean working copy, so just blow it away.
957 # obstructed but clean working copy, so just blow it away.
883 self.remove()
958 self.remove()
884 self.get(state, overwrite=False)
959 self.get(state, overwrite=False)
885 return
960 return
886 raise util.Abort((status or err).splitlines()[-1])
961 raise util.Abort((status or err).splitlines()[-1])
887 self._ui.status(status)
962 self._ui.status(status)
888
963
889 @annotatesubrepoerror
964 @annotatesubrepoerror
890 def merge(self, state):
965 def merge(self, state):
891 old = self._state[1]
966 old = self._state[1]
892 new = state[1]
967 new = state[1]
893 wcrev = self._wcrev()
968 wcrev = self._wcrev()
894 if new != wcrev:
969 if new != wcrev:
895 dirty = old == wcrev or self._wcchanged()[0]
970 dirty = old == wcrev or self._wcchanged()[0]
896 if _updateprompt(self._ui, self, dirty, wcrev, new):
971 if _updateprompt(self._ui, self, dirty, wcrev, new):
897 self.get(state, False)
972 self.get(state, False)
898
973
899 def push(self, opts):
974 def push(self, opts):
900 # push is a no-op for SVN
975 # push is a no-op for SVN
901 return True
976 return True
902
977
903 @annotatesubrepoerror
978 @annotatesubrepoerror
904 def files(self):
979 def files(self):
905 output = self._svncommand(['list', '--recursive', '--xml'])[0]
980 output = self._svncommand(['list', '--recursive', '--xml'])[0]
906 doc = xml.dom.minidom.parseString(output)
981 doc = xml.dom.minidom.parseString(output)
907 paths = []
982 paths = []
908 for e in doc.getElementsByTagName('entry'):
983 for e in doc.getElementsByTagName('entry'):
909 kind = str(e.getAttribute('kind'))
984 kind = str(e.getAttribute('kind'))
910 if kind != 'file':
985 if kind != 'file':
911 continue
986 continue
912 name = ''.join(c.data for c
987 name = ''.join(c.data for c
913 in e.getElementsByTagName('name')[0].childNodes
988 in e.getElementsByTagName('name')[0].childNodes
914 if c.nodeType == c.TEXT_NODE)
989 if c.nodeType == c.TEXT_NODE)
915 paths.append(name.encode('utf-8'))
990 paths.append(name.encode('utf-8'))
916 return paths
991 return paths
917
992
918 def filedata(self, name):
993 def filedata(self, name):
919 return self._svncommand(['cat'], name)[0]
994 return self._svncommand(['cat'], name)[0]
920
995
921
996
922 class gitsubrepo(abstractsubrepo):
997 class gitsubrepo(abstractsubrepo):
923 def __init__(self, ctx, path, state):
998 def __init__(self, ctx, path, state):
924 self._state = state
999 self._state = state
925 self._ctx = ctx
1000 self._ctx = ctx
926 self._path = path
1001 self._path = path
927 self._relpath = os.path.join(reporelpath(ctx._repo), path)
1002 self._relpath = os.path.join(reporelpath(ctx._repo), path)
928 self._abspath = ctx._repo.wjoin(path)
1003 self._abspath = ctx._repo.wjoin(path)
929 self._subparent = ctx._repo
1004 self._subparent = ctx._repo
930 self._ui = ctx._repo.ui
1005 self._ui = ctx._repo.ui
931 self._ensuregit()
1006 self._ensuregit()
932
1007
933 def _ensuregit(self):
1008 def _ensuregit(self):
934 try:
1009 try:
935 self._gitexecutable = 'git'
1010 self._gitexecutable = 'git'
936 out, err = self._gitnodir(['--version'])
1011 out, err = self._gitnodir(['--version'])
937 except OSError, e:
1012 except OSError, e:
938 if e.errno != 2 or os.name != 'nt':
1013 if e.errno != 2 or os.name != 'nt':
939 raise
1014 raise
940 self._gitexecutable = 'git.cmd'
1015 self._gitexecutable = 'git.cmd'
941 out, err = self._gitnodir(['--version'])
1016 out, err = self._gitnodir(['--version'])
942 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1017 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
943 if not m:
1018 if not m:
944 self._ui.warn(_('cannot retrieve git version'))
1019 self._ui.warn(_('cannot retrieve git version'))
945 return
1020 return
946 version = (int(m.group(1)), m.group(2), m.group(3))
1021 version = (int(m.group(1)), m.group(2), m.group(3))
947 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1022 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
948 # despite the docstring comment. For now, error on 1.4.0, warn on
1023 # despite the docstring comment. For now, error on 1.4.0, warn on
949 # 1.5.0 but attempt to continue.
1024 # 1.5.0 but attempt to continue.
950 if version < (1, 5, 0):
1025 if version < (1, 5, 0):
951 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
1026 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
952 elif version < (1, 6, 0):
1027 elif version < (1, 6, 0):
953 self._ui.warn(_('git subrepo requires at least 1.6.0 or later'))
1028 self._ui.warn(_('git subrepo requires at least 1.6.0 or later'))
954
1029
955 def _gitcommand(self, commands, env=None, stream=False):
1030 def _gitcommand(self, commands, env=None, stream=False):
956 return self._gitdir(commands, env=env, stream=stream)[0]
1031 return self._gitdir(commands, env=env, stream=stream)[0]
957
1032
958 def _gitdir(self, commands, env=None, stream=False):
1033 def _gitdir(self, commands, env=None, stream=False):
959 return self._gitnodir(commands, env=env, stream=stream,
1034 return self._gitnodir(commands, env=env, stream=stream,
960 cwd=self._abspath)
1035 cwd=self._abspath)
961
1036
962 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1037 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
963 """Calls the git command
1038 """Calls the git command
964
1039
965 The methods tries to call the git command. versions prior to 1.6.0
1040 The methods tries to call the git command. versions prior to 1.6.0
966 are not supported and very probably fail.
1041 are not supported and very probably fail.
967 """
1042 """
968 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1043 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
969 # unless ui.quiet is set, print git's stderr,
1044 # unless ui.quiet is set, print git's stderr,
970 # which is mostly progress and useful info
1045 # which is mostly progress and useful info
971 errpipe = None
1046 errpipe = None
972 if self._ui.quiet:
1047 if self._ui.quiet:
973 errpipe = open(os.devnull, 'w')
1048 errpipe = open(os.devnull, 'w')
974 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1049 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
975 cwd=cwd, env=env, close_fds=util.closefds,
1050 cwd=cwd, env=env, close_fds=util.closefds,
976 stdout=subprocess.PIPE, stderr=errpipe)
1051 stdout=subprocess.PIPE, stderr=errpipe)
977 if stream:
1052 if stream:
978 return p.stdout, None
1053 return p.stdout, None
979
1054
980 retdata = p.stdout.read().strip()
1055 retdata = p.stdout.read().strip()
981 # wait for the child to exit to avoid race condition.
1056 # wait for the child to exit to avoid race condition.
982 p.wait()
1057 p.wait()
983
1058
984 if p.returncode != 0 and p.returncode != 1:
1059 if p.returncode != 0 and p.returncode != 1:
985 # there are certain error codes that are ok
1060 # there are certain error codes that are ok
986 command = commands[0]
1061 command = commands[0]
987 if command in ('cat-file', 'symbolic-ref'):
1062 if command in ('cat-file', 'symbolic-ref'):
988 return retdata, p.returncode
1063 return retdata, p.returncode
989 # for all others, abort
1064 # for all others, abort
990 raise util.Abort('git %s error %d in %s' %
1065 raise util.Abort('git %s error %d in %s' %
991 (command, p.returncode, self._relpath))
1066 (command, p.returncode, self._relpath))
992
1067
993 return retdata, p.returncode
1068 return retdata, p.returncode
994
1069
995 def _gitmissing(self):
1070 def _gitmissing(self):
996 return not os.path.exists(os.path.join(self._abspath, '.git'))
1071 return not os.path.exists(os.path.join(self._abspath, '.git'))
997
1072
998 def _gitstate(self):
1073 def _gitstate(self):
999 return self._gitcommand(['rev-parse', 'HEAD'])
1074 return self._gitcommand(['rev-parse', 'HEAD'])
1000
1075
1001 def _gitcurrentbranch(self):
1076 def _gitcurrentbranch(self):
1002 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1077 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1003 if err:
1078 if err:
1004 current = None
1079 current = None
1005 return current
1080 return current
1006
1081
1007 def _gitremote(self, remote):
1082 def _gitremote(self, remote):
1008 out = self._gitcommand(['remote', 'show', '-n', remote])
1083 out = self._gitcommand(['remote', 'show', '-n', remote])
1009 line = out.split('\n')[1]
1084 line = out.split('\n')[1]
1010 i = line.index('URL: ') + len('URL: ')
1085 i = line.index('URL: ') + len('URL: ')
1011 return line[i:]
1086 return line[i:]
1012
1087
1013 def _githavelocally(self, revision):
1088 def _githavelocally(self, revision):
1014 out, code = self._gitdir(['cat-file', '-e', revision])
1089 out, code = self._gitdir(['cat-file', '-e', revision])
1015 return code == 0
1090 return code == 0
1016
1091
1017 def _gitisancestor(self, r1, r2):
1092 def _gitisancestor(self, r1, r2):
1018 base = self._gitcommand(['merge-base', r1, r2])
1093 base = self._gitcommand(['merge-base', r1, r2])
1019 return base == r1
1094 return base == r1
1020
1095
1021 def _gitisbare(self):
1096 def _gitisbare(self):
1022 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1097 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1023
1098
1024 def _gitupdatestat(self):
1099 def _gitupdatestat(self):
1025 """This must be run before git diff-index.
1100 """This must be run before git diff-index.
1026 diff-index only looks at changes to file stat;
1101 diff-index only looks at changes to file stat;
1027 this command looks at file contents and updates the stat."""
1102 this command looks at file contents and updates the stat."""
1028 self._gitcommand(['update-index', '-q', '--refresh'])
1103 self._gitcommand(['update-index', '-q', '--refresh'])
1029
1104
1030 def _gitbranchmap(self):
1105 def _gitbranchmap(self):
1031 '''returns 2 things:
1106 '''returns 2 things:
1032 a map from git branch to revision
1107 a map from git branch to revision
1033 a map from revision to branches'''
1108 a map from revision to branches'''
1034 branch2rev = {}
1109 branch2rev = {}
1035 rev2branch = {}
1110 rev2branch = {}
1036
1111
1037 out = self._gitcommand(['for-each-ref', '--format',
1112 out = self._gitcommand(['for-each-ref', '--format',
1038 '%(objectname) %(refname)'])
1113 '%(objectname) %(refname)'])
1039 for line in out.split('\n'):
1114 for line in out.split('\n'):
1040 revision, ref = line.split(' ')
1115 revision, ref = line.split(' ')
1041 if (not ref.startswith('refs/heads/') and
1116 if (not ref.startswith('refs/heads/') and
1042 not ref.startswith('refs/remotes/')):
1117 not ref.startswith('refs/remotes/')):
1043 continue
1118 continue
1044 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1119 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1045 continue # ignore remote/HEAD redirects
1120 continue # ignore remote/HEAD redirects
1046 branch2rev[ref] = revision
1121 branch2rev[ref] = revision
1047 rev2branch.setdefault(revision, []).append(ref)
1122 rev2branch.setdefault(revision, []).append(ref)
1048 return branch2rev, rev2branch
1123 return branch2rev, rev2branch
1049
1124
1050 def _gittracking(self, branches):
1125 def _gittracking(self, branches):
1051 'return map of remote branch to local tracking branch'
1126 'return map of remote branch to local tracking branch'
1052 # assumes no more than one local tracking branch for each remote
1127 # assumes no more than one local tracking branch for each remote
1053 tracking = {}
1128 tracking = {}
1054 for b in branches:
1129 for b in branches:
1055 if b.startswith('refs/remotes/'):
1130 if b.startswith('refs/remotes/'):
1056 continue
1131 continue
1057 bname = b.split('/', 2)[2]
1132 bname = b.split('/', 2)[2]
1058 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1133 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1059 if remote:
1134 if remote:
1060 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1135 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1061 tracking['refs/remotes/%s/%s' %
1136 tracking['refs/remotes/%s/%s' %
1062 (remote, ref.split('/', 2)[2])] = b
1137 (remote, ref.split('/', 2)[2])] = b
1063 return tracking
1138 return tracking
1064
1139
1065 def _abssource(self, source):
1140 def _abssource(self, source):
1066 if '://' not in source:
1141 if '://' not in source:
1067 # recognize the scp syntax as an absolute source
1142 # recognize the scp syntax as an absolute source
1068 colon = source.find(':')
1143 colon = source.find(':')
1069 if colon != -1 and '/' not in source[:colon]:
1144 if colon != -1 and '/' not in source[:colon]:
1070 return source
1145 return source
1071 self._subsource = source
1146 self._subsource = source
1072 return _abssource(self)
1147 return _abssource(self)
1073
1148
1074 def _fetch(self, source, revision):
1149 def _fetch(self, source, revision):
1075 if self._gitmissing():
1150 if self._gitmissing():
1076 source = self._abssource(source)
1151 source = self._abssource(source)
1077 self._ui.status(_('cloning subrepo %s from %s\n') %
1152 self._ui.status(_('cloning subrepo %s from %s\n') %
1078 (self._relpath, source))
1153 (self._relpath, source))
1079 self._gitnodir(['clone', source, self._abspath])
1154 self._gitnodir(['clone', source, self._abspath])
1080 if self._githavelocally(revision):
1155 if self._githavelocally(revision):
1081 return
1156 return
1082 self._ui.status(_('pulling subrepo %s from %s\n') %
1157 self._ui.status(_('pulling subrepo %s from %s\n') %
1083 (self._relpath, self._gitremote('origin')))
1158 (self._relpath, self._gitremote('origin')))
1084 # try only origin: the originally cloned repo
1159 # try only origin: the originally cloned repo
1085 self._gitcommand(['fetch'])
1160 self._gitcommand(['fetch'])
1086 if not self._githavelocally(revision):
1161 if not self._githavelocally(revision):
1087 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1162 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1088 (revision, self._relpath))
1163 (revision, self._relpath))
1089
1164
1090 @annotatesubrepoerror
1165 @annotatesubrepoerror
1091 def dirty(self, ignoreupdate=False):
1166 def dirty(self, ignoreupdate=False):
1092 if self._gitmissing():
1167 if self._gitmissing():
1093 return self._state[1] != ''
1168 return self._state[1] != ''
1094 if self._gitisbare():
1169 if self._gitisbare():
1095 return True
1170 return True
1096 if not ignoreupdate and self._state[1] != self._gitstate():
1171 if not ignoreupdate and self._state[1] != self._gitstate():
1097 # different version checked out
1172 # different version checked out
1098 return True
1173 return True
1099 # check for staged changes or modified files; ignore untracked files
1174 # check for staged changes or modified files; ignore untracked files
1100 self._gitupdatestat()
1175 self._gitupdatestat()
1101 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1176 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1102 return code == 1
1177 return code == 1
1103
1178
1104 def basestate(self):
1179 def basestate(self):
1105 return self._gitstate()
1180 return self._gitstate()
1106
1181
1107 @annotatesubrepoerror
1182 @annotatesubrepoerror
1108 def get(self, state, overwrite=False):
1183 def get(self, state, overwrite=False):
1109 source, revision, kind = state
1184 source, revision, kind = state
1110 if not revision:
1185 if not revision:
1111 self.remove()
1186 self.remove()
1112 return
1187 return
1113 self._fetch(source, revision)
1188 self._fetch(source, revision)
1114 # if the repo was set to be bare, unbare it
1189 # if the repo was set to be bare, unbare it
1115 if self._gitisbare():
1190 if self._gitisbare():
1116 self._gitcommand(['config', 'core.bare', 'false'])
1191 self._gitcommand(['config', 'core.bare', 'false'])
1117 if self._gitstate() == revision:
1192 if self._gitstate() == revision:
1118 self._gitcommand(['reset', '--hard', 'HEAD'])
1193 self._gitcommand(['reset', '--hard', 'HEAD'])
1119 return
1194 return
1120 elif self._gitstate() == revision:
1195 elif self._gitstate() == revision:
1121 if overwrite:
1196 if overwrite:
1122 # first reset the index to unmark new files for commit, because
1197 # first reset the index to unmark new files for commit, because
1123 # reset --hard will otherwise throw away files added for commit,
1198 # reset --hard will otherwise throw away files added for commit,
1124 # not just unmark them.
1199 # not just unmark them.
1125 self._gitcommand(['reset', 'HEAD'])
1200 self._gitcommand(['reset', 'HEAD'])
1126 self._gitcommand(['reset', '--hard', 'HEAD'])
1201 self._gitcommand(['reset', '--hard', 'HEAD'])
1127 return
1202 return
1128 branch2rev, rev2branch = self._gitbranchmap()
1203 branch2rev, rev2branch = self._gitbranchmap()
1129
1204
1130 def checkout(args):
1205 def checkout(args):
1131 cmd = ['checkout']
1206 cmd = ['checkout']
1132 if overwrite:
1207 if overwrite:
1133 # first reset the index to unmark new files for commit, because
1208 # first reset the index to unmark new files for commit, because
1134 # the -f option will otherwise throw away files added for
1209 # the -f option will otherwise throw away files added for
1135 # commit, not just unmark them.
1210 # commit, not just unmark them.
1136 self._gitcommand(['reset', 'HEAD'])
1211 self._gitcommand(['reset', 'HEAD'])
1137 cmd.append('-f')
1212 cmd.append('-f')
1138 self._gitcommand(cmd + args)
1213 self._gitcommand(cmd + args)
1139
1214
1140 def rawcheckout():
1215 def rawcheckout():
1141 # no branch to checkout, check it out with no branch
1216 # no branch to checkout, check it out with no branch
1142 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1217 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1143 self._relpath)
1218 self._relpath)
1144 self._ui.warn(_('check out a git branch if you intend '
1219 self._ui.warn(_('check out a git branch if you intend '
1145 'to make changes\n'))
1220 'to make changes\n'))
1146 checkout(['-q', revision])
1221 checkout(['-q', revision])
1147
1222
1148 if revision not in rev2branch:
1223 if revision not in rev2branch:
1149 rawcheckout()
1224 rawcheckout()
1150 return
1225 return
1151 branches = rev2branch[revision]
1226 branches = rev2branch[revision]
1152 firstlocalbranch = None
1227 firstlocalbranch = None
1153 for b in branches:
1228 for b in branches:
1154 if b == 'refs/heads/master':
1229 if b == 'refs/heads/master':
1155 # master trumps all other branches
1230 # master trumps all other branches
1156 checkout(['refs/heads/master'])
1231 checkout(['refs/heads/master'])
1157 return
1232 return
1158 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1233 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1159 firstlocalbranch = b
1234 firstlocalbranch = b
1160 if firstlocalbranch:
1235 if firstlocalbranch:
1161 checkout([firstlocalbranch])
1236 checkout([firstlocalbranch])
1162 return
1237 return
1163
1238
1164 tracking = self._gittracking(branch2rev.keys())
1239 tracking = self._gittracking(branch2rev.keys())
1165 # choose a remote branch already tracked if possible
1240 # choose a remote branch already tracked if possible
1166 remote = branches[0]
1241 remote = branches[0]
1167 if remote not in tracking:
1242 if remote not in tracking:
1168 for b in branches:
1243 for b in branches:
1169 if b in tracking:
1244 if b in tracking:
1170 remote = b
1245 remote = b
1171 break
1246 break
1172
1247
1173 if remote not in tracking:
1248 if remote not in tracking:
1174 # create a new local tracking branch
1249 # create a new local tracking branch
1175 local = remote.split('/', 2)[2]
1250 local = remote.split('/', 2)[2]
1176 checkout(['-b', local, remote])
1251 checkout(['-b', local, remote])
1177 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1252 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1178 # When updating to a tracked remote branch,
1253 # When updating to a tracked remote branch,
1179 # if the local tracking branch is downstream of it,
1254 # if the local tracking branch is downstream of it,
1180 # a normal `git pull` would have performed a "fast-forward merge"
1255 # a normal `git pull` would have performed a "fast-forward merge"
1181 # which is equivalent to updating the local branch to the remote.
1256 # which is equivalent to updating the local branch to the remote.
1182 # Since we are only looking at branching at update, we need to
1257 # Since we are only looking at branching at update, we need to
1183 # detect this situation and perform this action lazily.
1258 # detect this situation and perform this action lazily.
1184 if tracking[remote] != self._gitcurrentbranch():
1259 if tracking[remote] != self._gitcurrentbranch():
1185 checkout([tracking[remote]])
1260 checkout([tracking[remote]])
1186 self._gitcommand(['merge', '--ff', remote])
1261 self._gitcommand(['merge', '--ff', remote])
1187 else:
1262 else:
1188 # a real merge would be required, just checkout the revision
1263 # a real merge would be required, just checkout the revision
1189 rawcheckout()
1264 rawcheckout()
1190
1265
1191 @annotatesubrepoerror
1266 @annotatesubrepoerror
1192 def commit(self, text, user, date):
1267 def commit(self, text, user, date):
1193 if self._gitmissing():
1268 if self._gitmissing():
1194 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1269 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1195 cmd = ['commit', '-a', '-m', text]
1270 cmd = ['commit', '-a', '-m', text]
1196 env = os.environ.copy()
1271 env = os.environ.copy()
1197 if user:
1272 if user:
1198 cmd += ['--author', user]
1273 cmd += ['--author', user]
1199 if date:
1274 if date:
1200 # git's date parser silently ignores when seconds < 1e9
1275 # git's date parser silently ignores when seconds < 1e9
1201 # convert to ISO8601
1276 # convert to ISO8601
1202 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1277 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1203 '%Y-%m-%dT%H:%M:%S %1%2')
1278 '%Y-%m-%dT%H:%M:%S %1%2')
1204 self._gitcommand(cmd, env=env)
1279 self._gitcommand(cmd, env=env)
1205 # make sure commit works otherwise HEAD might not exist under certain
1280 # make sure commit works otherwise HEAD might not exist under certain
1206 # circumstances
1281 # circumstances
1207 return self._gitstate()
1282 return self._gitstate()
1208
1283
1209 @annotatesubrepoerror
1284 @annotatesubrepoerror
1210 def merge(self, state):
1285 def merge(self, state):
1211 source, revision, kind = state
1286 source, revision, kind = state
1212 self._fetch(source, revision)
1287 self._fetch(source, revision)
1213 base = self._gitcommand(['merge-base', revision, self._state[1]])
1288 base = self._gitcommand(['merge-base', revision, self._state[1]])
1214 self._gitupdatestat()
1289 self._gitupdatestat()
1215 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1290 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1216
1291
1217 def mergefunc():
1292 def mergefunc():
1218 if base == revision:
1293 if base == revision:
1219 self.get(state) # fast forward merge
1294 self.get(state) # fast forward merge
1220 elif base != self._state[1]:
1295 elif base != self._state[1]:
1221 self._gitcommand(['merge', '--no-commit', revision])
1296 self._gitcommand(['merge', '--no-commit', revision])
1222
1297
1223 if self.dirty():
1298 if self.dirty():
1224 if self._gitstate() != revision:
1299 if self._gitstate() != revision:
1225 dirty = self._gitstate() == self._state[1] or code != 0
1300 dirty = self._gitstate() == self._state[1] or code != 0
1226 if _updateprompt(self._ui, self, dirty,
1301 if _updateprompt(self._ui, self, dirty,
1227 self._state[1][:7], revision[:7]):
1302 self._state[1][:7], revision[:7]):
1228 mergefunc()
1303 mergefunc()
1229 else:
1304 else:
1230 mergefunc()
1305 mergefunc()
1231
1306
1232 @annotatesubrepoerror
1307 @annotatesubrepoerror
1233 def push(self, opts):
1308 def push(self, opts):
1234 force = opts.get('force')
1309 force = opts.get('force')
1235
1310
1236 if not self._state[1]:
1311 if not self._state[1]:
1237 return True
1312 return True
1238 if self._gitmissing():
1313 if self._gitmissing():
1239 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1314 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1240 # if a branch in origin contains the revision, nothing to do
1315 # if a branch in origin contains the revision, nothing to do
1241 branch2rev, rev2branch = self._gitbranchmap()
1316 branch2rev, rev2branch = self._gitbranchmap()
1242 if self._state[1] in rev2branch:
1317 if self._state[1] in rev2branch:
1243 for b in rev2branch[self._state[1]]:
1318 for b in rev2branch[self._state[1]]:
1244 if b.startswith('refs/remotes/origin/'):
1319 if b.startswith('refs/remotes/origin/'):
1245 return True
1320 return True
1246 for b, revision in branch2rev.iteritems():
1321 for b, revision in branch2rev.iteritems():
1247 if b.startswith('refs/remotes/origin/'):
1322 if b.startswith('refs/remotes/origin/'):
1248 if self._gitisancestor(self._state[1], revision):
1323 if self._gitisancestor(self._state[1], revision):
1249 return True
1324 return True
1250 # otherwise, try to push the currently checked out branch
1325 # otherwise, try to push the currently checked out branch
1251 cmd = ['push']
1326 cmd = ['push']
1252 if force:
1327 if force:
1253 cmd.append('--force')
1328 cmd.append('--force')
1254
1329
1255 current = self._gitcurrentbranch()
1330 current = self._gitcurrentbranch()
1256 if current:
1331 if current:
1257 # determine if the current branch is even useful
1332 # determine if the current branch is even useful
1258 if not self._gitisancestor(self._state[1], current):
1333 if not self._gitisancestor(self._state[1], current):
1259 self._ui.warn(_('unrelated git branch checked out '
1334 self._ui.warn(_('unrelated git branch checked out '
1260 'in subrepo %s\n') % self._relpath)
1335 'in subrepo %s\n') % self._relpath)
1261 return False
1336 return False
1262 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1337 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1263 (current.split('/', 2)[2], self._relpath))
1338 (current.split('/', 2)[2], self._relpath))
1264 self._gitcommand(cmd + ['origin', current])
1339 self._gitcommand(cmd + ['origin', current])
1265 return True
1340 return True
1266 else:
1341 else:
1267 self._ui.warn(_('no branch checked out in subrepo %s\n'
1342 self._ui.warn(_('no branch checked out in subrepo %s\n'
1268 'cannot push revision %s\n') %
1343 'cannot push revision %s\n') %
1269 (self._relpath, self._state[1]))
1344 (self._relpath, self._state[1]))
1270 return False
1345 return False
1271
1346
1272 @annotatesubrepoerror
1347 @annotatesubrepoerror
1273 def remove(self):
1348 def remove(self):
1274 if self._gitmissing():
1349 if self._gitmissing():
1275 return
1350 return
1276 if self.dirty():
1351 if self.dirty():
1277 self._ui.warn(_('not removing repo %s because '
1352 self._ui.warn(_('not removing repo %s because '
1278 'it has changes.\n') % self._relpath)
1353 'it has changes.\n') % self._relpath)
1279 return
1354 return
1280 # we can't fully delete the repository as it may contain
1355 # we can't fully delete the repository as it may contain
1281 # local-only history
1356 # local-only history
1282 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1357 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1283 self._gitcommand(['config', 'core.bare', 'true'])
1358 self._gitcommand(['config', 'core.bare', 'true'])
1284 for f in os.listdir(self._abspath):
1359 for f in os.listdir(self._abspath):
1285 if f == '.git':
1360 if f == '.git':
1286 continue
1361 continue
1287 path = os.path.join(self._abspath, f)
1362 path = os.path.join(self._abspath, f)
1288 if os.path.isdir(path) and not os.path.islink(path):
1363 if os.path.isdir(path) and not os.path.islink(path):
1289 shutil.rmtree(path)
1364 shutil.rmtree(path)
1290 else:
1365 else:
1291 os.remove(path)
1366 os.remove(path)
1292
1367
1293 def archive(self, ui, archiver, prefix, match=None):
1368 def archive(self, ui, archiver, prefix, match=None):
1294 source, revision = self._state
1369 source, revision = self._state
1295 if not revision:
1370 if not revision:
1296 return
1371 return
1297 self._fetch(source, revision)
1372 self._fetch(source, revision)
1298
1373
1299 # Parse git's native archive command.
1374 # Parse git's native archive command.
1300 # This should be much faster than manually traversing the trees
1375 # This should be much faster than manually traversing the trees
1301 # and objects with many subprocess calls.
1376 # and objects with many subprocess calls.
1302 tarstream = self._gitcommand(['archive', revision], stream=True)
1377 tarstream = self._gitcommand(['archive', revision], stream=True)
1303 tar = tarfile.open(fileobj=tarstream, mode='r|')
1378 tar = tarfile.open(fileobj=tarstream, mode='r|')
1304 relpath = subrelpath(self)
1379 relpath = subrelpath(self)
1305 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1380 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1306 for i, info in enumerate(tar):
1381 for i, info in enumerate(tar):
1307 if info.isdir():
1382 if info.isdir():
1308 continue
1383 continue
1309 if match and not match(info.name):
1384 if match and not match(info.name):
1310 continue
1385 continue
1311 if info.issym():
1386 if info.issym():
1312 data = info.linkname
1387 data = info.linkname
1313 else:
1388 else:
1314 data = tar.extractfile(info).read()
1389 data = tar.extractfile(info).read()
1315 archiver.addfile(os.path.join(prefix, self._path, info.name),
1390 archiver.addfile(os.path.join(prefix, self._path, info.name),
1316 info.mode, info.issym(), data)
1391 info.mode, info.issym(), data)
1317 ui.progress(_('archiving (%s)') % relpath, i + 1,
1392 ui.progress(_('archiving (%s)') % relpath, i + 1,
1318 unit=_('files'))
1393 unit=_('files'))
1319 ui.progress(_('archiving (%s)') % relpath, None)
1394 ui.progress(_('archiving (%s)') % relpath, None)
1320
1395
1321
1396
1322 @annotatesubrepoerror
1397 @annotatesubrepoerror
1323 def status(self, rev2, **opts):
1398 def status(self, rev2, **opts):
1324 rev1 = self._state[1]
1399 rev1 = self._state[1]
1325 if self._gitmissing() or not rev1:
1400 if self._gitmissing() or not rev1:
1326 # if the repo is missing, return no results
1401 # if the repo is missing, return no results
1327 return [], [], [], [], [], [], []
1402 return [], [], [], [], [], [], []
1328 modified, added, removed = [], [], []
1403 modified, added, removed = [], [], []
1329 self._gitupdatestat()
1404 self._gitupdatestat()
1330 if rev2:
1405 if rev2:
1331 command = ['diff-tree', rev1, rev2]
1406 command = ['diff-tree', rev1, rev2]
1332 else:
1407 else:
1333 command = ['diff-index', rev1]
1408 command = ['diff-index', rev1]
1334 out = self._gitcommand(command)
1409 out = self._gitcommand(command)
1335 for line in out.split('\n'):
1410 for line in out.split('\n'):
1336 tab = line.find('\t')
1411 tab = line.find('\t')
1337 if tab == -1:
1412 if tab == -1:
1338 continue
1413 continue
1339 status, f = line[tab - 1], line[tab + 1:]
1414 status, f = line[tab - 1], line[tab + 1:]
1340 if status == 'M':
1415 if status == 'M':
1341 modified.append(f)
1416 modified.append(f)
1342 elif status == 'A':
1417 elif status == 'A':
1343 added.append(f)
1418 added.append(f)
1344 elif status == 'D':
1419 elif status == 'D':
1345 removed.append(f)
1420 removed.append(f)
1346
1421
1347 deleted = unknown = ignored = clean = []
1422 deleted = unknown = ignored = clean = []
1348 return modified, added, removed, deleted, unknown, ignored, clean
1423 return modified, added, removed, deleted, unknown, ignored, clean
1349
1424
1350 types = {
1425 types = {
1351 'hg': hgsubrepo,
1426 'hg': hgsubrepo,
1352 'svn': svnsubrepo,
1427 'svn': svnsubrepo,
1353 'git': gitsubrepo,
1428 'git': gitsubrepo,
1354 }
1429 }
General Comments 0
You need to be logged in to leave comments. Login now