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