|
1 # subrepoutil.py - sub-repository operations and substate handling |
|
2 # |
|
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com> |
|
4 # |
|
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. |
|
7 |
|
8 from __future__ import absolute_import |
|
9 |
|
10 import errno |
|
11 import os |
|
12 import posixpath |
|
13 import re |
|
14 |
|
15 from .i18n import _ |
|
16 from . import ( |
|
17 config, |
|
18 error, |
|
19 filemerge, |
|
20 pathutil, |
|
21 phases, |
|
22 util, |
|
23 ) |
|
24 |
|
25 nullstate = ('', '', 'empty') |
|
26 |
|
27 def state(ctx, ui): |
|
28 """return a state dict, mapping subrepo paths configured in .hgsub |
|
29 to tuple: (source from .hgsub, revision from .hgsubstate, kind |
|
30 (key in types dict)) |
|
31 """ |
|
32 p = config.config() |
|
33 repo = ctx.repo() |
|
34 def read(f, sections=None, remap=None): |
|
35 if f in ctx: |
|
36 try: |
|
37 data = ctx[f].data() |
|
38 except IOError as err: |
|
39 if err.errno != errno.ENOENT: |
|
40 raise |
|
41 # handle missing subrepo spec files as removed |
|
42 ui.warn(_("warning: subrepo spec file \'%s\' not found\n") % |
|
43 repo.pathto(f)) |
|
44 return |
|
45 p.parse(f, data, sections, remap, read) |
|
46 else: |
|
47 raise error.Abort(_("subrepo spec file \'%s\' not found") % |
|
48 repo.pathto(f)) |
|
49 if '.hgsub' in ctx: |
|
50 read('.hgsub') |
|
51 |
|
52 for path, src in ui.configitems('subpaths'): |
|
53 p.set('subpaths', path, src, ui.configsource('subpaths', path)) |
|
54 |
|
55 rev = {} |
|
56 if '.hgsubstate' in ctx: |
|
57 try: |
|
58 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()): |
|
59 l = l.lstrip() |
|
60 if not l: |
|
61 continue |
|
62 try: |
|
63 revision, path = l.split(" ", 1) |
|
64 except ValueError: |
|
65 raise error.Abort(_("invalid subrepository revision " |
|
66 "specifier in \'%s\' line %d") |
|
67 % (repo.pathto('.hgsubstate'), (i + 1))) |
|
68 rev[path] = revision |
|
69 except IOError as err: |
|
70 if err.errno != errno.ENOENT: |
|
71 raise |
|
72 |
|
73 def remap(src): |
|
74 for pattern, repl in p.items('subpaths'): |
|
75 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub |
|
76 # does a string decode. |
|
77 repl = util.escapestr(repl) |
|
78 # However, we still want to allow back references to go |
|
79 # through unharmed, so we turn r'\\1' into r'\1'. Again, |
|
80 # extra escapes are needed because re.sub string decodes. |
|
81 repl = re.sub(br'\\\\([0-9]+)', br'\\\1', repl) |
|
82 try: |
|
83 src = re.sub(pattern, repl, src, 1) |
|
84 except re.error as e: |
|
85 raise error.Abort(_("bad subrepository pattern in %s: %s") |
|
86 % (p.source('subpaths', pattern), e)) |
|
87 return src |
|
88 |
|
89 state = {} |
|
90 for path, src in p[''].items(): |
|
91 kind = 'hg' |
|
92 if src.startswith('['): |
|
93 if ']' not in src: |
|
94 raise error.Abort(_('missing ] in subrepository source')) |
|
95 kind, src = src.split(']', 1) |
|
96 kind = kind[1:] |
|
97 src = src.lstrip() # strip any extra whitespace after ']' |
|
98 |
|
99 if not util.url(src).isabs(): |
|
100 parent = _abssource(repo, abort=False) |
|
101 if parent: |
|
102 parent = util.url(parent) |
|
103 parent.path = posixpath.join(parent.path or '', src) |
|
104 parent.path = posixpath.normpath(parent.path) |
|
105 joined = str(parent) |
|
106 # Remap the full joined path and use it if it changes, |
|
107 # else remap the original source. |
|
108 remapped = remap(joined) |
|
109 if remapped == joined: |
|
110 src = remap(src) |
|
111 else: |
|
112 src = remapped |
|
113 |
|
114 src = remap(src) |
|
115 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind) |
|
116 |
|
117 return state |
|
118 |
|
119 def writestate(repo, state): |
|
120 """rewrite .hgsubstate in (outer) repo with these subrepo states""" |
|
121 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state) |
|
122 if state[s][1] != nullstate[1]] |
|
123 repo.wwrite('.hgsubstate', ''.join(lines), '') |
|
124 |
|
125 def submerge(repo, wctx, mctx, actx, overwrite, labels=None): |
|
126 """delegated from merge.applyupdates: merging of .hgsubstate file |
|
127 in working context, merging context and ancestor context""" |
|
128 if mctx == actx: # backwards? |
|
129 actx = wctx.p1() |
|
130 s1 = wctx.substate |
|
131 s2 = mctx.substate |
|
132 sa = actx.substate |
|
133 sm = {} |
|
134 |
|
135 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx)) |
|
136 |
|
137 def debug(s, msg, r=""): |
|
138 if r: |
|
139 r = "%s:%s:%s" % r |
|
140 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r)) |
|
141 |
|
142 promptssrc = filemerge.partextras(labels) |
|
143 for s, l in sorted(s1.iteritems()): |
|
144 prompts = None |
|
145 a = sa.get(s, nullstate) |
|
146 ld = l # local state with possible dirty flag for compares |
|
147 if wctx.sub(s).dirty(): |
|
148 ld = (l[0], l[1] + "+") |
|
149 if wctx == actx: # overwrite |
|
150 a = ld |
|
151 |
|
152 prompts = promptssrc.copy() |
|
153 prompts['s'] = s |
|
154 if s in s2: |
|
155 r = s2[s] |
|
156 if ld == r or r == a: # no change or local is newer |
|
157 sm[s] = l |
|
158 continue |
|
159 elif ld == a: # other side changed |
|
160 debug(s, "other changed, get", r) |
|
161 wctx.sub(s).get(r, overwrite) |
|
162 sm[s] = r |
|
163 elif ld[0] != r[0]: # sources differ |
|
164 prompts['lo'] = l[0] |
|
165 prompts['ro'] = r[0] |
|
166 if repo.ui.promptchoice( |
|
167 _(' subrepository sources for %(s)s differ\n' |
|
168 'use (l)ocal%(l)s source (%(lo)s)' |
|
169 ' or (r)emote%(o)s source (%(ro)s)?' |
|
170 '$$ &Local $$ &Remote') % prompts, 0): |
|
171 debug(s, "prompt changed, get", r) |
|
172 wctx.sub(s).get(r, overwrite) |
|
173 sm[s] = r |
|
174 elif ld[1] == a[1]: # local side is unchanged |
|
175 debug(s, "other side changed, get", r) |
|
176 wctx.sub(s).get(r, overwrite) |
|
177 sm[s] = r |
|
178 else: |
|
179 debug(s, "both sides changed") |
|
180 srepo = wctx.sub(s) |
|
181 prompts['sl'] = srepo.shortid(l[1]) |
|
182 prompts['sr'] = srepo.shortid(r[1]) |
|
183 option = repo.ui.promptchoice( |
|
184 _(' subrepository %(s)s diverged (local revision: %(sl)s, ' |
|
185 'remote revision: %(sr)s)\n' |
|
186 '(M)erge, keep (l)ocal%(l)s or keep (r)emote%(o)s?' |
|
187 '$$ &Merge $$ &Local $$ &Remote') |
|
188 % prompts, 0) |
|
189 if option == 0: |
|
190 wctx.sub(s).merge(r) |
|
191 sm[s] = l |
|
192 debug(s, "merge with", r) |
|
193 elif option == 1: |
|
194 sm[s] = l |
|
195 debug(s, "keep local subrepo revision", l) |
|
196 else: |
|
197 wctx.sub(s).get(r, overwrite) |
|
198 sm[s] = r |
|
199 debug(s, "get remote subrepo revision", r) |
|
200 elif ld == a: # remote removed, local unchanged |
|
201 debug(s, "remote removed, remove") |
|
202 wctx.sub(s).remove() |
|
203 elif a == nullstate: # not present in remote or ancestor |
|
204 debug(s, "local added, keep") |
|
205 sm[s] = l |
|
206 continue |
|
207 else: |
|
208 if repo.ui.promptchoice( |
|
209 _(' local%(l)s changed subrepository %(s)s' |
|
210 ' which remote%(o)s removed\n' |
|
211 'use (c)hanged version or (d)elete?' |
|
212 '$$ &Changed $$ &Delete') % prompts, 0): |
|
213 debug(s, "prompt remove") |
|
214 wctx.sub(s).remove() |
|
215 |
|
216 for s, r in sorted(s2.items()): |
|
217 prompts = None |
|
218 if s in s1: |
|
219 continue |
|
220 elif s not in sa: |
|
221 debug(s, "remote added, get", r) |
|
222 mctx.sub(s).get(r) |
|
223 sm[s] = r |
|
224 elif r != sa[s]: |
|
225 prompts = promptssrc.copy() |
|
226 prompts['s'] = s |
|
227 if repo.ui.promptchoice( |
|
228 _(' remote%(o)s changed subrepository %(s)s' |
|
229 ' which local%(l)s removed\n' |
|
230 'use (c)hanged version or (d)elete?' |
|
231 '$$ &Changed $$ &Delete') % prompts, 0) == 0: |
|
232 debug(s, "prompt recreate", r) |
|
233 mctx.sub(s).get(r) |
|
234 sm[s] = r |
|
235 |
|
236 # record merged .hgsubstate |
|
237 writestate(repo, sm) |
|
238 return sm |
|
239 |
|
240 def precommit(ui, wctx, status, match, force=False): |
|
241 """Calculate .hgsubstate changes that should be applied before committing |
|
242 |
|
243 Returns (subs, commitsubs, newstate) where |
|
244 - subs: changed subrepos (including dirty ones) |
|
245 - commitsubs: dirty subrepos which the caller needs to commit recursively |
|
246 - newstate: new state dict which the caller must write to .hgsubstate |
|
247 |
|
248 This also updates the given status argument. |
|
249 """ |
|
250 subs = [] |
|
251 commitsubs = set() |
|
252 newstate = wctx.substate.copy() |
|
253 |
|
254 # only manage subrepos and .hgsubstate if .hgsub is present |
|
255 if '.hgsub' in wctx: |
|
256 # we'll decide whether to track this ourselves, thanks |
|
257 for c in status.modified, status.added, status.removed: |
|
258 if '.hgsubstate' in c: |
|
259 c.remove('.hgsubstate') |
|
260 |
|
261 # compare current state to last committed state |
|
262 # build new substate based on last committed state |
|
263 oldstate = wctx.p1().substate |
|
264 for s in sorted(newstate.keys()): |
|
265 if not match(s): |
|
266 # ignore working copy, use old state if present |
|
267 if s in oldstate: |
|
268 newstate[s] = oldstate[s] |
|
269 continue |
|
270 if not force: |
|
271 raise error.Abort( |
|
272 _("commit with new subrepo %s excluded") % s) |
|
273 dirtyreason = wctx.sub(s).dirtyreason(True) |
|
274 if dirtyreason: |
|
275 if not ui.configbool('ui', 'commitsubrepos'): |
|
276 raise error.Abort(dirtyreason, |
|
277 hint=_("use --subrepos for recursive commit")) |
|
278 subs.append(s) |
|
279 commitsubs.add(s) |
|
280 else: |
|
281 bs = wctx.sub(s).basestate() |
|
282 newstate[s] = (newstate[s][0], bs, newstate[s][2]) |
|
283 if oldstate.get(s, (None, None, None))[1] != bs: |
|
284 subs.append(s) |
|
285 |
|
286 # check for removed subrepos |
|
287 for p in wctx.parents(): |
|
288 r = [s for s in p.substate if s not in newstate] |
|
289 subs += [s for s in r if match(s)] |
|
290 if subs: |
|
291 if (not match('.hgsub') and |
|
292 '.hgsub' in (wctx.modified() + wctx.added())): |
|
293 raise error.Abort(_("can't commit subrepos without .hgsub")) |
|
294 status.modified.insert(0, '.hgsubstate') |
|
295 |
|
296 elif '.hgsub' in status.removed: |
|
297 # clean up .hgsubstate when .hgsub is removed |
|
298 if ('.hgsubstate' in wctx and |
|
299 '.hgsubstate' not in (status.modified + status.added + |
|
300 status.removed)): |
|
301 status.removed.insert(0, '.hgsubstate') |
|
302 |
|
303 return subs, commitsubs, newstate |
|
304 |
|
305 def reporelpath(repo): |
|
306 """return path to this (sub)repo as seen from outermost repo""" |
|
307 parent = repo |
|
308 while util.safehasattr(parent, '_subparent'): |
|
309 parent = parent._subparent |
|
310 return repo.root[len(pathutil.normasprefix(parent.root)):] |
|
311 |
|
312 def subrelpath(sub): |
|
313 """return path to this subrepo as seen from outermost repo""" |
|
314 return sub._relpath |
|
315 |
|
316 def _abssource(repo, push=False, abort=True): |
|
317 """return pull/push path of repo - either based on parent repo .hgsub info |
|
318 or on the top repo config. Abort or return None if no source found.""" |
|
319 if util.safehasattr(repo, '_subparent'): |
|
320 source = util.url(repo._subsource) |
|
321 if source.isabs(): |
|
322 return bytes(source) |
|
323 source.path = posixpath.normpath(source.path) |
|
324 parent = _abssource(repo._subparent, push, abort=False) |
|
325 if parent: |
|
326 parent = util.url(util.pconvert(parent)) |
|
327 parent.path = posixpath.join(parent.path or '', source.path) |
|
328 parent.path = posixpath.normpath(parent.path) |
|
329 return bytes(parent) |
|
330 else: # recursion reached top repo |
|
331 path = None |
|
332 if util.safehasattr(repo, '_subtoppath'): |
|
333 path = repo._subtoppath |
|
334 elif push and repo.ui.config('paths', 'default-push'): |
|
335 path = repo.ui.config('paths', 'default-push') |
|
336 elif repo.ui.config('paths', 'default'): |
|
337 path = repo.ui.config('paths', 'default') |
|
338 elif repo.shared(): |
|
339 # chop off the .hg component to get the default path form. This has |
|
340 # already run through vfsmod.vfs(..., realpath=True), so it doesn't |
|
341 # have problems with 'C:' |
|
342 return os.path.dirname(repo.sharedpath) |
|
343 if path: |
|
344 # issue5770: 'C:\' and 'C:' are not equivalent paths. The former is |
|
345 # as expected: an absolute path to the root of the C: drive. The |
|
346 # latter is a relative path, and works like so: |
|
347 # |
|
348 # C:\>cd C:\some\path |
|
349 # C:\>D: |
|
350 # D:\>python -c "import os; print os.path.abspath('C:')" |
|
351 # C:\some\path |
|
352 # |
|
353 # D:\>python -c "import os; print os.path.abspath('C:relative')" |
|
354 # C:\some\path\relative |
|
355 if util.hasdriveletter(path): |
|
356 if len(path) == 2 or path[2:3] not in br'\/': |
|
357 path = os.path.abspath(path) |
|
358 return path |
|
359 |
|
360 if abort: |
|
361 raise error.Abort(_("default path for subrepository not found")) |
|
362 |
|
363 def newcommitphase(ui, ctx): |
|
364 commitphase = phases.newcommitphase(ui) |
|
365 substate = getattr(ctx, "substate", None) |
|
366 if not substate: |
|
367 return commitphase |
|
368 check = ui.config('phases', 'checksubrepos') |
|
369 if check not in ('ignore', 'follow', 'abort'): |
|
370 raise error.Abort(_('invalid phases.checksubrepos configuration: %s') |
|
371 % (check)) |
|
372 if check == 'ignore': |
|
373 return commitphase |
|
374 maxphase = phases.public |
|
375 maxsub = None |
|
376 for s in sorted(substate): |
|
377 sub = ctx.sub(s) |
|
378 subphase = sub.phase(substate[s][1]) |
|
379 if maxphase < subphase: |
|
380 maxphase = subphase |
|
381 maxsub = s |
|
382 if commitphase < maxphase: |
|
383 if check == 'abort': |
|
384 raise error.Abort(_("can't commit in %s phase" |
|
385 " conflicting %s from subrepository %s") % |
|
386 (phases.phasenames[commitphase], |
|
387 phases.phasenames[maxphase], maxsub)) |
|
388 ui.warn(_("warning: changes are committed in" |
|
389 " %s phase from subrepository %s\n") % |
|
390 (phases.phasenames[maxphase], maxsub)) |
|
391 return maxphase |
|
392 return commitphase |