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