##// END OF EJS Templates
dirstate: localize a bunch of methods for findfiles
Matt Mackall -
r5000:46facb73 default
parent child Browse files
Show More
@@ -1,503 +1,514
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 178 for c in strutil.findall(path, '/'):
179 179 pc = path[:c]
180 180 self._dirs.setdefault(pc, 0)
181 181 self._dirs[pc] += 1
182 182
183 183 def _decpath(self, path):
184 184 for c in strutil.findall(path, '/'):
185 185 pc = path[:c]
186 186 self._dirs.setdefault(pc, 0)
187 187 self._dirs[pc] -= 1
188 188
189 189 def _incpathcheck(self, f):
190 190 if '\r' in f or '\n' in f:
191 191 raise util.Abort(_("'\\n' and '\\r' disallowed in filenames"))
192 192 # shadows
193 193 if f in self._dirs:
194 194 raise util.Abort(_('directory named %r already in dirstate') % f)
195 195 for c in strutil.rfindall(f, '/'):
196 196 d = f[:c]
197 197 if d in self._dirs:
198 198 break
199 199 if d in self._map:
200 200 raise util.Abort(_('file named %r already in dirstate') % d)
201 201 self._incpath(f)
202 202
203 203 def normal(self, f):
204 204 'mark a file normal'
205 205 self._dirty = True
206 206 s = os.lstat(self._join(f))
207 207 self._map[f] = ('n', s.st_mode, s.st_size, s.st_mtime)
208 208 if self._copymap.has_key(f):
209 209 del self._copymap[f]
210 210
211 211 def normaldirty(self, f):
212 212 'mark a file normal, but possibly dirty'
213 213 self._dirty = True
214 214 s = os.lstat(self._join(f))
215 215 self._map[f] = ('n', s.st_mode, -1, -1)
216 216 if f in self._copymap:
217 217 del self._copymap[f]
218 218
219 219 def add(self, f):
220 220 'mark a file added'
221 221 self._dirty = True
222 222 self._incpathcheck(f)
223 223 self._map[f] = ('a', 0, -1, -1)
224 224 if f in self._copymap:
225 225 del self._copymap[f]
226 226
227 227 def remove(self, f):
228 228 'mark a file removed'
229 229 self._dirty = True
230 230 self._map[f] = ('r', 0, 0, 0)
231 231 self._decpath(f)
232 232 if f in self._copymap:
233 233 del self._copymap[f]
234 234
235 235 def merge(self, f):
236 236 'mark a file merged'
237 237 self._dirty = True
238 238 s = os.lstat(self._join(f))
239 239 self._map[f] = ('m', s.st_mode, s.st_size, s.st_mtime)
240 240 if f in self._copymap:
241 241 del self._copymap[f]
242 242
243 243 def forget(self, f):
244 244 'forget a file'
245 245 self._dirty = True
246 246 try:
247 247 del self._map[f]
248 248 self._decpath(f)
249 249 except KeyError:
250 250 self._ui.warn(_("not in dirstate: %s!\n") % f)
251 251
252 252 def rebuild(self, parent, files):
253 253 self.invalidate()
254 254 for f in files:
255 255 if files.execf(f):
256 256 self._map[f] = ('n', 0777, -1, 0)
257 257 else:
258 258 self._map[f] = ('n', 0666, -1, 0)
259 259 self._pl = (parent, nullid)
260 260 self._dirty = True
261 261
262 262 def write(self):
263 263 if not self._dirty:
264 264 return
265 265 cs = cStringIO.StringIO()
266 266 cs.write("".join(self._pl))
267 267 for f, e in self._map.iteritems():
268 268 c = self.copied(f)
269 269 if c:
270 270 f = f + "\0" + c
271 271 e = struct.pack(_format, e[0], e[1], e[2], e[3], len(f))
272 272 cs.write(e)
273 273 cs.write(f)
274 274 st = self._opener("dirstate", "w", atomictemp=True)
275 275 st.write(cs.getvalue())
276 276 st.rename()
277 277 self._dirty = self._dirtypl = False
278 278
279 279 def _filter(self, files):
280 280 ret = {}
281 281 unknown = []
282 282
283 283 for x in files:
284 284 if x == '.':
285 285 return self._map.copy()
286 286 if x not in self._map:
287 287 unknown.append(x)
288 288 else:
289 289 ret[x] = self._map[x]
290 290
291 291 if not unknown:
292 292 return ret
293 293
294 294 b = self._map.keys()
295 295 b.sort()
296 296 blen = len(b)
297 297
298 298 for x in unknown:
299 299 bs = bisect.bisect(b, "%s%s" % (x, '/'))
300 300 while bs < blen:
301 301 s = b[bs]
302 302 if len(s) > len(x) and s.startswith(x):
303 303 ret[s] = self._map[s]
304 304 else:
305 305 break
306 306 bs += 1
307 307 return ret
308 308
309 309 def _supported(self, f, st, verbose=False):
310 310 if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
311 311 return True
312 312 if verbose:
313 313 kind = 'unknown'
314 314 if stat.S_ISCHR(st.st_mode): kind = _('character device')
315 315 elif stat.S_ISBLK(st.st_mode): kind = _('block device')
316 316 elif stat.S_ISFIFO(st.st_mode): kind = _('fifo')
317 317 elif stat.S_ISSOCK(st.st_mode): kind = _('socket')
318 318 elif stat.S_ISDIR(st.st_mode): kind = _('directory')
319 319 self._ui.warn(_('%s: unsupported file type (type is %s)\n')
320 320 % (self.pathto(f), kind))
321 321 return False
322 322
323 323 def walk(self, files=None, match=util.always, badmatch=None):
324 324 # filter out the stat
325 325 for src, f, st in self.statwalk(files, match, badmatch=badmatch):
326 326 yield src, f
327 327
328 328 def statwalk(self, files=None, match=util.always, ignored=False,
329 329 badmatch=None, directories=False):
330 330 '''
331 331 walk recursively through the directory tree, finding all files
332 332 matched by the match function
333 333
334 334 results are yielded in a tuple (src, filename, st), where src
335 335 is one of:
336 336 'f' the file was found in the directory tree
337 337 'd' the file is a directory of the tree
338 338 'm' the file was only in the dirstate and not in the tree
339 339 'b' file was not found and matched badmatch
340 340
341 341 and st is the stat result if the file was found in the directory.
342 342 '''
343 343
344 344 # walk all files by default
345 345 if not files:
346 346 files = ['.']
347 347 dc = self._map.copy()
348 348 else:
349 349 files = util.unique(files)
350 350 dc = self._filter(files)
351 351
352 352 def imatch(file_):
353 353 if file_ not in dc and self._ignore(file_):
354 354 return False
355 355 return match(file_)
356 356
357 357 ignore = self._ignore
358 358 if ignored:
359 359 imatch = match
360 360 ignore = util.never
361 361
362 362 # self._root may end with a path separator when self._root == '/'
363 363 common_prefix_len = len(self._root)
364 364 if not self._root.endswith(os.sep):
365 365 common_prefix_len += 1
366
366 367 # recursion free walker, faster than os.walk.
368 normpath = util.normpath
369 listdir = os.listdir
370 lstat = os.lstat
371 bisect_left = bisect.bisect_left
372 isdir = os.path.isdir
373 pconvert = util.pconvert
374 join = os.path.join
375 s_isdir = stat.S_ISDIR
376 supported = self._supported
377
367 378 def findfiles(s):
368 379 work = [s]
369 380 if directories:
370 yield 'd', util.normpath(s[common_prefix_len:]), os.lstat(s)
381 yield 'd', normpath(s[common_prefix_len:]), os.lstat(s)
371 382 while work:
372 383 top = work.pop()
373 names = os.listdir(top)
384 names = listdir(top)
374 385 names.sort()
375 386 # nd is the top of the repository dir tree
376 nd = util.normpath(top[common_prefix_len:])
387 nd = normpath(top[common_prefix_len:])
377 388 if nd == '.':
378 389 nd = ''
379 390 else:
380 391 # do not recurse into a repo contained in this
381 392 # one. use bisect to find .hg directory so speed
382 393 # is good on big directory.
383 hg = bisect.bisect_left(names, '.hg')
394 hg = bisect_left(names, '.hg')
384 395 if hg < len(names) and names[hg] == '.hg':
385 if os.path.isdir(os.path.join(top, '.hg')):
396 if isdir(join(top, '.hg')):
386 397 continue
387 398 for f in names:
388 np = util.pconvert(os.path.join(nd, f))
399 np = pconvert(os.path.join(nd, f))
389 400 if seen(np):
390 401 continue
391 p = os.path.join(top, f)
402 p = join(top, f)
392 403 # don't trip over symlinks
393 st = os.lstat(p)
394 if stat.S_ISDIR(st.st_mode):
404 st = lstat(p)
405 if s_isdir(st.st_mode):
395 406 if not ignore(np):
396 407 work.append(p)
397 408 if directories:
398 409 yield 'd', np, st
399 410 if imatch(np) and np in dc:
400 411 yield 'm', np, st
401 412 elif imatch(np):
402 if self._supported(np, st):
413 if supported(np, st):
403 414 yield 'f', np, st
404 415 elif np in dc:
405 416 yield 'm', np, st
406 417
407 418 known = {'.hg': 1}
408 419 def seen(fn):
409 420 if fn in known: return True
410 421 known[fn] = 1
411 422
412 423 # step one, find all files that match our criteria
413 424 files.sort()
414 425 for ff in files:
415 nf = util.normpath(ff)
426 nf = normpath(ff)
416 427 f = self._join(ff)
417 428 try:
418 st = os.lstat(f)
429 st = lstat(f)
419 430 except OSError, inst:
420 431 found = False
421 432 for fn in dc:
422 433 if nf == fn or (fn.startswith(nf) and fn[len(nf)] == '/'):
423 434 found = True
424 435 break
425 436 if not found:
426 437 if inst.errno != errno.ENOENT or not badmatch:
427 438 self._ui.warn('%s: %s\n' %
428 439 (self.pathto(ff), inst.strerror))
429 440 elif badmatch and badmatch(ff) and imatch(nf):
430 441 yield 'b', ff, None
431 442 continue
432 if stat.S_ISDIR(st.st_mode):
443 if s_isdir(st.st_mode):
433 444 cmp1 = (lambda x, y: cmp(x[1], y[1]))
434 445 sorted_ = [ x for x in findfiles(f) ]
435 446 sorted_.sort(cmp1)
436 447 for e in sorted_:
437 448 yield e
438 449 else:
439 450 if not seen(nf) and match(nf):
440 if self._supported(ff, st, verbose=True):
451 if supported(ff, st, verbose=True):
441 452 yield 'f', nf, st
442 453 elif ff in dc:
443 454 yield 'm', nf, st
444 455
445 456 # step two run through anything left in the dc hash and yield
446 457 # if we haven't already seen it
447 458 ks = dc.keys()
448 459 ks.sort()
449 460 for k in ks:
450 461 if not seen(k) and imatch(k):
451 462 yield 'm', k, None
452 463
453 464 def status(self, files, match, list_ignored, list_clean):
454 465 lookup, modified, added, unknown, ignored = [], [], [], [], []
455 466 removed, deleted, clean = [], [], []
456 467
457 468 for src, fn, st in self.statwalk(files, match, ignored=list_ignored):
458 469 try:
459 470 type_, mode, size, time = self._map[fn]
460 471 except KeyError:
461 472 if list_ignored and self._ignore(fn):
462 473 ignored.append(fn)
463 474 else:
464 475 unknown.append(fn)
465 476 continue
466 477 if src == 'm':
467 478 nonexistent = True
468 479 if not st:
469 480 try:
470 481 st = os.lstat(self._join(fn))
471 482 except OSError, inst:
472 483 if inst.errno != errno.ENOENT:
473 484 raise
474 485 st = None
475 486 # We need to re-check that it is a valid file
476 487 if st and self._supported(fn, st):
477 488 nonexistent = False
478 489 # XXX: what to do with file no longer present in the fs
479 490 # who are not removed in the dirstate ?
480 491 if nonexistent and type_ in "nm":
481 492 deleted.append(fn)
482 493 continue
483 494 # check the common case first
484 495 if type_ == 'n':
485 496 if not st:
486 497 st = os.lstat(self._join(fn))
487 498 if (size >= 0 and (size != st.st_size
488 499 or (mode ^ st.st_mode) & 0100)
489 500 or fn in self._copymap):
490 501 modified.append(fn)
491 502 elif time != int(st.st_mtime):
492 503 lookup.append(fn)
493 504 elif list_clean:
494 505 clean.append(fn)
495 506 elif type_ == 'm':
496 507 modified.append(fn)
497 508 elif type_ == 'a':
498 509 added.append(fn)
499 510 elif type_ == 'r':
500 511 removed.append(fn)
501 512
502 513 return (lookup, modified, added, removed, deleted, unknown, ignored,
503 514 clean)
General Comments 0
You need to be logged in to leave comments. Login now