##// END OF EJS Templates
dirstate: refactor granularity code, add a test...
Matt Mackall -
r6327:6d952dc2 default
parent child Browse files
Show More
@@ -0,0 +1,15
1 #!/bin/sh
2
3 mkdir t
4 cd t
5 hg init
6 echo a > a
7 hg add a
8 hg commit -m test
9
10 # do we ever miss a sub-second change?
11 for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
12 hg co -qC 0
13 echo b > a
14 hg st
15 done
@@ -0,0 +1,20
1 M a
2 M a
3 M a
4 M a
5 M a
6 M a
7 M a
8 M a
9 M a
10 M a
11 M a
12 M a
13 M a
14 M a
15 M a
16 M a
17 M a
18 M a
19 M a
20 M a
@@ -1,636 +1,635
1 1 """
2 2 dirstate.py - working directory tracking for mercurial
3 3
4 4 Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5
6 6 This software may be used and distributed according to the terms
7 7 of the GNU General Public License, incorporated herein by reference.
8 8 """
9 9
10 10 from node import nullid
11 11 from i18n import _
12 12 import struct, os, bisect, stat, strutil, util, errno, ignore
13 13 import cStringIO, osutil, sys
14 14
15 15 _unknown = ('?', 0, 0, 0)
16 16 _format = ">cllll"
17 17
18 18 class dirstate(object):
19 19
20 20 def __init__(self, opener, ui, root):
21 21 self._opener = opener
22 22 self._root = root
23 23 self._dirty = False
24 24 self._dirtypl = False
25 25 self._ui = ui
26 26
27 27 def __getattr__(self, name):
28 28 if name == '_map':
29 29 self._read()
30 30 return self._map
31 31 elif name == '_copymap':
32 32 self._read()
33 33 return self._copymap
34 34 elif name == '_branch':
35 35 try:
36 36 self._branch = (self._opener("branch").read().strip()
37 37 or "default")
38 38 except IOError:
39 39 self._branch = "default"
40 40 return self._branch
41 41 elif name == '_pl':
42 42 self._pl = [nullid, nullid]
43 43 try:
44 44 st = self._opener("dirstate").read(40)
45 45 if len(st) == 40:
46 46 self._pl = st[:20], st[20:40]
47 47 except IOError, err:
48 48 if err.errno != errno.ENOENT: raise
49 49 return self._pl
50 50 elif name == '_dirs':
51 51 self._dirs = {}
52 52 for f in self._map:
53 53 if self[f] != 'r':
54 54 self._incpath(f)
55 55 return self._dirs
56 56 elif name == '_ignore':
57 57 files = [self._join('.hgignore')]
58 58 for name, path in self._ui.configitems("ui"):
59 59 if name == 'ignore' or name.startswith('ignore.'):
60 60 files.append(os.path.expanduser(path))
61 61 self._ignore = ignore.ignore(self._root, files, self._ui.warn)
62 62 return self._ignore
63 63 elif name == '_slash':
64 64 self._slash = self._ui.configbool('ui', 'slash') and os.sep != '/'
65 65 return self._slash
66 66 elif name == '_checkexec':
67 67 self._checkexec = util.checkexec(self._root)
68 68 return self._checkexec
69 elif name == '_limit':
70 try:
71 self._limit = int(self._ui.config('ui', 'limit', 1))
72 except ValueError:
73 self._limit = 1
74 return self._limit
75 69 else:
76 70 raise AttributeError, name
77 71
78 72 def _join(self, f):
79 73 return os.path.join(self._root, f)
80 74
81 75 def getcwd(self):
82 76 cwd = os.getcwd()
83 77 if cwd == self._root: return ''
84 78 # self._root ends with a path separator if self._root is '/' or 'C:\'
85 79 rootsep = self._root
86 80 if not util.endswithsep(rootsep):
87 81 rootsep += os.sep
88 82 if cwd.startswith(rootsep):
89 83 return cwd[len(rootsep):]
90 84 else:
91 85 # we're outside the repo. return an absolute path.
92 86 return cwd
93 87
94 88 def pathto(self, f, cwd=None):
95 89 if cwd is None:
96 90 cwd = self.getcwd()
97 91 path = util.pathto(self._root, cwd, f)
98 92 if self._slash:
99 93 return util.normpath(path)
100 94 return path
101 95
102 96 def __getitem__(self, key):
103 97 ''' current states:
104 98 n normal
105 99 m needs merging
106 100 r marked for removal
107 101 a marked for addition
108 102 ? not tracked'''
109 103 return self._map.get(key, ("?",))[0]
110 104
111 105 def __contains__(self, key):
112 106 return key in self._map
113 107
114 108 def __iter__(self):
115 109 a = self._map.keys()
116 110 a.sort()
117 111 for x in a:
118 112 yield x
119 113
120 114 def parents(self):
121 115 return self._pl
122 116
123 117 def branch(self):
124 118 return self._branch
125 119
126 120 def setparents(self, p1, p2=nullid):
127 121 self._dirty = self._dirtypl = True
128 122 self._pl = p1, p2
129 123
130 124 def setbranch(self, branch):
131 125 self._branch = branch
132 126 self._opener("branch", "w").write(branch + '\n')
133 127
134 128 def _read(self):
135 129 self._map = {}
136 130 self._copymap = {}
137 131 if not self._dirtypl:
138 132 self._pl = [nullid, nullid]
139 133 try:
140 134 st = self._opener("dirstate").read()
141 135 except IOError, err:
142 136 if err.errno != errno.ENOENT: raise
143 137 return
144 138 if not st:
145 139 return
146 140
147 141 if not self._dirtypl:
148 142 self._pl = [st[:20], st[20: 40]]
149 143
150 144 # deref fields so they will be local in loop
151 145 dmap = self._map
152 146 copymap = self._copymap
153 147 unpack = struct.unpack
154 148 e_size = struct.calcsize(_format)
155 149 pos1 = 40
156 150 l = len(st)
157 151
158 152 # the inner loop
159 153 while pos1 < l:
160 154 pos2 = pos1 + e_size
161 155 e = unpack(">cllll", st[pos1:pos2]) # a literal here is faster
162 156 pos1 = pos2 + e[4]
163 157 f = st[pos2:pos1]
164 158 if '\0' in f:
165 159 f, c = f.split('\0')
166 160 copymap[f] = c
167 161 dmap[f] = e # we hold onto e[4] because making a subtuple is slow
168 162
169 163 def invalidate(self):
170 164 for a in "_map _copymap _branch _pl _dirs _ignore".split():
171 165 if a in self.__dict__:
172 166 delattr(self, a)
173 167 self._dirty = False
174 168
175 169 def copy(self, source, dest):
176 170 self._dirty = True
177 171 self._copymap[dest] = source
178 172
179 173 def copied(self, file):
180 174 return self._copymap.get(file, None)
181 175
182 176 def copies(self):
183 177 return self._copymap
184 178
185 179 def _incpath(self, path):
186 180 c = path.rfind('/')
187 181 if c >= 0:
188 182 dirs = self._dirs
189 183 base = path[:c]
190 184 if base not in dirs:
191 185 self._incpath(base)
192 186 dirs[base] = 1
193 187 else:
194 188 dirs[base] += 1
195 189
196 190 def _decpath(self, path):
197 191 c = path.rfind('/')
198 192 if c >= 0:
199 193 base = path[:c]
200 194 dirs = self._dirs
201 195 if dirs[base] == 1:
202 196 del dirs[base]
203 197 self._decpath(base)
204 198 else:
205 199 dirs[base] -= 1
206 200
207 201 def _incpathcheck(self, f):
208 202 if '\r' in f or '\n' in f:
209 203 raise util.Abort(_("'\\n' and '\\r' disallowed in filenames: %r")
210 204 % f)
211 205 # shadows
212 206 if f in self._dirs:
213 207 raise util.Abort(_('directory %r already in dirstate') % f)
214 208 for c in strutil.rfindall(f, '/'):
215 209 d = f[:c]
216 210 if d in self._dirs:
217 211 break
218 212 if d in self._map and self[d] != 'r':
219 213 raise util.Abort(_('file %r in dirstate clashes with %r') %
220 214 (d, f))
221 215 self._incpath(f)
222 216
223 217 def _changepath(self, f, newstate, relaxed=False):
224 218 # handle upcoming path changes
225 219 oldstate = self[f]
226 220 if oldstate not in "?r" and newstate in "?r":
227 221 if "_dirs" in self.__dict__:
228 222 self._decpath(f)
229 223 return
230 224 if oldstate in "?r" and newstate not in "?r":
231 225 if relaxed and oldstate == '?':
232 226 # XXX
233 227 # in relaxed mode we assume the caller knows
234 228 # what it is doing, workaround for updating
235 229 # dir-to-file revisions
236 230 if "_dirs" in self.__dict__:
237 231 self._incpath(f)
238 232 return
239 233 self._incpathcheck(f)
240 234 return
241 235
242 236 def normal(self, f):
243 237 'mark a file normal and clean'
244 238 self._dirty = True
245 239 self._changepath(f, 'n', True)
246 240 s = os.lstat(self._join(f))
247 241 self._map[f] = ('n', s.st_mode, s.st_size, s.st_mtime, 0)
248 242 if f in self._copymap:
249 243 del self._copymap[f]
250 244
251 245 def normallookup(self, f):
252 246 'mark a file normal, but possibly dirty'
253 247 if self._pl[1] != nullid and f in self._map:
254 248 # if there is a merge going on and the file was either
255 249 # in state 'm' or dirty before being removed, restore that state.
256 250 entry = self._map[f]
257 251 if entry[0] == 'r' and entry[2] in (-1, -2):
258 252 source = self._copymap.get(f)
259 253 if entry[2] == -1:
260 254 self.merge(f)
261 255 elif entry[2] == -2:
262 256 self.normaldirty(f)
263 257 if source:
264 258 self.copy(source, f)
265 259 return
266 260 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
267 261 return
268 262 self._dirty = True
269 263 self._changepath(f, 'n', True)
270 264 self._map[f] = ('n', 0, -1, -1, 0)
271 265 if f in self._copymap:
272 266 del self._copymap[f]
273 267
274 268 def normaldirty(self, f):
275 269 'mark a file normal, but dirty'
276 270 self._dirty = True
277 271 self._changepath(f, 'n', True)
278 272 self._map[f] = ('n', 0, -2, -1, 0)
279 273 if f in self._copymap:
280 274 del self._copymap[f]
281 275
282 276 def add(self, f):
283 277 'mark a file added'
284 278 self._dirty = True
285 279 self._changepath(f, 'a')
286 280 self._map[f] = ('a', 0, -1, -1, 0)
287 281 if f in self._copymap:
288 282 del self._copymap[f]
289 283
290 284 def remove(self, f):
291 285 'mark a file removed'
292 286 self._dirty = True
293 287 self._changepath(f, 'r')
294 288 size = 0
295 289 if self._pl[1] != nullid and f in self._map:
296 290 entry = self._map[f]
297 291 if entry[0] == 'm':
298 292 size = -1
299 293 elif entry[0] == 'n' and entry[2] == -2:
300 294 size = -2
301 295 self._map[f] = ('r', 0, size, 0, 0)
302 296 if size == 0 and f in self._copymap:
303 297 del self._copymap[f]
304 298
305 299 def merge(self, f):
306 300 'mark a file merged'
307 301 self._dirty = True
308 302 s = os.lstat(self._join(f))
309 303 self._changepath(f, 'm', True)
310 304 self._map[f] = ('m', s.st_mode, s.st_size, s.st_mtime, 0)
311 305 if f in self._copymap:
312 306 del self._copymap[f]
313 307
314 308 def forget(self, f):
315 309 'forget a file'
316 310 self._dirty = True
317 311 try:
318 312 self._changepath(f, '?')
319 313 del self._map[f]
320 314 except KeyError:
321 315 self._ui.warn(_("not in dirstate: %s\n") % f)
322 316
323 317 def clear(self):
324 318 self._map = {}
325 319 if "_dirs" in self.__dict__:
326 320 delattr(self, "_dirs");
327 321 self._copymap = {}
328 322 self._pl = [nullid, nullid]
329 323 self._dirty = True
330 324
331 325 def rebuild(self, parent, files):
332 326 self.clear()
333 327 for f in files:
334 328 if files.execf(f):
335 329 self._map[f] = ('n', 0777, -1, 0, 0)
336 330 else:
337 331 self._map[f] = ('n', 0666, -1, 0, 0)
338 332 self._pl = (parent, nullid)
339 333 self._dirty = True
340 334
341 335 def write(self):
342 336 if not self._dirty:
343 337 return
344 338 st = self._opener("dirstate", "w", atomictemp=True)
345 if self._limit > 0:
346 limit = util.fstat(st).st_mtime - self._limit
347 else:
339
340 try:
341 gran = int(self._ui.config('dirstate', 'granularity', 1))
342 except ValueError:
343 gran = 1
348 344 limit = sys.maxint
345 if gran > 0:
346 limit = util.fstat(st).st_mtime - gran
347
349 348 cs = cStringIO.StringIO()
350 349 copymap = self._copymap
351 350 pack = struct.pack
352 351 write = cs.write
353 352 write("".join(self._pl))
354 353 for f, e in self._map.iteritems():
355 354 if f in copymap:
356 355 f = "%s\0%s" % (f, copymap[f])
357 356 if e[3] > limit and e[0] == 'n':
358 357 e = (e[0], 0, -1, -1, 0)
359 358 e = pack(_format, e[0], e[1], e[2], e[3], len(f))
360 359 write(e)
361 360 write(f)
362 361 st.write(cs.getvalue())
363 362 st.rename()
364 363 self._dirty = self._dirtypl = False
365 364
366 365 def _filter(self, files):
367 366 ret = {}
368 367 unknown = []
369 368
370 369 for x in files:
371 370 if x == '.':
372 371 return self._map.copy()
373 372 if x not in self._map:
374 373 unknown.append(x)
375 374 else:
376 375 ret[x] = self._map[x]
377 376
378 377 if not unknown:
379 378 return ret
380 379
381 380 b = self._map.keys()
382 381 b.sort()
383 382 blen = len(b)
384 383
385 384 for x in unknown:
386 385 bs = bisect.bisect(b, "%s%s" % (x, '/'))
387 386 while bs < blen:
388 387 s = b[bs]
389 388 if len(s) > len(x) and s.startswith(x):
390 389 ret[s] = self._map[s]
391 390 else:
392 391 break
393 392 bs += 1
394 393 return ret
395 394
396 395 def _supported(self, f, mode, verbose=False):
397 396 if stat.S_ISREG(mode) or stat.S_ISLNK(mode):
398 397 return True
399 398 if verbose:
400 399 kind = 'unknown'
401 400 if stat.S_ISCHR(mode): kind = _('character device')
402 401 elif stat.S_ISBLK(mode): kind = _('block device')
403 402 elif stat.S_ISFIFO(mode): kind = _('fifo')
404 403 elif stat.S_ISSOCK(mode): kind = _('socket')
405 404 elif stat.S_ISDIR(mode): kind = _('directory')
406 405 self._ui.warn(_('%s: unsupported file type (type is %s)\n')
407 406 % (self.pathto(f), kind))
408 407 return False
409 408
410 409 def _dirignore(self, f):
411 410 if self._ignore(f):
412 411 return True
413 412 for c in strutil.findall(f, '/'):
414 413 if self._ignore(f[:c]):
415 414 return True
416 415 return False
417 416
418 417 def walk(self, files=None, match=util.always, badmatch=None):
419 418 # filter out the stat
420 419 for src, f, st in self.statwalk(files, match, badmatch=badmatch):
421 420 yield src, f
422 421
423 422 def statwalk(self, files=None, match=util.always, unknown=True,
424 423 ignored=False, badmatch=None, directories=False):
425 424 '''
426 425 walk recursively through the directory tree, finding all files
427 426 matched by the match function
428 427
429 428 results are yielded in a tuple (src, filename, st), where src
430 429 is one of:
431 430 'f' the file was found in the directory tree
432 431 'd' the file is a directory of the tree
433 432 'm' the file was only in the dirstate and not in the tree
434 433 'b' file was not found and matched badmatch
435 434
436 435 and st is the stat result if the file was found in the directory.
437 436 '''
438 437
439 438 # walk all files by default
440 439 if not files:
441 440 files = ['.']
442 441 dc = self._map.copy()
443 442 else:
444 443 files = util.unique(files)
445 444 dc = self._filter(files)
446 445
447 446 def imatch(file_):
448 447 if file_ not in dc and self._ignore(file_):
449 448 return False
450 449 return match(file_)
451 450
452 451 # TODO: don't walk unknown directories if unknown and ignored are False
453 452 ignore = self._ignore
454 453 dirignore = self._dirignore
455 454 if ignored:
456 455 imatch = match
457 456 ignore = util.never
458 457 dirignore = util.never
459 458
460 459 # self._root may end with a path separator when self._root == '/'
461 460 common_prefix_len = len(self._root)
462 461 if not util.endswithsep(self._root):
463 462 common_prefix_len += 1
464 463
465 464 normpath = util.normpath
466 465 listdir = osutil.listdir
467 466 lstat = os.lstat
468 467 bisect_left = bisect.bisect_left
469 468 isdir = os.path.isdir
470 469 pconvert = util.pconvert
471 470 join = os.path.join
472 471 s_isdir = stat.S_ISDIR
473 472 supported = self._supported
474 473 _join = self._join
475 474 known = {'.hg': 1}
476 475
477 476 # recursion free walker, faster than os.walk.
478 477 def findfiles(s):
479 478 work = [s]
480 479 wadd = work.append
481 480 found = []
482 481 add = found.append
483 482 if directories:
484 483 add((normpath(s[common_prefix_len:]), 'd', lstat(s)))
485 484 while work:
486 485 top = work.pop()
487 486 entries = listdir(top, stat=True)
488 487 # nd is the top of the repository dir tree
489 488 nd = normpath(top[common_prefix_len:])
490 489 if nd == '.':
491 490 nd = ''
492 491 else:
493 492 # do not recurse into a repo contained in this
494 493 # one. use bisect to find .hg directory so speed
495 494 # is good on big directory.
496 495 names = [e[0] for e in entries]
497 496 hg = bisect_left(names, '.hg')
498 497 if hg < len(names) and names[hg] == '.hg':
499 498 if isdir(join(top, '.hg')):
500 499 continue
501 500 for f, kind, st in entries:
502 501 np = pconvert(join(nd, f))
503 502 if np in known:
504 503 continue
505 504 known[np] = 1
506 505 p = join(top, f)
507 506 # don't trip over symlinks
508 507 if kind == stat.S_IFDIR:
509 508 if not ignore(np):
510 509 wadd(p)
511 510 if directories:
512 511 add((np, 'd', st))
513 512 if np in dc and match(np):
514 513 add((np, 'm', st))
515 514 elif imatch(np):
516 515 if supported(np, st.st_mode):
517 516 add((np, 'f', st))
518 517 elif np in dc:
519 518 add((np, 'm', st))
520 519 found.sort()
521 520 return found
522 521
523 522 # step one, find all files that match our criteria
524 523 files.sort()
525 524 for ff in files:
526 525 nf = normpath(ff)
527 526 f = _join(ff)
528 527 try:
529 528 st = lstat(f)
530 529 except OSError, inst:
531 530 found = False
532 531 for fn in dc:
533 532 if nf == fn or (fn.startswith(nf) and fn[len(nf)] == '/'):
534 533 found = True
535 534 break
536 535 if not found:
537 536 if inst.errno != errno.ENOENT or not badmatch:
538 537 self._ui.warn('%s: %s\n' %
539 538 (self.pathto(ff), inst.strerror))
540 539 elif badmatch and badmatch(ff) and imatch(nf):
541 540 yield 'b', ff, None
542 541 continue
543 542 if s_isdir(st.st_mode):
544 543 if not dirignore(nf):
545 544 for f, src, st in findfiles(f):
546 545 yield src, f, st
547 546 else:
548 547 if nf in known:
549 548 continue
550 549 known[nf] = 1
551 550 if match(nf):
552 551 if supported(ff, st.st_mode, verbose=True):
553 552 yield 'f', nf, st
554 553 elif ff in dc:
555 554 yield 'm', nf, st
556 555
557 556 # step two run through anything left in the dc hash and yield
558 557 # if we haven't already seen it
559 558 ks = dc.keys()
560 559 ks.sort()
561 560 for k in ks:
562 561 if k in known:
563 562 continue
564 563 known[k] = 1
565 564 if imatch(k):
566 565 yield 'm', k, None
567 566
568 567 def status(self, files, match, list_ignored, list_clean, list_unknown=True):
569 568 lookup, modified, added, unknown, ignored = [], [], [], [], []
570 569 removed, deleted, clean = [], [], []
571 570
572 571 files = files or []
573 572 _join = self._join
574 573 lstat = os.lstat
575 574 cmap = self._copymap
576 575 dmap = self._map
577 576 ladd = lookup.append
578 577 madd = modified.append
579 578 aadd = added.append
580 579 uadd = unknown.append
581 580 iadd = ignored.append
582 581 radd = removed.append
583 582 dadd = deleted.append
584 583 cadd = clean.append
585 584
586 585 for src, fn, st in self.statwalk(files, match, unknown=list_unknown,
587 586 ignored=list_ignored):
588 587 if fn in dmap:
589 588 type_, mode, size, time, foo = dmap[fn]
590 589 else:
591 590 if (list_ignored or fn in files) and self._dirignore(fn):
592 591 if list_ignored:
593 592 iadd(fn)
594 593 elif list_unknown:
595 594 uadd(fn)
596 595 continue
597 596 if src == 'm':
598 597 nonexistent = True
599 598 if not st:
600 599 try:
601 600 st = lstat(_join(fn))
602 601 except OSError, inst:
603 602 if inst.errno not in (errno.ENOENT, errno.ENOTDIR):
604 603 raise
605 604 st = None
606 605 # We need to re-check that it is a valid file
607 606 if st and self._supported(fn, st.st_mode):
608 607 nonexistent = False
609 608 # XXX: what to do with file no longer present in the fs
610 609 # who are not removed in the dirstate ?
611 610 if nonexistent and type_ in "nma":
612 611 dadd(fn)
613 612 continue
614 613 # check the common case first
615 614 if type_ == 'n':
616 615 if not st:
617 616 st = lstat(_join(fn))
618 617 if (size >= 0 and
619 618 (size != st.st_size
620 619 or ((mode ^ st.st_mode) & 0100 and self._checkexec))
621 620 or size == -2
622 621 or fn in self._copymap):
623 622 madd(fn)
624 623 elif time != int(st.st_mtime):
625 624 ladd(fn)
626 625 elif list_clean:
627 626 cadd(fn)
628 627 elif type_ == 'm':
629 628 madd(fn)
630 629 elif type_ == 'a':
631 630 aadd(fn)
632 631 elif type_ == 'r':
633 632 radd(fn)
634 633
635 634 return (lookup, modified, added, removed, deleted, unknown, ignored,
636 635 clean)
General Comments 0
You need to be logged in to leave comments. Login now