##// END OF EJS Templates
dirstate: stop caring about match.explicitdir...
Martin von Zweigbergk -
r44113:deacffd2 default
parent child Browse files
Show More
@@ -1,1848 +1,1843 b''
1 1 # dirstate.py - working directory tracking for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import 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 .node import nullid
18 18 from .pycompat import delattr
19 19
20 20 from hgdemandimport import tracing
21 21
22 22 from . import (
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 txnutil,
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 propertycache = util.propertycache
43 43 filecache = scmutil.filecache
44 44 _rangemask = 0x7FFFFFFF
45 45
46 46 dirstatetuple = parsers.dirstatetuple
47 47
48 48
49 49 class repocache(filecache):
50 50 """filecache for files in .hg/"""
51 51
52 52 def join(self, obj, fname):
53 53 return obj._opener.join(fname)
54 54
55 55
56 56 class rootcache(filecache):
57 57 """filecache for files in the repository root"""
58 58
59 59 def join(self, obj, fname):
60 60 return obj._join(fname)
61 61
62 62
63 63 def _getfsnow(vfs):
64 64 '''Get "now" timestamp on filesystem'''
65 65 tmpfd, tmpname = vfs.mkstemp()
66 66 try:
67 67 return os.fstat(tmpfd)[stat.ST_MTIME]
68 68 finally:
69 69 os.close(tmpfd)
70 70 vfs.unlink(tmpname)
71 71
72 72
73 73 @interfaceutil.implementer(intdirstate.idirstate)
74 74 class dirstate(object):
75 75 def __init__(self, opener, ui, root, validate, sparsematchfn):
76 76 '''Create a new dirstate object.
77 77
78 78 opener is an open()-like callable that can be used to open the
79 79 dirstate file; root is the root of the directory tracked by
80 80 the dirstate.
81 81 '''
82 82 self._opener = opener
83 83 self._validate = validate
84 84 self._root = root
85 85 self._sparsematchfn = sparsematchfn
86 86 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
87 87 # UNC path pointing to root share (issue4557)
88 88 self._rootdir = pathutil.normasprefix(root)
89 89 self._dirty = False
90 90 self._lastnormaltime = 0
91 91 self._ui = ui
92 92 self._filecache = {}
93 93 self._parentwriters = 0
94 94 self._filename = b'dirstate'
95 95 self._pendingfilename = b'%s.pending' % self._filename
96 96 self._plchangecallbacks = {}
97 97 self._origpl = None
98 98 self._updatedfiles = set()
99 99 self._mapcls = dirstatemap
100 100 # Access and cache cwd early, so we don't access it for the first time
101 101 # after a working-copy update caused it to not exist (accessing it then
102 102 # raises an exception).
103 103 self._cwd
104 104
105 105 @contextlib.contextmanager
106 106 def parentchange(self):
107 107 '''Context manager for handling dirstate parents.
108 108
109 109 If an exception occurs in the scope of the context manager,
110 110 the incoherent dirstate won't be written when wlock is
111 111 released.
112 112 '''
113 113 self._parentwriters += 1
114 114 yield
115 115 # Typically we want the "undo" step of a context manager in a
116 116 # finally block so it happens even when an exception
117 117 # occurs. In this case, however, we only want to decrement
118 118 # parentwriters if the code in the with statement exits
119 119 # normally, so we don't have a try/finally here on purpose.
120 120 self._parentwriters -= 1
121 121
122 122 def pendingparentchange(self):
123 123 '''Returns true if the dirstate is in the middle of a set of changes
124 124 that modify the dirstate parent.
125 125 '''
126 126 return self._parentwriters > 0
127 127
128 128 @propertycache
129 129 def _map(self):
130 130 """Return the dirstate contents (see documentation for dirstatemap)."""
131 131 self._map = self._mapcls(self._ui, self._opener, self._root)
132 132 return self._map
133 133
134 134 @property
135 135 def _sparsematcher(self):
136 136 """The matcher for the sparse checkout.
137 137
138 138 The working directory may not include every file from a manifest. The
139 139 matcher obtained by this property will match a path if it is to be
140 140 included in the working directory.
141 141 """
142 142 # TODO there is potential to cache this property. For now, the matcher
143 143 # is resolved on every access. (But the called function does use a
144 144 # cache to keep the lookup fast.)
145 145 return self._sparsematchfn()
146 146
147 147 @repocache(b'branch')
148 148 def _branch(self):
149 149 try:
150 150 return self._opener.read(b"branch").strip() or b"default"
151 151 except IOError as inst:
152 152 if inst.errno != errno.ENOENT:
153 153 raise
154 154 return b"default"
155 155
156 156 @property
157 157 def _pl(self):
158 158 return self._map.parents()
159 159
160 160 def hasdir(self, d):
161 161 return self._map.hastrackeddir(d)
162 162
163 163 @rootcache(b'.hgignore')
164 164 def _ignore(self):
165 165 files = self._ignorefiles()
166 166 if not files:
167 167 return matchmod.never()
168 168
169 169 pats = [b'include:%s' % f for f in files]
170 170 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
171 171
172 172 @propertycache
173 173 def _slash(self):
174 174 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
175 175
176 176 @propertycache
177 177 def _checklink(self):
178 178 return util.checklink(self._root)
179 179
180 180 @propertycache
181 181 def _checkexec(self):
182 182 return util.checkexec(self._root)
183 183
184 184 @propertycache
185 185 def _checkcase(self):
186 186 return not util.fscasesensitive(self._join(b'.hg'))
187 187
188 188 def _join(self, f):
189 189 # much faster than os.path.join()
190 190 # it's safe because f is always a relative path
191 191 return self._rootdir + f
192 192
193 193 def flagfunc(self, buildfallback):
194 194 if self._checklink and self._checkexec:
195 195
196 196 def f(x):
197 197 try:
198 198 st = os.lstat(self._join(x))
199 199 if util.statislink(st):
200 200 return b'l'
201 201 if util.statisexec(st):
202 202 return b'x'
203 203 except OSError:
204 204 pass
205 205 return b''
206 206
207 207 return f
208 208
209 209 fallback = buildfallback()
210 210 if self._checklink:
211 211
212 212 def f(x):
213 213 if os.path.islink(self._join(x)):
214 214 return b'l'
215 215 if b'x' in fallback(x):
216 216 return b'x'
217 217 return b''
218 218
219 219 return f
220 220 if self._checkexec:
221 221
222 222 def f(x):
223 223 if b'l' in fallback(x):
224 224 return b'l'
225 225 if util.isexec(self._join(x)):
226 226 return b'x'
227 227 return b''
228 228
229 229 return f
230 230 else:
231 231 return fallback
232 232
233 233 @propertycache
234 234 def _cwd(self):
235 235 # internal config: ui.forcecwd
236 236 forcecwd = self._ui.config(b'ui', b'forcecwd')
237 237 if forcecwd:
238 238 return forcecwd
239 239 return encoding.getcwd()
240 240
241 241 def getcwd(self):
242 242 '''Return the path from which a canonical path is calculated.
243 243
244 244 This path should be used to resolve file patterns or to convert
245 245 canonical paths back to file paths for display. It shouldn't be
246 246 used to get real file paths. Use vfs functions instead.
247 247 '''
248 248 cwd = self._cwd
249 249 if cwd == self._root:
250 250 return b''
251 251 # self._root ends with a path separator if self._root is '/' or 'C:\'
252 252 rootsep = self._root
253 253 if not util.endswithsep(rootsep):
254 254 rootsep += pycompat.ossep
255 255 if cwd.startswith(rootsep):
256 256 return cwd[len(rootsep) :]
257 257 else:
258 258 # we're outside the repo. return an absolute path.
259 259 return cwd
260 260
261 261 def pathto(self, f, cwd=None):
262 262 if cwd is None:
263 263 cwd = self.getcwd()
264 264 path = util.pathto(self._root, cwd, f)
265 265 if self._slash:
266 266 return util.pconvert(path)
267 267 return path
268 268
269 269 def __getitem__(self, key):
270 270 '''Return the current state of key (a filename) in the dirstate.
271 271
272 272 States are:
273 273 n normal
274 274 m needs merging
275 275 r marked for removal
276 276 a marked for addition
277 277 ? not tracked
278 278 '''
279 279 return self._map.get(key, (b"?",))[0]
280 280
281 281 def __contains__(self, key):
282 282 return key in self._map
283 283
284 284 def __iter__(self):
285 285 return iter(sorted(self._map))
286 286
287 287 def items(self):
288 288 return pycompat.iteritems(self._map)
289 289
290 290 iteritems = items
291 291
292 292 def parents(self):
293 293 return [self._validate(p) for p in self._pl]
294 294
295 295 def p1(self):
296 296 return self._validate(self._pl[0])
297 297
298 298 def p2(self):
299 299 return self._validate(self._pl[1])
300 300
301 301 def branch(self):
302 302 return encoding.tolocal(self._branch)
303 303
304 304 def setparents(self, p1, p2=nullid):
305 305 """Set dirstate parents to p1 and p2.
306 306
307 307 When moving from two parents to one, 'm' merged entries a
308 308 adjusted to normal and previous copy records discarded and
309 309 returned by the call.
310 310
311 311 See localrepo.setparents()
312 312 """
313 313 if self._parentwriters == 0:
314 314 raise ValueError(
315 315 b"cannot set dirstate parent outside of "
316 316 b"dirstate.parentchange context manager"
317 317 )
318 318
319 319 self._dirty = True
320 320 oldp2 = self._pl[1]
321 321 if self._origpl is None:
322 322 self._origpl = self._pl
323 323 self._map.setparents(p1, p2)
324 324 copies = {}
325 325 if oldp2 != nullid and p2 == nullid:
326 326 candidatefiles = self._map.nonnormalset.union(
327 327 self._map.otherparentset
328 328 )
329 329 for f in candidatefiles:
330 330 s = self._map.get(f)
331 331 if s is None:
332 332 continue
333 333
334 334 # Discard 'm' markers when moving away from a merge state
335 335 if s[0] == b'm':
336 336 source = self._map.copymap.get(f)
337 337 if source:
338 338 copies[f] = source
339 339 self.normallookup(f)
340 340 # Also fix up otherparent markers
341 341 elif s[0] == b'n' and s[2] == -2:
342 342 source = self._map.copymap.get(f)
343 343 if source:
344 344 copies[f] = source
345 345 self.add(f)
346 346 return copies
347 347
348 348 def setbranch(self, branch):
349 349 self.__class__._branch.set(self, encoding.fromlocal(branch))
350 350 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
351 351 try:
352 352 f.write(self._branch + b'\n')
353 353 f.close()
354 354
355 355 # make sure filecache has the correct stat info for _branch after
356 356 # replacing the underlying file
357 357 ce = self._filecache[b'_branch']
358 358 if ce:
359 359 ce.refresh()
360 360 except: # re-raises
361 361 f.discard()
362 362 raise
363 363
364 364 def invalidate(self):
365 365 '''Causes the next access to reread the dirstate.
366 366
367 367 This is different from localrepo.invalidatedirstate() because it always
368 368 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
369 369 check whether the dirstate has changed before rereading it.'''
370 370
371 371 for a in ("_map", "_branch", "_ignore"):
372 372 if a in self.__dict__:
373 373 delattr(self, a)
374 374 self._lastnormaltime = 0
375 375 self._dirty = False
376 376 self._updatedfiles.clear()
377 377 self._parentwriters = 0
378 378 self._origpl = None
379 379
380 380 def copy(self, source, dest):
381 381 """Mark dest as a copy of source. Unmark dest if source is None."""
382 382 if source == dest:
383 383 return
384 384 self._dirty = True
385 385 if source is not None:
386 386 self._map.copymap[dest] = source
387 387 self._updatedfiles.add(source)
388 388 self._updatedfiles.add(dest)
389 389 elif self._map.copymap.pop(dest, None):
390 390 self._updatedfiles.add(dest)
391 391
392 392 def copied(self, file):
393 393 return self._map.copymap.get(file, None)
394 394
395 395 def copies(self):
396 396 return self._map.copymap
397 397
398 398 def _addpath(self, f, state, mode, size, mtime):
399 399 oldstate = self[f]
400 400 if state == b'a' or oldstate == b'r':
401 401 scmutil.checkfilename(f)
402 402 if self._map.hastrackeddir(f):
403 403 raise error.Abort(
404 404 _(b'directory %r already in dirstate') % pycompat.bytestr(f)
405 405 )
406 406 # shadows
407 407 for d in pathutil.finddirs(f):
408 408 if self._map.hastrackeddir(d):
409 409 break
410 410 entry = self._map.get(d)
411 411 if entry is not None and entry[0] != b'r':
412 412 raise error.Abort(
413 413 _(b'file %r in dirstate clashes with %r')
414 414 % (pycompat.bytestr(d), pycompat.bytestr(f))
415 415 )
416 416 self._dirty = True
417 417 self._updatedfiles.add(f)
418 418 self._map.addfile(f, oldstate, state, mode, size, mtime)
419 419
420 420 def normal(self, f, parentfiledata=None):
421 421 '''Mark a file normal and clean.
422 422
423 423 parentfiledata: (mode, size, mtime) of the clean file
424 424
425 425 parentfiledata should be computed from memory (for mode,
426 426 size), as or close as possible from the point where we
427 427 determined the file was clean, to limit the risk of the
428 428 file having been changed by an external process between the
429 429 moment where the file was determined to be clean and now.'''
430 430 if parentfiledata:
431 431 (mode, size, mtime) = parentfiledata
432 432 else:
433 433 s = os.lstat(self._join(f))
434 434 mode = s.st_mode
435 435 size = s.st_size
436 436 mtime = s[stat.ST_MTIME]
437 437 self._addpath(f, b'n', mode, size & _rangemask, mtime & _rangemask)
438 438 self._map.copymap.pop(f, None)
439 439 if f in self._map.nonnormalset:
440 440 self._map.nonnormalset.remove(f)
441 441 if mtime > self._lastnormaltime:
442 442 # Remember the most recent modification timeslot for status(),
443 443 # to make sure we won't miss future size-preserving file content
444 444 # modifications that happen within the same timeslot.
445 445 self._lastnormaltime = mtime
446 446
447 447 def normallookup(self, f):
448 448 '''Mark a file normal, but possibly dirty.'''
449 449 if self._pl[1] != nullid:
450 450 # if there is a merge going on and the file was either
451 451 # in state 'm' (-1) or coming from other parent (-2) before
452 452 # being removed, restore that state.
453 453 entry = self._map.get(f)
454 454 if entry is not None:
455 455 if entry[0] == b'r' and entry[2] in (-1, -2):
456 456 source = self._map.copymap.get(f)
457 457 if entry[2] == -1:
458 458 self.merge(f)
459 459 elif entry[2] == -2:
460 460 self.otherparent(f)
461 461 if source:
462 462 self.copy(source, f)
463 463 return
464 464 if entry[0] == b'm' or entry[0] == b'n' and entry[2] == -2:
465 465 return
466 466 self._addpath(f, b'n', 0, -1, -1)
467 467 self._map.copymap.pop(f, None)
468 468
469 469 def otherparent(self, f):
470 470 '''Mark as coming from the other parent, always dirty.'''
471 471 if self._pl[1] == nullid:
472 472 raise error.Abort(
473 473 _(b"setting %r to other parent only allowed in merges") % f
474 474 )
475 475 if f in self and self[f] == b'n':
476 476 # merge-like
477 477 self._addpath(f, b'm', 0, -2, -1)
478 478 else:
479 479 # add-like
480 480 self._addpath(f, b'n', 0, -2, -1)
481 481 self._map.copymap.pop(f, None)
482 482
483 483 def add(self, f):
484 484 '''Mark a file added.'''
485 485 self._addpath(f, b'a', 0, -1, -1)
486 486 self._map.copymap.pop(f, None)
487 487
488 488 def remove(self, f):
489 489 '''Mark a file removed.'''
490 490 self._dirty = True
491 491 oldstate = self[f]
492 492 size = 0
493 493 if self._pl[1] != nullid:
494 494 entry = self._map.get(f)
495 495 if entry is not None:
496 496 # backup the previous state
497 497 if entry[0] == b'm': # merge
498 498 size = -1
499 499 elif entry[0] == b'n' and entry[2] == -2: # other parent
500 500 size = -2
501 501 self._map.otherparentset.add(f)
502 502 self._updatedfiles.add(f)
503 503 self._map.removefile(f, oldstate, size)
504 504 if size == 0:
505 505 self._map.copymap.pop(f, None)
506 506
507 507 def merge(self, f):
508 508 '''Mark a file merged.'''
509 509 if self._pl[1] == nullid:
510 510 return self.normallookup(f)
511 511 return self.otherparent(f)
512 512
513 513 def drop(self, f):
514 514 '''Drop a file from the dirstate'''
515 515 oldstate = self[f]
516 516 if self._map.dropfile(f, oldstate):
517 517 self._dirty = True
518 518 self._updatedfiles.add(f)
519 519 self._map.copymap.pop(f, None)
520 520
521 521 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
522 522 if exists is None:
523 523 exists = os.path.lexists(os.path.join(self._root, path))
524 524 if not exists:
525 525 # Maybe a path component exists
526 526 if not ignoremissing and b'/' in path:
527 527 d, f = path.rsplit(b'/', 1)
528 528 d = self._normalize(d, False, ignoremissing, None)
529 529 folded = d + b"/" + f
530 530 else:
531 531 # No path components, preserve original case
532 532 folded = path
533 533 else:
534 534 # recursively normalize leading directory components
535 535 # against dirstate
536 536 if b'/' in normed:
537 537 d, f = normed.rsplit(b'/', 1)
538 538 d = self._normalize(d, False, ignoremissing, True)
539 539 r = self._root + b"/" + d
540 540 folded = d + b"/" + util.fspath(f, r)
541 541 else:
542 542 folded = util.fspath(normed, self._root)
543 543 storemap[normed] = folded
544 544
545 545 return folded
546 546
547 547 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
548 548 normed = util.normcase(path)
549 549 folded = self._map.filefoldmap.get(normed, None)
550 550 if folded is None:
551 551 if isknown:
552 552 folded = path
553 553 else:
554 554 folded = self._discoverpath(
555 555 path, normed, ignoremissing, exists, self._map.filefoldmap
556 556 )
557 557 return folded
558 558
559 559 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
560 560 normed = util.normcase(path)
561 561 folded = self._map.filefoldmap.get(normed, None)
562 562 if folded is None:
563 563 folded = self._map.dirfoldmap.get(normed, None)
564 564 if folded is None:
565 565 if isknown:
566 566 folded = path
567 567 else:
568 568 # store discovered result in dirfoldmap so that future
569 569 # normalizefile calls don't start matching directories
570 570 folded = self._discoverpath(
571 571 path, normed, ignoremissing, exists, self._map.dirfoldmap
572 572 )
573 573 return folded
574 574
575 575 def normalize(self, path, isknown=False, ignoremissing=False):
576 576 '''
577 577 normalize the case of a pathname when on a casefolding filesystem
578 578
579 579 isknown specifies whether the filename came from walking the
580 580 disk, to avoid extra filesystem access.
581 581
582 582 If ignoremissing is True, missing path are returned
583 583 unchanged. Otherwise, we try harder to normalize possibly
584 584 existing path components.
585 585
586 586 The normalized case is determined based on the following precedence:
587 587
588 588 - version of name already stored in the dirstate
589 589 - version of name stored on disk
590 590 - version provided via command arguments
591 591 '''
592 592
593 593 if self._checkcase:
594 594 return self._normalize(path, isknown, ignoremissing)
595 595 return path
596 596
597 597 def clear(self):
598 598 self._map.clear()
599 599 self._lastnormaltime = 0
600 600 self._updatedfiles.clear()
601 601 self._dirty = True
602 602
603 603 def rebuild(self, parent, allfiles, changedfiles=None):
604 604 if changedfiles is None:
605 605 # Rebuild entire dirstate
606 606 changedfiles = allfiles
607 607 lastnormaltime = self._lastnormaltime
608 608 self.clear()
609 609 self._lastnormaltime = lastnormaltime
610 610
611 611 if self._origpl is None:
612 612 self._origpl = self._pl
613 613 self._map.setparents(parent, nullid)
614 614 for f in changedfiles:
615 615 if f in allfiles:
616 616 self.normallookup(f)
617 617 else:
618 618 self.drop(f)
619 619
620 620 self._dirty = True
621 621
622 622 def identity(self):
623 623 '''Return identity of dirstate itself to detect changing in storage
624 624
625 625 If identity of previous dirstate is equal to this, writing
626 626 changes based on the former dirstate out can keep consistency.
627 627 '''
628 628 return self._map.identity
629 629
630 630 def write(self, tr):
631 631 if not self._dirty:
632 632 return
633 633
634 634 filename = self._filename
635 635 if tr:
636 636 # 'dirstate.write()' is not only for writing in-memory
637 637 # changes out, but also for dropping ambiguous timestamp.
638 638 # delayed writing re-raise "ambiguous timestamp issue".
639 639 # See also the wiki page below for detail:
640 640 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
641 641
642 642 # emulate dropping timestamp in 'parsers.pack_dirstate'
643 643 now = _getfsnow(self._opener)
644 644 self._map.clearambiguoustimes(self._updatedfiles, now)
645 645
646 646 # emulate that all 'dirstate.normal' results are written out
647 647 self._lastnormaltime = 0
648 648 self._updatedfiles.clear()
649 649
650 650 # delay writing in-memory changes out
651 651 tr.addfilegenerator(
652 652 b'dirstate',
653 653 (self._filename,),
654 654 self._writedirstate,
655 655 location=b'plain',
656 656 )
657 657 return
658 658
659 659 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
660 660 self._writedirstate(st)
661 661
662 662 def addparentchangecallback(self, category, callback):
663 663 """add a callback to be called when the wd parents are changed
664 664
665 665 Callback will be called with the following arguments:
666 666 dirstate, (oldp1, oldp2), (newp1, newp2)
667 667
668 668 Category is a unique identifier to allow overwriting an old callback
669 669 with a newer callback.
670 670 """
671 671 self._plchangecallbacks[category] = callback
672 672
673 673 def _writedirstate(self, st):
674 674 # notify callbacks about parents change
675 675 if self._origpl is not None and self._origpl != self._pl:
676 676 for c, callback in sorted(
677 677 pycompat.iteritems(self._plchangecallbacks)
678 678 ):
679 679 callback(self, self._origpl, self._pl)
680 680 self._origpl = None
681 681 # use the modification time of the newly created temporary file as the
682 682 # filesystem's notion of 'now'
683 683 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
684 684
685 685 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
686 686 # timestamp of each entries in dirstate, because of 'now > mtime'
687 687 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
688 688 if delaywrite > 0:
689 689 # do we have any files to delay for?
690 690 for f, e in pycompat.iteritems(self._map):
691 691 if e[0] == b'n' and e[3] == now:
692 692 import time # to avoid useless import
693 693
694 694 # rather than sleep n seconds, sleep until the next
695 695 # multiple of n seconds
696 696 clock = time.time()
697 697 start = int(clock) - (int(clock) % delaywrite)
698 698 end = start + delaywrite
699 699 time.sleep(end - clock)
700 700 now = end # trust our estimate that the end is near now
701 701 break
702 702
703 703 self._map.write(st, now)
704 704 self._lastnormaltime = 0
705 705 self._dirty = False
706 706
707 707 def _dirignore(self, f):
708 708 if self._ignore(f):
709 709 return True
710 710 for p in pathutil.finddirs(f):
711 711 if self._ignore(p):
712 712 return True
713 713 return False
714 714
715 715 def _ignorefiles(self):
716 716 files = []
717 717 if os.path.exists(self._join(b'.hgignore')):
718 718 files.append(self._join(b'.hgignore'))
719 719 for name, path in self._ui.configitems(b"ui"):
720 720 if name == b'ignore' or name.startswith(b'ignore.'):
721 721 # we need to use os.path.join here rather than self._join
722 722 # because path is arbitrary and user-specified
723 723 files.append(os.path.join(self._rootdir, util.expandpath(path)))
724 724 return files
725 725
726 726 def _ignorefileandline(self, f):
727 727 files = collections.deque(self._ignorefiles())
728 728 visited = set()
729 729 while files:
730 730 i = files.popleft()
731 731 patterns = matchmod.readpatternfile(
732 732 i, self._ui.warn, sourceinfo=True
733 733 )
734 734 for pattern, lineno, line in patterns:
735 735 kind, p = matchmod._patsplit(pattern, b'glob')
736 736 if kind == b"subinclude":
737 737 if p not in visited:
738 738 files.append(p)
739 739 continue
740 740 m = matchmod.match(
741 741 self._root, b'', [], [pattern], warn=self._ui.warn
742 742 )
743 743 if m(f):
744 744 return (i, lineno, line)
745 745 visited.add(i)
746 746 return (None, -1, b"")
747 747
748 748 def _walkexplicit(self, match, subrepos):
749 749 '''Get stat data about the files explicitly specified by match.
750 750
751 751 Return a triple (results, dirsfound, dirsnotfound).
752 752 - results is a mapping from filename to stat result. It also contains
753 753 listings mapping subrepos and .hg to None.
754 754 - dirsfound is a list of files found to be directories.
755 755 - dirsnotfound is a list of files that the dirstate thinks are
756 756 directories and that were not found.'''
757 757
758 758 def badtype(mode):
759 759 kind = _(b'unknown')
760 760 if stat.S_ISCHR(mode):
761 761 kind = _(b'character device')
762 762 elif stat.S_ISBLK(mode):
763 763 kind = _(b'block device')
764 764 elif stat.S_ISFIFO(mode):
765 765 kind = _(b'fifo')
766 766 elif stat.S_ISSOCK(mode):
767 767 kind = _(b'socket')
768 768 elif stat.S_ISDIR(mode):
769 769 kind = _(b'directory')
770 770 return _(b'unsupported file type (type is %s)') % kind
771 771
772 matchedir = match.explicitdir
773 772 badfn = match.bad
774 773 dmap = self._map
775 774 lstat = os.lstat
776 775 getkind = stat.S_IFMT
777 776 dirkind = stat.S_IFDIR
778 777 regkind = stat.S_IFREG
779 778 lnkkind = stat.S_IFLNK
780 779 join = self._join
781 780 dirsfound = []
782 781 foundadd = dirsfound.append
783 782 dirsnotfound = []
784 783 notfoundadd = dirsnotfound.append
785 784
786 785 if not match.isexact() and self._checkcase:
787 786 normalize = self._normalize
788 787 else:
789 788 normalize = None
790 789
791 790 files = sorted(match.files())
792 791 subrepos.sort()
793 792 i, j = 0, 0
794 793 while i < len(files) and j < len(subrepos):
795 794 subpath = subrepos[j] + b"/"
796 795 if files[i] < subpath:
797 796 i += 1
798 797 continue
799 798 while i < len(files) and files[i].startswith(subpath):
800 799 del files[i]
801 800 j += 1
802 801
803 802 if not files or b'' in files:
804 803 files = [b'']
805 804 # constructing the foldmap is expensive, so don't do it for the
806 805 # common case where files is ['']
807 806 normalize = None
808 807 results = dict.fromkeys(subrepos)
809 808 results[b'.hg'] = None
810 809
811 810 for ff in files:
812 811 if normalize:
813 812 nf = normalize(ff, False, True)
814 813 else:
815 814 nf = ff
816 815 if nf in results:
817 816 continue
818 817
819 818 try:
820 819 st = lstat(join(nf))
821 820 kind = getkind(st.st_mode)
822 821 if kind == dirkind:
823 822 if nf in dmap:
824 823 # file replaced by dir on disk but still in dirstate
825 824 results[nf] = None
826 if matchedir:
827 matchedir(nf)
828 825 foundadd((nf, ff))
829 826 elif kind == regkind or kind == lnkkind:
830 827 results[nf] = st
831 828 else:
832 829 badfn(ff, badtype(kind))
833 830 if nf in dmap:
834 831 results[nf] = None
835 832 except OSError as inst: # nf not found on disk - it is dirstate only
836 833 if nf in dmap: # does it exactly match a missing file?
837 834 results[nf] = None
838 835 else: # does it match a missing directory?
839 836 if self._map.hasdir(nf):
840 if matchedir:
841 matchedir(nf)
842 837 notfoundadd(nf)
843 838 else:
844 839 badfn(ff, encoding.strtolocal(inst.strerror))
845 840
846 841 # match.files() may contain explicitly-specified paths that shouldn't
847 842 # be taken; drop them from the list of files found. dirsfound/notfound
848 843 # aren't filtered here because they will be tested later.
849 844 if match.anypats():
850 845 for f in list(results):
851 846 if f == b'.hg' or f in subrepos:
852 847 # keep sentinel to disable further out-of-repo walks
853 848 continue
854 849 if not match(f):
855 850 del results[f]
856 851
857 852 # Case insensitive filesystems cannot rely on lstat() failing to detect
858 853 # a case-only rename. Prune the stat object for any file that does not
859 854 # match the case in the filesystem, if there are multiple files that
860 855 # normalize to the same path.
861 856 if match.isexact() and self._checkcase:
862 857 normed = {}
863 858
864 859 for f, st in pycompat.iteritems(results):
865 860 if st is None:
866 861 continue
867 862
868 863 nc = util.normcase(f)
869 864 paths = normed.get(nc)
870 865
871 866 if paths is None:
872 867 paths = set()
873 868 normed[nc] = paths
874 869
875 870 paths.add(f)
876 871
877 872 for norm, paths in pycompat.iteritems(normed):
878 873 if len(paths) > 1:
879 874 for path in paths:
880 875 folded = self._discoverpath(
881 876 path, norm, True, None, self._map.dirfoldmap
882 877 )
883 878 if path != folded:
884 879 results[path] = None
885 880
886 881 return results, dirsfound, dirsnotfound
887 882
888 883 def walk(self, match, subrepos, unknown, ignored, full=True):
889 884 '''
890 885 Walk recursively through the directory tree, finding all files
891 886 matched by match.
892 887
893 888 If full is False, maybe skip some known-clean files.
894 889
895 890 Return a dict mapping filename to stat-like object (either
896 891 mercurial.osutil.stat instance or return value of os.stat()).
897 892
898 893 '''
899 894 # full is a flag that extensions that hook into walk can use -- this
900 895 # implementation doesn't use it at all. This satisfies the contract
901 896 # because we only guarantee a "maybe".
902 897
903 898 if ignored:
904 899 ignore = util.never
905 900 dirignore = util.never
906 901 elif unknown:
907 902 ignore = self._ignore
908 903 dirignore = self._dirignore
909 904 else:
910 905 # if not unknown and not ignored, drop dir recursion and step 2
911 906 ignore = util.always
912 907 dirignore = util.always
913 908
914 909 matchfn = match.matchfn
915 910 matchalways = match.always()
916 911 matchtdir = match.traversedir
917 912 dmap = self._map
918 913 listdir = util.listdir
919 914 lstat = os.lstat
920 915 dirkind = stat.S_IFDIR
921 916 regkind = stat.S_IFREG
922 917 lnkkind = stat.S_IFLNK
923 918 join = self._join
924 919
925 920 exact = skipstep3 = False
926 921 if match.isexact(): # match.exact
927 922 exact = True
928 923 dirignore = util.always # skip step 2
929 924 elif match.prefix(): # match.match, no patterns
930 925 skipstep3 = True
931 926
932 927 if not exact and self._checkcase:
933 928 normalize = self._normalize
934 929 normalizefile = self._normalizefile
935 930 skipstep3 = False
936 931 else:
937 932 normalize = self._normalize
938 933 normalizefile = None
939 934
940 935 # step 1: find all explicit files
941 936 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
942 937 if matchtdir:
943 938 for d in work:
944 939 matchtdir(d[0])
945 940 for d in dirsnotfound:
946 941 matchtdir(d)
947 942
948 943 skipstep3 = skipstep3 and not (work or dirsnotfound)
949 944 work = [d for d in work if not dirignore(d[0])]
950 945
951 946 # step 2: visit subdirectories
952 947 def traverse(work, alreadynormed):
953 948 wadd = work.append
954 949 while work:
955 950 tracing.counter('dirstate.walk work', len(work))
956 951 nd = work.pop()
957 952 visitentries = match.visitchildrenset(nd)
958 953 if not visitentries:
959 954 continue
960 955 if visitentries == b'this' or visitentries == b'all':
961 956 visitentries = None
962 957 skip = None
963 958 if nd != b'':
964 959 skip = b'.hg'
965 960 try:
966 961 with tracing.log('dirstate.walk.traverse listdir %s', nd):
967 962 entries = listdir(join(nd), stat=True, skip=skip)
968 963 except OSError as inst:
969 964 if inst.errno in (errno.EACCES, errno.ENOENT):
970 965 match.bad(
971 966 self.pathto(nd), encoding.strtolocal(inst.strerror)
972 967 )
973 968 continue
974 969 raise
975 970 for f, kind, st in entries:
976 971 # Some matchers may return files in the visitentries set,
977 972 # instead of 'this', if the matcher explicitly mentions them
978 973 # and is not an exactmatcher. This is acceptable; we do not
979 974 # make any hard assumptions about file-or-directory below
980 975 # based on the presence of `f` in visitentries. If
981 976 # visitchildrenset returned a set, we can always skip the
982 977 # entries *not* in the set it provided regardless of whether
983 978 # they're actually a file or a directory.
984 979 if visitentries and f not in visitentries:
985 980 continue
986 981 if normalizefile:
987 982 # even though f might be a directory, we're only
988 983 # interested in comparing it to files currently in the
989 984 # dmap -- therefore normalizefile is enough
990 985 nf = normalizefile(
991 986 nd and (nd + b"/" + f) or f, True, True
992 987 )
993 988 else:
994 989 nf = nd and (nd + b"/" + f) or f
995 990 if nf not in results:
996 991 if kind == dirkind:
997 992 if not ignore(nf):
998 993 if matchtdir:
999 994 matchtdir(nf)
1000 995 wadd(nf)
1001 996 if nf in dmap and (matchalways or matchfn(nf)):
1002 997 results[nf] = None
1003 998 elif kind == regkind or kind == lnkkind:
1004 999 if nf in dmap:
1005 1000 if matchalways or matchfn(nf):
1006 1001 results[nf] = st
1007 1002 elif (matchalways or matchfn(nf)) and not ignore(
1008 1003 nf
1009 1004 ):
1010 1005 # unknown file -- normalize if necessary
1011 1006 if not alreadynormed:
1012 1007 nf = normalize(nf, False, True)
1013 1008 results[nf] = st
1014 1009 elif nf in dmap and (matchalways or matchfn(nf)):
1015 1010 results[nf] = None
1016 1011
1017 1012 for nd, d in work:
1018 1013 # alreadynormed means that processwork doesn't have to do any
1019 1014 # expensive directory normalization
1020 1015 alreadynormed = not normalize or nd == d
1021 1016 traverse([d], alreadynormed)
1022 1017
1023 1018 for s in subrepos:
1024 1019 del results[s]
1025 1020 del results[b'.hg']
1026 1021
1027 1022 # step 3: visit remaining files from dmap
1028 1023 if not skipstep3 and not exact:
1029 1024 # If a dmap file is not in results yet, it was either
1030 1025 # a) not matching matchfn b) ignored, c) missing, or d) under a
1031 1026 # symlink directory.
1032 1027 if not results and matchalways:
1033 1028 visit = [f for f in dmap]
1034 1029 else:
1035 1030 visit = [f for f in dmap if f not in results and matchfn(f)]
1036 1031 visit.sort()
1037 1032
1038 1033 if unknown:
1039 1034 # unknown == True means we walked all dirs under the roots
1040 1035 # that wasn't ignored, and everything that matched was stat'ed
1041 1036 # and is already in results.
1042 1037 # The rest must thus be ignored or under a symlink.
1043 1038 audit_path = pathutil.pathauditor(self._root, cached=True)
1044 1039
1045 1040 for nf in iter(visit):
1046 1041 # If a stat for the same file was already added with a
1047 1042 # different case, don't add one for this, since that would
1048 1043 # make it appear as if the file exists under both names
1049 1044 # on disk.
1050 1045 if (
1051 1046 normalizefile
1052 1047 and normalizefile(nf, True, True) in results
1053 1048 ):
1054 1049 results[nf] = None
1055 1050 # Report ignored items in the dmap as long as they are not
1056 1051 # under a symlink directory.
1057 1052 elif audit_path.check(nf):
1058 1053 try:
1059 1054 results[nf] = lstat(join(nf))
1060 1055 # file was just ignored, no links, and exists
1061 1056 except OSError:
1062 1057 # file doesn't exist
1063 1058 results[nf] = None
1064 1059 else:
1065 1060 # It's either missing or under a symlink directory
1066 1061 # which we in this case report as missing
1067 1062 results[nf] = None
1068 1063 else:
1069 1064 # We may not have walked the full directory tree above,
1070 1065 # so stat and check everything we missed.
1071 1066 iv = iter(visit)
1072 1067 for st in util.statfiles([join(i) for i in visit]):
1073 1068 results[next(iv)] = st
1074 1069 return results
1075 1070
1076 1071 def status(self, match, subrepos, ignored, clean, unknown):
1077 1072 '''Determine the status of the working copy relative to the
1078 1073 dirstate and return a pair of (unsure, status), where status is of type
1079 1074 scmutil.status and:
1080 1075
1081 1076 unsure:
1082 1077 files that might have been modified since the dirstate was
1083 1078 written, but need to be read to be sure (size is the same
1084 1079 but mtime differs)
1085 1080 status.modified:
1086 1081 files that have definitely been modified since the dirstate
1087 1082 was written (different size or mode)
1088 1083 status.clean:
1089 1084 files that have definitely not been modified since the
1090 1085 dirstate was written
1091 1086 '''
1092 1087 listignored, listclean, listunknown = ignored, clean, unknown
1093 1088 lookup, modified, added, unknown, ignored = [], [], [], [], []
1094 1089 removed, deleted, clean = [], [], []
1095 1090
1096 1091 dmap = self._map
1097 1092 dmap.preload()
1098 1093
1099 1094 use_rust = True
1100 1095 if rustmod is None:
1101 1096 use_rust = False
1102 1097 elif subrepos:
1103 1098 use_rust = False
1104 1099 if bool(listunknown):
1105 1100 # Pathauditor does not exist yet in Rust, unknown files
1106 1101 # can't be trusted.
1107 1102 use_rust = False
1108 1103 elif self._ignorefiles() and listignored:
1109 1104 # Rust has no ignore mechanism yet, so don't use Rust for
1110 1105 # commands that need ignore.
1111 1106 use_rust = False
1112 1107 elif not match.always():
1113 1108 # Matchers have yet to be implemented
1114 1109 use_rust = False
1115 1110
1116 1111 if use_rust:
1117 1112 # Force Rayon (Rust parallelism library) to respect the number of
1118 1113 # workers. This is a temporary workaround until Rust code knows
1119 1114 # how to read the config file.
1120 1115 numcpus = self._ui.configint(b"worker", b"numcpus")
1121 1116 if numcpus is not None:
1122 1117 encoding.environ.setdefault(
1123 1118 b'RAYON_NUM_THREADS', b'%d' % numcpus
1124 1119 )
1125 1120
1126 1121 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1127 1122 if not workers_enabled:
1128 1123 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1129 1124
1130 1125 (
1131 1126 lookup,
1132 1127 modified,
1133 1128 added,
1134 1129 removed,
1135 1130 deleted,
1136 1131 unknown,
1137 1132 clean,
1138 1133 ) = rustmod.status(
1139 1134 dmap._rustmap,
1140 1135 self._rootdir,
1141 1136 bool(listclean),
1142 1137 self._lastnormaltime,
1143 1138 self._checkexec,
1144 1139 )
1145 1140
1146 1141 status = scmutil.status(
1147 1142 modified=modified,
1148 1143 added=added,
1149 1144 removed=removed,
1150 1145 deleted=deleted,
1151 1146 unknown=unknown,
1152 1147 ignored=ignored,
1153 1148 clean=clean,
1154 1149 )
1155 1150 return (lookup, status)
1156 1151
1157 1152 def noop(f):
1158 1153 pass
1159 1154
1160 1155 dcontains = dmap.__contains__
1161 1156 dget = dmap.__getitem__
1162 1157 ladd = lookup.append # aka "unsure"
1163 1158 madd = modified.append
1164 1159 aadd = added.append
1165 1160 uadd = unknown.append if listunknown else noop
1166 1161 iadd = ignored.append if listignored else noop
1167 1162 radd = removed.append
1168 1163 dadd = deleted.append
1169 1164 cadd = clean.append if listclean else noop
1170 1165 mexact = match.exact
1171 1166 dirignore = self._dirignore
1172 1167 checkexec = self._checkexec
1173 1168 copymap = self._map.copymap
1174 1169 lastnormaltime = self._lastnormaltime
1175 1170
1176 1171 # We need to do full walks when either
1177 1172 # - we're listing all clean files, or
1178 1173 # - match.traversedir does something, because match.traversedir should
1179 1174 # be called for every dir in the working dir
1180 1175 full = listclean or match.traversedir is not None
1181 1176 for fn, st in pycompat.iteritems(
1182 1177 self.walk(match, subrepos, listunknown, listignored, full=full)
1183 1178 ):
1184 1179 if not dcontains(fn):
1185 1180 if (listignored or mexact(fn)) and dirignore(fn):
1186 1181 if listignored:
1187 1182 iadd(fn)
1188 1183 else:
1189 1184 uadd(fn)
1190 1185 continue
1191 1186
1192 1187 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1193 1188 # written like that for performance reasons. dmap[fn] is not a
1194 1189 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1195 1190 # opcode has fast paths when the value to be unpacked is a tuple or
1196 1191 # a list, but falls back to creating a full-fledged iterator in
1197 1192 # general. That is much slower than simply accessing and storing the
1198 1193 # tuple members one by one.
1199 1194 t = dget(fn)
1200 1195 state = t[0]
1201 1196 mode = t[1]
1202 1197 size = t[2]
1203 1198 time = t[3]
1204 1199
1205 1200 if not st and state in b"nma":
1206 1201 dadd(fn)
1207 1202 elif state == b'n':
1208 1203 if (
1209 1204 size >= 0
1210 1205 and (
1211 1206 (size != st.st_size and size != st.st_size & _rangemask)
1212 1207 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1213 1208 )
1214 1209 or size == -2 # other parent
1215 1210 or fn in copymap
1216 1211 ):
1217 1212 madd(fn)
1218 1213 elif (
1219 1214 time != st[stat.ST_MTIME]
1220 1215 and time != st[stat.ST_MTIME] & _rangemask
1221 1216 ):
1222 1217 ladd(fn)
1223 1218 elif st[stat.ST_MTIME] == lastnormaltime:
1224 1219 # fn may have just been marked as normal and it may have
1225 1220 # changed in the same second without changing its size.
1226 1221 # This can happen if we quickly do multiple commits.
1227 1222 # Force lookup, so we don't miss such a racy file change.
1228 1223 ladd(fn)
1229 1224 elif listclean:
1230 1225 cadd(fn)
1231 1226 elif state == b'm':
1232 1227 madd(fn)
1233 1228 elif state == b'a':
1234 1229 aadd(fn)
1235 1230 elif state == b'r':
1236 1231 radd(fn)
1237 1232
1238 1233 return (
1239 1234 lookup,
1240 1235 scmutil.status(
1241 1236 modified, added, removed, deleted, unknown, ignored, clean
1242 1237 ),
1243 1238 )
1244 1239
1245 1240 def matches(self, match):
1246 1241 '''
1247 1242 return files in the dirstate (in whatever state) filtered by match
1248 1243 '''
1249 1244 dmap = self._map
1250 1245 if match.always():
1251 1246 return dmap.keys()
1252 1247 files = match.files()
1253 1248 if match.isexact():
1254 1249 # fast path -- filter the other way around, since typically files is
1255 1250 # much smaller than dmap
1256 1251 return [f for f in files if f in dmap]
1257 1252 if match.prefix() and all(fn in dmap for fn in files):
1258 1253 # fast path -- all the values are known to be files, so just return
1259 1254 # that
1260 1255 return list(files)
1261 1256 return [f for f in dmap if match(f)]
1262 1257
1263 1258 def _actualfilename(self, tr):
1264 1259 if tr:
1265 1260 return self._pendingfilename
1266 1261 else:
1267 1262 return self._filename
1268 1263
1269 1264 def savebackup(self, tr, backupname):
1270 1265 '''Save current dirstate into backup file'''
1271 1266 filename = self._actualfilename(tr)
1272 1267 assert backupname != filename
1273 1268
1274 1269 # use '_writedirstate' instead of 'write' to write changes certainly,
1275 1270 # because the latter omits writing out if transaction is running.
1276 1271 # output file will be used to create backup of dirstate at this point.
1277 1272 if self._dirty or not self._opener.exists(filename):
1278 1273 self._writedirstate(
1279 1274 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1280 1275 )
1281 1276
1282 1277 if tr:
1283 1278 # ensure that subsequent tr.writepending returns True for
1284 1279 # changes written out above, even if dirstate is never
1285 1280 # changed after this
1286 1281 tr.addfilegenerator(
1287 1282 b'dirstate',
1288 1283 (self._filename,),
1289 1284 self._writedirstate,
1290 1285 location=b'plain',
1291 1286 )
1292 1287
1293 1288 # ensure that pending file written above is unlinked at
1294 1289 # failure, even if tr.writepending isn't invoked until the
1295 1290 # end of this transaction
1296 1291 tr.registertmp(filename, location=b'plain')
1297 1292
1298 1293 self._opener.tryunlink(backupname)
1299 1294 # hardlink backup is okay because _writedirstate is always called
1300 1295 # with an "atomictemp=True" file.
1301 1296 util.copyfile(
1302 1297 self._opener.join(filename),
1303 1298 self._opener.join(backupname),
1304 1299 hardlink=True,
1305 1300 )
1306 1301
1307 1302 def restorebackup(self, tr, backupname):
1308 1303 '''Restore dirstate by backup file'''
1309 1304 # this "invalidate()" prevents "wlock.release()" from writing
1310 1305 # changes of dirstate out after restoring from backup file
1311 1306 self.invalidate()
1312 1307 filename = self._actualfilename(tr)
1313 1308 o = self._opener
1314 1309 if util.samefile(o.join(backupname), o.join(filename)):
1315 1310 o.unlink(backupname)
1316 1311 else:
1317 1312 o.rename(backupname, filename, checkambig=True)
1318 1313
1319 1314 def clearbackup(self, tr, backupname):
1320 1315 '''Clear backup file'''
1321 1316 self._opener.unlink(backupname)
1322 1317
1323 1318
1324 1319 class dirstatemap(object):
1325 1320 """Map encapsulating the dirstate's contents.
1326 1321
1327 1322 The dirstate contains the following state:
1328 1323
1329 1324 - `identity` is the identity of the dirstate file, which can be used to
1330 1325 detect when changes have occurred to the dirstate file.
1331 1326
1332 1327 - `parents` is a pair containing the parents of the working copy. The
1333 1328 parents are updated by calling `setparents`.
1334 1329
1335 1330 - the state map maps filenames to tuples of (state, mode, size, mtime),
1336 1331 where state is a single character representing 'normal', 'added',
1337 1332 'removed', or 'merged'. It is read by treating the dirstate as a
1338 1333 dict. File state is updated by calling the `addfile`, `removefile` and
1339 1334 `dropfile` methods.
1340 1335
1341 1336 - `copymap` maps destination filenames to their source filename.
1342 1337
1343 1338 The dirstate also provides the following views onto the state:
1344 1339
1345 1340 - `nonnormalset` is a set of the filenames that have state other
1346 1341 than 'normal', or are normal but have an mtime of -1 ('normallookup').
1347 1342
1348 1343 - `otherparentset` is a set of the filenames that are marked as coming
1349 1344 from the second parent when the dirstate is currently being merged.
1350 1345
1351 1346 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
1352 1347 form that they appear as in the dirstate.
1353 1348
1354 1349 - `dirfoldmap` is a dict mapping normalized directory names to the
1355 1350 denormalized form that they appear as in the dirstate.
1356 1351 """
1357 1352
1358 1353 def __init__(self, ui, opener, root):
1359 1354 self._ui = ui
1360 1355 self._opener = opener
1361 1356 self._root = root
1362 1357 self._filename = b'dirstate'
1363 1358
1364 1359 self._parents = None
1365 1360 self._dirtyparents = False
1366 1361
1367 1362 # for consistent view between _pl() and _read() invocations
1368 1363 self._pendingmode = None
1369 1364
1370 1365 @propertycache
1371 1366 def _map(self):
1372 1367 self._map = {}
1373 1368 self.read()
1374 1369 return self._map
1375 1370
1376 1371 @propertycache
1377 1372 def copymap(self):
1378 1373 self.copymap = {}
1379 1374 self._map
1380 1375 return self.copymap
1381 1376
1382 1377 def clear(self):
1383 1378 self._map.clear()
1384 1379 self.copymap.clear()
1385 1380 self.setparents(nullid, nullid)
1386 1381 util.clearcachedproperty(self, b"_dirs")
1387 1382 util.clearcachedproperty(self, b"_alldirs")
1388 1383 util.clearcachedproperty(self, b"filefoldmap")
1389 1384 util.clearcachedproperty(self, b"dirfoldmap")
1390 1385 util.clearcachedproperty(self, b"nonnormalset")
1391 1386 util.clearcachedproperty(self, b"otherparentset")
1392 1387
1393 1388 def items(self):
1394 1389 return pycompat.iteritems(self._map)
1395 1390
1396 1391 # forward for python2,3 compat
1397 1392 iteritems = items
1398 1393
1399 1394 def __len__(self):
1400 1395 return len(self._map)
1401 1396
1402 1397 def __iter__(self):
1403 1398 return iter(self._map)
1404 1399
1405 1400 def get(self, key, default=None):
1406 1401 return self._map.get(key, default)
1407 1402
1408 1403 def __contains__(self, key):
1409 1404 return key in self._map
1410 1405
1411 1406 def __getitem__(self, key):
1412 1407 return self._map[key]
1413 1408
1414 1409 def keys(self):
1415 1410 return self._map.keys()
1416 1411
1417 1412 def preload(self):
1418 1413 """Loads the underlying data, if it's not already loaded"""
1419 1414 self._map
1420 1415
1421 1416 def addfile(self, f, oldstate, state, mode, size, mtime):
1422 1417 """Add a tracked file to the dirstate."""
1423 1418 if oldstate in b"?r" and "_dirs" in self.__dict__:
1424 1419 self._dirs.addpath(f)
1425 1420 if oldstate == b"?" and "_alldirs" in self.__dict__:
1426 1421 self._alldirs.addpath(f)
1427 1422 self._map[f] = dirstatetuple(state, mode, size, mtime)
1428 1423 if state != b'n' or mtime == -1:
1429 1424 self.nonnormalset.add(f)
1430 1425 if size == -2:
1431 1426 self.otherparentset.add(f)
1432 1427
1433 1428 def removefile(self, f, oldstate, size):
1434 1429 """
1435 1430 Mark a file as removed in the dirstate.
1436 1431
1437 1432 The `size` parameter is used to store sentinel values that indicate
1438 1433 the file's previous state. In the future, we should refactor this
1439 1434 to be more explicit about what that state is.
1440 1435 """
1441 1436 if oldstate not in b"?r" and "_dirs" in self.__dict__:
1442 1437 self._dirs.delpath(f)
1443 1438 if oldstate == b"?" and "_alldirs" in self.__dict__:
1444 1439 self._alldirs.addpath(f)
1445 1440 if "filefoldmap" in self.__dict__:
1446 1441 normed = util.normcase(f)
1447 1442 self.filefoldmap.pop(normed, None)
1448 1443 self._map[f] = dirstatetuple(b'r', 0, size, 0)
1449 1444 self.nonnormalset.add(f)
1450 1445
1451 1446 def dropfile(self, f, oldstate):
1452 1447 """
1453 1448 Remove a file from the dirstate. Returns True if the file was
1454 1449 previously recorded.
1455 1450 """
1456 1451 exists = self._map.pop(f, None) is not None
1457 1452 if exists:
1458 1453 if oldstate != b"r" and "_dirs" in self.__dict__:
1459 1454 self._dirs.delpath(f)
1460 1455 if "_alldirs" in self.__dict__:
1461 1456 self._alldirs.delpath(f)
1462 1457 if "filefoldmap" in self.__dict__:
1463 1458 normed = util.normcase(f)
1464 1459 self.filefoldmap.pop(normed, None)
1465 1460 self.nonnormalset.discard(f)
1466 1461 return exists
1467 1462
1468 1463 def clearambiguoustimes(self, files, now):
1469 1464 for f in files:
1470 1465 e = self.get(f)
1471 1466 if e is not None and e[0] == b'n' and e[3] == now:
1472 1467 self._map[f] = dirstatetuple(e[0], e[1], e[2], -1)
1473 1468 self.nonnormalset.add(f)
1474 1469
1475 1470 def nonnormalentries(self):
1476 1471 '''Compute the nonnormal dirstate entries from the dmap'''
1477 1472 try:
1478 1473 return parsers.nonnormalotherparententries(self._map)
1479 1474 except AttributeError:
1480 1475 nonnorm = set()
1481 1476 otherparent = set()
1482 1477 for fname, e in pycompat.iteritems(self._map):
1483 1478 if e[0] != b'n' or e[3] == -1:
1484 1479 nonnorm.add(fname)
1485 1480 if e[0] == b'n' and e[2] == -2:
1486 1481 otherparent.add(fname)
1487 1482 return nonnorm, otherparent
1488 1483
1489 1484 @propertycache
1490 1485 def filefoldmap(self):
1491 1486 """Returns a dictionary mapping normalized case paths to their
1492 1487 non-normalized versions.
1493 1488 """
1494 1489 try:
1495 1490 makefilefoldmap = parsers.make_file_foldmap
1496 1491 except AttributeError:
1497 1492 pass
1498 1493 else:
1499 1494 return makefilefoldmap(
1500 1495 self._map, util.normcasespec, util.normcasefallback
1501 1496 )
1502 1497
1503 1498 f = {}
1504 1499 normcase = util.normcase
1505 1500 for name, s in pycompat.iteritems(self._map):
1506 1501 if s[0] != b'r':
1507 1502 f[normcase(name)] = name
1508 1503 f[b'.'] = b'.' # prevents useless util.fspath() invocation
1509 1504 return f
1510 1505
1511 1506 def hastrackeddir(self, d):
1512 1507 """
1513 1508 Returns True if the dirstate contains a tracked (not removed) file
1514 1509 in this directory.
1515 1510 """
1516 1511 return d in self._dirs
1517 1512
1518 1513 def hasdir(self, d):
1519 1514 """
1520 1515 Returns True if the dirstate contains a file (tracked or removed)
1521 1516 in this directory.
1522 1517 """
1523 1518 return d in self._alldirs
1524 1519
1525 1520 @propertycache
1526 1521 def _dirs(self):
1527 1522 return pathutil.dirs(self._map, b'r')
1528 1523
1529 1524 @propertycache
1530 1525 def _alldirs(self):
1531 1526 return pathutil.dirs(self._map)
1532 1527
1533 1528 def _opendirstatefile(self):
1534 1529 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
1535 1530 if self._pendingmode is not None and self._pendingmode != mode:
1536 1531 fp.close()
1537 1532 raise error.Abort(
1538 1533 _(b'working directory state may be changed parallelly')
1539 1534 )
1540 1535 self._pendingmode = mode
1541 1536 return fp
1542 1537
1543 1538 def parents(self):
1544 1539 if not self._parents:
1545 1540 try:
1546 1541 fp = self._opendirstatefile()
1547 1542 st = fp.read(40)
1548 1543 fp.close()
1549 1544 except IOError as err:
1550 1545 if err.errno != errno.ENOENT:
1551 1546 raise
1552 1547 # File doesn't exist, so the current state is empty
1553 1548 st = b''
1554 1549
1555 1550 l = len(st)
1556 1551 if l == 40:
1557 1552 self._parents = (st[:20], st[20:40])
1558 1553 elif l == 0:
1559 1554 self._parents = (nullid, nullid)
1560 1555 else:
1561 1556 raise error.Abort(
1562 1557 _(b'working directory state appears damaged!')
1563 1558 )
1564 1559
1565 1560 return self._parents
1566 1561
1567 1562 def setparents(self, p1, p2):
1568 1563 self._parents = (p1, p2)
1569 1564 self._dirtyparents = True
1570 1565
1571 1566 def read(self):
1572 1567 # ignore HG_PENDING because identity is used only for writing
1573 1568 self.identity = util.filestat.frompath(
1574 1569 self._opener.join(self._filename)
1575 1570 )
1576 1571
1577 1572 try:
1578 1573 fp = self._opendirstatefile()
1579 1574 try:
1580 1575 st = fp.read()
1581 1576 finally:
1582 1577 fp.close()
1583 1578 except IOError as err:
1584 1579 if err.errno != errno.ENOENT:
1585 1580 raise
1586 1581 return
1587 1582 if not st:
1588 1583 return
1589 1584
1590 1585 if util.safehasattr(parsers, b'dict_new_presized'):
1591 1586 # Make an estimate of the number of files in the dirstate based on
1592 1587 # its size. From a linear regression on a set of real-world repos,
1593 1588 # all over 10,000 files, the size of a dirstate entry is 85
1594 1589 # bytes. The cost of resizing is significantly higher than the cost
1595 1590 # of filling in a larger presized dict, so subtract 20% from the
1596 1591 # size.
1597 1592 #
1598 1593 # This heuristic is imperfect in many ways, so in a future dirstate
1599 1594 # format update it makes sense to just record the number of entries
1600 1595 # on write.
1601 1596 self._map = parsers.dict_new_presized(len(st) // 71)
1602 1597
1603 1598 # Python's garbage collector triggers a GC each time a certain number
1604 1599 # of container objects (the number being defined by
1605 1600 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
1606 1601 # for each file in the dirstate. The C version then immediately marks
1607 1602 # them as not to be tracked by the collector. However, this has no
1608 1603 # effect on when GCs are triggered, only on what objects the GC looks
1609 1604 # into. This means that O(number of files) GCs are unavoidable.
1610 1605 # Depending on when in the process's lifetime the dirstate is parsed,
1611 1606 # this can get very expensive. As a workaround, disable GC while
1612 1607 # parsing the dirstate.
1613 1608 #
1614 1609 # (we cannot decorate the function directly since it is in a C module)
1615 1610 parse_dirstate = util.nogc(parsers.parse_dirstate)
1616 1611 p = parse_dirstate(self._map, self.copymap, st)
1617 1612 if not self._dirtyparents:
1618 1613 self.setparents(*p)
1619 1614
1620 1615 # Avoid excess attribute lookups by fast pathing certain checks
1621 1616 self.__contains__ = self._map.__contains__
1622 1617 self.__getitem__ = self._map.__getitem__
1623 1618 self.get = self._map.get
1624 1619
1625 1620 def write(self, st, now):
1626 1621 st.write(
1627 1622 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
1628 1623 )
1629 1624 st.close()
1630 1625 self._dirtyparents = False
1631 1626 self.nonnormalset, self.otherparentset = self.nonnormalentries()
1632 1627
1633 1628 @propertycache
1634 1629 def nonnormalset(self):
1635 1630 nonnorm, otherparents = self.nonnormalentries()
1636 1631 self.otherparentset = otherparents
1637 1632 return nonnorm
1638 1633
1639 1634 @propertycache
1640 1635 def otherparentset(self):
1641 1636 nonnorm, otherparents = self.nonnormalentries()
1642 1637 self.nonnormalset = nonnorm
1643 1638 return otherparents
1644 1639
1645 1640 @propertycache
1646 1641 def identity(self):
1647 1642 self._map
1648 1643 return self.identity
1649 1644
1650 1645 @propertycache
1651 1646 def dirfoldmap(self):
1652 1647 f = {}
1653 1648 normcase = util.normcase
1654 1649 for name in self._dirs:
1655 1650 f[normcase(name)] = name
1656 1651 return f
1657 1652
1658 1653
1659 1654 if rustmod is not None:
1660 1655
1661 1656 class dirstatemap(object):
1662 1657 def __init__(self, ui, opener, root):
1663 1658 self._ui = ui
1664 1659 self._opener = opener
1665 1660 self._root = root
1666 1661 self._filename = b'dirstate'
1667 1662 self._parents = None
1668 1663 self._dirtyparents = False
1669 1664
1670 1665 # for consistent view between _pl() and _read() invocations
1671 1666 self._pendingmode = None
1672 1667
1673 1668 def addfile(self, *args, **kwargs):
1674 1669 return self._rustmap.addfile(*args, **kwargs)
1675 1670
1676 1671 def removefile(self, *args, **kwargs):
1677 1672 return self._rustmap.removefile(*args, **kwargs)
1678 1673
1679 1674 def dropfile(self, *args, **kwargs):
1680 1675 return self._rustmap.dropfile(*args, **kwargs)
1681 1676
1682 1677 def clearambiguoustimes(self, *args, **kwargs):
1683 1678 return self._rustmap.clearambiguoustimes(*args, **kwargs)
1684 1679
1685 1680 def nonnormalentries(self):
1686 1681 return self._rustmap.nonnormalentries()
1687 1682
1688 1683 def get(self, *args, **kwargs):
1689 1684 return self._rustmap.get(*args, **kwargs)
1690 1685
1691 1686 @propertycache
1692 1687 def _rustmap(self):
1693 1688 self._rustmap = rustmod.DirstateMap(self._root)
1694 1689 self.read()
1695 1690 return self._rustmap
1696 1691
1697 1692 @property
1698 1693 def copymap(self):
1699 1694 return self._rustmap.copymap()
1700 1695
1701 1696 def preload(self):
1702 1697 self._rustmap
1703 1698
1704 1699 def clear(self):
1705 1700 self._rustmap.clear()
1706 1701 self.setparents(nullid, nullid)
1707 1702 util.clearcachedproperty(self, b"_dirs")
1708 1703 util.clearcachedproperty(self, b"_alldirs")
1709 1704 util.clearcachedproperty(self, b"dirfoldmap")
1710 1705
1711 1706 def items(self):
1712 1707 return self._rustmap.items()
1713 1708
1714 1709 def keys(self):
1715 1710 return iter(self._rustmap)
1716 1711
1717 1712 def __contains__(self, key):
1718 1713 return key in self._rustmap
1719 1714
1720 1715 def __getitem__(self, item):
1721 1716 return self._rustmap[item]
1722 1717
1723 1718 def __len__(self):
1724 1719 return len(self._rustmap)
1725 1720
1726 1721 def __iter__(self):
1727 1722 return iter(self._rustmap)
1728 1723
1729 1724 # forward for python2,3 compat
1730 1725 iteritems = items
1731 1726
1732 1727 def _opendirstatefile(self):
1733 1728 fp, mode = txnutil.trypending(
1734 1729 self._root, self._opener, self._filename
1735 1730 )
1736 1731 if self._pendingmode is not None and self._pendingmode != mode:
1737 1732 fp.close()
1738 1733 raise error.Abort(
1739 1734 _(b'working directory state may be changed parallelly')
1740 1735 )
1741 1736 self._pendingmode = mode
1742 1737 return fp
1743 1738
1744 1739 def setparents(self, p1, p2):
1745 1740 self._rustmap.setparents(p1, p2)
1746 1741 self._parents = (p1, p2)
1747 1742 self._dirtyparents = True
1748 1743
1749 1744 def parents(self):
1750 1745 if not self._parents:
1751 1746 try:
1752 1747 fp = self._opendirstatefile()
1753 1748 st = fp.read(40)
1754 1749 fp.close()
1755 1750 except IOError as err:
1756 1751 if err.errno != errno.ENOENT:
1757 1752 raise
1758 1753 # File doesn't exist, so the current state is empty
1759 1754 st = b''
1760 1755
1761 1756 try:
1762 1757 self._parents = self._rustmap.parents(st)
1763 1758 except ValueError:
1764 1759 raise error.Abort(
1765 1760 _(b'working directory state appears damaged!')
1766 1761 )
1767 1762
1768 1763 return self._parents
1769 1764
1770 1765 def read(self):
1771 1766 # ignore HG_PENDING because identity is used only for writing
1772 1767 self.identity = util.filestat.frompath(
1773 1768 self._opener.join(self._filename)
1774 1769 )
1775 1770
1776 1771 try:
1777 1772 fp = self._opendirstatefile()
1778 1773 try:
1779 1774 st = fp.read()
1780 1775 finally:
1781 1776 fp.close()
1782 1777 except IOError as err:
1783 1778 if err.errno != errno.ENOENT:
1784 1779 raise
1785 1780 return
1786 1781 if not st:
1787 1782 return
1788 1783
1789 1784 parse_dirstate = util.nogc(self._rustmap.read)
1790 1785 parents = parse_dirstate(st)
1791 1786 if parents and not self._dirtyparents:
1792 1787 self.setparents(*parents)
1793 1788
1794 1789 self.__contains__ = self._rustmap.__contains__
1795 1790 self.__getitem__ = self._rustmap.__getitem__
1796 1791 self.get = self._rustmap.get
1797 1792
1798 1793 def write(self, st, now):
1799 1794 parents = self.parents()
1800 1795 st.write(self._rustmap.write(parents[0], parents[1], now))
1801 1796 st.close()
1802 1797 self._dirtyparents = False
1803 1798
1804 1799 @propertycache
1805 1800 def filefoldmap(self):
1806 1801 """Returns a dictionary mapping normalized case paths to their
1807 1802 non-normalized versions.
1808 1803 """
1809 1804 return self._rustmap.filefoldmapasdict()
1810 1805
1811 1806 def hastrackeddir(self, d):
1812 1807 self._dirs # Trigger Python's propertycache
1813 1808 return self._rustmap.hastrackeddir(d)
1814 1809
1815 1810 def hasdir(self, d):
1816 1811 self._dirs # Trigger Python's propertycache
1817 1812 return self._rustmap.hasdir(d)
1818 1813
1819 1814 @propertycache
1820 1815 def _dirs(self):
1821 1816 return self._rustmap.getdirs()
1822 1817
1823 1818 @propertycache
1824 1819 def _alldirs(self):
1825 1820 return self._rustmap.getalldirs()
1826 1821
1827 1822 @propertycache
1828 1823 def identity(self):
1829 1824 self._rustmap
1830 1825 return self.identity
1831 1826
1832 1827 @property
1833 1828 def nonnormalset(self):
1834 1829 nonnorm, otherparents = self._rustmap.nonnormalentries()
1835 1830 return nonnorm
1836 1831
1837 1832 @property
1838 1833 def otherparentset(self):
1839 1834 nonnorm, otherparents = self._rustmap.nonnormalentries()
1840 1835 return otherparents
1841 1836
1842 1837 @propertycache
1843 1838 def dirfoldmap(self):
1844 1839 f = {}
1845 1840 normcase = util.normcase
1846 1841 for name in self._dirs:
1847 1842 f[normcase(name)] = name
1848 1843 return f
General Comments 0
You need to be logged in to leave comments. Login now