Mercurial > public > mercurial-scm > hg-stable
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 |