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