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