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