##// END OF EJS Templates
dirstate: respect request to not list unknown/ignored/clean files (API)...
Martin von Zweigbergk -
r44019:dd773340 default
parent child Browse files
Show More
@@ -1,1840 +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 util.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 util.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 772 matchedir = match.explicitdir
773 773 badfn = match.bad
774 774 dmap = self._map
775 775 lstat = os.lstat
776 776 getkind = stat.S_IFMT
777 777 dirkind = stat.S_IFDIR
778 778 regkind = stat.S_IFREG
779 779 lnkkind = stat.S_IFLNK
780 780 join = self._join
781 781 dirsfound = []
782 782 foundadd = dirsfound.append
783 783 dirsnotfound = []
784 784 notfoundadd = dirsnotfound.append
785 785
786 786 if not match.isexact() and self._checkcase:
787 787 normalize = self._normalize
788 788 else:
789 789 normalize = None
790 790
791 791 files = sorted(match.files())
792 792 subrepos.sort()
793 793 i, j = 0, 0
794 794 while i < len(files) and j < len(subrepos):
795 795 subpath = subrepos[j] + b"/"
796 796 if files[i] < subpath:
797 797 i += 1
798 798 continue
799 799 while i < len(files) and files[i].startswith(subpath):
800 800 del files[i]
801 801 j += 1
802 802
803 803 if not files or b'' in files:
804 804 files = [b'']
805 805 # constructing the foldmap is expensive, so don't do it for the
806 806 # common case where files is ['']
807 807 normalize = None
808 808 results = dict.fromkeys(subrepos)
809 809 results[b'.hg'] = None
810 810
811 811 for ff in files:
812 812 if normalize:
813 813 nf = normalize(ff, False, True)
814 814 else:
815 815 nf = ff
816 816 if nf in results:
817 817 continue
818 818
819 819 try:
820 820 st = lstat(join(nf))
821 821 kind = getkind(st.st_mode)
822 822 if kind == dirkind:
823 823 if nf in dmap:
824 824 # file replaced by dir on disk but still in dirstate
825 825 results[nf] = None
826 826 if matchedir:
827 827 matchedir(nf)
828 828 foundadd((nf, ff))
829 829 elif kind == regkind or kind == lnkkind:
830 830 results[nf] = st
831 831 else:
832 832 badfn(ff, badtype(kind))
833 833 if nf in dmap:
834 834 results[nf] = None
835 835 except OSError as inst: # nf not found on disk - it is dirstate only
836 836 if nf in dmap: # does it exactly match a missing file?
837 837 results[nf] = None
838 838 else: # does it match a missing directory?
839 839 if self._map.hasdir(nf):
840 840 if matchedir:
841 841 matchedir(nf)
842 842 notfoundadd(nf)
843 843 else:
844 844 badfn(ff, encoding.strtolocal(inst.strerror))
845 845
846 846 # match.files() may contain explicitly-specified paths that shouldn't
847 847 # be taken; drop them from the list of files found. dirsfound/notfound
848 848 # aren't filtered here because they will be tested later.
849 849 if match.anypats():
850 850 for f in list(results):
851 851 if f == b'.hg' or f in subrepos:
852 852 # keep sentinel to disable further out-of-repo walks
853 853 continue
854 854 if not match(f):
855 855 del results[f]
856 856
857 857 # Case insensitive filesystems cannot rely on lstat() failing to detect
858 858 # a case-only rename. Prune the stat object for any file that does not
859 859 # match the case in the filesystem, if there are multiple files that
860 860 # normalize to the same path.
861 861 if match.isexact() and self._checkcase:
862 862 normed = {}
863 863
864 864 for f, st in pycompat.iteritems(results):
865 865 if st is None:
866 866 continue
867 867
868 868 nc = util.normcase(f)
869 869 paths = normed.get(nc)
870 870
871 871 if paths is None:
872 872 paths = set()
873 873 normed[nc] = paths
874 874
875 875 paths.add(f)
876 876
877 877 for norm, paths in pycompat.iteritems(normed):
878 878 if len(paths) > 1:
879 879 for path in paths:
880 880 folded = self._discoverpath(
881 881 path, norm, True, None, self._map.dirfoldmap
882 882 )
883 883 if path != folded:
884 884 results[path] = None
885 885
886 886 return results, dirsfound, dirsnotfound
887 887
888 888 def walk(self, match, subrepos, unknown, ignored, full=True):
889 889 '''
890 890 Walk recursively through the directory tree, finding all files
891 891 matched by match.
892 892
893 893 If full is False, maybe skip some known-clean files.
894 894
895 895 Return a dict mapping filename to stat-like object (either
896 896 mercurial.osutil.stat instance or return value of os.stat()).
897 897
898 898 '''
899 899 # full is a flag that extensions that hook into walk can use -- this
900 900 # implementation doesn't use it at all. This satisfies the contract
901 901 # because we only guarantee a "maybe".
902 902
903 903 if ignored:
904 904 ignore = util.never
905 905 dirignore = util.never
906 906 elif unknown:
907 907 ignore = self._ignore
908 908 dirignore = self._dirignore
909 909 else:
910 910 # if not unknown and not ignored, drop dir recursion and step 2
911 911 ignore = util.always
912 912 dirignore = util.always
913 913
914 914 matchfn = match.matchfn
915 915 matchalways = match.always()
916 916 matchtdir = match.traversedir
917 917 dmap = self._map
918 918 listdir = util.listdir
919 919 lstat = os.lstat
920 920 dirkind = stat.S_IFDIR
921 921 regkind = stat.S_IFREG
922 922 lnkkind = stat.S_IFLNK
923 923 join = self._join
924 924
925 925 exact = skipstep3 = False
926 926 if match.isexact(): # match.exact
927 927 exact = True
928 928 dirignore = util.always # skip step 2
929 929 elif match.prefix(): # match.match, no patterns
930 930 skipstep3 = True
931 931
932 932 if not exact and self._checkcase:
933 933 normalize = self._normalize
934 934 normalizefile = self._normalizefile
935 935 skipstep3 = False
936 936 else:
937 937 normalize = self._normalize
938 938 normalizefile = None
939 939
940 940 # step 1: find all explicit files
941 941 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
942 942
943 943 skipstep3 = skipstep3 and not (work or dirsnotfound)
944 944 work = [d for d in work if not dirignore(d[0])]
945 945
946 946 # step 2: visit subdirectories
947 947 def traverse(work, alreadynormed):
948 948 wadd = work.append
949 949 while work:
950 950 tracing.counter('dirstate.walk work', len(work))
951 951 nd = work.pop()
952 952 visitentries = match.visitchildrenset(nd)
953 953 if not visitentries:
954 954 continue
955 955 if visitentries == b'this' or visitentries == b'all':
956 956 visitentries = None
957 957 skip = None
958 958 if nd != b'':
959 959 skip = b'.hg'
960 960 try:
961 961 with tracing.log('dirstate.walk.traverse listdir %s', nd):
962 962 entries = listdir(join(nd), stat=True, skip=skip)
963 963 except OSError as inst:
964 964 if inst.errno in (errno.EACCES, errno.ENOENT):
965 965 match.bad(
966 966 self.pathto(nd), encoding.strtolocal(inst.strerror)
967 967 )
968 968 continue
969 969 raise
970 970 for f, kind, st in entries:
971 971 # Some matchers may return files in the visitentries set,
972 972 # instead of 'this', if the matcher explicitly mentions them
973 973 # and is not an exactmatcher. This is acceptable; we do not
974 974 # make any hard assumptions about file-or-directory below
975 975 # based on the presence of `f` in visitentries. If
976 976 # visitchildrenset returned a set, we can always skip the
977 977 # entries *not* in the set it provided regardless of whether
978 978 # they're actually a file or a directory.
979 979 if visitentries and f not in visitentries:
980 980 continue
981 981 if normalizefile:
982 982 # even though f might be a directory, we're only
983 983 # interested in comparing it to files currently in the
984 984 # dmap -- therefore normalizefile is enough
985 985 nf = normalizefile(
986 986 nd and (nd + b"/" + f) or f, True, True
987 987 )
988 988 else:
989 989 nf = nd and (nd + b"/" + f) or f
990 990 if nf not in results:
991 991 if kind == dirkind:
992 992 if not ignore(nf):
993 993 if matchtdir:
994 994 matchtdir(nf)
995 995 wadd(nf)
996 996 if nf in dmap and (matchalways or matchfn(nf)):
997 997 results[nf] = None
998 998 elif kind == regkind or kind == lnkkind:
999 999 if nf in dmap:
1000 1000 if matchalways or matchfn(nf):
1001 1001 results[nf] = st
1002 1002 elif (matchalways or matchfn(nf)) and not ignore(
1003 1003 nf
1004 1004 ):
1005 1005 # unknown file -- normalize if necessary
1006 1006 if not alreadynormed:
1007 1007 nf = normalize(nf, False, True)
1008 1008 results[nf] = st
1009 1009 elif nf in dmap and (matchalways or matchfn(nf)):
1010 1010 results[nf] = None
1011 1011
1012 1012 for nd, d in work:
1013 1013 # alreadynormed means that processwork doesn't have to do any
1014 1014 # expensive directory normalization
1015 1015 alreadynormed = not normalize or nd == d
1016 1016 traverse([d], alreadynormed)
1017 1017
1018 1018 for s in subrepos:
1019 1019 del results[s]
1020 1020 del results[b'.hg']
1021 1021
1022 1022 # step 3: visit remaining files from dmap
1023 1023 if not skipstep3 and not exact:
1024 1024 # If a dmap file is not in results yet, it was either
1025 1025 # a) not matching matchfn b) ignored, c) missing, or d) under a
1026 1026 # symlink directory.
1027 1027 if not results and matchalways:
1028 1028 visit = [f for f in dmap]
1029 1029 else:
1030 1030 visit = [f for f in dmap if f not in results and matchfn(f)]
1031 1031 visit.sort()
1032 1032
1033 1033 if unknown:
1034 1034 # unknown == True means we walked all dirs under the roots
1035 1035 # that wasn't ignored, and everything that matched was stat'ed
1036 1036 # and is already in results.
1037 1037 # The rest must thus be ignored or under a symlink.
1038 1038 audit_path = pathutil.pathauditor(self._root, cached=True)
1039 1039
1040 1040 for nf in iter(visit):
1041 1041 # If a stat for the same file was already added with a
1042 1042 # different case, don't add one for this, since that would
1043 1043 # make it appear as if the file exists under both names
1044 1044 # on disk.
1045 1045 if (
1046 1046 normalizefile
1047 1047 and normalizefile(nf, True, True) in results
1048 1048 ):
1049 1049 results[nf] = None
1050 1050 # Report ignored items in the dmap as long as they are not
1051 1051 # under a symlink directory.
1052 1052 elif audit_path.check(nf):
1053 1053 try:
1054 1054 results[nf] = lstat(join(nf))
1055 1055 # file was just ignored, no links, and exists
1056 1056 except OSError:
1057 1057 # file doesn't exist
1058 1058 results[nf] = None
1059 1059 else:
1060 1060 # It's either missing or under a symlink directory
1061 1061 # which we in this case report as missing
1062 1062 results[nf] = None
1063 1063 else:
1064 1064 # We may not have walked the full directory tree above,
1065 1065 # so stat and check everything we missed.
1066 1066 iv = iter(visit)
1067 1067 for st in util.statfiles([join(i) for i in visit]):
1068 1068 results[next(iv)] = st
1069 1069 return results
1070 1070
1071 1071 def status(self, match, subrepos, ignored, clean, unknown):
1072 1072 '''Determine the status of the working copy relative to the
1073 1073 dirstate and return a pair of (unsure, status), where status is of type
1074 1074 scmutil.status and:
1075 1075
1076 1076 unsure:
1077 1077 files that might have been modified since the dirstate was
1078 1078 written, but need to be read to be sure (size is the same
1079 1079 but mtime differs)
1080 1080 status.modified:
1081 1081 files that have definitely been modified since the dirstate
1082 1082 was written (different size or mode)
1083 1083 status.clean:
1084 1084 files that have definitely not been modified since the
1085 1085 dirstate was written
1086 1086 '''
1087 1087 listignored, listclean, listunknown = ignored, clean, unknown
1088 1088 lookup, modified, added, unknown, ignored = [], [], [], [], []
1089 1089 removed, deleted, clean = [], [], []
1090 1090
1091 1091 dmap = self._map
1092 1092 dmap.preload()
1093 1093
1094 1094 use_rust = True
1095 1095 if rustmod is None:
1096 1096 use_rust = False
1097 1097 elif subrepos:
1098 1098 use_rust = False
1099 1099 if bool(listunknown):
1100 1100 # Pathauditor does not exist yet in Rust, unknown files
1101 1101 # can't be trusted.
1102 1102 use_rust = False
1103 1103 elif self._ignorefiles() and listignored:
1104 1104 # Rust has no ignore mechanism yet, so don't use Rust for
1105 1105 # commands that need ignore.
1106 1106 use_rust = False
1107 1107 elif not match.always():
1108 1108 # Matchers have yet to be implemented
1109 1109 use_rust = False
1110 1110
1111 1111 if use_rust:
1112 1112 # Force Rayon (Rust parallelism library) to respect the number of
1113 1113 # workers. This is a temporary workaround until Rust code knows
1114 1114 # how to read the config file.
1115 1115 numcpus = self._ui.configint("worker", "numcpus")
1116 1116 if numcpus is not None:
1117 1117 encoding.environ.setdefault(
1118 1118 b'RAYON_NUM_THREADS', b'%d' % numcpus
1119 1119 )
1120 1120
1121 1121 workers_enabled = self._ui.configbool("worker", "enabled", True)
1122 1122 if not workers_enabled:
1123 1123 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1124 1124
1125 1125 (
1126 1126 lookup,
1127 1127 modified,
1128 1128 added,
1129 1129 removed,
1130 1130 deleted,
1131 1131 unknown,
1132 1132 clean,
1133 1133 ) = rustmod.status(
1134 1134 dmap._rustmap,
1135 1135 self._rootdir,
1136 1136 bool(listclean),
1137 1137 self._lastnormaltime,
1138 1138 self._checkexec,
1139 1139 )
1140 1140
1141 1141 status = scmutil.status(
1142 1142 modified=modified,
1143 1143 added=added,
1144 1144 removed=removed,
1145 1145 deleted=deleted,
1146 1146 unknown=unknown,
1147 1147 ignored=ignored,
1148 1148 clean=clean,
1149 1149 )
1150 1150 return (lookup, status)
1151 1151
1152 def noop(f):
1153 pass
1154
1152 1155 dcontains = dmap.__contains__
1153 1156 dget = dmap.__getitem__
1154 1157 ladd = lookup.append # aka "unsure"
1155 1158 madd = modified.append
1156 1159 aadd = added.append
1157 uadd = unknown.append
1158 iadd = ignored.append
1160 uadd = unknown.append if listunknown else noop
1161 iadd = ignored.append if listignored else noop
1159 1162 radd = removed.append
1160 1163 dadd = deleted.append
1161 cadd = clean.append
1164 cadd = clean.append if listclean else noop
1162 1165 mexact = match.exact
1163 1166 dirignore = self._dirignore
1164 1167 checkexec = self._checkexec
1165 1168 copymap = self._map.copymap
1166 1169 lastnormaltime = self._lastnormaltime
1167 1170
1168 1171 # We need to do full walks when either
1169 1172 # - we're listing all clean files, or
1170 1173 # - match.traversedir does something, because match.traversedir should
1171 1174 # be called for every dir in the working dir
1172 1175 full = listclean or match.traversedir is not None
1173 1176 for fn, st in pycompat.iteritems(
1174 1177 self.walk(match, subrepos, listunknown, listignored, full=full)
1175 1178 ):
1176 1179 if not dcontains(fn):
1177 1180 if (listignored or mexact(fn)) and dirignore(fn):
1178 1181 if listignored:
1179 1182 iadd(fn)
1180 1183 else:
1181 1184 uadd(fn)
1182 1185 continue
1183 1186
1184 1187 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1185 1188 # written like that for performance reasons. dmap[fn] is not a
1186 1189 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1187 1190 # opcode has fast paths when the value to be unpacked is a tuple or
1188 1191 # a list, but falls back to creating a full-fledged iterator in
1189 1192 # general. That is much slower than simply accessing and storing the
1190 1193 # tuple members one by one.
1191 1194 t = dget(fn)
1192 1195 state = t[0]
1193 1196 mode = t[1]
1194 1197 size = t[2]
1195 1198 time = t[3]
1196 1199
1197 1200 if not st and state in b"nma":
1198 1201 dadd(fn)
1199 1202 elif state == b'n':
1200 1203 if (
1201 1204 size >= 0
1202 1205 and (
1203 1206 (size != st.st_size and size != st.st_size & _rangemask)
1204 1207 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1205 1208 )
1206 1209 or size == -2 # other parent
1207 1210 or fn in copymap
1208 1211 ):
1209 1212 madd(fn)
1210 1213 elif (
1211 1214 time != st[stat.ST_MTIME]
1212 1215 and time != st[stat.ST_MTIME] & _rangemask
1213 1216 ):
1214 1217 ladd(fn)
1215 1218 elif st[stat.ST_MTIME] == lastnormaltime:
1216 1219 # fn may have just been marked as normal and it may have
1217 1220 # changed in the same second without changing its size.
1218 1221 # This can happen if we quickly do multiple commits.
1219 1222 # Force lookup, so we don't miss such a racy file change.
1220 1223 ladd(fn)
1221 1224 elif listclean:
1222 1225 cadd(fn)
1223 1226 elif state == b'm':
1224 1227 madd(fn)
1225 1228 elif state == b'a':
1226 1229 aadd(fn)
1227 1230 elif state == b'r':
1228 1231 radd(fn)
1229 1232
1230 1233 return (
1231 1234 lookup,
1232 1235 scmutil.status(
1233 1236 modified, added, removed, deleted, unknown, ignored, clean
1234 1237 ),
1235 1238 )
1236 1239
1237 1240 def matches(self, match):
1238 1241 '''
1239 1242 return files in the dirstate (in whatever state) filtered by match
1240 1243 '''
1241 1244 dmap = self._map
1242 1245 if match.always():
1243 1246 return dmap.keys()
1244 1247 files = match.files()
1245 1248 if match.isexact():
1246 1249 # fast path -- filter the other way around, since typically files is
1247 1250 # much smaller than dmap
1248 1251 return [f for f in files if f in dmap]
1249 1252 if match.prefix() and all(fn in dmap for fn in files):
1250 1253 # fast path -- all the values are known to be files, so just return
1251 1254 # that
1252 1255 return list(files)
1253 1256 return [f for f in dmap if match(f)]
1254 1257
1255 1258 def _actualfilename(self, tr):
1256 1259 if tr:
1257 1260 return self._pendingfilename
1258 1261 else:
1259 1262 return self._filename
1260 1263
1261 1264 def savebackup(self, tr, backupname):
1262 1265 '''Save current dirstate into backup file'''
1263 1266 filename = self._actualfilename(tr)
1264 1267 assert backupname != filename
1265 1268
1266 1269 # use '_writedirstate' instead of 'write' to write changes certainly,
1267 1270 # because the latter omits writing out if transaction is running.
1268 1271 # output file will be used to create backup of dirstate at this point.
1269 1272 if self._dirty or not self._opener.exists(filename):
1270 1273 self._writedirstate(
1271 1274 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1272 1275 )
1273 1276
1274 1277 if tr:
1275 1278 # ensure that subsequent tr.writepending returns True for
1276 1279 # changes written out above, even if dirstate is never
1277 1280 # changed after this
1278 1281 tr.addfilegenerator(
1279 1282 b'dirstate',
1280 1283 (self._filename,),
1281 1284 self._writedirstate,
1282 1285 location=b'plain',
1283 1286 )
1284 1287
1285 1288 # ensure that pending file written above is unlinked at
1286 1289 # failure, even if tr.writepending isn't invoked until the
1287 1290 # end of this transaction
1288 1291 tr.registertmp(filename, location=b'plain')
1289 1292
1290 1293 self._opener.tryunlink(backupname)
1291 1294 # hardlink backup is okay because _writedirstate is always called
1292 1295 # with an "atomictemp=True" file.
1293 1296 util.copyfile(
1294 1297 self._opener.join(filename),
1295 1298 self._opener.join(backupname),
1296 1299 hardlink=True,
1297 1300 )
1298 1301
1299 1302 def restorebackup(self, tr, backupname):
1300 1303 '''Restore dirstate by backup file'''
1301 1304 # this "invalidate()" prevents "wlock.release()" from writing
1302 1305 # changes of dirstate out after restoring from backup file
1303 1306 self.invalidate()
1304 1307 filename = self._actualfilename(tr)
1305 1308 o = self._opener
1306 1309 if util.samefile(o.join(backupname), o.join(filename)):
1307 1310 o.unlink(backupname)
1308 1311 else:
1309 1312 o.rename(backupname, filename, checkambig=True)
1310 1313
1311 1314 def clearbackup(self, tr, backupname):
1312 1315 '''Clear backup file'''
1313 1316 self._opener.unlink(backupname)
1314 1317
1315 1318
1316 1319 class dirstatemap(object):
1317 1320 """Map encapsulating the dirstate's contents.
1318 1321
1319 1322 The dirstate contains the following state:
1320 1323
1321 1324 - `identity` is the identity of the dirstate file, which can be used to
1322 1325 detect when changes have occurred to the dirstate file.
1323 1326
1324 1327 - `parents` is a pair containing the parents of the working copy. The
1325 1328 parents are updated by calling `setparents`.
1326 1329
1327 1330 - the state map maps filenames to tuples of (state, mode, size, mtime),
1328 1331 where state is a single character representing 'normal', 'added',
1329 1332 'removed', or 'merged'. It is read by treating the dirstate as a
1330 1333 dict. File state is updated by calling the `addfile`, `removefile` and
1331 1334 `dropfile` methods.
1332 1335
1333 1336 - `copymap` maps destination filenames to their source filename.
1334 1337
1335 1338 The dirstate also provides the following views onto the state:
1336 1339
1337 1340 - `nonnormalset` is a set of the filenames that have state other
1338 1341 than 'normal', or are normal but have an mtime of -1 ('normallookup').
1339 1342
1340 1343 - `otherparentset` is a set of the filenames that are marked as coming
1341 1344 from the second parent when the dirstate is currently being merged.
1342 1345
1343 1346 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
1344 1347 form that they appear as in the dirstate.
1345 1348
1346 1349 - `dirfoldmap` is a dict mapping normalized directory names to the
1347 1350 denormalized form that they appear as in the dirstate.
1348 1351 """
1349 1352
1350 1353 def __init__(self, ui, opener, root):
1351 1354 self._ui = ui
1352 1355 self._opener = opener
1353 1356 self._root = root
1354 1357 self._filename = b'dirstate'
1355 1358
1356 1359 self._parents = None
1357 1360 self._dirtyparents = False
1358 1361
1359 1362 # for consistent view between _pl() and _read() invocations
1360 1363 self._pendingmode = None
1361 1364
1362 1365 @propertycache
1363 1366 def _map(self):
1364 1367 self._map = {}
1365 1368 self.read()
1366 1369 return self._map
1367 1370
1368 1371 @propertycache
1369 1372 def copymap(self):
1370 1373 self.copymap = {}
1371 1374 self._map
1372 1375 return self.copymap
1373 1376
1374 1377 def clear(self):
1375 1378 self._map.clear()
1376 1379 self.copymap.clear()
1377 1380 self.setparents(nullid, nullid)
1378 1381 util.clearcachedproperty(self, b"_dirs")
1379 1382 util.clearcachedproperty(self, b"_alldirs")
1380 1383 util.clearcachedproperty(self, b"filefoldmap")
1381 1384 util.clearcachedproperty(self, b"dirfoldmap")
1382 1385 util.clearcachedproperty(self, b"nonnormalset")
1383 1386 util.clearcachedproperty(self, b"otherparentset")
1384 1387
1385 1388 def items(self):
1386 1389 return pycompat.iteritems(self._map)
1387 1390
1388 1391 # forward for python2,3 compat
1389 1392 iteritems = items
1390 1393
1391 1394 def __len__(self):
1392 1395 return len(self._map)
1393 1396
1394 1397 def __iter__(self):
1395 1398 return iter(self._map)
1396 1399
1397 1400 def get(self, key, default=None):
1398 1401 return self._map.get(key, default)
1399 1402
1400 1403 def __contains__(self, key):
1401 1404 return key in self._map
1402 1405
1403 1406 def __getitem__(self, key):
1404 1407 return self._map[key]
1405 1408
1406 1409 def keys(self):
1407 1410 return self._map.keys()
1408 1411
1409 1412 def preload(self):
1410 1413 """Loads the underlying data, if it's not already loaded"""
1411 1414 self._map
1412 1415
1413 1416 def addfile(self, f, oldstate, state, mode, size, mtime):
1414 1417 """Add a tracked file to the dirstate."""
1415 1418 if oldstate in b"?r" and "_dirs" in self.__dict__:
1416 1419 self._dirs.addpath(f)
1417 1420 if oldstate == b"?" and "_alldirs" in self.__dict__:
1418 1421 self._alldirs.addpath(f)
1419 1422 self._map[f] = dirstatetuple(state, mode, size, mtime)
1420 1423 if state != b'n' or mtime == -1:
1421 1424 self.nonnormalset.add(f)
1422 1425 if size == -2:
1423 1426 self.otherparentset.add(f)
1424 1427
1425 1428 def removefile(self, f, oldstate, size):
1426 1429 """
1427 1430 Mark a file as removed in the dirstate.
1428 1431
1429 1432 The `size` parameter is used to store sentinel values that indicate
1430 1433 the file's previous state. In the future, we should refactor this
1431 1434 to be more explicit about what that state is.
1432 1435 """
1433 1436 if oldstate not in b"?r" and "_dirs" in self.__dict__:
1434 1437 self._dirs.delpath(f)
1435 1438 if oldstate == b"?" and "_alldirs" in self.__dict__:
1436 1439 self._alldirs.addpath(f)
1437 1440 if "filefoldmap" in self.__dict__:
1438 1441 normed = util.normcase(f)
1439 1442 self.filefoldmap.pop(normed, None)
1440 1443 self._map[f] = dirstatetuple(b'r', 0, size, 0)
1441 1444 self.nonnormalset.add(f)
1442 1445
1443 1446 def dropfile(self, f, oldstate):
1444 1447 """
1445 1448 Remove a file from the dirstate. Returns True if the file was
1446 1449 previously recorded.
1447 1450 """
1448 1451 exists = self._map.pop(f, None) is not None
1449 1452 if exists:
1450 1453 if oldstate != b"r" and "_dirs" in self.__dict__:
1451 1454 self._dirs.delpath(f)
1452 1455 if "_alldirs" in self.__dict__:
1453 1456 self._alldirs.delpath(f)
1454 1457 if "filefoldmap" in self.__dict__:
1455 1458 normed = util.normcase(f)
1456 1459 self.filefoldmap.pop(normed, None)
1457 1460 self.nonnormalset.discard(f)
1458 1461 return exists
1459 1462
1460 1463 def clearambiguoustimes(self, files, now):
1461 1464 for f in files:
1462 1465 e = self.get(f)
1463 1466 if e is not None and e[0] == b'n' and e[3] == now:
1464 1467 self._map[f] = dirstatetuple(e[0], e[1], e[2], -1)
1465 1468 self.nonnormalset.add(f)
1466 1469
1467 1470 def nonnormalentries(self):
1468 1471 '''Compute the nonnormal dirstate entries from the dmap'''
1469 1472 try:
1470 1473 return parsers.nonnormalotherparententries(self._map)
1471 1474 except AttributeError:
1472 1475 nonnorm = set()
1473 1476 otherparent = set()
1474 1477 for fname, e in pycompat.iteritems(self._map):
1475 1478 if e[0] != b'n' or e[3] == -1:
1476 1479 nonnorm.add(fname)
1477 1480 if e[0] == b'n' and e[2] == -2:
1478 1481 otherparent.add(fname)
1479 1482 return nonnorm, otherparent
1480 1483
1481 1484 @propertycache
1482 1485 def filefoldmap(self):
1483 1486 """Returns a dictionary mapping normalized case paths to their
1484 1487 non-normalized versions.
1485 1488 """
1486 1489 try:
1487 1490 makefilefoldmap = parsers.make_file_foldmap
1488 1491 except AttributeError:
1489 1492 pass
1490 1493 else:
1491 1494 return makefilefoldmap(
1492 1495 self._map, util.normcasespec, util.normcasefallback
1493 1496 )
1494 1497
1495 1498 f = {}
1496 1499 normcase = util.normcase
1497 1500 for name, s in pycompat.iteritems(self._map):
1498 1501 if s[0] != b'r':
1499 1502 f[normcase(name)] = name
1500 1503 f[b'.'] = b'.' # prevents useless util.fspath() invocation
1501 1504 return f
1502 1505
1503 1506 def hastrackeddir(self, d):
1504 1507 """
1505 1508 Returns True if the dirstate contains a tracked (not removed) file
1506 1509 in this directory.
1507 1510 """
1508 1511 return d in self._dirs
1509 1512
1510 1513 def hasdir(self, d):
1511 1514 """
1512 1515 Returns True if the dirstate contains a file (tracked or removed)
1513 1516 in this directory.
1514 1517 """
1515 1518 return d in self._alldirs
1516 1519
1517 1520 @propertycache
1518 1521 def _dirs(self):
1519 1522 return pathutil.dirs(self._map, b'r')
1520 1523
1521 1524 @propertycache
1522 1525 def _alldirs(self):
1523 1526 return pathutil.dirs(self._map)
1524 1527
1525 1528 def _opendirstatefile(self):
1526 1529 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
1527 1530 if self._pendingmode is not None and self._pendingmode != mode:
1528 1531 fp.close()
1529 1532 raise error.Abort(
1530 1533 _(b'working directory state may be changed parallelly')
1531 1534 )
1532 1535 self._pendingmode = mode
1533 1536 return fp
1534 1537
1535 1538 def parents(self):
1536 1539 if not self._parents:
1537 1540 try:
1538 1541 fp = self._opendirstatefile()
1539 1542 st = fp.read(40)
1540 1543 fp.close()
1541 1544 except IOError as err:
1542 1545 if err.errno != errno.ENOENT:
1543 1546 raise
1544 1547 # File doesn't exist, so the current state is empty
1545 1548 st = b''
1546 1549
1547 1550 l = len(st)
1548 1551 if l == 40:
1549 1552 self._parents = (st[:20], st[20:40])
1550 1553 elif l == 0:
1551 1554 self._parents = (nullid, nullid)
1552 1555 else:
1553 1556 raise error.Abort(
1554 1557 _(b'working directory state appears damaged!')
1555 1558 )
1556 1559
1557 1560 return self._parents
1558 1561
1559 1562 def setparents(self, p1, p2):
1560 1563 self._parents = (p1, p2)
1561 1564 self._dirtyparents = True
1562 1565
1563 1566 def read(self):
1564 1567 # ignore HG_PENDING because identity is used only for writing
1565 1568 self.identity = util.filestat.frompath(
1566 1569 self._opener.join(self._filename)
1567 1570 )
1568 1571
1569 1572 try:
1570 1573 fp = self._opendirstatefile()
1571 1574 try:
1572 1575 st = fp.read()
1573 1576 finally:
1574 1577 fp.close()
1575 1578 except IOError as err:
1576 1579 if err.errno != errno.ENOENT:
1577 1580 raise
1578 1581 return
1579 1582 if not st:
1580 1583 return
1581 1584
1582 1585 if util.safehasattr(parsers, b'dict_new_presized'):
1583 1586 # Make an estimate of the number of files in the dirstate based on
1584 1587 # its size. From a linear regression on a set of real-world repos,
1585 1588 # all over 10,000 files, the size of a dirstate entry is 85
1586 1589 # bytes. The cost of resizing is significantly higher than the cost
1587 1590 # of filling in a larger presized dict, so subtract 20% from the
1588 1591 # size.
1589 1592 #
1590 1593 # This heuristic is imperfect in many ways, so in a future dirstate
1591 1594 # format update it makes sense to just record the number of entries
1592 1595 # on write.
1593 1596 self._map = parsers.dict_new_presized(len(st) // 71)
1594 1597
1595 1598 # Python's garbage collector triggers a GC each time a certain number
1596 1599 # of container objects (the number being defined by
1597 1600 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
1598 1601 # for each file in the dirstate. The C version then immediately marks
1599 1602 # them as not to be tracked by the collector. However, this has no
1600 1603 # effect on when GCs are triggered, only on what objects the GC looks
1601 1604 # into. This means that O(number of files) GCs are unavoidable.
1602 1605 # Depending on when in the process's lifetime the dirstate is parsed,
1603 1606 # this can get very expensive. As a workaround, disable GC while
1604 1607 # parsing the dirstate.
1605 1608 #
1606 1609 # (we cannot decorate the function directly since it is in a C module)
1607 1610 parse_dirstate = util.nogc(parsers.parse_dirstate)
1608 1611 p = parse_dirstate(self._map, self.copymap, st)
1609 1612 if not self._dirtyparents:
1610 1613 self.setparents(*p)
1611 1614
1612 1615 # Avoid excess attribute lookups by fast pathing certain checks
1613 1616 self.__contains__ = self._map.__contains__
1614 1617 self.__getitem__ = self._map.__getitem__
1615 1618 self.get = self._map.get
1616 1619
1617 1620 def write(self, st, now):
1618 1621 st.write(
1619 1622 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
1620 1623 )
1621 1624 st.close()
1622 1625 self._dirtyparents = False
1623 1626 self.nonnormalset, self.otherparentset = self.nonnormalentries()
1624 1627
1625 1628 @propertycache
1626 1629 def nonnormalset(self):
1627 1630 nonnorm, otherparents = self.nonnormalentries()
1628 1631 self.otherparentset = otherparents
1629 1632 return nonnorm
1630 1633
1631 1634 @propertycache
1632 1635 def otherparentset(self):
1633 1636 nonnorm, otherparents = self.nonnormalentries()
1634 1637 self.nonnormalset = nonnorm
1635 1638 return otherparents
1636 1639
1637 1640 @propertycache
1638 1641 def identity(self):
1639 1642 self._map
1640 1643 return self.identity
1641 1644
1642 1645 @propertycache
1643 1646 def dirfoldmap(self):
1644 1647 f = {}
1645 1648 normcase = util.normcase
1646 1649 for name in self._dirs:
1647 1650 f[normcase(name)] = name
1648 1651 return f
1649 1652
1650 1653
1651 1654 if rustmod is not None:
1652 1655
1653 1656 class dirstatemap(object):
1654 1657 def __init__(self, ui, opener, root):
1655 1658 self._ui = ui
1656 1659 self._opener = opener
1657 1660 self._root = root
1658 1661 self._filename = b'dirstate'
1659 1662 self._parents = None
1660 1663 self._dirtyparents = False
1661 1664
1662 1665 # for consistent view between _pl() and _read() invocations
1663 1666 self._pendingmode = None
1664 1667
1665 1668 def addfile(self, *args, **kwargs):
1666 1669 return self._rustmap.addfile(*args, **kwargs)
1667 1670
1668 1671 def removefile(self, *args, **kwargs):
1669 1672 return self._rustmap.removefile(*args, **kwargs)
1670 1673
1671 1674 def dropfile(self, *args, **kwargs):
1672 1675 return self._rustmap.dropfile(*args, **kwargs)
1673 1676
1674 1677 def clearambiguoustimes(self, *args, **kwargs):
1675 1678 return self._rustmap.clearambiguoustimes(*args, **kwargs)
1676 1679
1677 1680 def nonnormalentries(self):
1678 1681 return self._rustmap.nonnormalentries()
1679 1682
1680 1683 def get(self, *args, **kwargs):
1681 1684 return self._rustmap.get(*args, **kwargs)
1682 1685
1683 1686 @propertycache
1684 1687 def _rustmap(self):
1685 1688 self._rustmap = rustmod.DirstateMap(self._root)
1686 1689 self.read()
1687 1690 return self._rustmap
1688 1691
1689 1692 @property
1690 1693 def copymap(self):
1691 1694 return self._rustmap.copymap()
1692 1695
1693 1696 def preload(self):
1694 1697 self._rustmap
1695 1698
1696 1699 def clear(self):
1697 1700 self._rustmap.clear()
1698 1701 self.setparents(nullid, nullid)
1699 1702 util.clearcachedproperty(self, b"_dirs")
1700 1703 util.clearcachedproperty(self, b"_alldirs")
1701 1704 util.clearcachedproperty(self, b"dirfoldmap")
1702 1705
1703 1706 def items(self):
1704 1707 return self._rustmap.items()
1705 1708
1706 1709 def keys(self):
1707 1710 return iter(self._rustmap)
1708 1711
1709 1712 def __contains__(self, key):
1710 1713 return key in self._rustmap
1711 1714
1712 1715 def __getitem__(self, item):
1713 1716 return self._rustmap[item]
1714 1717
1715 1718 def __len__(self):
1716 1719 return len(self._rustmap)
1717 1720
1718 1721 def __iter__(self):
1719 1722 return iter(self._rustmap)
1720 1723
1721 1724 # forward for python2,3 compat
1722 1725 iteritems = items
1723 1726
1724 1727 def _opendirstatefile(self):
1725 1728 fp, mode = txnutil.trypending(
1726 1729 self._root, self._opener, self._filename
1727 1730 )
1728 1731 if self._pendingmode is not None and self._pendingmode != mode:
1729 1732 fp.close()
1730 1733 raise error.Abort(
1731 1734 _(b'working directory state may be changed parallelly')
1732 1735 )
1733 1736 self._pendingmode = mode
1734 1737 return fp
1735 1738
1736 1739 def setparents(self, p1, p2):
1737 1740 self._rustmap.setparents(p1, p2)
1738 1741 self._parents = (p1, p2)
1739 1742 self._dirtyparents = True
1740 1743
1741 1744 def parents(self):
1742 1745 if not self._parents:
1743 1746 try:
1744 1747 fp = self._opendirstatefile()
1745 1748 st = fp.read(40)
1746 1749 fp.close()
1747 1750 except IOError as err:
1748 1751 if err.errno != errno.ENOENT:
1749 1752 raise
1750 1753 # File doesn't exist, so the current state is empty
1751 1754 st = b''
1752 1755
1753 1756 try:
1754 1757 self._parents = self._rustmap.parents(st)
1755 1758 except ValueError:
1756 1759 raise error.Abort(
1757 1760 _(b'working directory state appears damaged!')
1758 1761 )
1759 1762
1760 1763 return self._parents
1761 1764
1762 1765 def read(self):
1763 1766 # ignore HG_PENDING because identity is used only for writing
1764 1767 self.identity = util.filestat.frompath(
1765 1768 self._opener.join(self._filename)
1766 1769 )
1767 1770
1768 1771 try:
1769 1772 fp = self._opendirstatefile()
1770 1773 try:
1771 1774 st = fp.read()
1772 1775 finally:
1773 1776 fp.close()
1774 1777 except IOError as err:
1775 1778 if err.errno != errno.ENOENT:
1776 1779 raise
1777 1780 return
1778 1781 if not st:
1779 1782 return
1780 1783
1781 1784 parse_dirstate = util.nogc(self._rustmap.read)
1782 1785 parents = parse_dirstate(st)
1783 1786 if parents and not self._dirtyparents:
1784 1787 self.setparents(*parents)
1785 1788
1786 1789 self.__contains__ = self._rustmap.__contains__
1787 1790 self.__getitem__ = self._rustmap.__getitem__
1788 1791 self.get = self._rustmap.get
1789 1792
1790 1793 def write(self, st, now):
1791 1794 parents = self.parents()
1792 1795 st.write(self._rustmap.write(parents[0], parents[1], now))
1793 1796 st.close()
1794 1797 self._dirtyparents = False
1795 1798
1796 1799 @propertycache
1797 1800 def filefoldmap(self):
1798 1801 """Returns a dictionary mapping normalized case paths to their
1799 1802 non-normalized versions.
1800 1803 """
1801 1804 return self._rustmap.filefoldmapasdict()
1802 1805
1803 1806 def hastrackeddir(self, d):
1804 1807 self._dirs # Trigger Python's propertycache
1805 1808 return self._rustmap.hastrackeddir(d)
1806 1809
1807 1810 def hasdir(self, d):
1808 1811 self._dirs # Trigger Python's propertycache
1809 1812 return self._rustmap.hasdir(d)
1810 1813
1811 1814 @propertycache
1812 1815 def _dirs(self):
1813 1816 return self._rustmap.getdirs()
1814 1817
1815 1818 @propertycache
1816 1819 def _alldirs(self):
1817 1820 return self._rustmap.getalldirs()
1818 1821
1819 1822 @propertycache
1820 1823 def identity(self):
1821 1824 self._rustmap
1822 1825 return self.identity
1823 1826
1824 1827 @property
1825 1828 def nonnormalset(self):
1826 1829 nonnorm, otherparents = self._rustmap.nonnormalentries()
1827 1830 return nonnorm
1828 1831
1829 1832 @property
1830 1833 def otherparentset(self):
1831 1834 nonnorm, otherparents = self._rustmap.nonnormalentries()
1832 1835 return otherparents
1833 1836
1834 1837 @propertycache
1835 1838 def dirfoldmap(self):
1836 1839 f = {}
1837 1840 normcase = util.normcase
1838 1841 for name in self._dirs:
1839 1842 f[normcase(name)] = name
1840 1843 return f
General Comments 0
You need to be logged in to leave comments. Login now