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