comparison mercurial/subrepo.py @ 36047:55e8efa2451a

subrepo: split non-core functions to new module Resolves import cycle caused by subrepo -> cmdutil. Still we have another cycle, cmdutil -> context -> subrepo, but where I think importing context is wrong. Perhaps we'll need repo.makememctx().
author Yuya Nishihara <yuya@tcha.org>
date Tue, 06 Feb 2018 22:36:38 +0900
parents 533f04d4cb6d
children b72c6ff4e4c0
comparison
equal deleted inserted replaced
36046:006ff7268c5c 36047:55e8efa2451a
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 #
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.
17 import subprocess 17 import subprocess
18 import sys 18 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,
34 pathutil, 31 pathutil,
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 nullstate = ('', '', 'empty')
46 45
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
50 ''' 49 '''
79 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo, 78 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
80 cause=sys.exc_info()) 79 cause=sys.exc_info())
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'
365 'use (l)ocal source (%s) or (r)emote source (%s)?' 86 'use (l)ocal source (%s) or (r)emote source (%s)?'
366 '$$ &Local $$ &Remote') 87 '$$ &Local $$ &Remote')
371 'use (l)ocal source (%s) or (r)emote source (%s)?' 92 'use (l)ocal source (%s) or (r)emote source (%s)?'
372 '$$ &Local $$ &Remote') 93 '$$ &Local $$ &Remote')
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):
437 if d.lower() == ignore: 100 if d.lower() == ignore:
438 del dirs[i] 101 del dirs[i]
506 _checktype(repo.ui, state[2]) 169 _checktype(repo.ui, state[2])
507 subrev = '' 170 subrev = ''
508 if state[2] == 'hg': 171 if state[2] == 'hg':
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
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 174
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):
546 178