##// END OF EJS Templates
dirstate: use getattr rather than lazyread
Matt Mackall -
r4603:0f6853c1 default
parent child Browse files
Show More
@@ -1,602 +1,593 b''
1 1 """
2 2 dirstate.py - working directory tracking for mercurial
3 3
4 4 Copyright 2005, 2006 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 *
11 11 from i18n import _
12 12 import struct, os, time, bisect, stat, strutil, util, re, errno
13 13 import cStringIO
14 14
15 15 class dirstate(object):
16 16 format = ">cllll"
17 17
18 18 def __init__(self, opener, ui, root):
19 19 self.opener = opener
20 20 self.root = root
21 21 self.dirty = 0
22 22 self.ui = ui
23 self.map = None
24 23 self.fp = None
25 24 self.pl = None
26 25 self.dirs = None
27 26 self.copymap = {}
28 27 self.ignorefunc = None
29 28 self._branch = None
30 29 self._slash = None
31 30
31 def __getattr__(self, name):
32 if name == 'map':
33 self.read()
34 return self.map
35 else:
36 raise AttributeError, name
37
32 38 def wjoin(self, f):
33 39 return os.path.join(self.root, f)
34 40
35 41 def getcwd(self):
36 42 cwd = os.getcwd()
37 43 if cwd == self.root: return ''
38 44 # self.root ends with a path separator if self.root is '/' or 'C:\'
39 45 rootsep = self.root
40 46 if not rootsep.endswith(os.sep):
41 47 rootsep += os.sep
42 48 if cwd.startswith(rootsep):
43 49 return cwd[len(rootsep):]
44 50 else:
45 51 # we're outside the repo. return an absolute path.
46 52 return cwd
47 53
48 54 def pathto(self, f, cwd=None):
49 55 if cwd is None:
50 56 cwd = self.getcwd()
51 57 path = util.pathto(self.root, cwd, f)
52 58 if self._slash is None:
53 59 self._slash = self.ui.configbool('ui', 'slash') and os.sep != '/'
54 60 if self._slash:
55 61 path = path.replace(os.sep, '/')
56 62 return path
57 63
58 64 def hgignore(self):
59 65 '''return the contents of .hgignore files as a list of patterns.
60 66
61 67 the files parsed for patterns include:
62 68 .hgignore in the repository root
63 69 any additional files specified in the [ui] section of ~/.hgrc
64 70
65 71 trailing white space is dropped.
66 72 the escape character is backslash.
67 73 comments start with #.
68 74 empty lines are skipped.
69 75
70 76 lines can be of the following formats:
71 77
72 78 syntax: regexp # defaults following lines to non-rooted regexps
73 79 syntax: glob # defaults following lines to non-rooted globs
74 80 re:pattern # non-rooted regular expression
75 81 glob:pattern # non-rooted glob
76 82 pattern # pattern of the current default type'''
77 83 syntaxes = {'re': 'relre:', 'regexp': 'relre:', 'glob': 'relglob:'}
78 84 def parselines(fp):
79 85 for line in fp:
80 86 if not line.endswith('\n'):
81 87 line += '\n'
82 88 escape = False
83 89 for i in xrange(len(line)):
84 90 if escape: escape = False
85 91 elif line[i] == '\\': escape = True
86 92 elif line[i] == '#': break
87 93 line = line[:i].rstrip()
88 94 if line: yield line
89 95 repoignore = self.wjoin('.hgignore')
90 96 files = [repoignore]
91 97 files.extend(self.ui.hgignorefiles())
92 98 pats = {}
93 99 for f in files:
94 100 try:
95 101 pats[f] = []
96 102 fp = open(f)
97 103 syntax = 'relre:'
98 104 for line in parselines(fp):
99 105 if line.startswith('syntax:'):
100 106 s = line[7:].strip()
101 107 try:
102 108 syntax = syntaxes[s]
103 109 except KeyError:
104 110 self.ui.warn(_("%s: ignoring invalid "
105 111 "syntax '%s'\n") % (f, s))
106 112 continue
107 113 pat = syntax + line
108 114 for s in syntaxes.values():
109 115 if line.startswith(s):
110 116 pat = line
111 117 break
112 118 pats[f].append(pat)
113 119 except IOError, inst:
114 120 if f != repoignore:
115 121 self.ui.warn(_("skipping unreadable ignore file"
116 122 " '%s': %s\n") % (f, inst.strerror))
117 123 return pats
118 124
119 125 def ignore(self, fn):
120 126 '''default match function used by dirstate and
121 127 localrepository. this honours the repository .hgignore file
122 128 and any other files specified in the [ui] section of .hgrc.'''
123 129 if not self.ignorefunc:
124 130 ignore = self.hgignore()
125 131 allpats = []
126 132 [allpats.extend(patlist) for patlist in ignore.values()]
127 133 if allpats:
128 134 try:
129 135 files, self.ignorefunc, anypats = (
130 136 util.matcher(self.root, inc=allpats, src='.hgignore'))
131 137 except util.Abort:
132 138 # Re-raise an exception where the src is the right file
133 139 for f, patlist in ignore.items():
134 140 files, self.ignorefunc, anypats = (
135 141 util.matcher(self.root, inc=patlist, src=f))
136 142 else:
137 143 self.ignorefunc = util.never
138 144 return self.ignorefunc(fn)
139 145
140 146 def __del__(self):
141 147 if self.dirty:
142 148 self.write()
143 149
144 150 def __getitem__(self, key):
145 try:
146 return self.map[key]
147 except TypeError:
148 self.lazyread()
149 return self[key]
151 return self.map[key]
150 152
151 153 _unknown = ('?', 0, 0, 0)
152 154
153 155 def get(self, key):
154 156 try:
155 157 return self[key]
156 158 except KeyError:
157 159 return self._unknown
158 160
159 161 def __contains__(self, key):
160 self.lazyread()
161 162 return key in self.map
162 163
163 164 def parents(self):
164 165 if self.pl is None:
165 166 self.pl = [nullid, nullid]
166 167 try:
167 168 self.fp = self.opener('dirstate')
168 169 st = self.fp.read(40)
169 170 if len(st) == 40:
170 171 self.pl = st[:20], st[20:40]
171 172 except IOError, err:
172 173 if err.errno != errno.ENOENT: raise
173 174 return self.pl
174 175
175 176 def branch(self):
176 177 if not self._branch:
177 178 try:
178 179 self._branch = self.opener("branch").read().strip()\
179 180 or "default"
180 181 except IOError:
181 182 self._branch = "default"
182 183 return self._branch
183 184
184 185 def markdirty(self):
185 186 if not self.dirty:
186 187 self.dirty = 1
187 188
188 189 def setparents(self, p1, p2=nullid):
189 self.lazyread()
190 190 self.markdirty()
191 191 self.pl = p1, p2
192 192
193 193 def setbranch(self, branch):
194 194 self._branch = branch
195 195 self.opener("branch", "w").write(branch + '\n')
196 196
197 197 def state(self, key):
198 198 try:
199 199 return self[key][0]
200 200 except KeyError:
201 201 return "?"
202 202
203 def lazyread(self):
204 if self.map is None:
205 self.read()
206
207 203 def parse(self, st):
208 204 self.pl = [st[:20], st[20: 40]]
209 205
210 206 # deref fields so they will be local in loop
211 207 map = self.map
212 208 copymap = self.copymap
213 209 format = self.format
214 210 unpack = struct.unpack
215 211
216 212 pos = 40
217 213 e_size = struct.calcsize(format)
218 214
219 215 while pos < len(st):
220 216 newpos = pos + e_size
221 217 e = unpack(format, st[pos:newpos])
222 218 l = e[4]
223 219 pos = newpos
224 220 newpos = pos + l
225 221 f = st[pos:newpos]
226 222 if '\0' in f:
227 223 f, c = f.split('\0')
228 224 copymap[f] = c
229 225 map[f] = e[:4]
230 226 pos = newpos
231 227
232 228 def read(self):
233 229 self.map = {}
234 230 self.pl = [nullid, nullid]
235 231 try:
236 232 if self.fp:
237 233 self.fp.seek(0)
238 234 st = self.fp.read()
239 235 self.fp = None
240 236 else:
241 237 st = self.opener("dirstate").read()
242 238 if st:
243 239 self.parse(st)
244 240 except IOError, err:
245 241 if err.errno != errno.ENOENT: raise
246 242
247 243 def reload(self):
248 244 def mtime():
249 245 m = self.map and self.map.get('.hgignore')
250 246 return m and m[-1]
251 247
252 248 old_mtime = self.ignorefunc and mtime()
253 249 self.read()
254 250 if old_mtime != mtime():
255 251 self.ignorefunc = None
256 252
257 253 def copy(self, source, dest):
258 self.lazyread()
259 254 self.markdirty()
260 255 self.copymap[dest] = source
261 256
262 257 def copied(self, file):
263 258 return self.copymap.get(file, None)
264 259
265 260 def copies(self):
266 261 return self.copymap
267 262
268 263 def initdirs(self):
269 264 if self.dirs is None:
270 265 self.dirs = {}
271 266 for f in self.map:
272 267 self.updatedirs(f, 1)
273 268
274 269 def updatedirs(self, path, delta):
275 270 if self.dirs is not None:
276 271 for c in strutil.findall(path, '/'):
277 272 pc = path[:c]
278 273 self.dirs.setdefault(pc, 0)
279 274 self.dirs[pc] += delta
280 275
281 276 def checkinterfering(self, files):
282 277 def prefixes(f):
283 278 for c in strutil.rfindall(f, '/'):
284 279 yield f[:c]
285 self.lazyread()
286 280 self.initdirs()
287 281 seendirs = {}
288 282 for f in files:
289 283 # shadows
290 284 if self.dirs.get(f):
291 285 raise util.Abort(_('directory named %r already in dirstate') %
292 286 f)
293 287 for d in prefixes(f):
294 288 if d in seendirs:
295 289 break
296 290 if d in self.map:
297 291 raise util.Abort(_('file named %r already in dirstate') %
298 292 d)
299 293 seendirs[d] = True
300 294 # disallowed
301 295 if '\r' in f or '\n' in f:
302 296 raise util.Abort(_("'\\n' and '\\r' disallowed in filenames"))
303 297
304 298 def update(self, files, state, **kw):
305 299 ''' current states:
306 300 n normal
307 301 m needs merging
308 302 r marked for removal
309 303 a marked for addition'''
310 304
311 305 if not files: return
312 self.lazyread()
313 306 self.markdirty()
314 307 if state == "a":
315 308 self.initdirs()
316 309 self.checkinterfering(files)
317 310 for f in files:
318 311 if state == "r":
319 312 self.map[f] = ('r', 0, 0, 0)
320 313 self.updatedirs(f, -1)
321 314 else:
322 315 if state == "a":
323 316 self.updatedirs(f, 1)
324 317 s = os.lstat(self.wjoin(f))
325 318 st_size = kw.get('st_size', s.st_size)
326 319 st_mtime = kw.get('st_mtime', s.st_mtime)
327 320 self.map[f] = (state, s.st_mode, st_size, st_mtime)
328 321 if self.copymap.has_key(f):
329 322 del self.copymap[f]
330 323
331 324 def forget(self, files):
332 325 if not files: return
333 self.lazyread()
334 326 self.markdirty()
335 327 self.initdirs()
336 328 for f in files:
337 329 try:
338 330 del self.map[f]
339 331 self.updatedirs(f, -1)
340 332 except KeyError:
341 333 self.ui.warn(_("not in dirstate: %s!\n") % f)
342 334 pass
343 335
344 336 def clear(self):
345 337 self.map = {}
346 338 self.copymap = {}
347 339 self.dirs = None
348 340 self.markdirty()
349 341
350 342 def rebuild(self, parent, files):
351 343 self.clear()
352 344 for f in files:
353 345 if files.execf(f):
354 346 self.map[f] = ('n', 0777, -1, 0)
355 347 else:
356 348 self.map[f] = ('n', 0666, -1, 0)
357 349 self.pl = (parent, nullid)
358 350 self.markdirty()
359 351
360 352 def write(self):
361 353 if not self.dirty:
362 354 return
363 355 cs = cStringIO.StringIO()
364 356 cs.write("".join(self.pl))
365 357 for f, e in self.map.iteritems():
366 358 c = self.copied(f)
367 359 if c:
368 360 f = f + "\0" + c
369 361 e = struct.pack(self.format, e[0], e[1], e[2], e[3], len(f))
370 362 cs.write(e)
371 363 cs.write(f)
372 364 st = self.opener("dirstate", "w", atomictemp=True)
373 365 st.write(cs.getvalue())
374 366 st.rename()
375 367 self.dirty = 0
376 368
377 369 def filterfiles(self, files):
378 370 ret = {}
379 371 unknown = []
380 372
381 373 for x in files:
382 374 if x == '.':
383 375 return self.map.copy()
384 376 if x not in self.map:
385 377 unknown.append(x)
386 378 else:
387 379 ret[x] = self.map[x]
388 380
389 381 if not unknown:
390 382 return ret
391 383
392 384 b = self.map.keys()
393 385 b.sort()
394 386 blen = len(b)
395 387
396 388 for x in unknown:
397 389 bs = bisect.bisect(b, "%s%s" % (x, '/'))
398 390 while bs < blen:
399 391 s = b[bs]
400 392 if len(s) > len(x) and s.startswith(x):
401 393 ret[s] = self.map[s]
402 394 else:
403 395 break
404 396 bs += 1
405 397 return ret
406 398
407 399 def supported_type(self, f, st, verbose=False):
408 400 if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
409 401 return True
410 402 if verbose:
411 403 kind = 'unknown'
412 404 if stat.S_ISCHR(st.st_mode): kind = _('character device')
413 405 elif stat.S_ISBLK(st.st_mode): kind = _('block device')
414 406 elif stat.S_ISFIFO(st.st_mode): kind = _('fifo')
415 407 elif stat.S_ISSOCK(st.st_mode): kind = _('socket')
416 408 elif stat.S_ISDIR(st.st_mode): kind = _('directory')
417 409 self.ui.warn(_('%s: unsupported file type (type is %s)\n')
418 410 % (self.pathto(f), kind))
419 411 return False
420 412
421 413 def walk(self, files=None, match=util.always, badmatch=None):
422 414 # filter out the stat
423 415 for src, f, st in self.statwalk(files, match, badmatch=badmatch):
424 416 yield src, f
425 417
426 418 def statwalk(self, files=None, match=util.always, ignored=False,
427 419 badmatch=None, directories=False):
428 420 '''
429 421 walk recursively through the directory tree, finding all files
430 422 matched by the match function
431 423
432 424 results are yielded in a tuple (src, filename, st), where src
433 425 is one of:
434 426 'f' the file was found in the directory tree
435 427 'd' the file is a directory of the tree
436 428 'm' the file was only in the dirstate and not in the tree
437 429 'b' file was not found and matched badmatch
438 430
439 431 and st is the stat result if the file was found in the directory.
440 432 '''
441 self.lazyread()
442 433
443 434 # walk all files by default
444 435 if not files:
445 436 files = ['.']
446 437 dc = self.map.copy()
447 438 else:
448 439 files = util.unique(files)
449 440 dc = self.filterfiles(files)
450 441
451 442 def imatch(file_):
452 443 if file_ not in dc and self.ignore(file_):
453 444 return False
454 445 return match(file_)
455 446
456 447 ignore = self.ignore
457 448 if ignored:
458 449 imatch = match
459 450 ignore = util.never
460 451
461 452 # self.root may end with a path separator when self.root == '/'
462 453 common_prefix_len = len(self.root)
463 454 if not self.root.endswith(os.sep):
464 455 common_prefix_len += 1
465 456 # recursion free walker, faster than os.walk.
466 457 def findfiles(s):
467 458 work = [s]
468 459 if directories:
469 460 yield 'd', util.normpath(s[common_prefix_len:]), os.lstat(s)
470 461 while work:
471 462 top = work.pop()
472 463 names = os.listdir(top)
473 464 names.sort()
474 465 # nd is the top of the repository dir tree
475 466 nd = util.normpath(top[common_prefix_len:])
476 467 if nd == '.':
477 468 nd = ''
478 469 else:
479 470 # do not recurse into a repo contained in this
480 471 # one. use bisect to find .hg directory so speed
481 472 # is good on big directory.
482 473 hg = bisect.bisect_left(names, '.hg')
483 474 if hg < len(names) and names[hg] == '.hg':
484 475 if os.path.isdir(os.path.join(top, '.hg')):
485 476 continue
486 477 for f in names:
487 478 np = util.pconvert(os.path.join(nd, f))
488 479 if seen(np):
489 480 continue
490 481 p = os.path.join(top, f)
491 482 # don't trip over symlinks
492 483 st = os.lstat(p)
493 484 if stat.S_ISDIR(st.st_mode):
494 485 if not ignore(np):
495 486 work.append(p)
496 487 if directories:
497 488 yield 'd', np, st
498 489 if imatch(np) and np in dc:
499 490 yield 'm', np, st
500 491 elif imatch(np):
501 492 if self.supported_type(np, st):
502 493 yield 'f', np, st
503 494 elif np in dc:
504 495 yield 'm', np, st
505 496
506 497 known = {'.hg': 1}
507 498 def seen(fn):
508 499 if fn in known: return True
509 500 known[fn] = 1
510 501
511 502 # step one, find all files that match our criteria
512 503 files.sort()
513 504 for ff in files:
514 505 nf = util.normpath(ff)
515 506 f = self.wjoin(ff)
516 507 try:
517 508 st = os.lstat(f)
518 509 except OSError, inst:
519 510 found = False
520 511 for fn in dc:
521 512 if nf == fn or (fn.startswith(nf) and fn[len(nf)] == '/'):
522 513 found = True
523 514 break
524 515 if not found:
525 516 if inst.errno != errno.ENOENT or not badmatch:
526 517 self.ui.warn('%s: %s\n' % (self.pathto(ff),
527 518 inst.strerror))
528 519 elif badmatch and badmatch(ff) and imatch(nf):
529 520 yield 'b', ff, None
530 521 continue
531 522 if stat.S_ISDIR(st.st_mode):
532 523 cmp1 = (lambda x, y: cmp(x[1], y[1]))
533 524 sorted_ = [ x for x in findfiles(f) ]
534 525 sorted_.sort(cmp1)
535 526 for e in sorted_:
536 527 yield e
537 528 else:
538 529 if not seen(nf) and match(nf):
539 530 if self.supported_type(ff, st, verbose=True):
540 531 yield 'f', nf, st
541 532 elif ff in dc:
542 533 yield 'm', nf, st
543 534
544 535 # step two run through anything left in the dc hash and yield
545 536 # if we haven't already seen it
546 537 ks = dc.keys()
547 538 ks.sort()
548 539 for k in ks:
549 540 if not seen(k) and imatch(k):
550 541 yield 'm', k, None
551 542
552 543 def status(self, files=None, match=util.always, list_ignored=False,
553 544 list_clean=False):
554 545 lookup, modified, added, unknown, ignored = [], [], [], [], []
555 546 removed, deleted, clean = [], [], []
556 547
557 548 for src, fn, st in self.statwalk(files, match, ignored=list_ignored):
558 549 try:
559 550 type_, mode, size, time = self[fn]
560 551 except KeyError:
561 552 if list_ignored and self.ignore(fn):
562 553 ignored.append(fn)
563 554 else:
564 555 unknown.append(fn)
565 556 continue
566 557 if src == 'm':
567 558 nonexistent = True
568 559 if not st:
569 560 try:
570 561 st = os.lstat(self.wjoin(fn))
571 562 except OSError, inst:
572 563 if inst.errno != errno.ENOENT:
573 564 raise
574 565 st = None
575 566 # We need to re-check that it is a valid file
576 567 if st and self.supported_type(fn, st):
577 568 nonexistent = False
578 569 # XXX: what to do with file no longer present in the fs
579 570 # who are not removed in the dirstate ?
580 571 if nonexistent and type_ in "nm":
581 572 deleted.append(fn)
582 573 continue
583 574 # check the common case first
584 575 if type_ == 'n':
585 576 if not st:
586 577 st = os.lstat(self.wjoin(fn))
587 578 if size >= 0 and (size != st.st_size
588 579 or (mode ^ st.st_mode) & 0100):
589 580 modified.append(fn)
590 581 elif time != int(st.st_mtime):
591 582 lookup.append(fn)
592 583 elif list_clean:
593 584 clean.append(fn)
594 585 elif type_ == 'm':
595 586 modified.append(fn)
596 587 elif type_ == 'a':
597 588 added.append(fn)
598 589 elif type_ == 'r':
599 590 removed.append(fn)
600 591
601 592 return (lookup, modified, added, removed, deleted, unknown, ignored,
602 593 clean)
General Comments 0
You need to be logged in to leave comments. Login now