##// END OF EJS Templates
subrepo: split non-core functions to new module...
Yuya Nishihara -
r36026:55e8efa2 default
parent child Browse files
Show More
@@ -94,7 +94,7 b' from mercurial import ('
94 revsetlang,
94 revsetlang,
95 scmutil,
95 scmutil,
96 smartset,
96 smartset,
97 subrepo,
97 subrepoutil,
98 util,
98 util,
99 vfs as vfsmod,
99 vfs as vfsmod,
100 )
100 )
@@ -970,8 +970,8 b' class queue(object):'
970 wctx = repo[None]
970 wctx = repo[None]
971 pctx = repo['.']
971 pctx = repo['.']
972 overwrite = False
972 overwrite = False
973 mergedsubstate = subrepo.submerge(repo, pctx, wctx, wctx,
973 mergedsubstate = subrepoutil.submerge(repo, pctx, wctx, wctx,
974 overwrite)
974 overwrite)
975 files += mergedsubstate.keys()
975 files += mergedsubstate.keys()
976
976
977 match = scmutil.matchfiles(repo, files or [])
977 match = scmutil.matchfiles(repo, files or [])
@@ -40,6 +40,7 b' from . import ('
40 rewriteutil,
40 rewriteutil,
41 scmutil,
41 scmutil,
42 smartset,
42 smartset,
43 subrepoutil,
43 templater,
44 templater,
44 util,
45 util,
45 vfs as vfsmod,
46 vfs as vfsmod,
@@ -2307,13 +2308,12 b' def amend(ui, repo, old, extra, pats, op'
2307 # subrepo.precommit(). To minimize the risk of this hack, we do
2308 # subrepo.precommit(). To minimize the risk of this hack, we do
2308 # nothing if .hgsub does not exist.
2309 # nothing if .hgsub does not exist.
2309 if '.hgsub' in wctx or '.hgsub' in old:
2310 if '.hgsub' in wctx or '.hgsub' in old:
2310 from . import subrepo # avoid cycle: cmdutil -> subrepo -> cmdutil
2311 subs, commitsubs, newsubstate = subrepoutil.precommit(
2311 subs, commitsubs, newsubstate = subrepo.precommit(
2312 ui, wctx, wctx._status, matcher)
2312 ui, wctx, wctx._status, matcher)
2313 # amend should abort if commitsubrepos is enabled
2313 # amend should abort if commitsubrepos is enabled
2314 assert not commitsubs
2314 assert not commitsubs
2315 if subs:
2315 if subs:
2316 subrepo.writestate(repo, newsubstate)
2316 subrepoutil.writestate(repo, newsubstate)
2317
2317
2318 filestoamend = set(f for f in wctx.files() if matcher(f))
2318 filestoamend = set(f for f in wctx.files() if matcher(f))
2319
2319
@@ -46,6 +46,7 b' from . import ('
46 scmutil,
46 scmutil,
47 sparse,
47 sparse,
48 subrepo,
48 subrepo,
49 subrepoutil,
49 util,
50 util,
50 )
51 )
51
52
@@ -173,7 +174,7 b' class basectx(object):'
173
174
174 @propertycache
175 @propertycache
175 def substate(self):
176 def substate(self):
176 return subrepo.state(self, self._repo.ui)
177 return subrepoutil.state(self, self._repo.ui)
177
178
178 def subrev(self, subpath):
179 def subrev(self, subpath):
179 return self.substate[subpath][1]
180 return self.substate[subpath][1]
@@ -57,7 +57,7 b' from . import ('
57 scmutil,
57 scmutil,
58 sparse,
58 sparse,
59 store,
59 store,
60 subrepo,
60 subrepoutil,
61 tags as tagsmod,
61 tags as tagsmod,
62 transaction,
62 transaction,
63 txnutil,
63 txnutil,
@@ -1833,7 +1833,7 b' class localrepository(object):'
1833 status.modified.extend(status.clean) # mq may commit clean files
1833 status.modified.extend(status.clean) # mq may commit clean files
1834
1834
1835 # check subrepos
1835 # check subrepos
1836 subs, commitsubs, newstate = subrepo.precommit(
1836 subs, commitsubs, newstate = subrepoutil.precommit(
1837 self.ui, wctx, status, match, force=force)
1837 self.ui, wctx, status, match, force=force)
1838
1838
1839 # make sure all explicit patterns are matched
1839 # make sure all explicit patterns are matched
@@ -1870,10 +1870,10 b' class localrepository(object):'
1870 for s in sorted(commitsubs):
1870 for s in sorted(commitsubs):
1871 sub = wctx.sub(s)
1871 sub = wctx.sub(s)
1872 self.ui.status(_('committing subrepository %s\n') %
1872 self.ui.status(_('committing subrepository %s\n') %
1873 subrepo.subrelpath(sub))
1873 subrepoutil.subrelpath(sub))
1874 sr = sub.commit(cctx._text, user, date)
1874 sr = sub.commit(cctx._text, user, date)
1875 newstate[s] = (newstate[s][0], sr)
1875 newstate[s] = (newstate[s][0], sr)
1876 subrepo.writestate(self, newstate)
1876 subrepoutil.writestate(self, newstate)
1877
1877
1878 p1, p2 = self.dirstate.parents()
1878 p1, p2 = self.dirstate.parents()
1879 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
1879 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
@@ -1983,7 +1983,7 b' class localrepository(object):'
1983 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
1983 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
1984 parent2=xp2)
1984 parent2=xp2)
1985 # set the new commit is proper phase
1985 # set the new commit is proper phase
1986 targetphase = subrepo.newcommitphase(self.ui, ctx)
1986 targetphase = subrepoutil.newcommitphase(self.ui, ctx)
1987 if targetphase:
1987 if targetphase:
1988 # retract boundary do not alter parent changeset.
1988 # retract boundary do not alter parent changeset.
1989 # if a parent have higher the resulting phase will
1989 # if a parent have higher the resulting phase will
@@ -31,7 +31,7 b' from . import ('
31 obsutil,
31 obsutil,
32 pycompat,
32 pycompat,
33 scmutil,
33 scmutil,
34 subrepo,
34 subrepoutil,
35 util,
35 util,
36 worker,
36 worker,
37 )
37 )
@@ -1445,7 +1445,7 b' def applyupdates(repo, actions, wctx, mc'
1445 z = 0
1445 z = 0
1446
1446
1447 if [a for a in actions['r'] if a[0] == '.hgsubstate']:
1447 if [a for a in actions['r'] if a[0] == '.hgsubstate']:
1448 subrepo.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1448 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1449
1449
1450 # record path conflicts
1450 # record path conflicts
1451 for f, args, msg in actions['p']:
1451 for f, args, msg in actions['p']:
@@ -1495,7 +1495,7 b' def applyupdates(repo, actions, wctx, mc'
1495 updated = len(actions['g'])
1495 updated = len(actions['g'])
1496
1496
1497 if [a for a in actions['g'] if a[0] == '.hgsubstate']:
1497 if [a for a in actions['g'] if a[0] == '.hgsubstate']:
1498 subrepo.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1498 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1499
1499
1500 # forget (manifest only, just log it) (must come first)
1500 # forget (manifest only, just log it) (must come first)
1501 for f, args, msg in actions['f']:
1501 for f, args, msg in actions['f']:
@@ -1583,8 +1583,8 b' def applyupdates(repo, actions, wctx, mc'
1583 z += 1
1583 z += 1
1584 progress(_updating, z, item=f, total=numupdates, unit=_files)
1584 progress(_updating, z, item=f, total=numupdates, unit=_files)
1585 if f == '.hgsubstate': # subrepo states need updating
1585 if f == '.hgsubstate': # subrepo states need updating
1586 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
1586 subrepoutil.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
1587 overwrite, labels)
1587 overwrite, labels)
1588 continue
1588 continue
1589 wctx[f].audit()
1589 wctx[f].audit()
1590 complete, r = ms.preresolve(f, wctx)
1590 complete, r = ms.preresolve(f, wctx)
@@ -1913,7 +1913,7 b' def update(repo, node, branchmerge, forc'
1913
1913
1914 # Prompt and create actions. Most of this is in the resolve phase
1914 # Prompt and create actions. Most of this is in the resolve phase
1915 # already, but we can't handle .hgsubstate in filemerge or
1915 # already, but we can't handle .hgsubstate in filemerge or
1916 # subrepo.submerge yet so we have to keep prompting for it.
1916 # subrepoutil.submerge yet so we have to keep prompting for it.
1917 if '.hgsubstate' in actionbyfile:
1917 if '.hgsubstate' in actionbyfile:
1918 f = '.hgsubstate'
1918 f = '.hgsubstate'
1919 m, args, msg = actionbyfile[f]
1919 m, args, msg = actionbyfile[f]
@@ -1,4 +1,4 b''
1 # subrepo.py - sub-repository handling for Mercurial
1 # subrepo.py - sub-repository classes and factory
2 #
2 #
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
@@ -19,15 +19,12 b' import sys'
19 import tarfile
19 import tarfile
20 import xml.dom.minidom
20 import xml.dom.minidom
21
21
22
23 from .i18n import _
22 from .i18n import _
24 from . import (
23 from . import (
25 cmdutil,
24 cmdutil,
26 config,
27 encoding,
25 encoding,
28 error,
26 error,
29 exchange,
27 exchange,
30 filemerge,
31 logcmdutil,
28 logcmdutil,
32 match as matchmod,
29 match as matchmod,
33 node,
30 node,
@@ -35,15 +32,17 b' from . import ('
35 phases,
32 phases,
36 pycompat,
33 pycompat,
37 scmutil,
34 scmutil,
35 subrepoutil,
38 util,
36 util,
39 vfs as vfsmod,
37 vfs as vfsmod,
40 )
38 )
41
39
42 hg = None
40 hg = None
41 reporelpath = subrepoutil.reporelpath
42 subrelpath = subrepoutil.subrelpath
43 _abssource = subrepoutil._abssource
43 propertycache = util.propertycache
44 propertycache = util.propertycache
44
45
45 nullstate = ('', '', 'empty')
46
47 def _expandedabspath(path):
46 def _expandedabspath(path):
48 '''
47 '''
49 get a path or url and if it is a path expand it and return an absolute path
48 get a path or url and if it is a path expand it and return an absolute path
@@ -81,284 +80,6 b' def annotatesubrepoerror(func):'
81 return res
80 return res
82 return decoratedmethod
81 return decoratedmethod
83
82
84 def state(ctx, ui):
85 """return a state dict, mapping subrepo paths configured in .hgsub
86 to tuple: (source from .hgsub, revision from .hgsubstate, kind
87 (key in types dict))
88 """
89 p = config.config()
90 repo = ctx.repo()
91 def read(f, sections=None, remap=None):
92 if f in ctx:
93 try:
94 data = ctx[f].data()
95 except IOError as err:
96 if err.errno != errno.ENOENT:
97 raise
98 # handle missing subrepo spec files as removed
99 ui.warn(_("warning: subrepo spec file \'%s\' not found\n") %
100 repo.pathto(f))
101 return
102 p.parse(f, data, sections, remap, read)
103 else:
104 raise error.Abort(_("subrepo spec file \'%s\' not found") %
105 repo.pathto(f))
106 if '.hgsub' in ctx:
107 read('.hgsub')
108
109 for path, src in ui.configitems('subpaths'):
110 p.set('subpaths', path, src, ui.configsource('subpaths', path))
111
112 rev = {}
113 if '.hgsubstate' in ctx:
114 try:
115 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
116 l = l.lstrip()
117 if not l:
118 continue
119 try:
120 revision, path = l.split(" ", 1)
121 except ValueError:
122 raise error.Abort(_("invalid subrepository revision "
123 "specifier in \'%s\' line %d")
124 % (repo.pathto('.hgsubstate'), (i + 1)))
125 rev[path] = revision
126 except IOError as err:
127 if err.errno != errno.ENOENT:
128 raise
129
130 def remap(src):
131 for pattern, repl in p.items('subpaths'):
132 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
133 # does a string decode.
134 repl = util.escapestr(repl)
135 # However, we still want to allow back references to go
136 # through unharmed, so we turn r'\\1' into r'\1'. Again,
137 # extra escapes are needed because re.sub string decodes.
138 repl = re.sub(br'\\\\([0-9]+)', br'\\\1', repl)
139 try:
140 src = re.sub(pattern, repl, src, 1)
141 except re.error as e:
142 raise error.Abort(_("bad subrepository pattern in %s: %s")
143 % (p.source('subpaths', pattern), e))
144 return src
145
146 state = {}
147 for path, src in p[''].items():
148 kind = 'hg'
149 if src.startswith('['):
150 if ']' not in src:
151 raise error.Abort(_('missing ] in subrepository source'))
152 kind, src = src.split(']', 1)
153 kind = kind[1:]
154 src = src.lstrip() # strip any extra whitespace after ']'
155
156 if not util.url(src).isabs():
157 parent = _abssource(repo, abort=False)
158 if parent:
159 parent = util.url(parent)
160 parent.path = posixpath.join(parent.path or '', src)
161 parent.path = posixpath.normpath(parent.path)
162 joined = str(parent)
163 # Remap the full joined path and use it if it changes,
164 # else remap the original source.
165 remapped = remap(joined)
166 if remapped == joined:
167 src = remap(src)
168 else:
169 src = remapped
170
171 src = remap(src)
172 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
173
174 return state
175
176 def writestate(repo, state):
177 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
178 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)
179 if state[s][1] != nullstate[1]]
180 repo.wwrite('.hgsubstate', ''.join(lines), '')
181
182 def submerge(repo, wctx, mctx, actx, overwrite, labels=None):
183 """delegated from merge.applyupdates: merging of .hgsubstate file
184 in working context, merging context and ancestor context"""
185 if mctx == actx: # backwards?
186 actx = wctx.p1()
187 s1 = wctx.substate
188 s2 = mctx.substate
189 sa = actx.substate
190 sm = {}
191
192 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
193
194 def debug(s, msg, r=""):
195 if r:
196 r = "%s:%s:%s" % r
197 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
198
199 promptssrc = filemerge.partextras(labels)
200 for s, l in sorted(s1.iteritems()):
201 prompts = None
202 a = sa.get(s, nullstate)
203 ld = l # local state with possible dirty flag for compares
204 if wctx.sub(s).dirty():
205 ld = (l[0], l[1] + "+")
206 if wctx == actx: # overwrite
207 a = ld
208
209 prompts = promptssrc.copy()
210 prompts['s'] = s
211 if s in s2:
212 r = s2[s]
213 if ld == r or r == a: # no change or local is newer
214 sm[s] = l
215 continue
216 elif ld == a: # other side changed
217 debug(s, "other changed, get", r)
218 wctx.sub(s).get(r, overwrite)
219 sm[s] = r
220 elif ld[0] != r[0]: # sources differ
221 prompts['lo'] = l[0]
222 prompts['ro'] = r[0]
223 if repo.ui.promptchoice(
224 _(' subrepository sources for %(s)s differ\n'
225 'use (l)ocal%(l)s source (%(lo)s)'
226 ' or (r)emote%(o)s source (%(ro)s)?'
227 '$$ &Local $$ &Remote') % prompts, 0):
228 debug(s, "prompt changed, get", r)
229 wctx.sub(s).get(r, overwrite)
230 sm[s] = r
231 elif ld[1] == a[1]: # local side is unchanged
232 debug(s, "other side changed, get", r)
233 wctx.sub(s).get(r, overwrite)
234 sm[s] = r
235 else:
236 debug(s, "both sides changed")
237 srepo = wctx.sub(s)
238 prompts['sl'] = srepo.shortid(l[1])
239 prompts['sr'] = srepo.shortid(r[1])
240 option = repo.ui.promptchoice(
241 _(' subrepository %(s)s diverged (local revision: %(sl)s, '
242 'remote revision: %(sr)s)\n'
243 '(M)erge, keep (l)ocal%(l)s or keep (r)emote%(o)s?'
244 '$$ &Merge $$ &Local $$ &Remote')
245 % prompts, 0)
246 if option == 0:
247 wctx.sub(s).merge(r)
248 sm[s] = l
249 debug(s, "merge with", r)
250 elif option == 1:
251 sm[s] = l
252 debug(s, "keep local subrepo revision", l)
253 else:
254 wctx.sub(s).get(r, overwrite)
255 sm[s] = r
256 debug(s, "get remote subrepo revision", r)
257 elif ld == a: # remote removed, local unchanged
258 debug(s, "remote removed, remove")
259 wctx.sub(s).remove()
260 elif a == nullstate: # not present in remote or ancestor
261 debug(s, "local added, keep")
262 sm[s] = l
263 continue
264 else:
265 if repo.ui.promptchoice(
266 _(' local%(l)s changed subrepository %(s)s'
267 ' which remote%(o)s removed\n'
268 'use (c)hanged version or (d)elete?'
269 '$$ &Changed $$ &Delete') % prompts, 0):
270 debug(s, "prompt remove")
271 wctx.sub(s).remove()
272
273 for s, r in sorted(s2.items()):
274 prompts = None
275 if s in s1:
276 continue
277 elif s not in sa:
278 debug(s, "remote added, get", r)
279 mctx.sub(s).get(r)
280 sm[s] = r
281 elif r != sa[s]:
282 prompts = promptssrc.copy()
283 prompts['s'] = s
284 if repo.ui.promptchoice(
285 _(' remote%(o)s changed subrepository %(s)s'
286 ' which local%(l)s removed\n'
287 'use (c)hanged version or (d)elete?'
288 '$$ &Changed $$ &Delete') % prompts, 0) == 0:
289 debug(s, "prompt recreate", r)
290 mctx.sub(s).get(r)
291 sm[s] = r
292
293 # record merged .hgsubstate
294 writestate(repo, sm)
295 return sm
296
297 def precommit(ui, wctx, status, match, force=False):
298 """Calculate .hgsubstate changes that should be applied before committing
299
300 Returns (subs, commitsubs, newstate) where
301 - subs: changed subrepos (including dirty ones)
302 - commitsubs: dirty subrepos which the caller needs to commit recursively
303 - newstate: new state dict which the caller must write to .hgsubstate
304
305 This also updates the given status argument.
306 """
307 subs = []
308 commitsubs = set()
309 newstate = wctx.substate.copy()
310
311 # only manage subrepos and .hgsubstate if .hgsub is present
312 if '.hgsub' in wctx:
313 # we'll decide whether to track this ourselves, thanks
314 for c in status.modified, status.added, status.removed:
315 if '.hgsubstate' in c:
316 c.remove('.hgsubstate')
317
318 # compare current state to last committed state
319 # build new substate based on last committed state
320 oldstate = wctx.p1().substate
321 for s in sorted(newstate.keys()):
322 if not match(s):
323 # ignore working copy, use old state if present
324 if s in oldstate:
325 newstate[s] = oldstate[s]
326 continue
327 if not force:
328 raise error.Abort(
329 _("commit with new subrepo %s excluded") % s)
330 dirtyreason = wctx.sub(s).dirtyreason(True)
331 if dirtyreason:
332 if not ui.configbool('ui', 'commitsubrepos'):
333 raise error.Abort(dirtyreason,
334 hint=_("use --subrepos for recursive commit"))
335 subs.append(s)
336 commitsubs.add(s)
337 else:
338 bs = wctx.sub(s).basestate()
339 newstate[s] = (newstate[s][0], bs, newstate[s][2])
340 if oldstate.get(s, (None, None, None))[1] != bs:
341 subs.append(s)
342
343 # check for removed subrepos
344 for p in wctx.parents():
345 r = [s for s in p.substate if s not in newstate]
346 subs += [s for s in r if match(s)]
347 if subs:
348 if (not match('.hgsub') and
349 '.hgsub' in (wctx.modified() + wctx.added())):
350 raise error.Abort(_("can't commit subrepos without .hgsub"))
351 status.modified.insert(0, '.hgsubstate')
352
353 elif '.hgsub' in status.removed:
354 # clean up .hgsubstate when .hgsub is removed
355 if ('.hgsubstate' in wctx and
356 '.hgsubstate' not in (status.modified + status.added +
357 status.removed)):
358 status.removed.insert(0, '.hgsubstate')
359
360 return subs, commitsubs, newstate
361
362 def _updateprompt(ui, sub, dirty, local, remote):
83 def _updateprompt(ui, sub, dirty, local, remote):
363 if dirty:
84 if dirty:
364 msg = (_(' subrepository sources for %s differ\n'
85 msg = (_(' subrepository sources for %s differ\n'
@@ -373,64 +94,6 b' def _updateprompt(ui, sub, dirty, local,'
373 % (subrelpath(sub), local, remote))
94 % (subrelpath(sub), local, remote))
374 return ui.promptchoice(msg, 0)
95 return ui.promptchoice(msg, 0)
375
96
376 def reporelpath(repo):
377 """return path to this (sub)repo as seen from outermost repo"""
378 parent = repo
379 while util.safehasattr(parent, '_subparent'):
380 parent = parent._subparent
381 return repo.root[len(pathutil.normasprefix(parent.root)):]
382
383 def subrelpath(sub):
384 """return path to this subrepo as seen from outermost repo"""
385 return sub._relpath
386
387 def _abssource(repo, push=False, abort=True):
388 """return pull/push path of repo - either based on parent repo .hgsub info
389 or on the top repo config. Abort or return None if no source found."""
390 if util.safehasattr(repo, '_subparent'):
391 source = util.url(repo._subsource)
392 if source.isabs():
393 return bytes(source)
394 source.path = posixpath.normpath(source.path)
395 parent = _abssource(repo._subparent, push, abort=False)
396 if parent:
397 parent = util.url(util.pconvert(parent))
398 parent.path = posixpath.join(parent.path or '', source.path)
399 parent.path = posixpath.normpath(parent.path)
400 return bytes(parent)
401 else: # recursion reached top repo
402 path = None
403 if util.safehasattr(repo, '_subtoppath'):
404 path = repo._subtoppath
405 elif push and repo.ui.config('paths', 'default-push'):
406 path = repo.ui.config('paths', 'default-push')
407 elif repo.ui.config('paths', 'default'):
408 path = repo.ui.config('paths', 'default')
409 elif repo.shared():
410 # chop off the .hg component to get the default path form. This has
411 # already run through vfsmod.vfs(..., realpath=True), so it doesn't
412 # have problems with 'C:'
413 return os.path.dirname(repo.sharedpath)
414 if path:
415 # issue5770: 'C:\' and 'C:' are not equivalent paths. The former is
416 # as expected: an absolute path to the root of the C: drive. The
417 # latter is a relative path, and works like so:
418 #
419 # C:\>cd C:\some\path
420 # C:\>D:
421 # D:\>python -c "import os; print os.path.abspath('C:')"
422 # C:\some\path
423 #
424 # D:\>python -c "import os; print os.path.abspath('C:relative')"
425 # C:\some\path\relative
426 if util.hasdriveletter(path):
427 if len(path) == 2 or path[2:3] not in br'\/':
428 path = os.path.abspath(path)
429 return path
430
431 if abort:
432 raise error.Abort(_("default path for subrepository not found"))
433
434 def _sanitize(ui, vfs, ignore):
97 def _sanitize(ui, vfs, ignore):
435 for dirname, dirs, names in vfs.walk():
98 for dirname, dirs, names in vfs.walk():
436 for i, d in enumerate(dirs):
99 for i, d in enumerate(dirs):
@@ -509,37 +172,6 b' def nullsubrepo(ctx, path, pctx):'
509 subrev = "0" * 40
172 subrev = "0" * 40
510 return types[state[2]](pctx, path, (state[0], subrev), True)
173 return types[state[2]](pctx, path, (state[0], subrev), True)
511
174
512 def newcommitphase(ui, ctx):
513 commitphase = phases.newcommitphase(ui)
514 substate = getattr(ctx, "substate", None)
515 if not substate:
516 return commitphase
517 check = ui.config('phases', 'checksubrepos')
518 if check not in ('ignore', 'follow', 'abort'):
519 raise error.Abort(_('invalid phases.checksubrepos configuration: %s')
520 % (check))
521 if check == 'ignore':
522 return commitphase
523 maxphase = phases.public
524 maxsub = None
525 for s in sorted(substate):
526 sub = ctx.sub(s)
527 subphase = sub.phase(substate[s][1])
528 if maxphase < subphase:
529 maxphase = subphase
530 maxsub = s
531 if commitphase < maxphase:
532 if check == 'abort':
533 raise error.Abort(_("can't commit in %s phase"
534 " conflicting %s from subrepository %s") %
535 (phases.phasenames[commitphase],
536 phases.phasenames[maxphase], maxsub))
537 ui.warn(_("warning: changes are committed in"
538 " %s phase from subrepository %s\n") %
539 (phases.phasenames[maxphase], maxsub))
540 return maxphase
541 return commitphase
542
543 # subrepo classes need to implement the following abstract class:
175 # subrepo classes need to implement the following abstract class:
544
176
545 class abstractsubrepo(object):
177 class abstractsubrepo(object):
This diff has been collapsed as it changes many lines, (1765 lines changed) Show them Hide them
@@ -1,4 +1,4 b''
1 # subrepo.py - sub-repository handling for Mercurial
1 # subrepoutil.py - sub-repository operations and substate handling
2 #
2 #
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
@@ -7,80 +7,23 b''
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import copy
11 import errno
10 import errno
12 import hashlib
13 import os
11 import os
14 import posixpath
12 import posixpath
15 import re
13 import re
16 import stat
17 import subprocess
18 import sys
19 import tarfile
20 import xml.dom.minidom
21
22
14
23 from .i18n import _
15 from .i18n import _
24 from . import (
16 from . import (
25 cmdutil,
26 config,
17 config,
27 encoding,
28 error,
18 error,
29 exchange,
30 filemerge,
19 filemerge,
31 logcmdutil,
32 match as matchmod,
33 node,
34 pathutil,
20 pathutil,
35 phases,
21 phases,
36 pycompat,
37 scmutil,
38 util,
22 util,
39 vfs as vfsmod,
40 )
23 )
41
24
42 hg = None
43 propertycache = util.propertycache
44
45 nullstate = ('', '', 'empty')
25 nullstate = ('', '', 'empty')
46
26
47 def _expandedabspath(path):
48 '''
49 get a path or url and if it is a path expand it and return an absolute path
50 '''
51 expandedpath = util.urllocalpath(util.expandpath(path))
52 u = util.url(expandedpath)
53 if not u.scheme:
54 path = util.normpath(os.path.abspath(u.path))
55 return path
56
57 def _getstorehashcachename(remotepath):
58 '''get a unique filename for the store hash cache of a remote repository'''
59 return node.hex(hashlib.sha1(_expandedabspath(remotepath)).digest())[0:12]
60
61 class SubrepoAbort(error.Abort):
62 """Exception class used to avoid handling a subrepo error more than once"""
63 def __init__(self, *args, **kw):
64 self.subrepo = kw.pop(r'subrepo', None)
65 self.cause = kw.pop(r'cause', None)
66 error.Abort.__init__(self, *args, **kw)
67
68 def annotatesubrepoerror(func):
69 def decoratedmethod(self, *args, **kargs):
70 try:
71 res = func(self, *args, **kargs)
72 except SubrepoAbort as ex:
73 # This exception has already been handled
74 raise ex
75 except error.Abort as ex:
76 subrepo = subrelpath(self)
77 errormsg = str(ex) + ' ' + _('(in subrepository "%s")') % subrepo
78 # avoid handling this exception by raising a SubrepoAbort exception
79 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
80 cause=sys.exc_info())
81 return res
82 return decoratedmethod
83
84 def state(ctx, ui):
27 def state(ctx, ui):
85 """return a state dict, mapping subrepo paths configured in .hgsub
28 """return a state dict, mapping subrepo paths configured in .hgsub
86 to tuple: (source from .hgsub, revision from .hgsubstate, kind
29 to tuple: (source from .hgsub, revision from .hgsubstate, kind
@@ -359,20 +302,6 b' def precommit(ui, wctx, status, match, f'
359
302
360 return subs, commitsubs, newstate
303 return subs, commitsubs, newstate
361
304
362 def _updateprompt(ui, sub, dirty, local, remote):
363 if dirty:
364 msg = (_(' subrepository sources for %s differ\n'
365 'use (l)ocal source (%s) or (r)emote source (%s)?'
366 '$$ &Local $$ &Remote')
367 % (subrelpath(sub), local, remote))
368 else:
369 msg = (_(' subrepository sources for %s differ (in checked out '
370 'version)\n'
371 'use (l)ocal source (%s) or (r)emote source (%s)?'
372 '$$ &Local $$ &Remote')
373 % (subrelpath(sub), local, remote))
374 return ui.promptchoice(msg, 0)
375
376 def reporelpath(repo):
305 def reporelpath(repo):
377 """return path to this (sub)repo as seen from outermost repo"""
306 """return path to this (sub)repo as seen from outermost repo"""
378 parent = repo
307 parent = repo
@@ -431,84 +360,6 b' def _abssource(repo, push=False, abort=T'
431 if abort:
360 if abort:
432 raise error.Abort(_("default path for subrepository not found"))
361 raise error.Abort(_("default path for subrepository not found"))
433
362
434 def _sanitize(ui, vfs, ignore):
435 for dirname, dirs, names in vfs.walk():
436 for i, d in enumerate(dirs):
437 if d.lower() == ignore:
438 del dirs[i]
439 break
440 if vfs.basename(dirname).lower() != '.hg':
441 continue
442 for f in names:
443 if f.lower() == 'hgrc':
444 ui.warn(_("warning: removing potentially hostile 'hgrc' "
445 "in '%s'\n") % vfs.join(dirname))
446 vfs.unlink(vfs.reljoin(dirname, f))
447
448 def _auditsubrepopath(repo, path):
449 # auditor doesn't check if the path itself is a symlink
450 pathutil.pathauditor(repo.root)(path)
451 if repo.wvfs.islink(path):
452 raise error.Abort(_("subrepo '%s' traverses symbolic link") % path)
453
454 SUBREPO_ALLOWED_DEFAULTS = {
455 'hg': True,
456 'git': False,
457 'svn': False,
458 }
459
460 def _checktype(ui, kind):
461 # subrepos.allowed is a master kill switch. If disabled, subrepos are
462 # disabled period.
463 if not ui.configbool('subrepos', 'allowed', True):
464 raise error.Abort(_('subrepos not enabled'),
465 hint=_("see 'hg help config.subrepos' for details"))
466
467 default = SUBREPO_ALLOWED_DEFAULTS.get(kind, False)
468 if not ui.configbool('subrepos', '%s:allowed' % kind, default):
469 raise error.Abort(_('%s subrepos not allowed') % kind,
470 hint=_("see 'hg help config.subrepos' for details"))
471
472 if kind not in types:
473 raise error.Abort(_('unknown subrepo type %s') % kind)
474
475 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
476 """return instance of the right subrepo class for subrepo in path"""
477 # subrepo inherently violates our import layering rules
478 # because it wants to make repo objects from deep inside the stack
479 # so we manually delay the circular imports to not break
480 # scripts that don't use our demand-loading
481 global hg
482 from . import hg as h
483 hg = h
484
485 repo = ctx.repo()
486 _auditsubrepopath(repo, path)
487 state = ctx.substate[path]
488 _checktype(repo.ui, state[2])
489 if allowwdir:
490 state = (state[0], ctx.subrev(path), state[2])
491 return types[state[2]](ctx, path, state[:2], allowcreate)
492
493 def nullsubrepo(ctx, path, pctx):
494 """return an empty subrepo in pctx for the extant subrepo in ctx"""
495 # subrepo inherently violates our import layering rules
496 # because it wants to make repo objects from deep inside the stack
497 # so we manually delay the circular imports to not break
498 # scripts that don't use our demand-loading
499 global hg
500 from . import hg as h
501 hg = h
502
503 repo = ctx.repo()
504 _auditsubrepopath(repo, path)
505 state = ctx.substate[path]
506 _checktype(repo.ui, state[2])
507 subrev = ''
508 if state[2] == 'hg':
509 subrev = "0" * 40
510 return types[state[2]](pctx, path, (state[0], subrev), True)
511
512 def newcommitphase(ui, ctx):
363 def newcommitphase(ui, ctx):
513 commitphase = phases.newcommitphase(ui)
364 commitphase = phases.newcommitphase(ui)
514 substate = getattr(ctx, "substate", None)
365 substate = getattr(ctx, "substate", None)
@@ -539,1617 +390,3 b' def newcommitphase(ui, ctx):'
539 (phases.phasenames[maxphase], maxsub))
390 (phases.phasenames[maxphase], maxsub))
540 return maxphase
391 return maxphase
541 return commitphase
392 return commitphase
542
543 # subrepo classes need to implement the following abstract class:
544
545 class abstractsubrepo(object):
546
547 def __init__(self, ctx, path):
548 """Initialize abstractsubrepo part
549
550 ``ctx`` is the context referring this subrepository in the
551 parent repository.
552
553 ``path`` is the path to this subrepository as seen from
554 innermost repository.
555 """
556 self.ui = ctx.repo().ui
557 self._ctx = ctx
558 self._path = path
559
560 def addwebdirpath(self, serverpath, webconf):
561 """Add the hgwebdir entries for this subrepo, and any of its subrepos.
562
563 ``serverpath`` is the path component of the URL for this repo.
564
565 ``webconf`` is the dictionary of hgwebdir entries.
566 """
567 pass
568
569 def storeclean(self, path):
570 """
571 returns true if the repository has not changed since it was last
572 cloned from or pushed to a given repository.
573 """
574 return False
575
576 def dirty(self, ignoreupdate=False, missing=False):
577 """returns true if the dirstate of the subrepo is dirty or does not
578 match current stored state. If ignoreupdate is true, only check
579 whether the subrepo has uncommitted changes in its dirstate. If missing
580 is true, check for deleted files.
581 """
582 raise NotImplementedError
583
584 def dirtyreason(self, ignoreupdate=False, missing=False):
585 """return reason string if it is ``dirty()``
586
587 Returned string should have enough information for the message
588 of exception.
589
590 This returns None, otherwise.
591 """
592 if self.dirty(ignoreupdate=ignoreupdate, missing=missing):
593 return _('uncommitted changes in subrepository "%s"'
594 ) % subrelpath(self)
595
596 def bailifchanged(self, ignoreupdate=False, hint=None):
597 """raise Abort if subrepository is ``dirty()``
598 """
599 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate,
600 missing=True)
601 if dirtyreason:
602 raise error.Abort(dirtyreason, hint=hint)
603
604 def basestate(self):
605 """current working directory base state, disregarding .hgsubstate
606 state and working directory modifications"""
607 raise NotImplementedError
608
609 def checknested(self, path):
610 """check if path is a subrepository within this repository"""
611 return False
612
613 def commit(self, text, user, date):
614 """commit the current changes to the subrepo with the given
615 log message. Use given user and date if possible. Return the
616 new state of the subrepo.
617 """
618 raise NotImplementedError
619
620 def phase(self, state):
621 """returns phase of specified state in the subrepository.
622 """
623 return phases.public
624
625 def remove(self):
626 """remove the subrepo
627
628 (should verify the dirstate is not dirty first)
629 """
630 raise NotImplementedError
631
632 def get(self, state, overwrite=False):
633 """run whatever commands are needed to put the subrepo into
634 this state
635 """
636 raise NotImplementedError
637
638 def merge(self, state):
639 """merge currently-saved state with the new state."""
640 raise NotImplementedError
641
642 def push(self, opts):
643 """perform whatever action is analogous to 'hg push'
644
645 This may be a no-op on some systems.
646 """
647 raise NotImplementedError
648
649 def add(self, ui, match, prefix, explicitonly, **opts):
650 return []
651
652 def addremove(self, matcher, prefix, opts, dry_run, similarity):
653 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
654 return 1
655
656 def cat(self, match, fm, fntemplate, prefix, **opts):
657 return 1
658
659 def status(self, rev2, **opts):
660 return scmutil.status([], [], [], [], [], [], [])
661
662 def diff(self, ui, diffopts, node2, match, prefix, **opts):
663 pass
664
665 def outgoing(self, ui, dest, opts):
666 return 1
667
668 def incoming(self, ui, source, opts):
669 return 1
670
671 def files(self):
672 """return filename iterator"""
673 raise NotImplementedError
674
675 def filedata(self, name, decode):
676 """return file data, optionally passed through repo decoders"""
677 raise NotImplementedError
678
679 def fileflags(self, name):
680 """return file flags"""
681 return ''
682
683 def getfileset(self, expr):
684 """Resolve the fileset expression for this repo"""
685 return set()
686
687 def printfiles(self, ui, m, fm, fmt, subrepos):
688 """handle the files command for this subrepo"""
689 return 1
690
691 def archive(self, archiver, prefix, match=None, decode=True):
692 if match is not None:
693 files = [f for f in self.files() if match(f)]
694 else:
695 files = self.files()
696 total = len(files)
697 relpath = subrelpath(self)
698 self.ui.progress(_('archiving (%s)') % relpath, 0,
699 unit=_('files'), total=total)
700 for i, name in enumerate(files):
701 flags = self.fileflags(name)
702 mode = 'x' in flags and 0o755 or 0o644
703 symlink = 'l' in flags
704 archiver.addfile(prefix + self._path + '/' + name,
705 mode, symlink, self.filedata(name, decode))
706 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
707 unit=_('files'), total=total)
708 self.ui.progress(_('archiving (%s)') % relpath, None)
709 return total
710
711 def walk(self, match):
712 '''
713 walk recursively through the directory tree, finding all files
714 matched by the match function
715 '''
716
717 def forget(self, match, prefix):
718 return ([], [])
719
720 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
721 """remove the matched files from the subrepository and the filesystem,
722 possibly by force and/or after the file has been removed from the
723 filesystem. Return 0 on success, 1 on any warning.
724 """
725 warnings.append(_("warning: removefiles not implemented (%s)")
726 % self._path)
727 return 1
728
729 def revert(self, substate, *pats, **opts):
730 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
731 % (substate[0], substate[2]))
732 return []
733
734 def shortid(self, revid):
735 return revid
736
737 def unshare(self):
738 '''
739 convert this repository from shared to normal storage.
740 '''
741
742 def verify(self):
743 '''verify the integrity of the repository. Return 0 on success or
744 warning, 1 on any error.
745 '''
746 return 0
747
748 @propertycache
749 def wvfs(self):
750 """return vfs to access the working directory of this subrepository
751 """
752 return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path))
753
754 @propertycache
755 def _relpath(self):
756 """return path to this subrepository as seen from outermost repository
757 """
758 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
759
760 class hgsubrepo(abstractsubrepo):
761 def __init__(self, ctx, path, state, allowcreate):
762 super(hgsubrepo, self).__init__(ctx, path)
763 self._state = state
764 r = ctx.repo()
765 root = r.wjoin(path)
766 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
767 self._repo = hg.repository(r.baseui, root, create=create)
768
769 # Propagate the parent's --hidden option
770 if r is r.unfiltered():
771 self._repo = self._repo.unfiltered()
772
773 self.ui = self._repo.ui
774 for s, k in [('ui', 'commitsubrepos')]:
775 v = r.ui.config(s, k)
776 if v:
777 self.ui.setconfig(s, k, v, 'subrepo')
778 # internal config: ui._usedassubrepo
779 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
780 self._initrepo(r, state[0], create)
781
782 @annotatesubrepoerror
783 def addwebdirpath(self, serverpath, webconf):
784 cmdutil.addwebdirpath(self._repo, subrelpath(self), webconf)
785
786 def storeclean(self, path):
787 with self._repo.lock():
788 return self._storeclean(path)
789
790 def _storeclean(self, path):
791 clean = True
792 itercache = self._calcstorehash(path)
793 for filehash in self._readstorehashcache(path):
794 if filehash != next(itercache, None):
795 clean = False
796 break
797 if clean:
798 # if not empty:
799 # the cached and current pull states have a different size
800 clean = next(itercache, None) is None
801 return clean
802
803 def _calcstorehash(self, remotepath):
804 '''calculate a unique "store hash"
805
806 This method is used to to detect when there are changes that may
807 require a push to a given remote path.'''
808 # sort the files that will be hashed in increasing (likely) file size
809 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
810 yield '# %s\n' % _expandedabspath(remotepath)
811 vfs = self._repo.vfs
812 for relname in filelist:
813 filehash = node.hex(hashlib.sha1(vfs.tryread(relname)).digest())
814 yield '%s = %s\n' % (relname, filehash)
815
816 @propertycache
817 def _cachestorehashvfs(self):
818 return vfsmod.vfs(self._repo.vfs.join('cache/storehash'))
819
820 def _readstorehashcache(self, remotepath):
821 '''read the store hash cache for a given remote repository'''
822 cachefile = _getstorehashcachename(remotepath)
823 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
824
825 def _cachestorehash(self, remotepath):
826 '''cache the current store hash
827
828 Each remote repo requires its own store hash cache, because a subrepo
829 store may be "clean" versus a given remote repo, but not versus another
830 '''
831 cachefile = _getstorehashcachename(remotepath)
832 with self._repo.lock():
833 storehash = list(self._calcstorehash(remotepath))
834 vfs = self._cachestorehashvfs
835 vfs.writelines(cachefile, storehash, mode='wb', notindexed=True)
836
837 def _getctx(self):
838 '''fetch the context for this subrepo revision, possibly a workingctx
839 '''
840 if self._ctx.rev() is None:
841 return self._repo[None] # workingctx if parent is workingctx
842 else:
843 rev = self._state[1]
844 return self._repo[rev]
845
846 @annotatesubrepoerror
847 def _initrepo(self, parentrepo, source, create):
848 self._repo._subparent = parentrepo
849 self._repo._subsource = source
850
851 if create:
852 lines = ['[paths]\n']
853
854 def addpathconfig(key, value):
855 if value:
856 lines.append('%s = %s\n' % (key, value))
857 self.ui.setconfig('paths', key, value, 'subrepo')
858
859 defpath = _abssource(self._repo, abort=False)
860 defpushpath = _abssource(self._repo, True, abort=False)
861 addpathconfig('default', defpath)
862 if defpath != defpushpath:
863 addpathconfig('default-push', defpushpath)
864
865 self._repo.vfs.write('hgrc', util.tonativeeol(''.join(lines)))
866
867 @annotatesubrepoerror
868 def add(self, ui, match, prefix, explicitonly, **opts):
869 return cmdutil.add(ui, self._repo, match,
870 self.wvfs.reljoin(prefix, self._path),
871 explicitonly, **opts)
872
873 @annotatesubrepoerror
874 def addremove(self, m, prefix, opts, dry_run, similarity):
875 # In the same way as sub directories are processed, once in a subrepo,
876 # always entry any of its subrepos. Don't corrupt the options that will
877 # be used to process sibling subrepos however.
878 opts = copy.copy(opts)
879 opts['subrepos'] = True
880 return scmutil.addremove(self._repo, m,
881 self.wvfs.reljoin(prefix, self._path), opts,
882 dry_run, similarity)
883
884 @annotatesubrepoerror
885 def cat(self, match, fm, fntemplate, prefix, **opts):
886 rev = self._state[1]
887 ctx = self._repo[rev]
888 return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
889 prefix, **opts)
890
891 @annotatesubrepoerror
892 def status(self, rev2, **opts):
893 try:
894 rev1 = self._state[1]
895 ctx1 = self._repo[rev1]
896 ctx2 = self._repo[rev2]
897 return self._repo.status(ctx1, ctx2, **opts)
898 except error.RepoLookupError as inst:
899 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
900 % (inst, subrelpath(self)))
901 return scmutil.status([], [], [], [], [], [], [])
902
903 @annotatesubrepoerror
904 def diff(self, ui, diffopts, node2, match, prefix, **opts):
905 try:
906 node1 = node.bin(self._state[1])
907 # We currently expect node2 to come from substate and be
908 # in hex format
909 if node2 is not None:
910 node2 = node.bin(node2)
911 logcmdutil.diffordiffstat(ui, self._repo, diffopts,
912 node1, node2, match,
913 prefix=posixpath.join(prefix, self._path),
914 listsubrepos=True, **opts)
915 except error.RepoLookupError as inst:
916 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
917 % (inst, subrelpath(self)))
918
919 @annotatesubrepoerror
920 def archive(self, archiver, prefix, match=None, decode=True):
921 self._get(self._state + ('hg',))
922 files = self.files()
923 if match:
924 files = [f for f in files if match(f)]
925 rev = self._state[1]
926 ctx = self._repo[rev]
927 cmdutil._prefetchfiles(self._repo, ctx, files)
928 total = abstractsubrepo.archive(self, archiver, prefix, match)
929 for subpath in ctx.substate:
930 s = subrepo(ctx, subpath, True)
931 submatch = matchmod.subdirmatcher(subpath, match)
932 total += s.archive(archiver, prefix + self._path + '/', submatch,
933 decode)
934 return total
935
936 @annotatesubrepoerror
937 def dirty(self, ignoreupdate=False, missing=False):
938 r = self._state[1]
939 if r == '' and not ignoreupdate: # no state recorded
940 return True
941 w = self._repo[None]
942 if r != w.p1().hex() and not ignoreupdate:
943 # different version checked out
944 return True
945 return w.dirty(missing=missing) # working directory changed
946
947 def basestate(self):
948 return self._repo['.'].hex()
949
950 def checknested(self, path):
951 return self._repo._checknested(self._repo.wjoin(path))
952
953 @annotatesubrepoerror
954 def commit(self, text, user, date):
955 # don't bother committing in the subrepo if it's only been
956 # updated
957 if not self.dirty(True):
958 return self._repo['.'].hex()
959 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
960 n = self._repo.commit(text, user, date)
961 if not n:
962 return self._repo['.'].hex() # different version checked out
963 return node.hex(n)
964
965 @annotatesubrepoerror
966 def phase(self, state):
967 return self._repo[state].phase()
968
969 @annotatesubrepoerror
970 def remove(self):
971 # we can't fully delete the repository as it may contain
972 # local-only history
973 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
974 hg.clean(self._repo, node.nullid, False)
975
976 def _get(self, state):
977 source, revision, kind = state
978 parentrepo = self._repo._subparent
979
980 if revision in self._repo.unfiltered():
981 # Allow shared subrepos tracked at null to setup the sharedpath
982 if len(self._repo) != 0 or not parentrepo.shared():
983 return True
984 self._repo._subsource = source
985 srcurl = _abssource(self._repo)
986 other = hg.peer(self._repo, {}, srcurl)
987 if len(self._repo) == 0:
988 # use self._repo.vfs instead of self.wvfs to remove .hg only
989 self._repo.vfs.rmtree()
990 if parentrepo.shared():
991 self.ui.status(_('sharing subrepo %s from %s\n')
992 % (subrelpath(self), srcurl))
993 shared = hg.share(self._repo._subparent.baseui,
994 other, self._repo.root,
995 update=False, bookmarks=False)
996 self._repo = shared.local()
997 else:
998 self.ui.status(_('cloning subrepo %s from %s\n')
999 % (subrelpath(self), srcurl))
1000 other, cloned = hg.clone(self._repo._subparent.baseui, {},
1001 other, self._repo.root,
1002 update=False)
1003 self._repo = cloned.local()
1004 self._initrepo(parentrepo, source, create=True)
1005 self._cachestorehash(srcurl)
1006 else:
1007 self.ui.status(_('pulling subrepo %s from %s\n')
1008 % (subrelpath(self), srcurl))
1009 cleansub = self.storeclean(srcurl)
1010 exchange.pull(self._repo, other)
1011 if cleansub:
1012 # keep the repo clean after pull
1013 self._cachestorehash(srcurl)
1014 return False
1015
1016 @annotatesubrepoerror
1017 def get(self, state, overwrite=False):
1018 inrepo = self._get(state)
1019 source, revision, kind = state
1020 repo = self._repo
1021 repo.ui.debug("getting subrepo %s\n" % self._path)
1022 if inrepo:
1023 urepo = repo.unfiltered()
1024 ctx = urepo[revision]
1025 if ctx.hidden():
1026 urepo.ui.warn(
1027 _('revision %s in subrepository "%s" is hidden\n') \
1028 % (revision[0:12], self._path))
1029 repo = urepo
1030 hg.updaterepo(repo, revision, overwrite)
1031
1032 @annotatesubrepoerror
1033 def merge(self, state):
1034 self._get(state)
1035 cur = self._repo['.']
1036 dst = self._repo[state[1]]
1037 anc = dst.ancestor(cur)
1038
1039 def mergefunc():
1040 if anc == cur and dst.branch() == cur.branch():
1041 self.ui.debug('updating subrepository "%s"\n'
1042 % subrelpath(self))
1043 hg.update(self._repo, state[1])
1044 elif anc == dst:
1045 self.ui.debug('skipping subrepository "%s"\n'
1046 % subrelpath(self))
1047 else:
1048 self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
1049 hg.merge(self._repo, state[1], remind=False)
1050
1051 wctx = self._repo[None]
1052 if self.dirty():
1053 if anc != dst:
1054 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
1055 mergefunc()
1056 else:
1057 mergefunc()
1058 else:
1059 mergefunc()
1060
1061 @annotatesubrepoerror
1062 def push(self, opts):
1063 force = opts.get('force')
1064 newbranch = opts.get('new_branch')
1065 ssh = opts.get('ssh')
1066
1067 # push subrepos depth-first for coherent ordering
1068 c = self._repo['']
1069 subs = c.substate # only repos that are committed
1070 for s in sorted(subs):
1071 if c.sub(s).push(opts) == 0:
1072 return False
1073
1074 dsturl = _abssource(self._repo, True)
1075 if not force:
1076 if self.storeclean(dsturl):
1077 self.ui.status(
1078 _('no changes made to subrepo %s since last push to %s\n')
1079 % (subrelpath(self), dsturl))
1080 return None
1081 self.ui.status(_('pushing subrepo %s to %s\n') %
1082 (subrelpath(self), dsturl))
1083 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
1084 res = exchange.push(self._repo, other, force, newbranch=newbranch)
1085
1086 # the repo is now clean
1087 self._cachestorehash(dsturl)
1088 return res.cgresult
1089
1090 @annotatesubrepoerror
1091 def outgoing(self, ui, dest, opts):
1092 if 'rev' in opts or 'branch' in opts:
1093 opts = copy.copy(opts)
1094 opts.pop('rev', None)
1095 opts.pop('branch', None)
1096 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
1097
1098 @annotatesubrepoerror
1099 def incoming(self, ui, source, opts):
1100 if 'rev' in opts or 'branch' in opts:
1101 opts = copy.copy(opts)
1102 opts.pop('rev', None)
1103 opts.pop('branch', None)
1104 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
1105
1106 @annotatesubrepoerror
1107 def files(self):
1108 rev = self._state[1]
1109 ctx = self._repo[rev]
1110 return ctx.manifest().keys()
1111
1112 def filedata(self, name, decode):
1113 rev = self._state[1]
1114 data = self._repo[rev][name].data()
1115 if decode:
1116 data = self._repo.wwritedata(name, data)
1117 return data
1118
1119 def fileflags(self, name):
1120 rev = self._state[1]
1121 ctx = self._repo[rev]
1122 return ctx.flags(name)
1123
1124 @annotatesubrepoerror
1125 def printfiles(self, ui, m, fm, fmt, subrepos):
1126 # If the parent context is a workingctx, use the workingctx here for
1127 # consistency.
1128 if self._ctx.rev() is None:
1129 ctx = self._repo[None]
1130 else:
1131 rev = self._state[1]
1132 ctx = self._repo[rev]
1133 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
1134
1135 @annotatesubrepoerror
1136 def getfileset(self, expr):
1137 if self._ctx.rev() is None:
1138 ctx = self._repo[None]
1139 else:
1140 rev = self._state[1]
1141 ctx = self._repo[rev]
1142
1143 files = ctx.getfileset(expr)
1144
1145 for subpath in ctx.substate:
1146 sub = ctx.sub(subpath)
1147
1148 try:
1149 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
1150 except error.LookupError:
1151 self.ui.status(_("skipping missing subrepository: %s\n")
1152 % self.wvfs.reljoin(reporelpath(self), subpath))
1153 return files
1154
1155 def walk(self, match):
1156 ctx = self._repo[None]
1157 return ctx.walk(match)
1158
1159 @annotatesubrepoerror
1160 def forget(self, match, prefix):
1161 return cmdutil.forget(self.ui, self._repo, match,
1162 self.wvfs.reljoin(prefix, self._path), True)
1163
1164 @annotatesubrepoerror
1165 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
1166 return cmdutil.remove(self.ui, self._repo, matcher,
1167 self.wvfs.reljoin(prefix, self._path),
1168 after, force, subrepos)
1169
1170 @annotatesubrepoerror
1171 def revert(self, substate, *pats, **opts):
1172 # reverting a subrepo is a 2 step process:
1173 # 1. if the no_backup is not set, revert all modified
1174 # files inside the subrepo
1175 # 2. update the subrepo to the revision specified in
1176 # the corresponding substate dictionary
1177 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1178 if not opts.get(r'no_backup'):
1179 # Revert all files on the subrepo, creating backups
1180 # Note that this will not recursively revert subrepos
1181 # We could do it if there was a set:subrepos() predicate
1182 opts = opts.copy()
1183 opts[r'date'] = None
1184 opts[r'rev'] = substate[1]
1185
1186 self.filerevert(*pats, **opts)
1187
1188 # Update the repo to the revision specified in the given substate
1189 if not opts.get(r'dry_run'):
1190 self.get(substate, overwrite=True)
1191
1192 def filerevert(self, *pats, **opts):
1193 ctx = self._repo[opts[r'rev']]
1194 parents = self._repo.dirstate.parents()
1195 if opts.get(r'all'):
1196 pats = ['set:modified()']
1197 else:
1198 pats = []
1199 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1200
1201 def shortid(self, revid):
1202 return revid[:12]
1203
1204 @annotatesubrepoerror
1205 def unshare(self):
1206 # subrepo inherently violates our import layering rules
1207 # because it wants to make repo objects from deep inside the stack
1208 # so we manually delay the circular imports to not break
1209 # scripts that don't use our demand-loading
1210 global hg
1211 from . import hg as h
1212 hg = h
1213
1214 # Nothing prevents a user from sharing in a repo, and then making that a
1215 # subrepo. Alternately, the previous unshare attempt may have failed
1216 # part way through. So recurse whether or not this layer is shared.
1217 if self._repo.shared():
1218 self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
1219
1220 hg.unshare(self.ui, self._repo)
1221
1222 def verify(self):
1223 try:
1224 rev = self._state[1]
1225 ctx = self._repo.unfiltered()[rev]
1226 if ctx.hidden():
1227 # Since hidden revisions aren't pushed/pulled, it seems worth an
1228 # explicit warning.
1229 ui = self._repo.ui
1230 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1231 (self._relpath, node.short(self._ctx.node())))
1232 return 0
1233 except error.RepoLookupError:
1234 # A missing subrepo revision may be a case of needing to pull it, so
1235 # don't treat this as an error.
1236 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1237 (self._relpath, node.short(self._ctx.node())))
1238 return 0
1239
1240 @propertycache
1241 def wvfs(self):
1242 """return own wvfs for efficiency and consistency
1243 """
1244 return self._repo.wvfs
1245
1246 @propertycache
1247 def _relpath(self):
1248 """return path to this subrepository as seen from outermost repository
1249 """
1250 # Keep consistent dir separators by avoiding vfs.join(self._path)
1251 return reporelpath(self._repo)
1252
1253 class svnsubrepo(abstractsubrepo):
1254 def __init__(self, ctx, path, state, allowcreate):
1255 super(svnsubrepo, self).__init__(ctx, path)
1256 self._state = state
1257 self._exe = util.findexe('svn')
1258 if not self._exe:
1259 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
1260 % self._path)
1261
1262 def _svncommand(self, commands, filename='', failok=False):
1263 cmd = [self._exe]
1264 extrakw = {}
1265 if not self.ui.interactive():
1266 # Making stdin be a pipe should prevent svn from behaving
1267 # interactively even if we can't pass --non-interactive.
1268 extrakw[r'stdin'] = subprocess.PIPE
1269 # Starting in svn 1.5 --non-interactive is a global flag
1270 # instead of being per-command, but we need to support 1.4 so
1271 # we have to be intelligent about what commands take
1272 # --non-interactive.
1273 if commands[0] in ('update', 'checkout', 'commit'):
1274 cmd.append('--non-interactive')
1275 cmd.extend(commands)
1276 if filename is not None:
1277 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1278 self._path, filename)
1279 cmd.append(path)
1280 env = dict(encoding.environ)
1281 # Avoid localized output, preserve current locale for everything else.
1282 lc_all = env.get('LC_ALL')
1283 if lc_all:
1284 env['LANG'] = lc_all
1285 del env['LC_ALL']
1286 env['LC_MESSAGES'] = 'C'
1287 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1288 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1289 universal_newlines=True, env=env, **extrakw)
1290 stdout, stderr = p.communicate()
1291 stderr = stderr.strip()
1292 if not failok:
1293 if p.returncode:
1294 raise error.Abort(stderr or 'exited with code %d'
1295 % p.returncode)
1296 if stderr:
1297 self.ui.warn(stderr + '\n')
1298 return stdout, stderr
1299
1300 @propertycache
1301 def _svnversion(self):
1302 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1303 m = re.search(br'^(\d+)\.(\d+)', output)
1304 if not m:
1305 raise error.Abort(_('cannot retrieve svn tool version'))
1306 return (int(m.group(1)), int(m.group(2)))
1307
1308 def _svnmissing(self):
1309 return not self.wvfs.exists('.svn')
1310
1311 def _wcrevs(self):
1312 # Get the working directory revision as well as the last
1313 # commit revision so we can compare the subrepo state with
1314 # both. We used to store the working directory one.
1315 output, err = self._svncommand(['info', '--xml'])
1316 doc = xml.dom.minidom.parseString(output)
1317 entries = doc.getElementsByTagName('entry')
1318 lastrev, rev = '0', '0'
1319 if entries:
1320 rev = str(entries[0].getAttribute('revision')) or '0'
1321 commits = entries[0].getElementsByTagName('commit')
1322 if commits:
1323 lastrev = str(commits[0].getAttribute('revision')) or '0'
1324 return (lastrev, rev)
1325
1326 def _wcrev(self):
1327 return self._wcrevs()[0]
1328
1329 def _wcchanged(self):
1330 """Return (changes, extchanges, missing) where changes is True
1331 if the working directory was changed, extchanges is
1332 True if any of these changes concern an external entry and missing
1333 is True if any change is a missing entry.
1334 """
1335 output, err = self._svncommand(['status', '--xml'])
1336 externals, changes, missing = [], [], []
1337 doc = xml.dom.minidom.parseString(output)
1338 for e in doc.getElementsByTagName('entry'):
1339 s = e.getElementsByTagName('wc-status')
1340 if not s:
1341 continue
1342 item = s[0].getAttribute('item')
1343 props = s[0].getAttribute('props')
1344 path = e.getAttribute('path')
1345 if item == 'external':
1346 externals.append(path)
1347 elif item == 'missing':
1348 missing.append(path)
1349 if (item not in ('', 'normal', 'unversioned', 'external')
1350 or props not in ('', 'none', 'normal')):
1351 changes.append(path)
1352 for path in changes:
1353 for ext in externals:
1354 if path == ext or path.startswith(ext + pycompat.ossep):
1355 return True, True, bool(missing)
1356 return bool(changes), False, bool(missing)
1357
1358 @annotatesubrepoerror
1359 def dirty(self, ignoreupdate=False, missing=False):
1360 if self._svnmissing():
1361 return self._state[1] != ''
1362 wcchanged = self._wcchanged()
1363 changed = wcchanged[0] or (missing and wcchanged[2])
1364 if not changed:
1365 if self._state[1] in self._wcrevs() or ignoreupdate:
1366 return False
1367 return True
1368
1369 def basestate(self):
1370 lastrev, rev = self._wcrevs()
1371 if lastrev != rev:
1372 # Last committed rev is not the same than rev. We would
1373 # like to take lastrev but we do not know if the subrepo
1374 # URL exists at lastrev. Test it and fallback to rev it
1375 # is not there.
1376 try:
1377 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1378 return lastrev
1379 except error.Abort:
1380 pass
1381 return rev
1382
1383 @annotatesubrepoerror
1384 def commit(self, text, user, date):
1385 # user and date are out of our hands since svn is centralized
1386 changed, extchanged, missing = self._wcchanged()
1387 if not changed:
1388 return self.basestate()
1389 if extchanged:
1390 # Do not try to commit externals
1391 raise error.Abort(_('cannot commit svn externals'))
1392 if missing:
1393 # svn can commit with missing entries but aborting like hg
1394 # seems a better approach.
1395 raise error.Abort(_('cannot commit missing svn entries'))
1396 commitinfo, err = self._svncommand(['commit', '-m', text])
1397 self.ui.status(commitinfo)
1398 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1399 if not newrev:
1400 if not commitinfo.strip():
1401 # Sometimes, our definition of "changed" differs from
1402 # svn one. For instance, svn ignores missing files
1403 # when committing. If there are only missing files, no
1404 # commit is made, no output and no error code.
1405 raise error.Abort(_('failed to commit svn changes'))
1406 raise error.Abort(commitinfo.splitlines()[-1])
1407 newrev = newrev.groups()[0]
1408 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1409 return newrev
1410
1411 @annotatesubrepoerror
1412 def remove(self):
1413 if self.dirty():
1414 self.ui.warn(_('not removing repo %s because '
1415 'it has changes.\n') % self._path)
1416 return
1417 self.ui.note(_('removing subrepo %s\n') % self._path)
1418
1419 self.wvfs.rmtree(forcibly=True)
1420 try:
1421 pwvfs = self._ctx.repo().wvfs
1422 pwvfs.removedirs(pwvfs.dirname(self._path))
1423 except OSError:
1424 pass
1425
1426 @annotatesubrepoerror
1427 def get(self, state, overwrite=False):
1428 if overwrite:
1429 self._svncommand(['revert', '--recursive'])
1430 args = ['checkout']
1431 if self._svnversion >= (1, 5):
1432 args.append('--force')
1433 # The revision must be specified at the end of the URL to properly
1434 # update to a directory which has since been deleted and recreated.
1435 args.append('%s@%s' % (state[0], state[1]))
1436
1437 # SEC: check that the ssh url is safe
1438 util.checksafessh(state[0])
1439
1440 status, err = self._svncommand(args, failok=True)
1441 _sanitize(self.ui, self.wvfs, '.svn')
1442 if not re.search('Checked out revision [0-9]+.', status):
1443 if ('is already a working copy for a different URL' in err
1444 and (self._wcchanged()[:2] == (False, False))):
1445 # obstructed but clean working copy, so just blow it away.
1446 self.remove()
1447 self.get(state, overwrite=False)
1448 return
1449 raise error.Abort((status or err).splitlines()[-1])
1450 self.ui.status(status)
1451
1452 @annotatesubrepoerror
1453 def merge(self, state):
1454 old = self._state[1]
1455 new = state[1]
1456 wcrev = self._wcrev()
1457 if new != wcrev:
1458 dirty = old == wcrev or self._wcchanged()[0]
1459 if _updateprompt(self.ui, self, dirty, wcrev, new):
1460 self.get(state, False)
1461
1462 def push(self, opts):
1463 # push is a no-op for SVN
1464 return True
1465
1466 @annotatesubrepoerror
1467 def files(self):
1468 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1469 doc = xml.dom.minidom.parseString(output)
1470 paths = []
1471 for e in doc.getElementsByTagName('entry'):
1472 kind = str(e.getAttribute('kind'))
1473 if kind != 'file':
1474 continue
1475 name = ''.join(c.data for c
1476 in e.getElementsByTagName('name')[0].childNodes
1477 if c.nodeType == c.TEXT_NODE)
1478 paths.append(name.encode('utf-8'))
1479 return paths
1480
1481 def filedata(self, name, decode):
1482 return self._svncommand(['cat'], name)[0]
1483
1484
1485 class gitsubrepo(abstractsubrepo):
1486 def __init__(self, ctx, path, state, allowcreate):
1487 super(gitsubrepo, self).__init__(ctx, path)
1488 self._state = state
1489 self._abspath = ctx.repo().wjoin(path)
1490 self._subparent = ctx.repo()
1491 self._ensuregit()
1492
1493 def _ensuregit(self):
1494 try:
1495 self._gitexecutable = 'git'
1496 out, err = self._gitnodir(['--version'])
1497 except OSError as e:
1498 genericerror = _("error executing git for subrepo '%s': %s")
1499 notfoundhint = _("check git is installed and in your PATH")
1500 if e.errno != errno.ENOENT:
1501 raise error.Abort(genericerror % (
1502 self._path, encoding.strtolocal(e.strerror)))
1503 elif pycompat.iswindows:
1504 try:
1505 self._gitexecutable = 'git.cmd'
1506 out, err = self._gitnodir(['--version'])
1507 except OSError as e2:
1508 if e2.errno == errno.ENOENT:
1509 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1510 " for subrepo '%s'") % self._path,
1511 hint=notfoundhint)
1512 else:
1513 raise error.Abort(genericerror % (self._path,
1514 encoding.strtolocal(e2.strerror)))
1515 else:
1516 raise error.Abort(_("couldn't find git for subrepo '%s'")
1517 % self._path, hint=notfoundhint)
1518 versionstatus = self._checkversion(out)
1519 if versionstatus == 'unknown':
1520 self.ui.warn(_('cannot retrieve git version\n'))
1521 elif versionstatus == 'abort':
1522 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1523 elif versionstatus == 'warning':
1524 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1525
1526 @staticmethod
1527 def _gitversion(out):
1528 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1529 if m:
1530 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1531
1532 m = re.search(br'^git version (\d+)\.(\d+)', out)
1533 if m:
1534 return (int(m.group(1)), int(m.group(2)), 0)
1535
1536 return -1
1537
1538 @staticmethod
1539 def _checkversion(out):
1540 '''ensure git version is new enough
1541
1542 >>> _checkversion = gitsubrepo._checkversion
1543 >>> _checkversion(b'git version 1.6.0')
1544 'ok'
1545 >>> _checkversion(b'git version 1.8.5')
1546 'ok'
1547 >>> _checkversion(b'git version 1.4.0')
1548 'abort'
1549 >>> _checkversion(b'git version 1.5.0')
1550 'warning'
1551 >>> _checkversion(b'git version 1.9-rc0')
1552 'ok'
1553 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1554 'ok'
1555 >>> _checkversion(b'git version 1.9.0.GIT')
1556 'ok'
1557 >>> _checkversion(b'git version 12345')
1558 'unknown'
1559 >>> _checkversion(b'no')
1560 'unknown'
1561 '''
1562 version = gitsubrepo._gitversion(out)
1563 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1564 # despite the docstring comment. For now, error on 1.4.0, warn on
1565 # 1.5.0 but attempt to continue.
1566 if version == -1:
1567 return 'unknown'
1568 if version < (1, 5, 0):
1569 return 'abort'
1570 elif version < (1, 6, 0):
1571 return 'warning'
1572 return 'ok'
1573
1574 def _gitcommand(self, commands, env=None, stream=False):
1575 return self._gitdir(commands, env=env, stream=stream)[0]
1576
1577 def _gitdir(self, commands, env=None, stream=False):
1578 return self._gitnodir(commands, env=env, stream=stream,
1579 cwd=self._abspath)
1580
1581 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1582 """Calls the git command
1583
1584 The methods tries to call the git command. versions prior to 1.6.0
1585 are not supported and very probably fail.
1586 """
1587 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1588 if env is None:
1589 env = encoding.environ.copy()
1590 # disable localization for Git output (issue5176)
1591 env['LC_ALL'] = 'C'
1592 # fix for Git CVE-2015-7545
1593 if 'GIT_ALLOW_PROTOCOL' not in env:
1594 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1595 # unless ui.quiet is set, print git's stderr,
1596 # which is mostly progress and useful info
1597 errpipe = None
1598 if self.ui.quiet:
1599 errpipe = open(os.devnull, 'w')
1600 if self.ui._colormode and len(commands) and commands[0] == "diff":
1601 # insert the argument in the front,
1602 # the end of git diff arguments is used for paths
1603 commands.insert(1, '--color')
1604 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1605 cwd=cwd, env=env, close_fds=util.closefds,
1606 stdout=subprocess.PIPE, stderr=errpipe)
1607 if stream:
1608 return p.stdout, None
1609
1610 retdata = p.stdout.read().strip()
1611 # wait for the child to exit to avoid race condition.
1612 p.wait()
1613
1614 if p.returncode != 0 and p.returncode != 1:
1615 # there are certain error codes that are ok
1616 command = commands[0]
1617 if command in ('cat-file', 'symbolic-ref'):
1618 return retdata, p.returncode
1619 # for all others, abort
1620 raise error.Abort(_('git %s error %d in %s') %
1621 (command, p.returncode, self._relpath))
1622
1623 return retdata, p.returncode
1624
1625 def _gitmissing(self):
1626 return not self.wvfs.exists('.git')
1627
1628 def _gitstate(self):
1629 return self._gitcommand(['rev-parse', 'HEAD'])
1630
1631 def _gitcurrentbranch(self):
1632 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1633 if err:
1634 current = None
1635 return current
1636
1637 def _gitremote(self, remote):
1638 out = self._gitcommand(['remote', 'show', '-n', remote])
1639 line = out.split('\n')[1]
1640 i = line.index('URL: ') + len('URL: ')
1641 return line[i:]
1642
1643 def _githavelocally(self, revision):
1644 out, code = self._gitdir(['cat-file', '-e', revision])
1645 return code == 0
1646
1647 def _gitisancestor(self, r1, r2):
1648 base = self._gitcommand(['merge-base', r1, r2])
1649 return base == r1
1650
1651 def _gitisbare(self):
1652 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1653
1654 def _gitupdatestat(self):
1655 """This must be run before git diff-index.
1656 diff-index only looks at changes to file stat;
1657 this command looks at file contents and updates the stat."""
1658 self._gitcommand(['update-index', '-q', '--refresh'])
1659
1660 def _gitbranchmap(self):
1661 '''returns 2 things:
1662 a map from git branch to revision
1663 a map from revision to branches'''
1664 branch2rev = {}
1665 rev2branch = {}
1666
1667 out = self._gitcommand(['for-each-ref', '--format',
1668 '%(objectname) %(refname)'])
1669 for line in out.split('\n'):
1670 revision, ref = line.split(' ')
1671 if (not ref.startswith('refs/heads/') and
1672 not ref.startswith('refs/remotes/')):
1673 continue
1674 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1675 continue # ignore remote/HEAD redirects
1676 branch2rev[ref] = revision
1677 rev2branch.setdefault(revision, []).append(ref)
1678 return branch2rev, rev2branch
1679
1680 def _gittracking(self, branches):
1681 'return map of remote branch to local tracking branch'
1682 # assumes no more than one local tracking branch for each remote
1683 tracking = {}
1684 for b in branches:
1685 if b.startswith('refs/remotes/'):
1686 continue
1687 bname = b.split('/', 2)[2]
1688 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1689 if remote:
1690 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1691 tracking['refs/remotes/%s/%s' %
1692 (remote, ref.split('/', 2)[2])] = b
1693 return tracking
1694
1695 def _abssource(self, source):
1696 if '://' not in source:
1697 # recognize the scp syntax as an absolute source
1698 colon = source.find(':')
1699 if colon != -1 and '/' not in source[:colon]:
1700 return source
1701 self._subsource = source
1702 return _abssource(self)
1703
1704 def _fetch(self, source, revision):
1705 if self._gitmissing():
1706 # SEC: check for safe ssh url
1707 util.checksafessh(source)
1708
1709 source = self._abssource(source)
1710 self.ui.status(_('cloning subrepo %s from %s\n') %
1711 (self._relpath, source))
1712 self._gitnodir(['clone', source, self._abspath])
1713 if self._githavelocally(revision):
1714 return
1715 self.ui.status(_('pulling subrepo %s from %s\n') %
1716 (self._relpath, self._gitremote('origin')))
1717 # try only origin: the originally cloned repo
1718 self._gitcommand(['fetch'])
1719 if not self._githavelocally(revision):
1720 raise error.Abort(_('revision %s does not exist in subrepository '
1721 '"%s"\n') % (revision, self._relpath))
1722
1723 @annotatesubrepoerror
1724 def dirty(self, ignoreupdate=False, missing=False):
1725 if self._gitmissing():
1726 return self._state[1] != ''
1727 if self._gitisbare():
1728 return True
1729 if not ignoreupdate and self._state[1] != self._gitstate():
1730 # different version checked out
1731 return True
1732 # check for staged changes or modified files; ignore untracked files
1733 self._gitupdatestat()
1734 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1735 return code == 1
1736
1737 def basestate(self):
1738 return self._gitstate()
1739
1740 @annotatesubrepoerror
1741 def get(self, state, overwrite=False):
1742 source, revision, kind = state
1743 if not revision:
1744 self.remove()
1745 return
1746 self._fetch(source, revision)
1747 # if the repo was set to be bare, unbare it
1748 if self._gitisbare():
1749 self._gitcommand(['config', 'core.bare', 'false'])
1750 if self._gitstate() == revision:
1751 self._gitcommand(['reset', '--hard', 'HEAD'])
1752 return
1753 elif self._gitstate() == revision:
1754 if overwrite:
1755 # first reset the index to unmark new files for commit, because
1756 # reset --hard will otherwise throw away files added for commit,
1757 # not just unmark them.
1758 self._gitcommand(['reset', 'HEAD'])
1759 self._gitcommand(['reset', '--hard', 'HEAD'])
1760 return
1761 branch2rev, rev2branch = self._gitbranchmap()
1762
1763 def checkout(args):
1764 cmd = ['checkout']
1765 if overwrite:
1766 # first reset the index to unmark new files for commit, because
1767 # the -f option will otherwise throw away files added for
1768 # commit, not just unmark them.
1769 self._gitcommand(['reset', 'HEAD'])
1770 cmd.append('-f')
1771 self._gitcommand(cmd + args)
1772 _sanitize(self.ui, self.wvfs, '.git')
1773
1774 def rawcheckout():
1775 # no branch to checkout, check it out with no branch
1776 self.ui.warn(_('checking out detached HEAD in '
1777 'subrepository "%s"\n') % self._relpath)
1778 self.ui.warn(_('check out a git branch if you intend '
1779 'to make changes\n'))
1780 checkout(['-q', revision])
1781
1782 if revision not in rev2branch:
1783 rawcheckout()
1784 return
1785 branches = rev2branch[revision]
1786 firstlocalbranch = None
1787 for b in branches:
1788 if b == 'refs/heads/master':
1789 # master trumps all other branches
1790 checkout(['refs/heads/master'])
1791 return
1792 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1793 firstlocalbranch = b
1794 if firstlocalbranch:
1795 checkout([firstlocalbranch])
1796 return
1797
1798 tracking = self._gittracking(branch2rev.keys())
1799 # choose a remote branch already tracked if possible
1800 remote = branches[0]
1801 if remote not in tracking:
1802 for b in branches:
1803 if b in tracking:
1804 remote = b
1805 break
1806
1807 if remote not in tracking:
1808 # create a new local tracking branch
1809 local = remote.split('/', 3)[3]
1810 checkout(['-b', local, remote])
1811 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1812 # When updating to a tracked remote branch,
1813 # if the local tracking branch is downstream of it,
1814 # a normal `git pull` would have performed a "fast-forward merge"
1815 # which is equivalent to updating the local branch to the remote.
1816 # Since we are only looking at branching at update, we need to
1817 # detect this situation and perform this action lazily.
1818 if tracking[remote] != self._gitcurrentbranch():
1819 checkout([tracking[remote]])
1820 self._gitcommand(['merge', '--ff', remote])
1821 _sanitize(self.ui, self.wvfs, '.git')
1822 else:
1823 # a real merge would be required, just checkout the revision
1824 rawcheckout()
1825
1826 @annotatesubrepoerror
1827 def commit(self, text, user, date):
1828 if self._gitmissing():
1829 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1830 cmd = ['commit', '-a', '-m', text]
1831 env = encoding.environ.copy()
1832 if user:
1833 cmd += ['--author', user]
1834 if date:
1835 # git's date parser silently ignores when seconds < 1e9
1836 # convert to ISO8601
1837 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1838 '%Y-%m-%dT%H:%M:%S %1%2')
1839 self._gitcommand(cmd, env=env)
1840 # make sure commit works otherwise HEAD might not exist under certain
1841 # circumstances
1842 return self._gitstate()
1843
1844 @annotatesubrepoerror
1845 def merge(self, state):
1846 source, revision, kind = state
1847 self._fetch(source, revision)
1848 base = self._gitcommand(['merge-base', revision, self._state[1]])
1849 self._gitupdatestat()
1850 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1851
1852 def mergefunc():
1853 if base == revision:
1854 self.get(state) # fast forward merge
1855 elif base != self._state[1]:
1856 self._gitcommand(['merge', '--no-commit', revision])
1857 _sanitize(self.ui, self.wvfs, '.git')
1858
1859 if self.dirty():
1860 if self._gitstate() != revision:
1861 dirty = self._gitstate() == self._state[1] or code != 0
1862 if _updateprompt(self.ui, self, dirty,
1863 self._state[1][:7], revision[:7]):
1864 mergefunc()
1865 else:
1866 mergefunc()
1867
1868 @annotatesubrepoerror
1869 def push(self, opts):
1870 force = opts.get('force')
1871
1872 if not self._state[1]:
1873 return True
1874 if self._gitmissing():
1875 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1876 # if a branch in origin contains the revision, nothing to do
1877 branch2rev, rev2branch = self._gitbranchmap()
1878 if self._state[1] in rev2branch:
1879 for b in rev2branch[self._state[1]]:
1880 if b.startswith('refs/remotes/origin/'):
1881 return True
1882 for b, revision in branch2rev.iteritems():
1883 if b.startswith('refs/remotes/origin/'):
1884 if self._gitisancestor(self._state[1], revision):
1885 return True
1886 # otherwise, try to push the currently checked out branch
1887 cmd = ['push']
1888 if force:
1889 cmd.append('--force')
1890
1891 current = self._gitcurrentbranch()
1892 if current:
1893 # determine if the current branch is even useful
1894 if not self._gitisancestor(self._state[1], current):
1895 self.ui.warn(_('unrelated git branch checked out '
1896 'in subrepository "%s"\n') % self._relpath)
1897 return False
1898 self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
1899 (current.split('/', 2)[2], self._relpath))
1900 ret = self._gitdir(cmd + ['origin', current])
1901 return ret[1] == 0
1902 else:
1903 self.ui.warn(_('no branch checked out in subrepository "%s"\n'
1904 'cannot push revision %s\n') %
1905 (self._relpath, self._state[1]))
1906 return False
1907
1908 @annotatesubrepoerror
1909 def add(self, ui, match, prefix, explicitonly, **opts):
1910 if self._gitmissing():
1911 return []
1912
1913 (modified, added, removed,
1914 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1915 clean=True)
1916
1917 tracked = set()
1918 # dirstates 'amn' warn, 'r' is added again
1919 for l in (modified, added, deleted, clean):
1920 tracked.update(l)
1921
1922 # Unknown files not of interest will be rejected by the matcher
1923 files = unknown
1924 files.extend(match.files())
1925
1926 rejected = []
1927
1928 files = [f for f in sorted(set(files)) if match(f)]
1929 for f in files:
1930 exact = match.exact(f)
1931 command = ["add"]
1932 if exact:
1933 command.append("-f") #should be added, even if ignored
1934 if ui.verbose or not exact:
1935 ui.status(_('adding %s\n') % match.rel(f))
1936
1937 if f in tracked: # hg prints 'adding' even if already tracked
1938 if exact:
1939 rejected.append(f)
1940 continue
1941 if not opts.get(r'dry_run'):
1942 self._gitcommand(command + [f])
1943
1944 for f in rejected:
1945 ui.warn(_("%s already tracked!\n") % match.abs(f))
1946
1947 return rejected
1948
1949 @annotatesubrepoerror
1950 def remove(self):
1951 if self._gitmissing():
1952 return
1953 if self.dirty():
1954 self.ui.warn(_('not removing repo %s because '
1955 'it has changes.\n') % self._relpath)
1956 return
1957 # we can't fully delete the repository as it may contain
1958 # local-only history
1959 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1960 self._gitcommand(['config', 'core.bare', 'true'])
1961 for f, kind in self.wvfs.readdir():
1962 if f == '.git':
1963 continue
1964 if kind == stat.S_IFDIR:
1965 self.wvfs.rmtree(f)
1966 else:
1967 self.wvfs.unlink(f)
1968
1969 def archive(self, archiver, prefix, match=None, decode=True):
1970 total = 0
1971 source, revision = self._state
1972 if not revision:
1973 return total
1974 self._fetch(source, revision)
1975
1976 # Parse git's native archive command.
1977 # This should be much faster than manually traversing the trees
1978 # and objects with many subprocess calls.
1979 tarstream = self._gitcommand(['archive', revision], stream=True)
1980 tar = tarfile.open(fileobj=tarstream, mode='r|')
1981 relpath = subrelpath(self)
1982 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1983 for i, info in enumerate(tar):
1984 if info.isdir():
1985 continue
1986 if match and not match(info.name):
1987 continue
1988 if info.issym():
1989 data = info.linkname
1990 else:
1991 data = tar.extractfile(info).read()
1992 archiver.addfile(prefix + self._path + '/' + info.name,
1993 info.mode, info.issym(), data)
1994 total += 1
1995 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1996 unit=_('files'))
1997 self.ui.progress(_('archiving (%s)') % relpath, None)
1998 return total
1999
2000
2001 @annotatesubrepoerror
2002 def cat(self, match, fm, fntemplate, prefix, **opts):
2003 rev = self._state[1]
2004 if match.anypats():
2005 return 1 #No support for include/exclude yet
2006
2007 if not match.files():
2008 return 1
2009
2010 # TODO: add support for non-plain formatter (see cmdutil.cat())
2011 for f in match.files():
2012 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
2013 fp = cmdutil.makefileobj(self._subparent, fntemplate,
2014 self._ctx.node(),
2015 pathname=self.wvfs.reljoin(prefix, f))
2016 fp.write(output)
2017 fp.close()
2018 return 0
2019
2020
2021 @annotatesubrepoerror
2022 def status(self, rev2, **opts):
2023 rev1 = self._state[1]
2024 if self._gitmissing() or not rev1:
2025 # if the repo is missing, return no results
2026 return scmutil.status([], [], [], [], [], [], [])
2027 modified, added, removed = [], [], []
2028 self._gitupdatestat()
2029 if rev2:
2030 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
2031 else:
2032 command = ['diff-index', '--no-renames', rev1]
2033 out = self._gitcommand(command)
2034 for line in out.split('\n'):
2035 tab = line.find('\t')
2036 if tab == -1:
2037 continue
2038 status, f = line[tab - 1], line[tab + 1:]
2039 if status == 'M':
2040 modified.append(f)
2041 elif status == 'A':
2042 added.append(f)
2043 elif status == 'D':
2044 removed.append(f)
2045
2046 deleted, unknown, ignored, clean = [], [], [], []
2047
2048 command = ['status', '--porcelain', '-z']
2049 if opts.get(r'unknown'):
2050 command += ['--untracked-files=all']
2051 if opts.get(r'ignored'):
2052 command += ['--ignored']
2053 out = self._gitcommand(command)
2054
2055 changedfiles = set()
2056 changedfiles.update(modified)
2057 changedfiles.update(added)
2058 changedfiles.update(removed)
2059 for line in out.split('\0'):
2060 if not line:
2061 continue
2062 st = line[0:2]
2063 #moves and copies show 2 files on one line
2064 if line.find('\0') >= 0:
2065 filename1, filename2 = line[3:].split('\0')
2066 else:
2067 filename1 = line[3:]
2068 filename2 = None
2069
2070 changedfiles.add(filename1)
2071 if filename2:
2072 changedfiles.add(filename2)
2073
2074 if st == '??':
2075 unknown.append(filename1)
2076 elif st == '!!':
2077 ignored.append(filename1)
2078
2079 if opts.get(r'clean'):
2080 out = self._gitcommand(['ls-files'])
2081 for f in out.split('\n'):
2082 if not f in changedfiles:
2083 clean.append(f)
2084
2085 return scmutil.status(modified, added, removed, deleted,
2086 unknown, ignored, clean)
2087
2088 @annotatesubrepoerror
2089 def diff(self, ui, diffopts, node2, match, prefix, **opts):
2090 node1 = self._state[1]
2091 cmd = ['diff', '--no-renames']
2092 if opts[r'stat']:
2093 cmd.append('--stat')
2094 else:
2095 # for Git, this also implies '-p'
2096 cmd.append('-U%d' % diffopts.context)
2097
2098 gitprefix = self.wvfs.reljoin(prefix, self._path)
2099
2100 if diffopts.noprefix:
2101 cmd.extend(['--src-prefix=%s/' % gitprefix,
2102 '--dst-prefix=%s/' % gitprefix])
2103 else:
2104 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
2105 '--dst-prefix=b/%s/' % gitprefix])
2106
2107 if diffopts.ignorews:
2108 cmd.append('--ignore-all-space')
2109 if diffopts.ignorewsamount:
2110 cmd.append('--ignore-space-change')
2111 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
2112 and diffopts.ignoreblanklines:
2113 cmd.append('--ignore-blank-lines')
2114
2115 cmd.append(node1)
2116 if node2:
2117 cmd.append(node2)
2118
2119 output = ""
2120 if match.always():
2121 output += self._gitcommand(cmd) + '\n'
2122 else:
2123 st = self.status(node2)[:3]
2124 files = [f for sublist in st for f in sublist]
2125 for f in files:
2126 if match(f):
2127 output += self._gitcommand(cmd + ['--', f]) + '\n'
2128
2129 if output.strip():
2130 ui.write(output)
2131
2132 @annotatesubrepoerror
2133 def revert(self, substate, *pats, **opts):
2134 self.ui.status(_('reverting subrepo %s\n') % substate[0])
2135 if not opts.get(r'no_backup'):
2136 status = self.status(None)
2137 names = status.modified
2138 for name in names:
2139 bakname = scmutil.origpath(self.ui, self._subparent, name)
2140 self.ui.note(_('saving current version of %s as %s\n') %
2141 (name, bakname))
2142 self.wvfs.rename(name, bakname)
2143
2144 if not opts.get(r'dry_run'):
2145 self.get(substate, overwrite=True)
2146 return []
2147
2148 def shortid(self, revid):
2149 return revid[:7]
2150
2151 types = {
2152 'hg': hgsubrepo,
2153 'svn': svnsubrepo,
2154 'git': gitsubrepo,
2155 }
General Comments 0
You need to be logged in to leave comments. Login now