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