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