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] |