##// END OF EJS Templates
dirstate: add a function to update tracking status while "moving" parents...
marmoute -
r48392:0f5c203e default
parent child Browse files
Show More
@@ -1,1448 +1,1497
1 1 # dirstate.py - working directory tracking for mercurial
2 2 #
3 3 # Copyright 2005-2007 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 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import contextlib
12 12 import errno
13 13 import os
14 14 import stat
15 15
16 16 from .i18n import _
17 17 from .pycompat import delattr
18 18
19 19 from hgdemandimport import tracing
20 20
21 21 from . import (
22 22 dirstatemap,
23 23 encoding,
24 24 error,
25 25 match as matchmod,
26 26 pathutil,
27 27 policy,
28 28 pycompat,
29 29 scmutil,
30 30 sparse,
31 31 util,
32 32 )
33 33
34 34 from .interfaces import (
35 35 dirstate as intdirstate,
36 36 util as interfaceutil,
37 37 )
38 38
39 39 parsers = policy.importmod('parsers')
40 40 rustmod = policy.importrust('dirstate')
41 41
42 42 SUPPORTS_DIRSTATE_V2 = rustmod is not None
43 43
44 44 propertycache = util.propertycache
45 45 filecache = scmutil.filecache
46 46 _rangemask = dirstatemap.rangemask
47 47
48 48 DirstateItem = parsers.DirstateItem
49 49
50 50
51 51 class repocache(filecache):
52 52 """filecache for files in .hg/"""
53 53
54 54 def join(self, obj, fname):
55 55 return obj._opener.join(fname)
56 56
57 57
58 58 class rootcache(filecache):
59 59 """filecache for files in the repository root"""
60 60
61 61 def join(self, obj, fname):
62 62 return obj._join(fname)
63 63
64 64
65 65 def _getfsnow(vfs):
66 66 '''Get "now" timestamp on filesystem'''
67 67 tmpfd, tmpname = vfs.mkstemp()
68 68 try:
69 69 return os.fstat(tmpfd)[stat.ST_MTIME]
70 70 finally:
71 71 os.close(tmpfd)
72 72 vfs.unlink(tmpname)
73 73
74 74
75 def requires_parents_change(func):
76 def wrap(self, *args, **kwargs):
77 if not self.pendingparentchange():
78 msg = 'calling `%s` outside of a parentchange context'
79 msg %= func.__name__
80 raise error.ProgrammingError(msg)
81 return func(self, *args, **kwargs)
82
83 return wrap
84
85
75 86 @interfaceutil.implementer(intdirstate.idirstate)
76 87 class dirstate(object):
77 88 def __init__(
78 89 self,
79 90 opener,
80 91 ui,
81 92 root,
82 93 validate,
83 94 sparsematchfn,
84 95 nodeconstants,
85 96 use_dirstate_v2,
86 97 ):
87 98 """Create a new dirstate object.
88 99
89 100 opener is an open()-like callable that can be used to open the
90 101 dirstate file; root is the root of the directory tracked by
91 102 the dirstate.
92 103 """
93 104 self._use_dirstate_v2 = use_dirstate_v2
94 105 self._nodeconstants = nodeconstants
95 106 self._opener = opener
96 107 self._validate = validate
97 108 self._root = root
98 109 self._sparsematchfn = sparsematchfn
99 110 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
100 111 # UNC path pointing to root share (issue4557)
101 112 self._rootdir = pathutil.normasprefix(root)
102 113 self._dirty = False
103 114 self._lastnormaltime = 0
104 115 self._ui = ui
105 116 self._filecache = {}
106 117 self._parentwriters = 0
107 118 self._filename = b'dirstate'
108 119 self._pendingfilename = b'%s.pending' % self._filename
109 120 self._plchangecallbacks = {}
110 121 self._origpl = None
111 122 self._updatedfiles = set()
112 123 self._mapcls = dirstatemap.dirstatemap
113 124 # Access and cache cwd early, so we don't access it for the first time
114 125 # after a working-copy update caused it to not exist (accessing it then
115 126 # raises an exception).
116 127 self._cwd
117 128
118 129 def prefetch_parents(self):
119 130 """make sure the parents are loaded
120 131
121 132 Used to avoid a race condition.
122 133 """
123 134 self._pl
124 135
125 136 @contextlib.contextmanager
126 137 def parentchange(self):
127 138 """Context manager for handling dirstate parents.
128 139
129 140 If an exception occurs in the scope of the context manager,
130 141 the incoherent dirstate won't be written when wlock is
131 142 released.
132 143 """
133 144 self._parentwriters += 1
134 145 yield
135 146 # Typically we want the "undo" step of a context manager in a
136 147 # finally block so it happens even when an exception
137 148 # occurs. In this case, however, we only want to decrement
138 149 # parentwriters if the code in the with statement exits
139 150 # normally, so we don't have a try/finally here on purpose.
140 151 self._parentwriters -= 1
141 152
142 153 def pendingparentchange(self):
143 154 """Returns true if the dirstate is in the middle of a set of changes
144 155 that modify the dirstate parent.
145 156 """
146 157 return self._parentwriters > 0
147 158
148 159 @propertycache
149 160 def _map(self):
150 161 """Return the dirstate contents (see documentation for dirstatemap)."""
151 162 self._map = self._mapcls(
152 163 self._ui,
153 164 self._opener,
154 165 self._root,
155 166 self._nodeconstants,
156 167 self._use_dirstate_v2,
157 168 )
158 169 return self._map
159 170
160 171 @property
161 172 def _sparsematcher(self):
162 173 """The matcher for the sparse checkout.
163 174
164 175 The working directory may not include every file from a manifest. The
165 176 matcher obtained by this property will match a path if it is to be
166 177 included in the working directory.
167 178 """
168 179 # TODO there is potential to cache this property. For now, the matcher
169 180 # is resolved on every access. (But the called function does use a
170 181 # cache to keep the lookup fast.)
171 182 return self._sparsematchfn()
172 183
173 184 @repocache(b'branch')
174 185 def _branch(self):
175 186 try:
176 187 return self._opener.read(b"branch").strip() or b"default"
177 188 except IOError as inst:
178 189 if inst.errno != errno.ENOENT:
179 190 raise
180 191 return b"default"
181 192
182 193 @property
183 194 def _pl(self):
184 195 return self._map.parents()
185 196
186 197 def hasdir(self, d):
187 198 return self._map.hastrackeddir(d)
188 199
189 200 @rootcache(b'.hgignore')
190 201 def _ignore(self):
191 202 files = self._ignorefiles()
192 203 if not files:
193 204 return matchmod.never()
194 205
195 206 pats = [b'include:%s' % f for f in files]
196 207 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
197 208
198 209 @propertycache
199 210 def _slash(self):
200 211 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
201 212
202 213 @propertycache
203 214 def _checklink(self):
204 215 return util.checklink(self._root)
205 216
206 217 @propertycache
207 218 def _checkexec(self):
208 219 return bool(util.checkexec(self._root))
209 220
210 221 @propertycache
211 222 def _checkcase(self):
212 223 return not util.fscasesensitive(self._join(b'.hg'))
213 224
214 225 def _join(self, f):
215 226 # much faster than os.path.join()
216 227 # it's safe because f is always a relative path
217 228 return self._rootdir + f
218 229
219 230 def flagfunc(self, buildfallback):
220 231 if self._checklink and self._checkexec:
221 232
222 233 def f(x):
223 234 try:
224 235 st = os.lstat(self._join(x))
225 236 if util.statislink(st):
226 237 return b'l'
227 238 if util.statisexec(st):
228 239 return b'x'
229 240 except OSError:
230 241 pass
231 242 return b''
232 243
233 244 return f
234 245
235 246 fallback = buildfallback()
236 247 if self._checklink:
237 248
238 249 def f(x):
239 250 if os.path.islink(self._join(x)):
240 251 return b'l'
241 252 if b'x' in fallback(x):
242 253 return b'x'
243 254 return b''
244 255
245 256 return f
246 257 if self._checkexec:
247 258
248 259 def f(x):
249 260 if b'l' in fallback(x):
250 261 return b'l'
251 262 if util.isexec(self._join(x)):
252 263 return b'x'
253 264 return b''
254 265
255 266 return f
256 267 else:
257 268 return fallback
258 269
259 270 @propertycache
260 271 def _cwd(self):
261 272 # internal config: ui.forcecwd
262 273 forcecwd = self._ui.config(b'ui', b'forcecwd')
263 274 if forcecwd:
264 275 return forcecwd
265 276 return encoding.getcwd()
266 277
267 278 def getcwd(self):
268 279 """Return the path from which a canonical path is calculated.
269 280
270 281 This path should be used to resolve file patterns or to convert
271 282 canonical paths back to file paths for display. It shouldn't be
272 283 used to get real file paths. Use vfs functions instead.
273 284 """
274 285 cwd = self._cwd
275 286 if cwd == self._root:
276 287 return b''
277 288 # self._root ends with a path separator if self._root is '/' or 'C:\'
278 289 rootsep = self._root
279 290 if not util.endswithsep(rootsep):
280 291 rootsep += pycompat.ossep
281 292 if cwd.startswith(rootsep):
282 293 return cwd[len(rootsep) :]
283 294 else:
284 295 # we're outside the repo. return an absolute path.
285 296 return cwd
286 297
287 298 def pathto(self, f, cwd=None):
288 299 if cwd is None:
289 300 cwd = self.getcwd()
290 301 path = util.pathto(self._root, cwd, f)
291 302 if self._slash:
292 303 return util.pconvert(path)
293 304 return path
294 305
295 306 def __getitem__(self, key):
296 307 """Return the current state of key (a filename) in the dirstate.
297 308
298 309 States are:
299 310 n normal
300 311 m needs merging
301 312 r marked for removal
302 313 a marked for addition
303 314 ? not tracked
304 315
305 316 XXX The "state" is a bit obscure to be in the "public" API. we should
306 317 consider migrating all user of this to going through the dirstate entry
307 318 instead.
308 319 """
309 320 entry = self._map.get(key)
310 321 if entry is not None:
311 322 return entry.state
312 323 return b'?'
313 324
314 325 def __contains__(self, key):
315 326 return key in self._map
316 327
317 328 def __iter__(self):
318 329 return iter(sorted(self._map))
319 330
320 331 def items(self):
321 332 return pycompat.iteritems(self._map)
322 333
323 334 iteritems = items
324 335
325 336 def directories(self):
326 337 return self._map.directories()
327 338
328 339 def parents(self):
329 340 return [self._validate(p) for p in self._pl]
330 341
331 342 def p1(self):
332 343 return self._validate(self._pl[0])
333 344
334 345 def p2(self):
335 346 return self._validate(self._pl[1])
336 347
337 348 @property
338 349 def in_merge(self):
339 350 """True if a merge is in progress"""
340 351 return self._pl[1] != self._nodeconstants.nullid
341 352
342 353 def branch(self):
343 354 return encoding.tolocal(self._branch)
344 355
345 356 def setparents(self, p1, p2=None):
346 357 """Set dirstate parents to p1 and p2.
347 358
348 359 When moving from two parents to one, "merged" entries a
349 360 adjusted to normal and previous copy records discarded and
350 361 returned by the call.
351 362
352 363 See localrepo.setparents()
353 364 """
354 365 if p2 is None:
355 366 p2 = self._nodeconstants.nullid
356 367 if self._parentwriters == 0:
357 368 raise ValueError(
358 369 b"cannot set dirstate parent outside of "
359 370 b"dirstate.parentchange context manager"
360 371 )
361 372
362 373 self._dirty = True
363 374 oldp2 = self._pl[1]
364 375 if self._origpl is None:
365 376 self._origpl = self._pl
366 377 self._map.setparents(p1, p2)
367 378 copies = {}
368 379 if (
369 380 oldp2 != self._nodeconstants.nullid
370 381 and p2 == self._nodeconstants.nullid
371 382 ):
372 383 candidatefiles = self._map.non_normal_or_other_parent_paths()
373 384
374 385 for f in candidatefiles:
375 386 s = self._map.get(f)
376 387 if s is None:
377 388 continue
378 389
379 390 # Discard "merged" markers when moving away from a merge state
380 391 if s.merged:
381 392 source = self._map.copymap.get(f)
382 393 if source:
383 394 copies[f] = source
384 395 self.normallookup(f)
385 396 # Also fix up otherparent markers
386 397 elif s.from_p2:
387 398 source = self._map.copymap.get(f)
388 399 if source:
389 400 copies[f] = source
390 401 self._add(f)
391 402 return copies
392 403
393 404 def setbranch(self, branch):
394 405 self.__class__._branch.set(self, encoding.fromlocal(branch))
395 406 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
396 407 try:
397 408 f.write(self._branch + b'\n')
398 409 f.close()
399 410
400 411 # make sure filecache has the correct stat info for _branch after
401 412 # replacing the underlying file
402 413 ce = self._filecache[b'_branch']
403 414 if ce:
404 415 ce.refresh()
405 416 except: # re-raises
406 417 f.discard()
407 418 raise
408 419
409 420 def invalidate(self):
410 421 """Causes the next access to reread the dirstate.
411 422
412 423 This is different from localrepo.invalidatedirstate() because it always
413 424 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
414 425 check whether the dirstate has changed before rereading it."""
415 426
416 427 for a in ("_map", "_branch", "_ignore"):
417 428 if a in self.__dict__:
418 429 delattr(self, a)
419 430 self._lastnormaltime = 0
420 431 self._dirty = False
421 432 self._updatedfiles.clear()
422 433 self._parentwriters = 0
423 434 self._origpl = None
424 435
425 436 def copy(self, source, dest):
426 437 """Mark dest as a copy of source. Unmark dest if source is None."""
427 438 if source == dest:
428 439 return
429 440 self._dirty = True
430 441 if source is not None:
431 442 self._map.copymap[dest] = source
432 443 self._updatedfiles.add(source)
433 444 self._updatedfiles.add(dest)
434 445 elif self._map.copymap.pop(dest, None):
435 446 self._updatedfiles.add(dest)
436 447
437 448 def copied(self, file):
438 449 return self._map.copymap.get(file, None)
439 450
440 451 def copies(self):
441 452 return self._map.copymap
442 453
454 @requires_parents_change
455 def update_file_reference(
456 self,
457 filename,
458 p1_tracked,
459 ):
460 """Set a file as tracked in the parent (or not)
461
462 This is to be called when adjust the dirstate to a new parent after an history
463 rewriting operation.
464
465 It should not be called during a merge (p2 != nullid) and only within
466 a `with dirstate.parentchange():` context.
467 """
468 if self.in_merge:
469 msg = b'update_file_reference should not be called when merging'
470 raise error.ProgrammingError(msg)
471 entry = self._map.get(filename)
472 if entry is None:
473 wc_tracked = False
474 else:
475 wc_tracked = entry.tracked
476 if p1_tracked and wc_tracked:
477 # the underlying reference might have changed, we will have to
478 # check it.
479 self.normallookup(filename)
480 elif not (p1_tracked or wc_tracked):
481 # the file is no longer relevant to anyone
482 self._drop(filename)
483 elif (not p1_tracked) and wc_tracked:
484 if not entry.added:
485 self._add(filename)
486 elif p1_tracked and not wc_tracked:
487 if entry is None or not entry.removed:
488 self._remove(filename)
489 else:
490 assert False, 'unreachable'
491
443 492 def _addpath(
444 493 self,
445 494 f,
446 495 mode=0,
447 496 size=None,
448 497 mtime=None,
449 498 added=False,
450 499 merged=False,
451 500 from_p2=False,
452 501 possibly_dirty=False,
453 502 ):
454 503 entry = self._map.get(f)
455 504 if added or entry is not None and entry.removed:
456 505 scmutil.checkfilename(f)
457 506 if self._map.hastrackeddir(f):
458 507 msg = _(b'directory %r already in dirstate')
459 508 msg %= pycompat.bytestr(f)
460 509 raise error.Abort(msg)
461 510 # shadows
462 511 for d in pathutil.finddirs(f):
463 512 if self._map.hastrackeddir(d):
464 513 break
465 514 entry = self._map.get(d)
466 515 if entry is not None and not entry.removed:
467 516 msg = _(b'file %r in dirstate clashes with %r')
468 517 msg %= (pycompat.bytestr(d), pycompat.bytestr(f))
469 518 raise error.Abort(msg)
470 519 self._dirty = True
471 520 self._updatedfiles.add(f)
472 521 self._map.addfile(
473 522 f,
474 523 mode=mode,
475 524 size=size,
476 525 mtime=mtime,
477 526 added=added,
478 527 merged=merged,
479 528 from_p2=from_p2,
480 529 possibly_dirty=possibly_dirty,
481 530 )
482 531
483 532 def normal(self, f, parentfiledata=None):
484 533 """Mark a file normal and clean.
485 534
486 535 parentfiledata: (mode, size, mtime) of the clean file
487 536
488 537 parentfiledata should be computed from memory (for mode,
489 538 size), as or close as possible from the point where we
490 539 determined the file was clean, to limit the risk of the
491 540 file having been changed by an external process between the
492 541 moment where the file was determined to be clean and now."""
493 542 if parentfiledata:
494 543 (mode, size, mtime) = parentfiledata
495 544 else:
496 545 s = os.lstat(self._join(f))
497 546 mode = s.st_mode
498 547 size = s.st_size
499 548 mtime = s[stat.ST_MTIME]
500 549 self._addpath(f, mode=mode, size=size, mtime=mtime)
501 550 self._map.copymap.pop(f, None)
502 551 if f in self._map.nonnormalset:
503 552 self._map.nonnormalset.remove(f)
504 553 if mtime > self._lastnormaltime:
505 554 # Remember the most recent modification timeslot for status(),
506 555 # to make sure we won't miss future size-preserving file content
507 556 # modifications that happen within the same timeslot.
508 557 self._lastnormaltime = mtime
509 558
510 559 def normallookup(self, f):
511 560 '''Mark a file normal, but possibly dirty.'''
512 561 if self.in_merge:
513 562 # if there is a merge going on and the file was either
514 563 # "merged" or coming from other parent (-2) before
515 564 # being removed, restore that state.
516 565 entry = self._map.get(f)
517 566 if entry is not None:
518 567 # XXX this should probably be dealt with a a lower level
519 568 # (see `merged_removed` and `from_p2_removed`)
520 569 if entry.merged_removed or entry.from_p2_removed:
521 570 source = self._map.copymap.get(f)
522 571 if entry.merged_removed:
523 572 self.merge(f)
524 573 elif entry.from_p2_removed:
525 574 self.otherparent(f)
526 575 if source is not None:
527 576 self.copy(source, f)
528 577 return
529 578 elif entry.merged or entry.from_p2:
530 579 return
531 580 self._addpath(f, possibly_dirty=True)
532 581 self._map.copymap.pop(f, None)
533 582
534 583 def otherparent(self, f):
535 584 '''Mark as coming from the other parent, always dirty.'''
536 585 if not self.in_merge:
537 586 msg = _(b"setting %r to other parent only allowed in merges") % f
538 587 raise error.Abort(msg)
539 588 entry = self._map.get(f)
540 589 if entry is not None and entry.tracked:
541 590 # merge-like
542 591 self._addpath(f, merged=True)
543 592 else:
544 593 # add-like
545 594 self._addpath(f, from_p2=True)
546 595 self._map.copymap.pop(f, None)
547 596
548 597 def add(self, f):
549 598 '''Mark a file added.'''
550 599 self._add(f)
551 600
552 601 def _add(self, filename):
553 602 """internal function to mark a file as added"""
554 603 self._addpath(filename, added=True)
555 604 self._map.copymap.pop(filename, None)
556 605
557 606 def remove(self, f):
558 607 '''Mark a file removed'''
559 608 self._remove(f)
560 609
561 610 def _remove(self, filename):
562 611 """internal function to mark a file removed"""
563 612 self._dirty = True
564 613 self._updatedfiles.add(filename)
565 614 self._map.removefile(filename, in_merge=self.in_merge)
566 615
567 616 def merge(self, f):
568 617 '''Mark a file merged.'''
569 618 if not self.in_merge:
570 619 return self.normallookup(f)
571 620 return self.otherparent(f)
572 621
573 622 def drop(self, f):
574 623 '''Drop a file from the dirstate'''
575 624 self._drop(f)
576 625
577 626 def _drop(self, filename):
578 627 """internal function to drop a file from the dirstate"""
579 628 if self._map.dropfile(filename):
580 629 self._dirty = True
581 630 self._updatedfiles.add(filename)
582 631 self._map.copymap.pop(filename, None)
583 632
584 633 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
585 634 if exists is None:
586 635 exists = os.path.lexists(os.path.join(self._root, path))
587 636 if not exists:
588 637 # Maybe a path component exists
589 638 if not ignoremissing and b'/' in path:
590 639 d, f = path.rsplit(b'/', 1)
591 640 d = self._normalize(d, False, ignoremissing, None)
592 641 folded = d + b"/" + f
593 642 else:
594 643 # No path components, preserve original case
595 644 folded = path
596 645 else:
597 646 # recursively normalize leading directory components
598 647 # against dirstate
599 648 if b'/' in normed:
600 649 d, f = normed.rsplit(b'/', 1)
601 650 d = self._normalize(d, False, ignoremissing, True)
602 651 r = self._root + b"/" + d
603 652 folded = d + b"/" + util.fspath(f, r)
604 653 else:
605 654 folded = util.fspath(normed, self._root)
606 655 storemap[normed] = folded
607 656
608 657 return folded
609 658
610 659 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
611 660 normed = util.normcase(path)
612 661 folded = self._map.filefoldmap.get(normed, None)
613 662 if folded is None:
614 663 if isknown:
615 664 folded = path
616 665 else:
617 666 folded = self._discoverpath(
618 667 path, normed, ignoremissing, exists, self._map.filefoldmap
619 668 )
620 669 return folded
621 670
622 671 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
623 672 normed = util.normcase(path)
624 673 folded = self._map.filefoldmap.get(normed, None)
625 674 if folded is None:
626 675 folded = self._map.dirfoldmap.get(normed, None)
627 676 if folded is None:
628 677 if isknown:
629 678 folded = path
630 679 else:
631 680 # store discovered result in dirfoldmap so that future
632 681 # normalizefile calls don't start matching directories
633 682 folded = self._discoverpath(
634 683 path, normed, ignoremissing, exists, self._map.dirfoldmap
635 684 )
636 685 return folded
637 686
638 687 def normalize(self, path, isknown=False, ignoremissing=False):
639 688 """
640 689 normalize the case of a pathname when on a casefolding filesystem
641 690
642 691 isknown specifies whether the filename came from walking the
643 692 disk, to avoid extra filesystem access.
644 693
645 694 If ignoremissing is True, missing path are returned
646 695 unchanged. Otherwise, we try harder to normalize possibly
647 696 existing path components.
648 697
649 698 The normalized case is determined based on the following precedence:
650 699
651 700 - version of name already stored in the dirstate
652 701 - version of name stored on disk
653 702 - version provided via command arguments
654 703 """
655 704
656 705 if self._checkcase:
657 706 return self._normalize(path, isknown, ignoremissing)
658 707 return path
659 708
660 709 def clear(self):
661 710 self._map.clear()
662 711 self._lastnormaltime = 0
663 712 self._updatedfiles.clear()
664 713 self._dirty = True
665 714
666 715 def rebuild(self, parent, allfiles, changedfiles=None):
667 716 if changedfiles is None:
668 717 # Rebuild entire dirstate
669 718 to_lookup = allfiles
670 719 to_drop = []
671 720 lastnormaltime = self._lastnormaltime
672 721 self.clear()
673 722 self._lastnormaltime = lastnormaltime
674 723 elif len(changedfiles) < 10:
675 724 # Avoid turning allfiles into a set, which can be expensive if it's
676 725 # large.
677 726 to_lookup = []
678 727 to_drop = []
679 728 for f in changedfiles:
680 729 if f in allfiles:
681 730 to_lookup.append(f)
682 731 else:
683 732 to_drop.append(f)
684 733 else:
685 734 changedfilesset = set(changedfiles)
686 735 to_lookup = changedfilesset & set(allfiles)
687 736 to_drop = changedfilesset - to_lookup
688 737
689 738 if self._origpl is None:
690 739 self._origpl = self._pl
691 740 self._map.setparents(parent, self._nodeconstants.nullid)
692 741
693 742 for f in to_lookup:
694 743 self.normallookup(f)
695 744 for f in to_drop:
696 745 self._drop(f)
697 746
698 747 self._dirty = True
699 748
700 749 def identity(self):
701 750 """Return identity of dirstate itself to detect changing in storage
702 751
703 752 If identity of previous dirstate is equal to this, writing
704 753 changes based on the former dirstate out can keep consistency.
705 754 """
706 755 return self._map.identity
707 756
708 757 def write(self, tr):
709 758 if not self._dirty:
710 759 return
711 760
712 761 filename = self._filename
713 762 if tr:
714 763 # 'dirstate.write()' is not only for writing in-memory
715 764 # changes out, but also for dropping ambiguous timestamp.
716 765 # delayed writing re-raise "ambiguous timestamp issue".
717 766 # See also the wiki page below for detail:
718 767 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
719 768
720 769 # emulate dropping timestamp in 'parsers.pack_dirstate'
721 770 now = _getfsnow(self._opener)
722 771 self._map.clearambiguoustimes(self._updatedfiles, now)
723 772
724 773 # emulate that all 'dirstate.normal' results are written out
725 774 self._lastnormaltime = 0
726 775 self._updatedfiles.clear()
727 776
728 777 # delay writing in-memory changes out
729 778 tr.addfilegenerator(
730 779 b'dirstate',
731 780 (self._filename,),
732 781 self._writedirstate,
733 782 location=b'plain',
734 783 )
735 784 return
736 785
737 786 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
738 787 self._writedirstate(st)
739 788
740 789 def addparentchangecallback(self, category, callback):
741 790 """add a callback to be called when the wd parents are changed
742 791
743 792 Callback will be called with the following arguments:
744 793 dirstate, (oldp1, oldp2), (newp1, newp2)
745 794
746 795 Category is a unique identifier to allow overwriting an old callback
747 796 with a newer callback.
748 797 """
749 798 self._plchangecallbacks[category] = callback
750 799
751 800 def _writedirstate(self, st):
752 801 # notify callbacks about parents change
753 802 if self._origpl is not None and self._origpl != self._pl:
754 803 for c, callback in sorted(
755 804 pycompat.iteritems(self._plchangecallbacks)
756 805 ):
757 806 callback(self, self._origpl, self._pl)
758 807 self._origpl = None
759 808 # use the modification time of the newly created temporary file as the
760 809 # filesystem's notion of 'now'
761 810 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
762 811
763 812 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
764 813 # timestamp of each entries in dirstate, because of 'now > mtime'
765 814 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
766 815 if delaywrite > 0:
767 816 # do we have any files to delay for?
768 817 for f, e in pycompat.iteritems(self._map):
769 818 if e.need_delay(now):
770 819 import time # to avoid useless import
771 820
772 821 # rather than sleep n seconds, sleep until the next
773 822 # multiple of n seconds
774 823 clock = time.time()
775 824 start = int(clock) - (int(clock) % delaywrite)
776 825 end = start + delaywrite
777 826 time.sleep(end - clock)
778 827 now = end # trust our estimate that the end is near now
779 828 break
780 829
781 830 self._map.write(st, now)
782 831 self._lastnormaltime = 0
783 832 self._dirty = False
784 833
785 834 def _dirignore(self, f):
786 835 if self._ignore(f):
787 836 return True
788 837 for p in pathutil.finddirs(f):
789 838 if self._ignore(p):
790 839 return True
791 840 return False
792 841
793 842 def _ignorefiles(self):
794 843 files = []
795 844 if os.path.exists(self._join(b'.hgignore')):
796 845 files.append(self._join(b'.hgignore'))
797 846 for name, path in self._ui.configitems(b"ui"):
798 847 if name == b'ignore' or name.startswith(b'ignore.'):
799 848 # we need to use os.path.join here rather than self._join
800 849 # because path is arbitrary and user-specified
801 850 files.append(os.path.join(self._rootdir, util.expandpath(path)))
802 851 return files
803 852
804 853 def _ignorefileandline(self, f):
805 854 files = collections.deque(self._ignorefiles())
806 855 visited = set()
807 856 while files:
808 857 i = files.popleft()
809 858 patterns = matchmod.readpatternfile(
810 859 i, self._ui.warn, sourceinfo=True
811 860 )
812 861 for pattern, lineno, line in patterns:
813 862 kind, p = matchmod._patsplit(pattern, b'glob')
814 863 if kind == b"subinclude":
815 864 if p not in visited:
816 865 files.append(p)
817 866 continue
818 867 m = matchmod.match(
819 868 self._root, b'', [], [pattern], warn=self._ui.warn
820 869 )
821 870 if m(f):
822 871 return (i, lineno, line)
823 872 visited.add(i)
824 873 return (None, -1, b"")
825 874
826 875 def _walkexplicit(self, match, subrepos):
827 876 """Get stat data about the files explicitly specified by match.
828 877
829 878 Return a triple (results, dirsfound, dirsnotfound).
830 879 - results is a mapping from filename to stat result. It also contains
831 880 listings mapping subrepos and .hg to None.
832 881 - dirsfound is a list of files found to be directories.
833 882 - dirsnotfound is a list of files that the dirstate thinks are
834 883 directories and that were not found."""
835 884
836 885 def badtype(mode):
837 886 kind = _(b'unknown')
838 887 if stat.S_ISCHR(mode):
839 888 kind = _(b'character device')
840 889 elif stat.S_ISBLK(mode):
841 890 kind = _(b'block device')
842 891 elif stat.S_ISFIFO(mode):
843 892 kind = _(b'fifo')
844 893 elif stat.S_ISSOCK(mode):
845 894 kind = _(b'socket')
846 895 elif stat.S_ISDIR(mode):
847 896 kind = _(b'directory')
848 897 return _(b'unsupported file type (type is %s)') % kind
849 898
850 899 badfn = match.bad
851 900 dmap = self._map
852 901 lstat = os.lstat
853 902 getkind = stat.S_IFMT
854 903 dirkind = stat.S_IFDIR
855 904 regkind = stat.S_IFREG
856 905 lnkkind = stat.S_IFLNK
857 906 join = self._join
858 907 dirsfound = []
859 908 foundadd = dirsfound.append
860 909 dirsnotfound = []
861 910 notfoundadd = dirsnotfound.append
862 911
863 912 if not match.isexact() and self._checkcase:
864 913 normalize = self._normalize
865 914 else:
866 915 normalize = None
867 916
868 917 files = sorted(match.files())
869 918 subrepos.sort()
870 919 i, j = 0, 0
871 920 while i < len(files) and j < len(subrepos):
872 921 subpath = subrepos[j] + b"/"
873 922 if files[i] < subpath:
874 923 i += 1
875 924 continue
876 925 while i < len(files) and files[i].startswith(subpath):
877 926 del files[i]
878 927 j += 1
879 928
880 929 if not files or b'' in files:
881 930 files = [b'']
882 931 # constructing the foldmap is expensive, so don't do it for the
883 932 # common case where files is ['']
884 933 normalize = None
885 934 results = dict.fromkeys(subrepos)
886 935 results[b'.hg'] = None
887 936
888 937 for ff in files:
889 938 if normalize:
890 939 nf = normalize(ff, False, True)
891 940 else:
892 941 nf = ff
893 942 if nf in results:
894 943 continue
895 944
896 945 try:
897 946 st = lstat(join(nf))
898 947 kind = getkind(st.st_mode)
899 948 if kind == dirkind:
900 949 if nf in dmap:
901 950 # file replaced by dir on disk but still in dirstate
902 951 results[nf] = None
903 952 foundadd((nf, ff))
904 953 elif kind == regkind or kind == lnkkind:
905 954 results[nf] = st
906 955 else:
907 956 badfn(ff, badtype(kind))
908 957 if nf in dmap:
909 958 results[nf] = None
910 959 except OSError as inst: # nf not found on disk - it is dirstate only
911 960 if nf in dmap: # does it exactly match a missing file?
912 961 results[nf] = None
913 962 else: # does it match a missing directory?
914 963 if self._map.hasdir(nf):
915 964 notfoundadd(nf)
916 965 else:
917 966 badfn(ff, encoding.strtolocal(inst.strerror))
918 967
919 968 # match.files() may contain explicitly-specified paths that shouldn't
920 969 # be taken; drop them from the list of files found. dirsfound/notfound
921 970 # aren't filtered here because they will be tested later.
922 971 if match.anypats():
923 972 for f in list(results):
924 973 if f == b'.hg' or f in subrepos:
925 974 # keep sentinel to disable further out-of-repo walks
926 975 continue
927 976 if not match(f):
928 977 del results[f]
929 978
930 979 # Case insensitive filesystems cannot rely on lstat() failing to detect
931 980 # a case-only rename. Prune the stat object for any file that does not
932 981 # match the case in the filesystem, if there are multiple files that
933 982 # normalize to the same path.
934 983 if match.isexact() and self._checkcase:
935 984 normed = {}
936 985
937 986 for f, st in pycompat.iteritems(results):
938 987 if st is None:
939 988 continue
940 989
941 990 nc = util.normcase(f)
942 991 paths = normed.get(nc)
943 992
944 993 if paths is None:
945 994 paths = set()
946 995 normed[nc] = paths
947 996
948 997 paths.add(f)
949 998
950 999 for norm, paths in pycompat.iteritems(normed):
951 1000 if len(paths) > 1:
952 1001 for path in paths:
953 1002 folded = self._discoverpath(
954 1003 path, norm, True, None, self._map.dirfoldmap
955 1004 )
956 1005 if path != folded:
957 1006 results[path] = None
958 1007
959 1008 return results, dirsfound, dirsnotfound
960 1009
961 1010 def walk(self, match, subrepos, unknown, ignored, full=True):
962 1011 """
963 1012 Walk recursively through the directory tree, finding all files
964 1013 matched by match.
965 1014
966 1015 If full is False, maybe skip some known-clean files.
967 1016
968 1017 Return a dict mapping filename to stat-like object (either
969 1018 mercurial.osutil.stat instance or return value of os.stat()).
970 1019
971 1020 """
972 1021 # full is a flag that extensions that hook into walk can use -- this
973 1022 # implementation doesn't use it at all. This satisfies the contract
974 1023 # because we only guarantee a "maybe".
975 1024
976 1025 if ignored:
977 1026 ignore = util.never
978 1027 dirignore = util.never
979 1028 elif unknown:
980 1029 ignore = self._ignore
981 1030 dirignore = self._dirignore
982 1031 else:
983 1032 # if not unknown and not ignored, drop dir recursion and step 2
984 1033 ignore = util.always
985 1034 dirignore = util.always
986 1035
987 1036 matchfn = match.matchfn
988 1037 matchalways = match.always()
989 1038 matchtdir = match.traversedir
990 1039 dmap = self._map
991 1040 listdir = util.listdir
992 1041 lstat = os.lstat
993 1042 dirkind = stat.S_IFDIR
994 1043 regkind = stat.S_IFREG
995 1044 lnkkind = stat.S_IFLNK
996 1045 join = self._join
997 1046
998 1047 exact = skipstep3 = False
999 1048 if match.isexact(): # match.exact
1000 1049 exact = True
1001 1050 dirignore = util.always # skip step 2
1002 1051 elif match.prefix(): # match.match, no patterns
1003 1052 skipstep3 = True
1004 1053
1005 1054 if not exact and self._checkcase:
1006 1055 normalize = self._normalize
1007 1056 normalizefile = self._normalizefile
1008 1057 skipstep3 = False
1009 1058 else:
1010 1059 normalize = self._normalize
1011 1060 normalizefile = None
1012 1061
1013 1062 # step 1: find all explicit files
1014 1063 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1015 1064 if matchtdir:
1016 1065 for d in work:
1017 1066 matchtdir(d[0])
1018 1067 for d in dirsnotfound:
1019 1068 matchtdir(d)
1020 1069
1021 1070 skipstep3 = skipstep3 and not (work or dirsnotfound)
1022 1071 work = [d for d in work if not dirignore(d[0])]
1023 1072
1024 1073 # step 2: visit subdirectories
1025 1074 def traverse(work, alreadynormed):
1026 1075 wadd = work.append
1027 1076 while work:
1028 1077 tracing.counter('dirstate.walk work', len(work))
1029 1078 nd = work.pop()
1030 1079 visitentries = match.visitchildrenset(nd)
1031 1080 if not visitentries:
1032 1081 continue
1033 1082 if visitentries == b'this' or visitentries == b'all':
1034 1083 visitentries = None
1035 1084 skip = None
1036 1085 if nd != b'':
1037 1086 skip = b'.hg'
1038 1087 try:
1039 1088 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1040 1089 entries = listdir(join(nd), stat=True, skip=skip)
1041 1090 except OSError as inst:
1042 1091 if inst.errno in (errno.EACCES, errno.ENOENT):
1043 1092 match.bad(
1044 1093 self.pathto(nd), encoding.strtolocal(inst.strerror)
1045 1094 )
1046 1095 continue
1047 1096 raise
1048 1097 for f, kind, st in entries:
1049 1098 # Some matchers may return files in the visitentries set,
1050 1099 # instead of 'this', if the matcher explicitly mentions them
1051 1100 # and is not an exactmatcher. This is acceptable; we do not
1052 1101 # make any hard assumptions about file-or-directory below
1053 1102 # based on the presence of `f` in visitentries. If
1054 1103 # visitchildrenset returned a set, we can always skip the
1055 1104 # entries *not* in the set it provided regardless of whether
1056 1105 # they're actually a file or a directory.
1057 1106 if visitentries and f not in visitentries:
1058 1107 continue
1059 1108 if normalizefile:
1060 1109 # even though f might be a directory, we're only
1061 1110 # interested in comparing it to files currently in the
1062 1111 # dmap -- therefore normalizefile is enough
1063 1112 nf = normalizefile(
1064 1113 nd and (nd + b"/" + f) or f, True, True
1065 1114 )
1066 1115 else:
1067 1116 nf = nd and (nd + b"/" + f) or f
1068 1117 if nf not in results:
1069 1118 if kind == dirkind:
1070 1119 if not ignore(nf):
1071 1120 if matchtdir:
1072 1121 matchtdir(nf)
1073 1122 wadd(nf)
1074 1123 if nf in dmap and (matchalways or matchfn(nf)):
1075 1124 results[nf] = None
1076 1125 elif kind == regkind or kind == lnkkind:
1077 1126 if nf in dmap:
1078 1127 if matchalways or matchfn(nf):
1079 1128 results[nf] = st
1080 1129 elif (matchalways or matchfn(nf)) and not ignore(
1081 1130 nf
1082 1131 ):
1083 1132 # unknown file -- normalize if necessary
1084 1133 if not alreadynormed:
1085 1134 nf = normalize(nf, False, True)
1086 1135 results[nf] = st
1087 1136 elif nf in dmap and (matchalways or matchfn(nf)):
1088 1137 results[nf] = None
1089 1138
1090 1139 for nd, d in work:
1091 1140 # alreadynormed means that processwork doesn't have to do any
1092 1141 # expensive directory normalization
1093 1142 alreadynormed = not normalize or nd == d
1094 1143 traverse([d], alreadynormed)
1095 1144
1096 1145 for s in subrepos:
1097 1146 del results[s]
1098 1147 del results[b'.hg']
1099 1148
1100 1149 # step 3: visit remaining files from dmap
1101 1150 if not skipstep3 and not exact:
1102 1151 # If a dmap file is not in results yet, it was either
1103 1152 # a) not matching matchfn b) ignored, c) missing, or d) under a
1104 1153 # symlink directory.
1105 1154 if not results and matchalways:
1106 1155 visit = [f for f in dmap]
1107 1156 else:
1108 1157 visit = [f for f in dmap if f not in results and matchfn(f)]
1109 1158 visit.sort()
1110 1159
1111 1160 if unknown:
1112 1161 # unknown == True means we walked all dirs under the roots
1113 1162 # that wasn't ignored, and everything that matched was stat'ed
1114 1163 # and is already in results.
1115 1164 # The rest must thus be ignored or under a symlink.
1116 1165 audit_path = pathutil.pathauditor(self._root, cached=True)
1117 1166
1118 1167 for nf in iter(visit):
1119 1168 # If a stat for the same file was already added with a
1120 1169 # different case, don't add one for this, since that would
1121 1170 # make it appear as if the file exists under both names
1122 1171 # on disk.
1123 1172 if (
1124 1173 normalizefile
1125 1174 and normalizefile(nf, True, True) in results
1126 1175 ):
1127 1176 results[nf] = None
1128 1177 # Report ignored items in the dmap as long as they are not
1129 1178 # under a symlink directory.
1130 1179 elif audit_path.check(nf):
1131 1180 try:
1132 1181 results[nf] = lstat(join(nf))
1133 1182 # file was just ignored, no links, and exists
1134 1183 except OSError:
1135 1184 # file doesn't exist
1136 1185 results[nf] = None
1137 1186 else:
1138 1187 # It's either missing or under a symlink directory
1139 1188 # which we in this case report as missing
1140 1189 results[nf] = None
1141 1190 else:
1142 1191 # We may not have walked the full directory tree above,
1143 1192 # so stat and check everything we missed.
1144 1193 iv = iter(visit)
1145 1194 for st in util.statfiles([join(i) for i in visit]):
1146 1195 results[next(iv)] = st
1147 1196 return results
1148 1197
1149 1198 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1150 1199 # Force Rayon (Rust parallelism library) to respect the number of
1151 1200 # workers. This is a temporary workaround until Rust code knows
1152 1201 # how to read the config file.
1153 1202 numcpus = self._ui.configint(b"worker", b"numcpus")
1154 1203 if numcpus is not None:
1155 1204 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1156 1205
1157 1206 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1158 1207 if not workers_enabled:
1159 1208 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1160 1209
1161 1210 (
1162 1211 lookup,
1163 1212 modified,
1164 1213 added,
1165 1214 removed,
1166 1215 deleted,
1167 1216 clean,
1168 1217 ignored,
1169 1218 unknown,
1170 1219 warnings,
1171 1220 bad,
1172 1221 traversed,
1173 1222 dirty,
1174 1223 ) = rustmod.status(
1175 1224 self._map._rustmap,
1176 1225 matcher,
1177 1226 self._rootdir,
1178 1227 self._ignorefiles(),
1179 1228 self._checkexec,
1180 1229 self._lastnormaltime,
1181 1230 bool(list_clean),
1182 1231 bool(list_ignored),
1183 1232 bool(list_unknown),
1184 1233 bool(matcher.traversedir),
1185 1234 )
1186 1235
1187 1236 self._dirty |= dirty
1188 1237
1189 1238 if matcher.traversedir:
1190 1239 for dir in traversed:
1191 1240 matcher.traversedir(dir)
1192 1241
1193 1242 if self._ui.warn:
1194 1243 for item in warnings:
1195 1244 if isinstance(item, tuple):
1196 1245 file_path, syntax = item
1197 1246 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1198 1247 file_path,
1199 1248 syntax,
1200 1249 )
1201 1250 self._ui.warn(msg)
1202 1251 else:
1203 1252 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1204 1253 self._ui.warn(
1205 1254 msg
1206 1255 % (
1207 1256 pathutil.canonpath(
1208 1257 self._rootdir, self._rootdir, item
1209 1258 ),
1210 1259 b"No such file or directory",
1211 1260 )
1212 1261 )
1213 1262
1214 1263 for (fn, message) in bad:
1215 1264 matcher.bad(fn, encoding.strtolocal(message))
1216 1265
1217 1266 status = scmutil.status(
1218 1267 modified=modified,
1219 1268 added=added,
1220 1269 removed=removed,
1221 1270 deleted=deleted,
1222 1271 unknown=unknown,
1223 1272 ignored=ignored,
1224 1273 clean=clean,
1225 1274 )
1226 1275 return (lookup, status)
1227 1276
1228 1277 def status(self, match, subrepos, ignored, clean, unknown):
1229 1278 """Determine the status of the working copy relative to the
1230 1279 dirstate and return a pair of (unsure, status), where status is of type
1231 1280 scmutil.status and:
1232 1281
1233 1282 unsure:
1234 1283 files that might have been modified since the dirstate was
1235 1284 written, but need to be read to be sure (size is the same
1236 1285 but mtime differs)
1237 1286 status.modified:
1238 1287 files that have definitely been modified since the dirstate
1239 1288 was written (different size or mode)
1240 1289 status.clean:
1241 1290 files that have definitely not been modified since the
1242 1291 dirstate was written
1243 1292 """
1244 1293 listignored, listclean, listunknown = ignored, clean, unknown
1245 1294 lookup, modified, added, unknown, ignored = [], [], [], [], []
1246 1295 removed, deleted, clean = [], [], []
1247 1296
1248 1297 dmap = self._map
1249 1298 dmap.preload()
1250 1299
1251 1300 use_rust = True
1252 1301
1253 1302 allowed_matchers = (
1254 1303 matchmod.alwaysmatcher,
1255 1304 matchmod.exactmatcher,
1256 1305 matchmod.includematcher,
1257 1306 )
1258 1307
1259 1308 if rustmod is None:
1260 1309 use_rust = False
1261 1310 elif self._checkcase:
1262 1311 # Case-insensitive filesystems are not handled yet
1263 1312 use_rust = False
1264 1313 elif subrepos:
1265 1314 use_rust = False
1266 1315 elif sparse.enabled:
1267 1316 use_rust = False
1268 1317 elif not isinstance(match, allowed_matchers):
1269 1318 # Some matchers have yet to be implemented
1270 1319 use_rust = False
1271 1320
1272 1321 if use_rust:
1273 1322 try:
1274 1323 return self._rust_status(
1275 1324 match, listclean, listignored, listunknown
1276 1325 )
1277 1326 except rustmod.FallbackError:
1278 1327 pass
1279 1328
1280 1329 def noop(f):
1281 1330 pass
1282 1331
1283 1332 dcontains = dmap.__contains__
1284 1333 dget = dmap.__getitem__
1285 1334 ladd = lookup.append # aka "unsure"
1286 1335 madd = modified.append
1287 1336 aadd = added.append
1288 1337 uadd = unknown.append if listunknown else noop
1289 1338 iadd = ignored.append if listignored else noop
1290 1339 radd = removed.append
1291 1340 dadd = deleted.append
1292 1341 cadd = clean.append if listclean else noop
1293 1342 mexact = match.exact
1294 1343 dirignore = self._dirignore
1295 1344 checkexec = self._checkexec
1296 1345 copymap = self._map.copymap
1297 1346 lastnormaltime = self._lastnormaltime
1298 1347
1299 1348 # We need to do full walks when either
1300 1349 # - we're listing all clean files, or
1301 1350 # - match.traversedir does something, because match.traversedir should
1302 1351 # be called for every dir in the working dir
1303 1352 full = listclean or match.traversedir is not None
1304 1353 for fn, st in pycompat.iteritems(
1305 1354 self.walk(match, subrepos, listunknown, listignored, full=full)
1306 1355 ):
1307 1356 if not dcontains(fn):
1308 1357 if (listignored or mexact(fn)) and dirignore(fn):
1309 1358 if listignored:
1310 1359 iadd(fn)
1311 1360 else:
1312 1361 uadd(fn)
1313 1362 continue
1314 1363
1315 1364 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1316 1365 # written like that for performance reasons. dmap[fn] is not a
1317 1366 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1318 1367 # opcode has fast paths when the value to be unpacked is a tuple or
1319 1368 # a list, but falls back to creating a full-fledged iterator in
1320 1369 # general. That is much slower than simply accessing and storing the
1321 1370 # tuple members one by one.
1322 1371 t = dget(fn)
1323 1372 mode = t.mode
1324 1373 size = t.size
1325 1374 time = t.mtime
1326 1375
1327 1376 if not st and t.tracked:
1328 1377 dadd(fn)
1329 1378 elif t.merged:
1330 1379 madd(fn)
1331 1380 elif t.added:
1332 1381 aadd(fn)
1333 1382 elif t.removed:
1334 1383 radd(fn)
1335 1384 elif t.tracked:
1336 1385 if (
1337 1386 size >= 0
1338 1387 and (
1339 1388 (size != st.st_size and size != st.st_size & _rangemask)
1340 1389 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1341 1390 )
1342 1391 or t.from_p2
1343 1392 or fn in copymap
1344 1393 ):
1345 1394 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1346 1395 # issue6456: Size returned may be longer due to
1347 1396 # encryption on EXT-4 fscrypt, undecided.
1348 1397 ladd(fn)
1349 1398 else:
1350 1399 madd(fn)
1351 1400 elif (
1352 1401 time != st[stat.ST_MTIME]
1353 1402 and time != st[stat.ST_MTIME] & _rangemask
1354 1403 ):
1355 1404 ladd(fn)
1356 1405 elif st[stat.ST_MTIME] == lastnormaltime:
1357 1406 # fn may have just been marked as normal and it may have
1358 1407 # changed in the same second without changing its size.
1359 1408 # This can happen if we quickly do multiple commits.
1360 1409 # Force lookup, so we don't miss such a racy file change.
1361 1410 ladd(fn)
1362 1411 elif listclean:
1363 1412 cadd(fn)
1364 1413 status = scmutil.status(
1365 1414 modified, added, removed, deleted, unknown, ignored, clean
1366 1415 )
1367 1416 return (lookup, status)
1368 1417
1369 1418 def matches(self, match):
1370 1419 """
1371 1420 return files in the dirstate (in whatever state) filtered by match
1372 1421 """
1373 1422 dmap = self._map
1374 1423 if rustmod is not None:
1375 1424 dmap = self._map._rustmap
1376 1425
1377 1426 if match.always():
1378 1427 return dmap.keys()
1379 1428 files = match.files()
1380 1429 if match.isexact():
1381 1430 # fast path -- filter the other way around, since typically files is
1382 1431 # much smaller than dmap
1383 1432 return [f for f in files if f in dmap]
1384 1433 if match.prefix() and all(fn in dmap for fn in files):
1385 1434 # fast path -- all the values are known to be files, so just return
1386 1435 # that
1387 1436 return list(files)
1388 1437 return [f for f in dmap if match(f)]
1389 1438
1390 1439 def _actualfilename(self, tr):
1391 1440 if tr:
1392 1441 return self._pendingfilename
1393 1442 else:
1394 1443 return self._filename
1395 1444
1396 1445 def savebackup(self, tr, backupname):
1397 1446 '''Save current dirstate into backup file'''
1398 1447 filename = self._actualfilename(tr)
1399 1448 assert backupname != filename
1400 1449
1401 1450 # use '_writedirstate' instead of 'write' to write changes certainly,
1402 1451 # because the latter omits writing out if transaction is running.
1403 1452 # output file will be used to create backup of dirstate at this point.
1404 1453 if self._dirty or not self._opener.exists(filename):
1405 1454 self._writedirstate(
1406 1455 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1407 1456 )
1408 1457
1409 1458 if tr:
1410 1459 # ensure that subsequent tr.writepending returns True for
1411 1460 # changes written out above, even if dirstate is never
1412 1461 # changed after this
1413 1462 tr.addfilegenerator(
1414 1463 b'dirstate',
1415 1464 (self._filename,),
1416 1465 self._writedirstate,
1417 1466 location=b'plain',
1418 1467 )
1419 1468
1420 1469 # ensure that pending file written above is unlinked at
1421 1470 # failure, even if tr.writepending isn't invoked until the
1422 1471 # end of this transaction
1423 1472 tr.registertmp(filename, location=b'plain')
1424 1473
1425 1474 self._opener.tryunlink(backupname)
1426 1475 # hardlink backup is okay because _writedirstate is always called
1427 1476 # with an "atomictemp=True" file.
1428 1477 util.copyfile(
1429 1478 self._opener.join(filename),
1430 1479 self._opener.join(backupname),
1431 1480 hardlink=True,
1432 1481 )
1433 1482
1434 1483 def restorebackup(self, tr, backupname):
1435 1484 '''Restore dirstate by backup file'''
1436 1485 # this "invalidate()" prevents "wlock.release()" from writing
1437 1486 # changes of dirstate out after restoring from backup file
1438 1487 self.invalidate()
1439 1488 filename = self._actualfilename(tr)
1440 1489 o = self._opener
1441 1490 if util.samefile(o.join(backupname), o.join(filename)):
1442 1491 o.unlink(backupname)
1443 1492 else:
1444 1493 o.rename(backupname, filename, checkambig=True)
1445 1494
1446 1495 def clearbackup(self, tr, backupname):
1447 1496 '''Clear backup file'''
1448 1497 self._opener.unlink(backupname)
@@ -1,2299 +1,2289
1 1 # scmutil.py - Mercurial core utility functions
2 2 #
3 3 # Copyright 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 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import glob
12 12 import os
13 13 import posixpath
14 14 import re
15 15 import subprocess
16 16 import weakref
17 17
18 18 from .i18n import _
19 19 from .node import (
20 20 bin,
21 21 hex,
22 22 nullrev,
23 23 short,
24 24 wdirrev,
25 25 )
26 26 from .pycompat import getattr
27 27 from .thirdparty import attr
28 28 from . import (
29 29 copies as copiesmod,
30 30 encoding,
31 31 error,
32 32 match as matchmod,
33 33 obsolete,
34 34 obsutil,
35 35 pathutil,
36 36 phases,
37 37 policy,
38 38 pycompat,
39 39 requirements as requirementsmod,
40 40 revsetlang,
41 41 similar,
42 42 smartset,
43 43 url,
44 44 util,
45 45 vfs,
46 46 )
47 47
48 48 from .utils import (
49 49 hashutil,
50 50 procutil,
51 51 stringutil,
52 52 )
53 53
54 54 if pycompat.iswindows:
55 55 from . import scmwindows as scmplatform
56 56 else:
57 57 from . import scmposix as scmplatform
58 58
59 59 parsers = policy.importmod('parsers')
60 60 rustrevlog = policy.importrust('revlog')
61 61
62 62 termsize = scmplatform.termsize
63 63
64 64
65 65 @attr.s(slots=True, repr=False)
66 66 class status(object):
67 67 """Struct with a list of files per status.
68 68
69 69 The 'deleted', 'unknown' and 'ignored' properties are only
70 70 relevant to the working copy.
71 71 """
72 72
73 73 modified = attr.ib(default=attr.Factory(list))
74 74 added = attr.ib(default=attr.Factory(list))
75 75 removed = attr.ib(default=attr.Factory(list))
76 76 deleted = attr.ib(default=attr.Factory(list))
77 77 unknown = attr.ib(default=attr.Factory(list))
78 78 ignored = attr.ib(default=attr.Factory(list))
79 79 clean = attr.ib(default=attr.Factory(list))
80 80
81 81 def __iter__(self):
82 82 yield self.modified
83 83 yield self.added
84 84 yield self.removed
85 85 yield self.deleted
86 86 yield self.unknown
87 87 yield self.ignored
88 88 yield self.clean
89 89
90 90 def __repr__(self):
91 91 return (
92 92 r'<status modified=%s, added=%s, removed=%s, deleted=%s, '
93 93 r'unknown=%s, ignored=%s, clean=%s>'
94 94 ) % tuple(pycompat.sysstr(stringutil.pprint(v)) for v in self)
95 95
96 96
97 97 def itersubrepos(ctx1, ctx2):
98 98 """find subrepos in ctx1 or ctx2"""
99 99 # Create a (subpath, ctx) mapping where we prefer subpaths from
100 100 # ctx1. The subpaths from ctx2 are important when the .hgsub file
101 101 # has been modified (in ctx2) but not yet committed (in ctx1).
102 102 subpaths = dict.fromkeys(ctx2.substate, ctx2)
103 103 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
104 104
105 105 missing = set()
106 106
107 107 for subpath in ctx2.substate:
108 108 if subpath not in ctx1.substate:
109 109 del subpaths[subpath]
110 110 missing.add(subpath)
111 111
112 112 for subpath, ctx in sorted(pycompat.iteritems(subpaths)):
113 113 yield subpath, ctx.sub(subpath)
114 114
115 115 # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way,
116 116 # status and diff will have an accurate result when it does
117 117 # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared
118 118 # against itself.
119 119 for subpath in missing:
120 120 yield subpath, ctx2.nullsub(subpath, ctx1)
121 121
122 122
123 123 def nochangesfound(ui, repo, excluded=None):
124 124 """Report no changes for push/pull, excluded is None or a list of
125 125 nodes excluded from the push/pull.
126 126 """
127 127 secretlist = []
128 128 if excluded:
129 129 for n in excluded:
130 130 ctx = repo[n]
131 131 if ctx.phase() >= phases.secret and not ctx.extinct():
132 132 secretlist.append(n)
133 133
134 134 if secretlist:
135 135 ui.status(
136 136 _(b"no changes found (ignored %d secret changesets)\n")
137 137 % len(secretlist)
138 138 )
139 139 else:
140 140 ui.status(_(b"no changes found\n"))
141 141
142 142
143 143 def callcatch(ui, func):
144 144 """call func() with global exception handling
145 145
146 146 return func() if no exception happens. otherwise do some error handling
147 147 and return an exit code accordingly. does not handle all exceptions.
148 148 """
149 149 coarse_exit_code = -1
150 150 detailed_exit_code = -1
151 151 try:
152 152 try:
153 153 return func()
154 154 except: # re-raises
155 155 ui.traceback()
156 156 raise
157 157 # Global exception handling, alphabetically
158 158 # Mercurial-specific first, followed by built-in and library exceptions
159 159 except error.LockHeld as inst:
160 160 detailed_exit_code = 20
161 161 if inst.errno == errno.ETIMEDOUT:
162 162 reason = _(b'timed out waiting for lock held by %r') % (
163 163 pycompat.bytestr(inst.locker)
164 164 )
165 165 else:
166 166 reason = _(b'lock held by %r') % inst.locker
167 167 ui.error(
168 168 _(b"abort: %s: %s\n")
169 169 % (inst.desc or stringutil.forcebytestr(inst.filename), reason)
170 170 )
171 171 if not inst.locker:
172 172 ui.error(_(b"(lock might be very busy)\n"))
173 173 except error.LockUnavailable as inst:
174 174 detailed_exit_code = 20
175 175 ui.error(
176 176 _(b"abort: could not lock %s: %s\n")
177 177 % (
178 178 inst.desc or stringutil.forcebytestr(inst.filename),
179 179 encoding.strtolocal(inst.strerror),
180 180 )
181 181 )
182 182 except error.RepoError as inst:
183 183 ui.error(_(b"abort: %s\n") % inst)
184 184 if inst.hint:
185 185 ui.error(_(b"(%s)\n") % inst.hint)
186 186 except error.ResponseError as inst:
187 187 ui.error(_(b"abort: %s") % inst.args[0])
188 188 msg = inst.args[1]
189 189 if isinstance(msg, type(u'')):
190 190 msg = pycompat.sysbytes(msg)
191 191 if msg is None:
192 192 ui.error(b"\n")
193 193 elif not isinstance(msg, bytes):
194 194 ui.error(b" %r\n" % (msg,))
195 195 elif not msg:
196 196 ui.error(_(b" empty string\n"))
197 197 else:
198 198 ui.error(b"\n%r\n" % pycompat.bytestr(stringutil.ellipsis(msg)))
199 199 except error.CensoredNodeError as inst:
200 200 ui.error(_(b"abort: file censored %s\n") % inst)
201 201 except error.WdirUnsupported:
202 202 ui.error(_(b"abort: working directory revision cannot be specified\n"))
203 203 except error.Error as inst:
204 204 if inst.detailed_exit_code is not None:
205 205 detailed_exit_code = inst.detailed_exit_code
206 206 if inst.coarse_exit_code is not None:
207 207 coarse_exit_code = inst.coarse_exit_code
208 208 ui.error(inst.format())
209 209 except error.WorkerError as inst:
210 210 # Don't print a message -- the worker already should have
211 211 return inst.status_code
212 212 except ImportError as inst:
213 213 ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst))
214 214 m = stringutil.forcebytestr(inst).split()[-1]
215 215 if m in b"mpatch bdiff".split():
216 216 ui.error(_(b"(did you forget to compile extensions?)\n"))
217 217 elif m in b"zlib".split():
218 218 ui.error(_(b"(is your Python install correct?)\n"))
219 219 except util.urlerr.httperror as inst:
220 220 detailed_exit_code = 100
221 221 ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst))
222 222 except util.urlerr.urlerror as inst:
223 223 detailed_exit_code = 100
224 224 try: # usually it is in the form (errno, strerror)
225 225 reason = inst.reason.args[1]
226 226 except (AttributeError, IndexError):
227 227 # it might be anything, for example a string
228 228 reason = inst.reason
229 229 if isinstance(reason, pycompat.unicode):
230 230 # SSLError of Python 2.7.9 contains a unicode
231 231 reason = encoding.unitolocal(reason)
232 232 ui.error(_(b"abort: error: %s\n") % stringutil.forcebytestr(reason))
233 233 except (IOError, OSError) as inst:
234 234 if (
235 235 util.safehasattr(inst, b"args")
236 236 and inst.args
237 237 and inst.args[0] == errno.EPIPE
238 238 ):
239 239 pass
240 240 elif getattr(inst, "strerror", None): # common IOError or OSError
241 241 if getattr(inst, "filename", None) is not None:
242 242 ui.error(
243 243 _(b"abort: %s: '%s'\n")
244 244 % (
245 245 encoding.strtolocal(inst.strerror),
246 246 stringutil.forcebytestr(inst.filename),
247 247 )
248 248 )
249 249 else:
250 250 ui.error(_(b"abort: %s\n") % encoding.strtolocal(inst.strerror))
251 251 else: # suspicious IOError
252 252 raise
253 253 except MemoryError:
254 254 ui.error(_(b"abort: out of memory\n"))
255 255 except SystemExit as inst:
256 256 # Commands shouldn't sys.exit directly, but give a return code.
257 257 # Just in case catch this and and pass exit code to caller.
258 258 detailed_exit_code = 254
259 259 coarse_exit_code = inst.code
260 260
261 261 if ui.configbool(b'ui', b'detailed-exit-code'):
262 262 return detailed_exit_code
263 263 else:
264 264 return coarse_exit_code
265 265
266 266
267 267 def checknewlabel(repo, lbl, kind):
268 268 # Do not use the "kind" parameter in ui output.
269 269 # It makes strings difficult to translate.
270 270 if lbl in [b'tip', b'.', b'null']:
271 271 raise error.InputError(_(b"the name '%s' is reserved") % lbl)
272 272 for c in (b':', b'\0', b'\n', b'\r'):
273 273 if c in lbl:
274 274 raise error.InputError(
275 275 _(b"%r cannot be used in a name") % pycompat.bytestr(c)
276 276 )
277 277 try:
278 278 int(lbl)
279 279 raise error.InputError(_(b"cannot use an integer as a name"))
280 280 except ValueError:
281 281 pass
282 282 if lbl.strip() != lbl:
283 283 raise error.InputError(
284 284 _(b"leading or trailing whitespace in name %r") % lbl
285 285 )
286 286
287 287
288 288 def checkfilename(f):
289 289 '''Check that the filename f is an acceptable filename for a tracked file'''
290 290 if b'\r' in f or b'\n' in f:
291 291 raise error.InputError(
292 292 _(b"'\\n' and '\\r' disallowed in filenames: %r")
293 293 % pycompat.bytestr(f)
294 294 )
295 295
296 296
297 297 def checkportable(ui, f):
298 298 '''Check if filename f is portable and warn or abort depending on config'''
299 299 checkfilename(f)
300 300 abort, warn = checkportabilityalert(ui)
301 301 if abort or warn:
302 302 msg = util.checkwinfilename(f)
303 303 if msg:
304 304 msg = b"%s: %s" % (msg, procutil.shellquote(f))
305 305 if abort:
306 306 raise error.InputError(msg)
307 307 ui.warn(_(b"warning: %s\n") % msg)
308 308
309 309
310 310 def checkportabilityalert(ui):
311 311 """check if the user's config requests nothing, a warning, or abort for
312 312 non-portable filenames"""
313 313 val = ui.config(b'ui', b'portablefilenames')
314 314 lval = val.lower()
315 315 bval = stringutil.parsebool(val)
316 316 abort = pycompat.iswindows or lval == b'abort'
317 317 warn = bval or lval == b'warn'
318 318 if bval is None and not (warn or abort or lval == b'ignore'):
319 319 raise error.ConfigError(
320 320 _(b"ui.portablefilenames value is invalid ('%s')") % val
321 321 )
322 322 return abort, warn
323 323
324 324
325 325 class casecollisionauditor(object):
326 326 def __init__(self, ui, abort, dirstate):
327 327 self._ui = ui
328 328 self._abort = abort
329 329 allfiles = b'\0'.join(dirstate)
330 330 self._loweredfiles = set(encoding.lower(allfiles).split(b'\0'))
331 331 self._dirstate = dirstate
332 332 # The purpose of _newfiles is so that we don't complain about
333 333 # case collisions if someone were to call this object with the
334 334 # same filename twice.
335 335 self._newfiles = set()
336 336
337 337 def __call__(self, f):
338 338 if f in self._newfiles:
339 339 return
340 340 fl = encoding.lower(f)
341 341 if fl in self._loweredfiles and f not in self._dirstate:
342 342 msg = _(b'possible case-folding collision for %s') % f
343 343 if self._abort:
344 344 raise error.Abort(msg)
345 345 self._ui.warn(_(b"warning: %s\n") % msg)
346 346 self._loweredfiles.add(fl)
347 347 self._newfiles.add(f)
348 348
349 349
350 350 def filteredhash(repo, maxrev):
351 351 """build hash of filtered revisions in the current repoview.
352 352
353 353 Multiple caches perform up-to-date validation by checking that the
354 354 tiprev and tipnode stored in the cache file match the current repository.
355 355 However, this is not sufficient for validating repoviews because the set
356 356 of revisions in the view may change without the repository tiprev and
357 357 tipnode changing.
358 358
359 359 This function hashes all the revs filtered from the view and returns
360 360 that SHA-1 digest.
361 361 """
362 362 cl = repo.changelog
363 363 if not cl.filteredrevs:
364 364 return None
365 365 key = cl._filteredrevs_hashcache.get(maxrev)
366 366 if not key:
367 367 revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
368 368 if revs:
369 369 s = hashutil.sha1()
370 370 for rev in revs:
371 371 s.update(b'%d;' % rev)
372 372 key = s.digest()
373 373 cl._filteredrevs_hashcache[maxrev] = key
374 374 return key
375 375
376 376
377 377 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
378 378 """yield every hg repository under path, always recursively.
379 379 The recurse flag will only control recursion into repo working dirs"""
380 380
381 381 def errhandler(err):
382 382 if err.filename == path:
383 383 raise err
384 384
385 385 samestat = getattr(os.path, 'samestat', None)
386 386 if followsym and samestat is not None:
387 387
388 388 def adddir(dirlst, dirname):
389 389 dirstat = os.stat(dirname)
390 390 match = any(samestat(dirstat, lstdirstat) for lstdirstat in dirlst)
391 391 if not match:
392 392 dirlst.append(dirstat)
393 393 return not match
394 394
395 395 else:
396 396 followsym = False
397 397
398 398 if (seen_dirs is None) and followsym:
399 399 seen_dirs = []
400 400 adddir(seen_dirs, path)
401 401 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
402 402 dirs.sort()
403 403 if b'.hg' in dirs:
404 404 yield root # found a repository
405 405 qroot = os.path.join(root, b'.hg', b'patches')
406 406 if os.path.isdir(os.path.join(qroot, b'.hg')):
407 407 yield qroot # we have a patch queue repo here
408 408 if recurse:
409 409 # avoid recursing inside the .hg directory
410 410 dirs.remove(b'.hg')
411 411 else:
412 412 dirs[:] = [] # don't descend further
413 413 elif followsym:
414 414 newdirs = []
415 415 for d in dirs:
416 416 fname = os.path.join(root, d)
417 417 if adddir(seen_dirs, fname):
418 418 if os.path.islink(fname):
419 419 for hgname in walkrepos(fname, True, seen_dirs):
420 420 yield hgname
421 421 else:
422 422 newdirs.append(d)
423 423 dirs[:] = newdirs
424 424
425 425
426 426 def binnode(ctx):
427 427 """Return binary node id for a given basectx"""
428 428 node = ctx.node()
429 429 if node is None:
430 430 return ctx.repo().nodeconstants.wdirid
431 431 return node
432 432
433 433
434 434 def intrev(ctx):
435 435 """Return integer for a given basectx that can be used in comparison or
436 436 arithmetic operation"""
437 437 rev = ctx.rev()
438 438 if rev is None:
439 439 return wdirrev
440 440 return rev
441 441
442 442
443 443 def formatchangeid(ctx):
444 444 """Format changectx as '{rev}:{node|formatnode}', which is the default
445 445 template provided by logcmdutil.changesettemplater"""
446 446 repo = ctx.repo()
447 447 return formatrevnode(repo.ui, intrev(ctx), binnode(ctx))
448 448
449 449
450 450 def formatrevnode(ui, rev, node):
451 451 """Format given revision and node depending on the current verbosity"""
452 452 if ui.debugflag:
453 453 hexfunc = hex
454 454 else:
455 455 hexfunc = short
456 456 return b'%d:%s' % (rev, hexfunc(node))
457 457
458 458
459 459 def resolvehexnodeidprefix(repo, prefix):
460 460 if prefix.startswith(b'x'):
461 461 prefix = prefix[1:]
462 462 try:
463 463 # Uses unfiltered repo because it's faster when prefix is ambiguous/
464 464 # This matches the shortesthexnodeidprefix() function below.
465 465 node = repo.unfiltered().changelog._partialmatch(prefix)
466 466 except error.AmbiguousPrefixLookupError:
467 467 revset = repo.ui.config(
468 468 b'experimental', b'revisions.disambiguatewithin'
469 469 )
470 470 if revset:
471 471 # Clear config to avoid infinite recursion
472 472 configoverrides = {
473 473 (b'experimental', b'revisions.disambiguatewithin'): None
474 474 }
475 475 with repo.ui.configoverride(configoverrides):
476 476 revs = repo.anyrevs([revset], user=True)
477 477 matches = []
478 478 for rev in revs:
479 479 node = repo.changelog.node(rev)
480 480 if hex(node).startswith(prefix):
481 481 matches.append(node)
482 482 if len(matches) == 1:
483 483 return matches[0]
484 484 raise
485 485 if node is None:
486 486 return
487 487 repo.changelog.rev(node) # make sure node isn't filtered
488 488 return node
489 489
490 490
491 491 def mayberevnum(repo, prefix):
492 492 """Checks if the given prefix may be mistaken for a revision number"""
493 493 try:
494 494 i = int(prefix)
495 495 # if we are a pure int, then starting with zero will not be
496 496 # confused as a rev; or, obviously, if the int is larger
497 497 # than the value of the tip rev. We still need to disambiguate if
498 498 # prefix == '0', since that *is* a valid revnum.
499 499 if (prefix != b'0' and prefix[0:1] == b'0') or i >= len(repo):
500 500 return False
501 501 return True
502 502 except ValueError:
503 503 return False
504 504
505 505
506 506 def shortesthexnodeidprefix(repo, node, minlength=1, cache=None):
507 507 """Find the shortest unambiguous prefix that matches hexnode.
508 508
509 509 If "cache" is not None, it must be a dictionary that can be used for
510 510 caching between calls to this method.
511 511 """
512 512 # _partialmatch() of filtered changelog could take O(len(repo)) time,
513 513 # which would be unacceptably slow. so we look for hash collision in
514 514 # unfiltered space, which means some hashes may be slightly longer.
515 515
516 516 minlength = max(minlength, 1)
517 517
518 518 def disambiguate(prefix):
519 519 """Disambiguate against revnums."""
520 520 if repo.ui.configbool(b'experimental', b'revisions.prefixhexnode'):
521 521 if mayberevnum(repo, prefix):
522 522 return b'x' + prefix
523 523 else:
524 524 return prefix
525 525
526 526 hexnode = hex(node)
527 527 for length in range(len(prefix), len(hexnode) + 1):
528 528 prefix = hexnode[:length]
529 529 if not mayberevnum(repo, prefix):
530 530 return prefix
531 531
532 532 cl = repo.unfiltered().changelog
533 533 revset = repo.ui.config(b'experimental', b'revisions.disambiguatewithin')
534 534 if revset:
535 535 revs = None
536 536 if cache is not None:
537 537 revs = cache.get(b'disambiguationrevset')
538 538 if revs is None:
539 539 revs = repo.anyrevs([revset], user=True)
540 540 if cache is not None:
541 541 cache[b'disambiguationrevset'] = revs
542 542 if cl.rev(node) in revs:
543 543 hexnode = hex(node)
544 544 nodetree = None
545 545 if cache is not None:
546 546 nodetree = cache.get(b'disambiguationnodetree')
547 547 if not nodetree:
548 548 if util.safehasattr(parsers, 'nodetree'):
549 549 # The CExt is the only implementation to provide a nodetree
550 550 # class so far.
551 551 index = cl.index
552 552 if util.safehasattr(index, 'get_cindex'):
553 553 # the rust wrapped need to give access to its internal index
554 554 index = index.get_cindex()
555 555 nodetree = parsers.nodetree(index, len(revs))
556 556 for r in revs:
557 557 nodetree.insert(r)
558 558 if cache is not None:
559 559 cache[b'disambiguationnodetree'] = nodetree
560 560 if nodetree is not None:
561 561 length = max(nodetree.shortest(node), minlength)
562 562 prefix = hexnode[:length]
563 563 return disambiguate(prefix)
564 564 for length in range(minlength, len(hexnode) + 1):
565 565 matches = []
566 566 prefix = hexnode[:length]
567 567 for rev in revs:
568 568 otherhexnode = repo[rev].hex()
569 569 if prefix == otherhexnode[:length]:
570 570 matches.append(otherhexnode)
571 571 if len(matches) == 1:
572 572 return disambiguate(prefix)
573 573
574 574 try:
575 575 return disambiguate(cl.shortest(node, minlength))
576 576 except error.LookupError:
577 577 raise error.RepoLookupError()
578 578
579 579
580 580 def isrevsymbol(repo, symbol):
581 581 """Checks if a symbol exists in the repo.
582 582
583 583 See revsymbol() for details. Raises error.AmbiguousPrefixLookupError if the
584 584 symbol is an ambiguous nodeid prefix.
585 585 """
586 586 try:
587 587 revsymbol(repo, symbol)
588 588 return True
589 589 except error.RepoLookupError:
590 590 return False
591 591
592 592
593 593 def revsymbol(repo, symbol):
594 594 """Returns a context given a single revision symbol (as string).
595 595
596 596 This is similar to revsingle(), but accepts only a single revision symbol,
597 597 i.e. things like ".", "tip", "1234", "deadbeef", "my-bookmark" work, but
598 598 not "max(public())".
599 599 """
600 600 if not isinstance(symbol, bytes):
601 601 msg = (
602 602 b"symbol (%s of type %s) was not a string, did you mean "
603 603 b"repo[symbol]?" % (symbol, type(symbol))
604 604 )
605 605 raise error.ProgrammingError(msg)
606 606 try:
607 607 if symbol in (b'.', b'tip', b'null'):
608 608 return repo[symbol]
609 609
610 610 try:
611 611 r = int(symbol)
612 612 if b'%d' % r != symbol:
613 613 raise ValueError
614 614 l = len(repo.changelog)
615 615 if r < 0:
616 616 r += l
617 617 if r < 0 or r >= l and r != wdirrev:
618 618 raise ValueError
619 619 return repo[r]
620 620 except error.FilteredIndexError:
621 621 raise
622 622 except (ValueError, OverflowError, IndexError):
623 623 pass
624 624
625 625 if len(symbol) == 2 * repo.nodeconstants.nodelen:
626 626 try:
627 627 node = bin(symbol)
628 628 rev = repo.changelog.rev(node)
629 629 return repo[rev]
630 630 except error.FilteredLookupError:
631 631 raise
632 632 except (TypeError, LookupError):
633 633 pass
634 634
635 635 # look up bookmarks through the name interface
636 636 try:
637 637 node = repo.names.singlenode(repo, symbol)
638 638 rev = repo.changelog.rev(node)
639 639 return repo[rev]
640 640 except KeyError:
641 641 pass
642 642
643 643 node = resolvehexnodeidprefix(repo, symbol)
644 644 if node is not None:
645 645 rev = repo.changelog.rev(node)
646 646 return repo[rev]
647 647
648 648 raise error.RepoLookupError(_(b"unknown revision '%s'") % symbol)
649 649
650 650 except error.WdirUnsupported:
651 651 return repo[None]
652 652 except (
653 653 error.FilteredIndexError,
654 654 error.FilteredLookupError,
655 655 error.FilteredRepoLookupError,
656 656 ):
657 657 raise _filterederror(repo, symbol)
658 658
659 659
660 660 def _filterederror(repo, changeid):
661 661 """build an exception to be raised about a filtered changeid
662 662
663 663 This is extracted in a function to help extensions (eg: evolve) to
664 664 experiment with various message variants."""
665 665 if repo.filtername.startswith(b'visible'):
666 666
667 667 # Check if the changeset is obsolete
668 668 unfilteredrepo = repo.unfiltered()
669 669 ctx = revsymbol(unfilteredrepo, changeid)
670 670
671 671 # If the changeset is obsolete, enrich the message with the reason
672 672 # that made this changeset not visible
673 673 if ctx.obsolete():
674 674 msg = obsutil._getfilteredreason(repo, changeid, ctx)
675 675 else:
676 676 msg = _(b"hidden revision '%s'") % changeid
677 677
678 678 hint = _(b'use --hidden to access hidden revisions')
679 679
680 680 return error.FilteredRepoLookupError(msg, hint=hint)
681 681 msg = _(b"filtered revision '%s' (not in '%s' subset)")
682 682 msg %= (changeid, repo.filtername)
683 683 return error.FilteredRepoLookupError(msg)
684 684
685 685
686 686 def revsingle(repo, revspec, default=b'.', localalias=None):
687 687 if not revspec and revspec != 0:
688 688 return repo[default]
689 689
690 690 l = revrange(repo, [revspec], localalias=localalias)
691 691 if not l:
692 692 raise error.Abort(_(b'empty revision set'))
693 693 return repo[l.last()]
694 694
695 695
696 696 def _pairspec(revspec):
697 697 tree = revsetlang.parse(revspec)
698 698 return tree and tree[0] in (
699 699 b'range',
700 700 b'rangepre',
701 701 b'rangepost',
702 702 b'rangeall',
703 703 )
704 704
705 705
706 706 def revpair(repo, revs):
707 707 if not revs:
708 708 return repo[b'.'], repo[None]
709 709
710 710 l = revrange(repo, revs)
711 711
712 712 if not l:
713 713 raise error.Abort(_(b'empty revision range'))
714 714
715 715 first = l.first()
716 716 second = l.last()
717 717
718 718 if (
719 719 first == second
720 720 and len(revs) >= 2
721 721 and not all(revrange(repo, [r]) for r in revs)
722 722 ):
723 723 raise error.Abort(_(b'empty revision on one side of range'))
724 724
725 725 # if top-level is range expression, the result must always be a pair
726 726 if first == second and len(revs) == 1 and not _pairspec(revs[0]):
727 727 return repo[first], repo[None]
728 728
729 729 return repo[first], repo[second]
730 730
731 731
732 732 def revrange(repo, specs, localalias=None):
733 733 """Execute 1 to many revsets and return the union.
734 734
735 735 This is the preferred mechanism for executing revsets using user-specified
736 736 config options, such as revset aliases.
737 737
738 738 The revsets specified by ``specs`` will be executed via a chained ``OR``
739 739 expression. If ``specs`` is empty, an empty result is returned.
740 740
741 741 ``specs`` can contain integers, in which case they are assumed to be
742 742 revision numbers.
743 743
744 744 It is assumed the revsets are already formatted. If you have arguments
745 745 that need to be expanded in the revset, call ``revsetlang.formatspec()``
746 746 and pass the result as an element of ``specs``.
747 747
748 748 Specifying a single revset is allowed.
749 749
750 750 Returns a ``smartset.abstractsmartset`` which is a list-like interface over
751 751 integer revisions.
752 752 """
753 753 allspecs = []
754 754 for spec in specs:
755 755 if isinstance(spec, int):
756 756 spec = revsetlang.formatspec(b'%d', spec)
757 757 allspecs.append(spec)
758 758 return repo.anyrevs(allspecs, user=True, localalias=localalias)
759 759
760 760
761 761 def increasingwindows(windowsize=8, sizelimit=512):
762 762 while True:
763 763 yield windowsize
764 764 if windowsize < sizelimit:
765 765 windowsize *= 2
766 766
767 767
768 768 def walkchangerevs(repo, revs, makefilematcher, prepare):
769 769 """Iterate over files and the revs in a "windowed" way.
770 770
771 771 Callers most commonly need to iterate backwards over the history
772 772 in which they are interested. Doing so has awful (quadratic-looking)
773 773 performance, so we use iterators in a "windowed" way.
774 774
775 775 We walk a window of revisions in the desired order. Within the
776 776 window, we first walk forwards to gather data, then in the desired
777 777 order (usually backwards) to display it.
778 778
779 779 This function returns an iterator yielding contexts. Before
780 780 yielding each context, the iterator will first call the prepare
781 781 function on each context in the window in forward order."""
782 782
783 783 if not revs:
784 784 return []
785 785 change = repo.__getitem__
786 786
787 787 def iterate():
788 788 it = iter(revs)
789 789 stopiteration = False
790 790 for windowsize in increasingwindows():
791 791 nrevs = []
792 792 for i in pycompat.xrange(windowsize):
793 793 rev = next(it, None)
794 794 if rev is None:
795 795 stopiteration = True
796 796 break
797 797 nrevs.append(rev)
798 798 for rev in sorted(nrevs):
799 799 ctx = change(rev)
800 800 prepare(ctx, makefilematcher(ctx))
801 801 for rev in nrevs:
802 802 yield change(rev)
803 803
804 804 if stopiteration:
805 805 break
806 806
807 807 return iterate()
808 808
809 809
810 810 def meaningfulparents(repo, ctx):
811 811 """Return list of meaningful (or all if debug) parentrevs for rev.
812 812
813 813 For merges (two non-nullrev revisions) both parents are meaningful.
814 814 Otherwise the first parent revision is considered meaningful if it
815 815 is not the preceding revision.
816 816 """
817 817 parents = ctx.parents()
818 818 if len(parents) > 1:
819 819 return parents
820 820 if repo.ui.debugflag:
821 821 return [parents[0], repo[nullrev]]
822 822 if parents[0].rev() >= intrev(ctx) - 1:
823 823 return []
824 824 return parents
825 825
826 826
827 827 def getuipathfn(repo, legacyrelativevalue=False, forcerelativevalue=None):
828 828 """Return a function that produced paths for presenting to the user.
829 829
830 830 The returned function takes a repo-relative path and produces a path
831 831 that can be presented in the UI.
832 832
833 833 Depending on the value of ui.relative-paths, either a repo-relative or
834 834 cwd-relative path will be produced.
835 835
836 836 legacyrelativevalue is the value to use if ui.relative-paths=legacy
837 837
838 838 If forcerelativevalue is not None, then that value will be used regardless
839 839 of what ui.relative-paths is set to.
840 840 """
841 841 if forcerelativevalue is not None:
842 842 relative = forcerelativevalue
843 843 else:
844 844 config = repo.ui.config(b'ui', b'relative-paths')
845 845 if config == b'legacy':
846 846 relative = legacyrelativevalue
847 847 else:
848 848 relative = stringutil.parsebool(config)
849 849 if relative is None:
850 850 raise error.ConfigError(
851 851 _(b"ui.relative-paths is not a boolean ('%s')") % config
852 852 )
853 853
854 854 if relative:
855 855 cwd = repo.getcwd()
856 856 if cwd != b'':
857 857 # this branch would work even if cwd == b'' (ie cwd = repo
858 858 # root), but its generality makes the returned function slower
859 859 pathto = repo.pathto
860 860 return lambda f: pathto(f, cwd)
861 861 if repo.ui.configbool(b'ui', b'slash'):
862 862 return lambda f: f
863 863 else:
864 864 return util.localpath
865 865
866 866
867 867 def subdiruipathfn(subpath, uipathfn):
868 868 '''Create a new uipathfn that treats the file as relative to subpath.'''
869 869 return lambda f: uipathfn(posixpath.join(subpath, f))
870 870
871 871
872 872 def anypats(pats, opts):
873 873 """Checks if any patterns, including --include and --exclude were given.
874 874
875 875 Some commands (e.g. addremove) use this condition for deciding whether to
876 876 print absolute or relative paths.
877 877 """
878 878 return bool(pats or opts.get(b'include') or opts.get(b'exclude'))
879 879
880 880
881 881 def expandpats(pats):
882 882 """Expand bare globs when running on windows.
883 883 On posix we assume it already has already been done by sh."""
884 884 if not util.expandglobs:
885 885 return list(pats)
886 886 ret = []
887 887 for kindpat in pats:
888 888 kind, pat = matchmod._patsplit(kindpat, None)
889 889 if kind is None:
890 890 try:
891 891 globbed = glob.glob(pat)
892 892 except re.error:
893 893 globbed = [pat]
894 894 if globbed:
895 895 ret.extend(globbed)
896 896 continue
897 897 ret.append(kindpat)
898 898 return ret
899 899
900 900
901 901 def matchandpats(
902 902 ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None
903 903 ):
904 904 """Return a matcher and the patterns that were used.
905 905 The matcher will warn about bad matches, unless an alternate badfn callback
906 906 is provided."""
907 907 if opts is None:
908 908 opts = {}
909 909 if not globbed and default == b'relpath':
910 910 pats = expandpats(pats or [])
911 911
912 912 uipathfn = getuipathfn(ctx.repo(), legacyrelativevalue=True)
913 913
914 914 def bad(f, msg):
915 915 ctx.repo().ui.warn(b"%s: %s\n" % (uipathfn(f), msg))
916 916
917 917 if badfn is None:
918 918 badfn = bad
919 919
920 920 m = ctx.match(
921 921 pats,
922 922 opts.get(b'include'),
923 923 opts.get(b'exclude'),
924 924 default,
925 925 listsubrepos=opts.get(b'subrepos'),
926 926 badfn=badfn,
927 927 )
928 928
929 929 if m.always():
930 930 pats = []
931 931 return m, pats
932 932
933 933
934 934 def match(
935 935 ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None
936 936 ):
937 937 '''Return a matcher that will warn about bad matches.'''
938 938 return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
939 939
940 940
941 941 def matchall(repo):
942 942 '''Return a matcher that will efficiently match everything.'''
943 943 return matchmod.always()
944 944
945 945
946 946 def matchfiles(repo, files, badfn=None):
947 947 '''Return a matcher that will efficiently match exactly these files.'''
948 948 return matchmod.exact(files, badfn=badfn)
949 949
950 950
951 951 def parsefollowlinespattern(repo, rev, pat, msg):
952 952 """Return a file name from `pat` pattern suitable for usage in followlines
953 953 logic.
954 954 """
955 955 if not matchmod.patkind(pat):
956 956 return pathutil.canonpath(repo.root, repo.getcwd(), pat)
957 957 else:
958 958 ctx = repo[rev]
959 959 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=ctx)
960 960 files = [f for f in ctx if m(f)]
961 961 if len(files) != 1:
962 962 raise error.ParseError(msg)
963 963 return files[0]
964 964
965 965
966 966 def getorigvfs(ui, repo):
967 967 """return a vfs suitable to save 'orig' file
968 968
969 969 return None if no special directory is configured"""
970 970 origbackuppath = ui.config(b'ui', b'origbackuppath')
971 971 if not origbackuppath:
972 972 return None
973 973 return vfs.vfs(repo.wvfs.join(origbackuppath))
974 974
975 975
976 976 def backuppath(ui, repo, filepath):
977 977 """customize where working copy backup files (.orig files) are created
978 978
979 979 Fetch user defined path from config file: [ui] origbackuppath = <path>
980 980 Fall back to default (filepath with .orig suffix) if not specified
981 981
982 982 filepath is repo-relative
983 983
984 984 Returns an absolute path
985 985 """
986 986 origvfs = getorigvfs(ui, repo)
987 987 if origvfs is None:
988 988 return repo.wjoin(filepath + b".orig")
989 989
990 990 origbackupdir = origvfs.dirname(filepath)
991 991 if not origvfs.isdir(origbackupdir) or origvfs.islink(origbackupdir):
992 992 ui.note(_(b'creating directory: %s\n') % origvfs.join(origbackupdir))
993 993
994 994 # Remove any files that conflict with the backup file's path
995 995 for f in reversed(list(pathutil.finddirs(filepath))):
996 996 if origvfs.isfileorlink(f):
997 997 ui.note(_(b'removing conflicting file: %s\n') % origvfs.join(f))
998 998 origvfs.unlink(f)
999 999 break
1000 1000
1001 1001 origvfs.makedirs(origbackupdir)
1002 1002
1003 1003 if origvfs.isdir(filepath) and not origvfs.islink(filepath):
1004 1004 ui.note(
1005 1005 _(b'removing conflicting directory: %s\n') % origvfs.join(filepath)
1006 1006 )
1007 1007 origvfs.rmtree(filepath, forcibly=True)
1008 1008
1009 1009 return origvfs.join(filepath)
1010 1010
1011 1011
1012 1012 class _containsnode(object):
1013 1013 """proxy __contains__(node) to container.__contains__ which accepts revs"""
1014 1014
1015 1015 def __init__(self, repo, revcontainer):
1016 1016 self._torev = repo.changelog.rev
1017 1017 self._revcontains = revcontainer.__contains__
1018 1018
1019 1019 def __contains__(self, node):
1020 1020 return self._revcontains(self._torev(node))
1021 1021
1022 1022
1023 1023 def cleanupnodes(
1024 1024 repo,
1025 1025 replacements,
1026 1026 operation,
1027 1027 moves=None,
1028 1028 metadata=None,
1029 1029 fixphase=False,
1030 1030 targetphase=None,
1031 1031 backup=True,
1032 1032 ):
1033 1033 """do common cleanups when old nodes are replaced by new nodes
1034 1034
1035 1035 That includes writing obsmarkers or stripping nodes, and moving bookmarks.
1036 1036 (we might also want to move working directory parent in the future)
1037 1037
1038 1038 By default, bookmark moves are calculated automatically from 'replacements',
1039 1039 but 'moves' can be used to override that. Also, 'moves' may include
1040 1040 additional bookmark moves that should not have associated obsmarkers.
1041 1041
1042 1042 replacements is {oldnode: [newnode]} or a iterable of nodes if they do not
1043 1043 have replacements. operation is a string, like "rebase".
1044 1044
1045 1045 metadata is dictionary containing metadata to be stored in obsmarker if
1046 1046 obsolescence is enabled.
1047 1047 """
1048 1048 assert fixphase or targetphase is None
1049 1049 if not replacements and not moves:
1050 1050 return
1051 1051
1052 1052 # translate mapping's other forms
1053 1053 if not util.safehasattr(replacements, b'items'):
1054 1054 replacements = {(n,): () for n in replacements}
1055 1055 else:
1056 1056 # upgrading non tuple "source" to tuple ones for BC
1057 1057 repls = {}
1058 1058 for key, value in replacements.items():
1059 1059 if not isinstance(key, tuple):
1060 1060 key = (key,)
1061 1061 repls[key] = value
1062 1062 replacements = repls
1063 1063
1064 1064 # Unfiltered repo is needed since nodes in replacements might be hidden.
1065 1065 unfi = repo.unfiltered()
1066 1066
1067 1067 # Calculate bookmark movements
1068 1068 if moves is None:
1069 1069 moves = {}
1070 1070 for oldnodes, newnodes in replacements.items():
1071 1071 for oldnode in oldnodes:
1072 1072 if oldnode in moves:
1073 1073 continue
1074 1074 if len(newnodes) > 1:
1075 1075 # usually a split, take the one with biggest rev number
1076 1076 newnode = next(unfi.set(b'max(%ln)', newnodes)).node()
1077 1077 elif len(newnodes) == 0:
1078 1078 # move bookmark backwards
1079 1079 allreplaced = []
1080 1080 for rep in replacements:
1081 1081 allreplaced.extend(rep)
1082 1082 roots = list(
1083 1083 unfi.set(b'max((::%n) - %ln)', oldnode, allreplaced)
1084 1084 )
1085 1085 if roots:
1086 1086 newnode = roots[0].node()
1087 1087 else:
1088 1088 newnode = repo.nullid
1089 1089 else:
1090 1090 newnode = newnodes[0]
1091 1091 moves[oldnode] = newnode
1092 1092
1093 1093 allnewnodes = [n for ns in replacements.values() for n in ns]
1094 1094 toretract = {}
1095 1095 toadvance = {}
1096 1096 if fixphase:
1097 1097 precursors = {}
1098 1098 for oldnodes, newnodes in replacements.items():
1099 1099 for oldnode in oldnodes:
1100 1100 for newnode in newnodes:
1101 1101 precursors.setdefault(newnode, []).append(oldnode)
1102 1102
1103 1103 allnewnodes.sort(key=lambda n: unfi[n].rev())
1104 1104 newphases = {}
1105 1105
1106 1106 def phase(ctx):
1107 1107 return newphases.get(ctx.node(), ctx.phase())
1108 1108
1109 1109 for newnode in allnewnodes:
1110 1110 ctx = unfi[newnode]
1111 1111 parentphase = max(phase(p) for p in ctx.parents())
1112 1112 if targetphase is None:
1113 1113 oldphase = max(
1114 1114 unfi[oldnode].phase() for oldnode in precursors[newnode]
1115 1115 )
1116 1116 newphase = max(oldphase, parentphase)
1117 1117 else:
1118 1118 newphase = max(targetphase, parentphase)
1119 1119 newphases[newnode] = newphase
1120 1120 if newphase > ctx.phase():
1121 1121 toretract.setdefault(newphase, []).append(newnode)
1122 1122 elif newphase < ctx.phase():
1123 1123 toadvance.setdefault(newphase, []).append(newnode)
1124 1124
1125 1125 with repo.transaction(b'cleanup') as tr:
1126 1126 # Move bookmarks
1127 1127 bmarks = repo._bookmarks
1128 1128 bmarkchanges = []
1129 1129 for oldnode, newnode in moves.items():
1130 1130 oldbmarks = repo.nodebookmarks(oldnode)
1131 1131 if not oldbmarks:
1132 1132 continue
1133 1133 from . import bookmarks # avoid import cycle
1134 1134
1135 1135 repo.ui.debug(
1136 1136 b'moving bookmarks %r from %s to %s\n'
1137 1137 % (
1138 1138 pycompat.rapply(pycompat.maybebytestr, oldbmarks),
1139 1139 hex(oldnode),
1140 1140 hex(newnode),
1141 1141 )
1142 1142 )
1143 1143 # Delete divergent bookmarks being parents of related newnodes
1144 1144 deleterevs = repo.revs(
1145 1145 b'parents(roots(%ln & (::%n))) - parents(%n)',
1146 1146 allnewnodes,
1147 1147 newnode,
1148 1148 oldnode,
1149 1149 )
1150 1150 deletenodes = _containsnode(repo, deleterevs)
1151 1151 for name in oldbmarks:
1152 1152 bmarkchanges.append((name, newnode))
1153 1153 for b in bookmarks.divergent2delete(repo, deletenodes, name):
1154 1154 bmarkchanges.append((b, None))
1155 1155
1156 1156 if bmarkchanges:
1157 1157 bmarks.applychanges(repo, tr, bmarkchanges)
1158 1158
1159 1159 for phase, nodes in toretract.items():
1160 1160 phases.retractboundary(repo, tr, phase, nodes)
1161 1161 for phase, nodes in toadvance.items():
1162 1162 phases.advanceboundary(repo, tr, phase, nodes)
1163 1163
1164 1164 mayusearchived = repo.ui.config(b'experimental', b'cleanup-as-archived')
1165 1165 # Obsolete or strip nodes
1166 1166 if obsolete.isenabled(repo, obsolete.createmarkersopt):
1167 1167 # If a node is already obsoleted, and we want to obsolete it
1168 1168 # without a successor, skip that obssolete request since it's
1169 1169 # unnecessary. That's the "if s or not isobs(n)" check below.
1170 1170 # Also sort the node in topology order, that might be useful for
1171 1171 # some obsstore logic.
1172 1172 # NOTE: the sorting might belong to createmarkers.
1173 1173 torev = unfi.changelog.rev
1174 1174 sortfunc = lambda ns: torev(ns[0][0])
1175 1175 rels = []
1176 1176 for ns, s in sorted(replacements.items(), key=sortfunc):
1177 1177 rel = (tuple(unfi[n] for n in ns), tuple(unfi[m] for m in s))
1178 1178 rels.append(rel)
1179 1179 if rels:
1180 1180 obsolete.createmarkers(
1181 1181 repo, rels, operation=operation, metadata=metadata
1182 1182 )
1183 1183 elif phases.supportinternal(repo) and mayusearchived:
1184 1184 # this assume we do not have "unstable" nodes above the cleaned ones
1185 1185 allreplaced = set()
1186 1186 for ns in replacements.keys():
1187 1187 allreplaced.update(ns)
1188 1188 if backup:
1189 1189 from . import repair # avoid import cycle
1190 1190
1191 1191 node = min(allreplaced, key=repo.changelog.rev)
1192 1192 repair.backupbundle(
1193 1193 repo, allreplaced, allreplaced, node, operation
1194 1194 )
1195 1195 phases.retractboundary(repo, tr, phases.archived, allreplaced)
1196 1196 else:
1197 1197 from . import repair # avoid import cycle
1198 1198
1199 1199 tostrip = list(n for ns in replacements for n in ns)
1200 1200 if tostrip:
1201 1201 repair.delayedstrip(
1202 1202 repo.ui, repo, tostrip, operation, backup=backup
1203 1203 )
1204 1204
1205 1205
1206 1206 def addremove(repo, matcher, prefix, uipathfn, opts=None):
1207 1207 if opts is None:
1208 1208 opts = {}
1209 1209 m = matcher
1210 1210 dry_run = opts.get(b'dry_run')
1211 1211 try:
1212 1212 similarity = float(opts.get(b'similarity') or 0)
1213 1213 except ValueError:
1214 1214 raise error.Abort(_(b'similarity must be a number'))
1215 1215 if similarity < 0 or similarity > 100:
1216 1216 raise error.Abort(_(b'similarity must be between 0 and 100'))
1217 1217 similarity /= 100.0
1218 1218
1219 1219 ret = 0
1220 1220
1221 1221 wctx = repo[None]
1222 1222 for subpath in sorted(wctx.substate):
1223 1223 submatch = matchmod.subdirmatcher(subpath, m)
1224 1224 if opts.get(b'subrepos') or m.exact(subpath) or any(submatch.files()):
1225 1225 sub = wctx.sub(subpath)
1226 1226 subprefix = repo.wvfs.reljoin(prefix, subpath)
1227 1227 subuipathfn = subdiruipathfn(subpath, uipathfn)
1228 1228 try:
1229 1229 if sub.addremove(submatch, subprefix, subuipathfn, opts):
1230 1230 ret = 1
1231 1231 except error.LookupError:
1232 1232 repo.ui.status(
1233 1233 _(b"skipping missing subrepository: %s\n")
1234 1234 % uipathfn(subpath)
1235 1235 )
1236 1236
1237 1237 rejected = []
1238 1238
1239 1239 def badfn(f, msg):
1240 1240 if f in m.files():
1241 1241 m.bad(f, msg)
1242 1242 rejected.append(f)
1243 1243
1244 1244 badmatch = matchmod.badmatch(m, badfn)
1245 1245 added, unknown, deleted, removed, forgotten = _interestingfiles(
1246 1246 repo, badmatch
1247 1247 )
1248 1248
1249 1249 unknownset = set(unknown + forgotten)
1250 1250 toprint = unknownset.copy()
1251 1251 toprint.update(deleted)
1252 1252 for abs in sorted(toprint):
1253 1253 if repo.ui.verbose or not m.exact(abs):
1254 1254 if abs in unknownset:
1255 1255 status = _(b'adding %s\n') % uipathfn(abs)
1256 1256 label = b'ui.addremove.added'
1257 1257 else:
1258 1258 status = _(b'removing %s\n') % uipathfn(abs)
1259 1259 label = b'ui.addremove.removed'
1260 1260 repo.ui.status(status, label=label)
1261 1261
1262 1262 renames = _findrenames(
1263 1263 repo, m, added + unknown, removed + deleted, similarity, uipathfn
1264 1264 )
1265 1265
1266 1266 if not dry_run:
1267 1267 _markchanges(repo, unknown + forgotten, deleted, renames)
1268 1268
1269 1269 for f in rejected:
1270 1270 if f in m.files():
1271 1271 return 1
1272 1272 return ret
1273 1273
1274 1274
1275 1275 def marktouched(repo, files, similarity=0.0):
1276 1276 """Assert that files have somehow been operated upon. files are relative to
1277 1277 the repo root."""
1278 1278 m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
1279 1279 rejected = []
1280 1280
1281 1281 added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m)
1282 1282
1283 1283 if repo.ui.verbose:
1284 1284 unknownset = set(unknown + forgotten)
1285 1285 toprint = unknownset.copy()
1286 1286 toprint.update(deleted)
1287 1287 for abs in sorted(toprint):
1288 1288 if abs in unknownset:
1289 1289 status = _(b'adding %s\n') % abs
1290 1290 else:
1291 1291 status = _(b'removing %s\n') % abs
1292 1292 repo.ui.status(status)
1293 1293
1294 1294 # TODO: We should probably have the caller pass in uipathfn and apply it to
1295 1295 # the messages above too. legacyrelativevalue=True is consistent with how
1296 1296 # it used to work.
1297 1297 uipathfn = getuipathfn(repo, legacyrelativevalue=True)
1298 1298 renames = _findrenames(
1299 1299 repo, m, added + unknown, removed + deleted, similarity, uipathfn
1300 1300 )
1301 1301
1302 1302 _markchanges(repo, unknown + forgotten, deleted, renames)
1303 1303
1304 1304 for f in rejected:
1305 1305 if f in m.files():
1306 1306 return 1
1307 1307 return 0
1308 1308
1309 1309
1310 1310 def _interestingfiles(repo, matcher):
1311 1311 """Walk dirstate with matcher, looking for files that addremove would care
1312 1312 about.
1313 1313
1314 1314 This is different from dirstate.status because it doesn't care about
1315 1315 whether files are modified or clean."""
1316 1316 added, unknown, deleted, removed, forgotten = [], [], [], [], []
1317 1317 audit_path = pathutil.pathauditor(repo.root, cached=True)
1318 1318
1319 1319 ctx = repo[None]
1320 1320 dirstate = repo.dirstate
1321 1321 matcher = repo.narrowmatch(matcher, includeexact=True)
1322 1322 walkresults = dirstate.walk(
1323 1323 matcher,
1324 1324 subrepos=sorted(ctx.substate),
1325 1325 unknown=True,
1326 1326 ignored=False,
1327 1327 full=False,
1328 1328 )
1329 1329 for abs, st in pycompat.iteritems(walkresults):
1330 1330 dstate = dirstate[abs]
1331 1331 if dstate == b'?' and audit_path.check(abs):
1332 1332 unknown.append(abs)
1333 1333 elif dstate != b'r' and not st:
1334 1334 deleted.append(abs)
1335 1335 elif dstate == b'r' and st:
1336 1336 forgotten.append(abs)
1337 1337 # for finding renames
1338 1338 elif dstate == b'r' and not st:
1339 1339 removed.append(abs)
1340 1340 elif dstate == b'a':
1341 1341 added.append(abs)
1342 1342
1343 1343 return added, unknown, deleted, removed, forgotten
1344 1344
1345 1345
1346 1346 def _findrenames(repo, matcher, added, removed, similarity, uipathfn):
1347 1347 '''Find renames from removed files to added ones.'''
1348 1348 renames = {}
1349 1349 if similarity > 0:
1350 1350 for old, new, score in similar.findrenames(
1351 1351 repo, added, removed, similarity
1352 1352 ):
1353 1353 if (
1354 1354 repo.ui.verbose
1355 1355 or not matcher.exact(old)
1356 1356 or not matcher.exact(new)
1357 1357 ):
1358 1358 repo.ui.status(
1359 1359 _(
1360 1360 b'recording removal of %s as rename to %s '
1361 1361 b'(%d%% similar)\n'
1362 1362 )
1363 1363 % (uipathfn(old), uipathfn(new), score * 100)
1364 1364 )
1365 1365 renames[new] = old
1366 1366 return renames
1367 1367
1368 1368
1369 1369 def _markchanges(repo, unknown, deleted, renames):
1370 1370 """Marks the files in unknown as added, the files in deleted as removed,
1371 1371 and the files in renames as copied."""
1372 1372 wctx = repo[None]
1373 1373 with repo.wlock():
1374 1374 wctx.forget(deleted)
1375 1375 wctx.add(unknown)
1376 1376 for new, old in pycompat.iteritems(renames):
1377 1377 wctx.copy(old, new)
1378 1378
1379 1379
1380 1380 def getrenamedfn(repo, endrev=None):
1381 1381 if copiesmod.usechangesetcentricalgo(repo):
1382 1382
1383 1383 def getrenamed(fn, rev):
1384 1384 ctx = repo[rev]
1385 1385 p1copies = ctx.p1copies()
1386 1386 if fn in p1copies:
1387 1387 return p1copies[fn]
1388 1388 p2copies = ctx.p2copies()
1389 1389 if fn in p2copies:
1390 1390 return p2copies[fn]
1391 1391 return None
1392 1392
1393 1393 return getrenamed
1394 1394
1395 1395 rcache = {}
1396 1396 if endrev is None:
1397 1397 endrev = len(repo)
1398 1398
1399 1399 def getrenamed(fn, rev):
1400 1400 """looks up all renames for a file (up to endrev) the first
1401 1401 time the file is given. It indexes on the changerev and only
1402 1402 parses the manifest if linkrev != changerev.
1403 1403 Returns rename info for fn at changerev rev."""
1404 1404 if fn not in rcache:
1405 1405 rcache[fn] = {}
1406 1406 fl = repo.file(fn)
1407 1407 for i in fl:
1408 1408 lr = fl.linkrev(i)
1409 1409 renamed = fl.renamed(fl.node(i))
1410 1410 rcache[fn][lr] = renamed and renamed[0]
1411 1411 if lr >= endrev:
1412 1412 break
1413 1413 if rev in rcache[fn]:
1414 1414 return rcache[fn][rev]
1415 1415
1416 1416 # If linkrev != rev (i.e. rev not found in rcache) fallback to
1417 1417 # filectx logic.
1418 1418 try:
1419 1419 return repo[rev][fn].copysource()
1420 1420 except error.LookupError:
1421 1421 return None
1422 1422
1423 1423 return getrenamed
1424 1424
1425 1425
1426 1426 def getcopiesfn(repo, endrev=None):
1427 1427 if copiesmod.usechangesetcentricalgo(repo):
1428 1428
1429 1429 def copiesfn(ctx):
1430 1430 if ctx.p2copies():
1431 1431 allcopies = ctx.p1copies().copy()
1432 1432 # There should be no overlap
1433 1433 allcopies.update(ctx.p2copies())
1434 1434 return sorted(allcopies.items())
1435 1435 else:
1436 1436 return sorted(ctx.p1copies().items())
1437 1437
1438 1438 else:
1439 1439 getrenamed = getrenamedfn(repo, endrev)
1440 1440
1441 1441 def copiesfn(ctx):
1442 1442 copies = []
1443 1443 for fn in ctx.files():
1444 1444 rename = getrenamed(fn, ctx.rev())
1445 1445 if rename:
1446 1446 copies.append((fn, rename))
1447 1447 return copies
1448 1448
1449 1449 return copiesfn
1450 1450
1451 1451
1452 1452 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
1453 1453 """Update the dirstate to reflect the intent of copying src to dst. For
1454 1454 different reasons it might not end with dst being marked as copied from src.
1455 1455 """
1456 1456 origsrc = repo.dirstate.copied(src) or src
1457 1457 if dst == origsrc: # copying back a copy?
1458 1458 if repo.dirstate[dst] not in b'mn' and not dryrun:
1459 1459 repo.dirstate.normallookup(dst)
1460 1460 else:
1461 1461 if repo.dirstate[origsrc] == b'a' and origsrc == src:
1462 1462 if not ui.quiet:
1463 1463 ui.warn(
1464 1464 _(
1465 1465 b"%s has not been committed yet, so no copy "
1466 1466 b"data will be stored for %s.\n"
1467 1467 )
1468 1468 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd))
1469 1469 )
1470 1470 if repo.dirstate[dst] in b'?r' and not dryrun:
1471 1471 wctx.add([dst])
1472 1472 elif not dryrun:
1473 1473 wctx.copy(origsrc, dst)
1474 1474
1475 1475
1476 1476 def movedirstate(repo, newctx, match=None):
1477 1477 """Move the dirstate to newctx and adjust it as necessary.
1478 1478
1479 1479 A matcher can be provided as an optimization. It is probably a bug to pass
1480 1480 a matcher that doesn't match all the differences between the parent of the
1481 1481 working copy and newctx.
1482 1482 """
1483 1483 oldctx = repo[b'.']
1484 1484 ds = repo.dirstate
1485 1485 copies = dict(ds.copies())
1486 1486 ds.setparents(newctx.node(), repo.nullid)
1487 1487 s = newctx.status(oldctx, match=match)
1488
1488 1489 for f in s.modified:
1489 if ds[f] == b'r':
1490 # modified + removed -> removed
1491 continue
1492 ds.normallookup(f)
1490 ds.update_file_reference(f, p1_tracked=True)
1493 1491
1494 1492 for f in s.added:
1495 if ds[f] == b'r':
1496 # added + removed -> unknown
1497 ds.drop(f)
1498 elif ds[f] != b'a':
1499 ds.add(f)
1493 ds.update_file_reference(f, p1_tracked=False)
1500 1494
1501 1495 for f in s.removed:
1502 if ds[f] == b'a':
1503 # removed + added -> normal
1504 ds.normallookup(f)
1505 elif ds[f] != b'r':
1506 ds.remove(f)
1496 ds.update_file_reference(f, p1_tracked=True)
1507 1497
1508 1498 # Merge old parent and old working dir copies
1509 1499 oldcopies = copiesmod.pathcopies(newctx, oldctx, match)
1510 1500 oldcopies.update(copies)
1511 1501 copies = {
1512 1502 dst: oldcopies.get(src, src)
1513 1503 for dst, src in pycompat.iteritems(oldcopies)
1514 1504 }
1515 1505 # Adjust the dirstate copies
1516 1506 for dst, src in pycompat.iteritems(copies):
1517 1507 if src not in newctx or dst in newctx or ds[dst] != b'a':
1518 1508 src = None
1519 1509 ds.copy(src, dst)
1520 1510 repo._quick_access_changeid_invalidate()
1521 1511
1522 1512
1523 1513 def filterrequirements(requirements):
1524 1514 """filters the requirements into two sets:
1525 1515
1526 1516 wcreq: requirements which should be written in .hg/requires
1527 1517 storereq: which should be written in .hg/store/requires
1528 1518
1529 1519 Returns (wcreq, storereq)
1530 1520 """
1531 1521 if requirementsmod.SHARESAFE_REQUIREMENT in requirements:
1532 1522 wc, store = set(), set()
1533 1523 for r in requirements:
1534 1524 if r in requirementsmod.WORKING_DIR_REQUIREMENTS:
1535 1525 wc.add(r)
1536 1526 else:
1537 1527 store.add(r)
1538 1528 return wc, store
1539 1529 return requirements, None
1540 1530
1541 1531
1542 1532 def istreemanifest(repo):
1543 1533 """returns whether the repository is using treemanifest or not"""
1544 1534 return requirementsmod.TREEMANIFEST_REQUIREMENT in repo.requirements
1545 1535
1546 1536
1547 1537 def writereporequirements(repo, requirements=None):
1548 1538 """writes requirements for the repo
1549 1539
1550 1540 Requirements are written to .hg/requires and .hg/store/requires based
1551 1541 on whether share-safe mode is enabled and which requirements are wdir
1552 1542 requirements and which are store requirements
1553 1543 """
1554 1544 if requirements:
1555 1545 repo.requirements = requirements
1556 1546 wcreq, storereq = filterrequirements(repo.requirements)
1557 1547 if wcreq is not None:
1558 1548 writerequires(repo.vfs, wcreq)
1559 1549 if storereq is not None:
1560 1550 writerequires(repo.svfs, storereq)
1561 1551 elif repo.ui.configbool(b'format', b'usestore'):
1562 1552 # only remove store requires if we are using store
1563 1553 repo.svfs.tryunlink(b'requires')
1564 1554
1565 1555
1566 1556 def writerequires(opener, requirements):
1567 1557 with opener(b'requires', b'w', atomictemp=True) as fp:
1568 1558 for r in sorted(requirements):
1569 1559 fp.write(b"%s\n" % r)
1570 1560
1571 1561
1572 1562 class filecachesubentry(object):
1573 1563 def __init__(self, path, stat):
1574 1564 self.path = path
1575 1565 self.cachestat = None
1576 1566 self._cacheable = None
1577 1567
1578 1568 if stat:
1579 1569 self.cachestat = filecachesubentry.stat(self.path)
1580 1570
1581 1571 if self.cachestat:
1582 1572 self._cacheable = self.cachestat.cacheable()
1583 1573 else:
1584 1574 # None means we don't know yet
1585 1575 self._cacheable = None
1586 1576
1587 1577 def refresh(self):
1588 1578 if self.cacheable():
1589 1579 self.cachestat = filecachesubentry.stat(self.path)
1590 1580
1591 1581 def cacheable(self):
1592 1582 if self._cacheable is not None:
1593 1583 return self._cacheable
1594 1584
1595 1585 # we don't know yet, assume it is for now
1596 1586 return True
1597 1587
1598 1588 def changed(self):
1599 1589 # no point in going further if we can't cache it
1600 1590 if not self.cacheable():
1601 1591 return True
1602 1592
1603 1593 newstat = filecachesubentry.stat(self.path)
1604 1594
1605 1595 # we may not know if it's cacheable yet, check again now
1606 1596 if newstat and self._cacheable is None:
1607 1597 self._cacheable = newstat.cacheable()
1608 1598
1609 1599 # check again
1610 1600 if not self._cacheable:
1611 1601 return True
1612 1602
1613 1603 if self.cachestat != newstat:
1614 1604 self.cachestat = newstat
1615 1605 return True
1616 1606 else:
1617 1607 return False
1618 1608
1619 1609 @staticmethod
1620 1610 def stat(path):
1621 1611 try:
1622 1612 return util.cachestat(path)
1623 1613 except OSError as e:
1624 1614 if e.errno != errno.ENOENT:
1625 1615 raise
1626 1616
1627 1617
1628 1618 class filecacheentry(object):
1629 1619 def __init__(self, paths, stat=True):
1630 1620 self._entries = []
1631 1621 for path in paths:
1632 1622 self._entries.append(filecachesubentry(path, stat))
1633 1623
1634 1624 def changed(self):
1635 1625 '''true if any entry has changed'''
1636 1626 for entry in self._entries:
1637 1627 if entry.changed():
1638 1628 return True
1639 1629 return False
1640 1630
1641 1631 def refresh(self):
1642 1632 for entry in self._entries:
1643 1633 entry.refresh()
1644 1634
1645 1635
1646 1636 class filecache(object):
1647 1637 """A property like decorator that tracks files under .hg/ for updates.
1648 1638
1649 1639 On first access, the files defined as arguments are stat()ed and the
1650 1640 results cached. The decorated function is called. The results are stashed
1651 1641 away in a ``_filecache`` dict on the object whose method is decorated.
1652 1642
1653 1643 On subsequent access, the cached result is used as it is set to the
1654 1644 instance dictionary.
1655 1645
1656 1646 On external property set/delete operations, the caller must update the
1657 1647 corresponding _filecache entry appropriately. Use __class__.<attr>.set()
1658 1648 instead of directly setting <attr>.
1659 1649
1660 1650 When using the property API, the cached data is always used if available.
1661 1651 No stat() is performed to check if the file has changed.
1662 1652
1663 1653 Others can muck about with the state of the ``_filecache`` dict. e.g. they
1664 1654 can populate an entry before the property's getter is called. In this case,
1665 1655 entries in ``_filecache`` will be used during property operations,
1666 1656 if available. If the underlying file changes, it is up to external callers
1667 1657 to reflect this by e.g. calling ``delattr(obj, attr)`` to remove the cached
1668 1658 method result as well as possibly calling ``del obj._filecache[attr]`` to
1669 1659 remove the ``filecacheentry``.
1670 1660 """
1671 1661
1672 1662 def __init__(self, *paths):
1673 1663 self.paths = paths
1674 1664
1675 1665 def join(self, obj, fname):
1676 1666 """Used to compute the runtime path of a cached file.
1677 1667
1678 1668 Users should subclass filecache and provide their own version of this
1679 1669 function to call the appropriate join function on 'obj' (an instance
1680 1670 of the class that its member function was decorated).
1681 1671 """
1682 1672 raise NotImplementedError
1683 1673
1684 1674 def __call__(self, func):
1685 1675 self.func = func
1686 1676 self.sname = func.__name__
1687 1677 self.name = pycompat.sysbytes(self.sname)
1688 1678 return self
1689 1679
1690 1680 def __get__(self, obj, type=None):
1691 1681 # if accessed on the class, return the descriptor itself.
1692 1682 if obj is None:
1693 1683 return self
1694 1684
1695 1685 assert self.sname not in obj.__dict__
1696 1686
1697 1687 entry = obj._filecache.get(self.name)
1698 1688
1699 1689 if entry:
1700 1690 if entry.changed():
1701 1691 entry.obj = self.func(obj)
1702 1692 else:
1703 1693 paths = [self.join(obj, path) for path in self.paths]
1704 1694
1705 1695 # We stat -before- creating the object so our cache doesn't lie if
1706 1696 # a writer modified between the time we read and stat
1707 1697 entry = filecacheentry(paths, True)
1708 1698 entry.obj = self.func(obj)
1709 1699
1710 1700 obj._filecache[self.name] = entry
1711 1701
1712 1702 obj.__dict__[self.sname] = entry.obj
1713 1703 return entry.obj
1714 1704
1715 1705 # don't implement __set__(), which would make __dict__ lookup as slow as
1716 1706 # function call.
1717 1707
1718 1708 def set(self, obj, value):
1719 1709 if self.name not in obj._filecache:
1720 1710 # we add an entry for the missing value because X in __dict__
1721 1711 # implies X in _filecache
1722 1712 paths = [self.join(obj, path) for path in self.paths]
1723 1713 ce = filecacheentry(paths, False)
1724 1714 obj._filecache[self.name] = ce
1725 1715 else:
1726 1716 ce = obj._filecache[self.name]
1727 1717
1728 1718 ce.obj = value # update cached copy
1729 1719 obj.__dict__[self.sname] = value # update copy returned by obj.x
1730 1720
1731 1721
1732 1722 def extdatasource(repo, source):
1733 1723 """Gather a map of rev -> value dict from the specified source
1734 1724
1735 1725 A source spec is treated as a URL, with a special case shell: type
1736 1726 for parsing the output from a shell command.
1737 1727
1738 1728 The data is parsed as a series of newline-separated records where
1739 1729 each record is a revision specifier optionally followed by a space
1740 1730 and a freeform string value. If the revision is known locally, it
1741 1731 is converted to a rev, otherwise the record is skipped.
1742 1732
1743 1733 Note that both key and value are treated as UTF-8 and converted to
1744 1734 the local encoding. This allows uniformity between local and
1745 1735 remote data sources.
1746 1736 """
1747 1737
1748 1738 spec = repo.ui.config(b"extdata", source)
1749 1739 if not spec:
1750 1740 raise error.Abort(_(b"unknown extdata source '%s'") % source)
1751 1741
1752 1742 data = {}
1753 1743 src = proc = None
1754 1744 try:
1755 1745 if spec.startswith(b"shell:"):
1756 1746 # external commands should be run relative to the repo root
1757 1747 cmd = spec[6:]
1758 1748 proc = subprocess.Popen(
1759 1749 procutil.tonativestr(cmd),
1760 1750 shell=True,
1761 1751 bufsize=-1,
1762 1752 close_fds=procutil.closefds,
1763 1753 stdout=subprocess.PIPE,
1764 1754 cwd=procutil.tonativestr(repo.root),
1765 1755 )
1766 1756 src = proc.stdout
1767 1757 else:
1768 1758 # treat as a URL or file
1769 1759 src = url.open(repo.ui, spec)
1770 1760 for l in src:
1771 1761 if b" " in l:
1772 1762 k, v = l.strip().split(b" ", 1)
1773 1763 else:
1774 1764 k, v = l.strip(), b""
1775 1765
1776 1766 k = encoding.tolocal(k)
1777 1767 try:
1778 1768 data[revsingle(repo, k).rev()] = encoding.tolocal(v)
1779 1769 except (error.LookupError, error.RepoLookupError, error.InputError):
1780 1770 pass # we ignore data for nodes that don't exist locally
1781 1771 finally:
1782 1772 if proc:
1783 1773 try:
1784 1774 proc.communicate()
1785 1775 except ValueError:
1786 1776 # This happens if we started iterating src and then
1787 1777 # get a parse error on a line. It should be safe to ignore.
1788 1778 pass
1789 1779 if src:
1790 1780 src.close()
1791 1781 if proc and proc.returncode != 0:
1792 1782 raise error.Abort(
1793 1783 _(b"extdata command '%s' failed: %s")
1794 1784 % (cmd, procutil.explainexit(proc.returncode))
1795 1785 )
1796 1786
1797 1787 return data
1798 1788
1799 1789
1800 1790 class progress(object):
1801 1791 def __init__(self, ui, updatebar, topic, unit=b"", total=None):
1802 1792 self.ui = ui
1803 1793 self.pos = 0
1804 1794 self.topic = topic
1805 1795 self.unit = unit
1806 1796 self.total = total
1807 1797 self.debug = ui.configbool(b'progress', b'debug')
1808 1798 self._updatebar = updatebar
1809 1799
1810 1800 def __enter__(self):
1811 1801 return self
1812 1802
1813 1803 def __exit__(self, exc_type, exc_value, exc_tb):
1814 1804 self.complete()
1815 1805
1816 1806 def update(self, pos, item=b"", total=None):
1817 1807 assert pos is not None
1818 1808 if total:
1819 1809 self.total = total
1820 1810 self.pos = pos
1821 1811 self._updatebar(self.topic, self.pos, item, self.unit, self.total)
1822 1812 if self.debug:
1823 1813 self._printdebug(item)
1824 1814
1825 1815 def increment(self, step=1, item=b"", total=None):
1826 1816 self.update(self.pos + step, item, total)
1827 1817
1828 1818 def complete(self):
1829 1819 self.pos = None
1830 1820 self.unit = b""
1831 1821 self.total = None
1832 1822 self._updatebar(self.topic, self.pos, b"", self.unit, self.total)
1833 1823
1834 1824 def _printdebug(self, item):
1835 1825 unit = b''
1836 1826 if self.unit:
1837 1827 unit = b' ' + self.unit
1838 1828 if item:
1839 1829 item = b' ' + item
1840 1830
1841 1831 if self.total:
1842 1832 pct = 100.0 * self.pos / self.total
1843 1833 self.ui.debug(
1844 1834 b'%s:%s %d/%d%s (%4.2f%%)\n'
1845 1835 % (self.topic, item, self.pos, self.total, unit, pct)
1846 1836 )
1847 1837 else:
1848 1838 self.ui.debug(b'%s:%s %d%s\n' % (self.topic, item, self.pos, unit))
1849 1839
1850 1840
1851 1841 def gdinitconfig(ui):
1852 1842 """helper function to know if a repo should be created as general delta"""
1853 1843 # experimental config: format.generaldelta
1854 1844 return ui.configbool(b'format', b'generaldelta') or ui.configbool(
1855 1845 b'format', b'usegeneraldelta'
1856 1846 )
1857 1847
1858 1848
1859 1849 def gddeltaconfig(ui):
1860 1850 """helper function to know if incoming delta should be optimised"""
1861 1851 # experimental config: format.generaldelta
1862 1852 return ui.configbool(b'format', b'generaldelta')
1863 1853
1864 1854
1865 1855 class simplekeyvaluefile(object):
1866 1856 """A simple file with key=value lines
1867 1857
1868 1858 Keys must be alphanumerics and start with a letter, values must not
1869 1859 contain '\n' characters"""
1870 1860
1871 1861 firstlinekey = b'__firstline'
1872 1862
1873 1863 def __init__(self, vfs, path, keys=None):
1874 1864 self.vfs = vfs
1875 1865 self.path = path
1876 1866
1877 1867 def read(self, firstlinenonkeyval=False):
1878 1868 """Read the contents of a simple key-value file
1879 1869
1880 1870 'firstlinenonkeyval' indicates whether the first line of file should
1881 1871 be treated as a key-value pair or reuturned fully under the
1882 1872 __firstline key."""
1883 1873 lines = self.vfs.readlines(self.path)
1884 1874 d = {}
1885 1875 if firstlinenonkeyval:
1886 1876 if not lines:
1887 1877 e = _(b"empty simplekeyvalue file")
1888 1878 raise error.CorruptedState(e)
1889 1879 # we don't want to include '\n' in the __firstline
1890 1880 d[self.firstlinekey] = lines[0][:-1]
1891 1881 del lines[0]
1892 1882
1893 1883 try:
1894 1884 # the 'if line.strip()' part prevents us from failing on empty
1895 1885 # lines which only contain '\n' therefore are not skipped
1896 1886 # by 'if line'
1897 1887 updatedict = dict(
1898 1888 line[:-1].split(b'=', 1) for line in lines if line.strip()
1899 1889 )
1900 1890 if self.firstlinekey in updatedict:
1901 1891 e = _(b"%r can't be used as a key")
1902 1892 raise error.CorruptedState(e % self.firstlinekey)
1903 1893 d.update(updatedict)
1904 1894 except ValueError as e:
1905 1895 raise error.CorruptedState(stringutil.forcebytestr(e))
1906 1896 return d
1907 1897
1908 1898 def write(self, data, firstline=None):
1909 1899 """Write key=>value mapping to a file
1910 1900 data is a dict. Keys must be alphanumerical and start with a letter.
1911 1901 Values must not contain newline characters.
1912 1902
1913 1903 If 'firstline' is not None, it is written to file before
1914 1904 everything else, as it is, not in a key=value form"""
1915 1905 lines = []
1916 1906 if firstline is not None:
1917 1907 lines.append(b'%s\n' % firstline)
1918 1908
1919 1909 for k, v in data.items():
1920 1910 if k == self.firstlinekey:
1921 1911 e = b"key name '%s' is reserved" % self.firstlinekey
1922 1912 raise error.ProgrammingError(e)
1923 1913 if not k[0:1].isalpha():
1924 1914 e = b"keys must start with a letter in a key-value file"
1925 1915 raise error.ProgrammingError(e)
1926 1916 if not k.isalnum():
1927 1917 e = b"invalid key name in a simple key-value file"
1928 1918 raise error.ProgrammingError(e)
1929 1919 if b'\n' in v:
1930 1920 e = b"invalid value in a simple key-value file"
1931 1921 raise error.ProgrammingError(e)
1932 1922 lines.append(b"%s=%s\n" % (k, v))
1933 1923 with self.vfs(self.path, mode=b'wb', atomictemp=True) as fp:
1934 1924 fp.write(b''.join(lines))
1935 1925
1936 1926
1937 1927 _reportobsoletedsource = [
1938 1928 b'debugobsolete',
1939 1929 b'pull',
1940 1930 b'push',
1941 1931 b'serve',
1942 1932 b'unbundle',
1943 1933 ]
1944 1934
1945 1935 _reportnewcssource = [
1946 1936 b'pull',
1947 1937 b'unbundle',
1948 1938 ]
1949 1939
1950 1940
1951 1941 def prefetchfiles(repo, revmatches):
1952 1942 """Invokes the registered file prefetch functions, allowing extensions to
1953 1943 ensure the corresponding files are available locally, before the command
1954 1944 uses them.
1955 1945
1956 1946 Args:
1957 1947 revmatches: a list of (revision, match) tuples to indicate the files to
1958 1948 fetch at each revision. If any of the match elements is None, it matches
1959 1949 all files.
1960 1950 """
1961 1951
1962 1952 def _matcher(m):
1963 1953 if m:
1964 1954 assert isinstance(m, matchmod.basematcher)
1965 1955 # The command itself will complain about files that don't exist, so
1966 1956 # don't duplicate the message.
1967 1957 return matchmod.badmatch(m, lambda fn, msg: None)
1968 1958 else:
1969 1959 return matchall(repo)
1970 1960
1971 1961 revbadmatches = [(rev, _matcher(match)) for (rev, match) in revmatches]
1972 1962
1973 1963 fileprefetchhooks(repo, revbadmatches)
1974 1964
1975 1965
1976 1966 # a list of (repo, revs, match) prefetch functions
1977 1967 fileprefetchhooks = util.hooks()
1978 1968
1979 1969 # A marker that tells the evolve extension to suppress its own reporting
1980 1970 _reportstroubledchangesets = True
1981 1971
1982 1972
1983 1973 def registersummarycallback(repo, otr, txnname=b'', as_validator=False):
1984 1974 """register a callback to issue a summary after the transaction is closed
1985 1975
1986 1976 If as_validator is true, then the callbacks are registered as transaction
1987 1977 validators instead
1988 1978 """
1989 1979
1990 1980 def txmatch(sources):
1991 1981 return any(txnname.startswith(source) for source in sources)
1992 1982
1993 1983 categories = []
1994 1984
1995 1985 def reportsummary(func):
1996 1986 """decorator for report callbacks."""
1997 1987 # The repoview life cycle is shorter than the one of the actual
1998 1988 # underlying repository. So the filtered object can die before the
1999 1989 # weakref is used leading to troubles. We keep a reference to the
2000 1990 # unfiltered object and restore the filtering when retrieving the
2001 1991 # repository through the weakref.
2002 1992 filtername = repo.filtername
2003 1993 reporef = weakref.ref(repo.unfiltered())
2004 1994
2005 1995 def wrapped(tr):
2006 1996 repo = reporef()
2007 1997 if filtername:
2008 1998 assert repo is not None # help pytype
2009 1999 repo = repo.filtered(filtername)
2010 2000 func(repo, tr)
2011 2001
2012 2002 newcat = b'%02i-txnreport' % len(categories)
2013 2003 if as_validator:
2014 2004 otr.addvalidator(newcat, wrapped)
2015 2005 else:
2016 2006 otr.addpostclose(newcat, wrapped)
2017 2007 categories.append(newcat)
2018 2008 return wrapped
2019 2009
2020 2010 @reportsummary
2021 2011 def reportchangegroup(repo, tr):
2022 2012 cgchangesets = tr.changes.get(b'changegroup-count-changesets', 0)
2023 2013 cgrevisions = tr.changes.get(b'changegroup-count-revisions', 0)
2024 2014 cgfiles = tr.changes.get(b'changegroup-count-files', 0)
2025 2015 cgheads = tr.changes.get(b'changegroup-count-heads', 0)
2026 2016 if cgchangesets or cgrevisions or cgfiles:
2027 2017 htext = b""
2028 2018 if cgheads:
2029 2019 htext = _(b" (%+d heads)") % cgheads
2030 2020 msg = _(b"added %d changesets with %d changes to %d files%s\n")
2031 2021 if as_validator:
2032 2022 msg = _(b"adding %d changesets with %d changes to %d files%s\n")
2033 2023 assert repo is not None # help pytype
2034 2024 repo.ui.status(msg % (cgchangesets, cgrevisions, cgfiles, htext))
2035 2025
2036 2026 if txmatch(_reportobsoletedsource):
2037 2027
2038 2028 @reportsummary
2039 2029 def reportobsoleted(repo, tr):
2040 2030 obsoleted = obsutil.getobsoleted(repo, tr)
2041 2031 newmarkers = len(tr.changes.get(b'obsmarkers', ()))
2042 2032 if newmarkers:
2043 2033 repo.ui.status(_(b'%i new obsolescence markers\n') % newmarkers)
2044 2034 if obsoleted:
2045 2035 msg = _(b'obsoleted %i changesets\n')
2046 2036 if as_validator:
2047 2037 msg = _(b'obsoleting %i changesets\n')
2048 2038 repo.ui.status(msg % len(obsoleted))
2049 2039
2050 2040 if obsolete.isenabled(
2051 2041 repo, obsolete.createmarkersopt
2052 2042 ) and repo.ui.configbool(
2053 2043 b'experimental', b'evolution.report-instabilities'
2054 2044 ):
2055 2045 instabilitytypes = [
2056 2046 (b'orphan', b'orphan'),
2057 2047 (b'phase-divergent', b'phasedivergent'),
2058 2048 (b'content-divergent', b'contentdivergent'),
2059 2049 ]
2060 2050
2061 2051 def getinstabilitycounts(repo):
2062 2052 filtered = repo.changelog.filteredrevs
2063 2053 counts = {}
2064 2054 for instability, revset in instabilitytypes:
2065 2055 counts[instability] = len(
2066 2056 set(obsolete.getrevs(repo, revset)) - filtered
2067 2057 )
2068 2058 return counts
2069 2059
2070 2060 oldinstabilitycounts = getinstabilitycounts(repo)
2071 2061
2072 2062 @reportsummary
2073 2063 def reportnewinstabilities(repo, tr):
2074 2064 newinstabilitycounts = getinstabilitycounts(repo)
2075 2065 for instability, revset in instabilitytypes:
2076 2066 delta = (
2077 2067 newinstabilitycounts[instability]
2078 2068 - oldinstabilitycounts[instability]
2079 2069 )
2080 2070 msg = getinstabilitymessage(delta, instability)
2081 2071 if msg:
2082 2072 repo.ui.warn(msg)
2083 2073
2084 2074 if txmatch(_reportnewcssource):
2085 2075
2086 2076 @reportsummary
2087 2077 def reportnewcs(repo, tr):
2088 2078 """Report the range of new revisions pulled/unbundled."""
2089 2079 origrepolen = tr.changes.get(b'origrepolen', len(repo))
2090 2080 unfi = repo.unfiltered()
2091 2081 if origrepolen >= len(unfi):
2092 2082 return
2093 2083
2094 2084 # Compute the bounds of new visible revisions' range.
2095 2085 revs = smartset.spanset(repo, start=origrepolen)
2096 2086 if revs:
2097 2087 minrev, maxrev = repo[revs.min()], repo[revs.max()]
2098 2088
2099 2089 if minrev == maxrev:
2100 2090 revrange = minrev
2101 2091 else:
2102 2092 revrange = b'%s:%s' % (minrev, maxrev)
2103 2093 draft = len(repo.revs(b'%ld and draft()', revs))
2104 2094 secret = len(repo.revs(b'%ld and secret()', revs))
2105 2095 if not (draft or secret):
2106 2096 msg = _(b'new changesets %s\n') % revrange
2107 2097 elif draft and secret:
2108 2098 msg = _(b'new changesets %s (%d drafts, %d secrets)\n')
2109 2099 msg %= (revrange, draft, secret)
2110 2100 elif draft:
2111 2101 msg = _(b'new changesets %s (%d drafts)\n')
2112 2102 msg %= (revrange, draft)
2113 2103 elif secret:
2114 2104 msg = _(b'new changesets %s (%d secrets)\n')
2115 2105 msg %= (revrange, secret)
2116 2106 else:
2117 2107 errormsg = b'entered unreachable condition'
2118 2108 raise error.ProgrammingError(errormsg)
2119 2109 repo.ui.status(msg)
2120 2110
2121 2111 # search new changesets directly pulled as obsolete
2122 2112 duplicates = tr.changes.get(b'revduplicates', ())
2123 2113 obsadded = unfi.revs(
2124 2114 b'(%d: + %ld) and obsolete()', origrepolen, duplicates
2125 2115 )
2126 2116 cl = repo.changelog
2127 2117 extinctadded = [r for r in obsadded if r not in cl]
2128 2118 if extinctadded:
2129 2119 # They are not just obsolete, but obsolete and invisible
2130 2120 # we call them "extinct" internally but the terms have not been
2131 2121 # exposed to users.
2132 2122 msg = b'(%d other changesets obsolete on arrival)\n'
2133 2123 repo.ui.status(msg % len(extinctadded))
2134 2124
2135 2125 @reportsummary
2136 2126 def reportphasechanges(repo, tr):
2137 2127 """Report statistics of phase changes for changesets pre-existing
2138 2128 pull/unbundle.
2139 2129 """
2140 2130 origrepolen = tr.changes.get(b'origrepolen', len(repo))
2141 2131 published = []
2142 2132 for revs, (old, new) in tr.changes.get(b'phases', []):
2143 2133 if new != phases.public:
2144 2134 continue
2145 2135 published.extend(rev for rev in revs if rev < origrepolen)
2146 2136 if not published:
2147 2137 return
2148 2138 msg = _(b'%d local changesets published\n')
2149 2139 if as_validator:
2150 2140 msg = _(b'%d local changesets will be published\n')
2151 2141 repo.ui.status(msg % len(published))
2152 2142
2153 2143
2154 2144 def getinstabilitymessage(delta, instability):
2155 2145 """function to return the message to show warning about new instabilities
2156 2146
2157 2147 exists as a separate function so that extension can wrap to show more
2158 2148 information like how to fix instabilities"""
2159 2149 if delta > 0:
2160 2150 return _(b'%i new %s changesets\n') % (delta, instability)
2161 2151
2162 2152
2163 2153 def nodesummaries(repo, nodes, maxnumnodes=4):
2164 2154 if len(nodes) <= maxnumnodes or repo.ui.verbose:
2165 2155 return b' '.join(short(h) for h in nodes)
2166 2156 first = b' '.join(short(h) for h in nodes[:maxnumnodes])
2167 2157 return _(b"%s and %d others") % (first, len(nodes) - maxnumnodes)
2168 2158
2169 2159
2170 2160 def enforcesinglehead(repo, tr, desc, accountclosed, filtername):
2171 2161 """check that no named branch has multiple heads"""
2172 2162 if desc in (b'strip', b'repair'):
2173 2163 # skip the logic during strip
2174 2164 return
2175 2165 visible = repo.filtered(filtername)
2176 2166 # possible improvement: we could restrict the check to affected branch
2177 2167 bm = visible.branchmap()
2178 2168 for name in bm:
2179 2169 heads = bm.branchheads(name, closed=accountclosed)
2180 2170 if len(heads) > 1:
2181 2171 msg = _(b'rejecting multiple heads on branch "%s"')
2182 2172 msg %= name
2183 2173 hint = _(b'%d heads: %s')
2184 2174 hint %= (len(heads), nodesummaries(repo, heads))
2185 2175 raise error.Abort(msg, hint=hint)
2186 2176
2187 2177
2188 2178 def wrapconvertsink(sink):
2189 2179 """Allow extensions to wrap the sink returned by convcmd.convertsink()
2190 2180 before it is used, whether or not the convert extension was formally loaded.
2191 2181 """
2192 2182 return sink
2193 2183
2194 2184
2195 2185 def unhidehashlikerevs(repo, specs, hiddentype):
2196 2186 """parse the user specs and unhide changesets whose hash or revision number
2197 2187 is passed.
2198 2188
2199 2189 hiddentype can be: 1) 'warn': warn while unhiding changesets
2200 2190 2) 'nowarn': don't warn while unhiding changesets
2201 2191
2202 2192 returns a repo object with the required changesets unhidden
2203 2193 """
2204 2194 if not repo.filtername or not repo.ui.configbool(
2205 2195 b'experimental', b'directaccess'
2206 2196 ):
2207 2197 return repo
2208 2198
2209 2199 if repo.filtername not in (b'visible', b'visible-hidden'):
2210 2200 return repo
2211 2201
2212 2202 symbols = set()
2213 2203 for spec in specs:
2214 2204 try:
2215 2205 tree = revsetlang.parse(spec)
2216 2206 except error.ParseError: # will be reported by scmutil.revrange()
2217 2207 continue
2218 2208
2219 2209 symbols.update(revsetlang.gethashlikesymbols(tree))
2220 2210
2221 2211 if not symbols:
2222 2212 return repo
2223 2213
2224 2214 revs = _getrevsfromsymbols(repo, symbols)
2225 2215
2226 2216 if not revs:
2227 2217 return repo
2228 2218
2229 2219 if hiddentype == b'warn':
2230 2220 unfi = repo.unfiltered()
2231 2221 revstr = b", ".join([pycompat.bytestr(unfi[l]) for l in revs])
2232 2222 repo.ui.warn(
2233 2223 _(
2234 2224 b"warning: accessing hidden changesets for write "
2235 2225 b"operation: %s\n"
2236 2226 )
2237 2227 % revstr
2238 2228 )
2239 2229
2240 2230 # we have to use new filtername to separate branch/tags cache until we can
2241 2231 # disbale these cache when revisions are dynamically pinned.
2242 2232 return repo.filtered(b'visible-hidden', revs)
2243 2233
2244 2234
2245 2235 def _getrevsfromsymbols(repo, symbols):
2246 2236 """parse the list of symbols and returns a set of revision numbers of hidden
2247 2237 changesets present in symbols"""
2248 2238 revs = set()
2249 2239 unfi = repo.unfiltered()
2250 2240 unficl = unfi.changelog
2251 2241 cl = repo.changelog
2252 2242 tiprev = len(unficl)
2253 2243 allowrevnums = repo.ui.configbool(b'experimental', b'directaccess.revnums')
2254 2244 for s in symbols:
2255 2245 try:
2256 2246 n = int(s)
2257 2247 if n <= tiprev:
2258 2248 if not allowrevnums:
2259 2249 continue
2260 2250 else:
2261 2251 if n not in cl:
2262 2252 revs.add(n)
2263 2253 continue
2264 2254 except ValueError:
2265 2255 pass
2266 2256
2267 2257 try:
2268 2258 s = resolvehexnodeidprefix(unfi, s)
2269 2259 except (error.LookupError, error.WdirUnsupported):
2270 2260 s = None
2271 2261
2272 2262 if s is not None:
2273 2263 rev = unficl.rev(s)
2274 2264 if rev not in cl:
2275 2265 revs.add(rev)
2276 2266
2277 2267 return revs
2278 2268
2279 2269
2280 2270 def bookmarkrevs(repo, mark):
2281 2271 """Select revisions reachable by a given bookmark
2282 2272
2283 2273 If the bookmarked revision isn't a head, an empty set will be returned.
2284 2274 """
2285 2275 return repo.revs(format_bookmark_revspec(mark))
2286 2276
2287 2277
2288 2278 def format_bookmark_revspec(mark):
2289 2279 """Build a revset expression to select revisions reachable by a given
2290 2280 bookmark"""
2291 2281 mark = b'literal:' + mark
2292 2282 return revsetlang.formatspec(
2293 2283 b"ancestors(bookmark(%s)) - "
2294 2284 b"ancestors(head() and not bookmark(%s)) - "
2295 2285 b"ancestors(bookmark() and not bookmark(%s))",
2296 2286 mark,
2297 2287 mark,
2298 2288 mark,
2299 2289 )
General Comments 0
You need to be logged in to leave comments. Login now