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