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