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