121
|
1 # dirstate.py - working directory tracking for mercurial
|
|
2 #
|
|
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
|
|
4 #
|
|
5 # This software may be used and distributed according to the terms of the
|
|
6 # GNU General Public License version 2, incorporated herein by reference.
|
|
7
|
|
8 from node import nullid
|
|
9 from i18n import _
|
|
10 import util, ignore, osutil, parsers
|
|
11 import struct, os, stat, errno
|
|
12 import cStringIO, sys
|
|
13
|
|
14 _unknown = ('?', 0, 0, 0)
|
|
15 _format = ">cllll"
|
|
16 propertycache = util.propertycache
|
|
17
|
|
18 def _finddirs(path):
|
|
19 pos = path.rfind('/')
|
|
20 while pos != -1:
|
|
21 yield path[:pos]
|
|
22 pos = path.rfind('/', 0, pos)
|
|
23
|
|
24 def _incdirs(dirs, path):
|
|
25 for base in _finddirs(path):
|
|
26 if base in dirs:
|
|
27 dirs[base] += 1
|
|
28 return
|
|
29 dirs[base] = 1
|
|
30
|
|
31 def _decdirs(dirs, path):
|
|
32 for base in _finddirs(path):
|
|
33 if dirs[base] > 1:
|
|
34 dirs[base] -= 1
|
|
35 return
|
|
36 del dirs[base]
|
|
37
|
|
38 class dirstate(object):
|
|
39
|
|
40 def __init__(self, opener, ui, root):
|
|
41 self._opener = opener
|
|
42 self._root = root
|
|
43 self._rootdir = os.path.join(root, '')
|
|
44 self._dirty = False
|
|
45 self._dirtypl = False
|
|
46 self._ui = ui
|
|
47
|
|
48 @propertycache
|
|
49 def _map(self):
|
|
50 self._read()
|
|
51 return self._map
|
|
52
|
|
53 @propertycache
|
|
54 def _copymap(self):
|
|
55 self._read()
|
|
56 return self._copymap
|
|
57
|
|
58 @propertycache
|
|
59 def _foldmap(self):
|
|
60 f = {}
|
|
61 for name in self._map:
|
|
62 f[os.path.normcase(name)] = name
|
|
63 return f
|
|
64
|
|
65 @propertycache
|
|
66 def _branch(self):
|
|
67 try:
|
|
68 return self._opener("branch").read().strip() or "default"
|
|
69 except IOError:
|
|
70 return "default"
|
|
71
|
|
72 @propertycache
|
|
73 def _pl(self):
|
|
74 try:
|
|
75 st = self._opener("dirstate").read(40)
|
|
76 l = len(st)
|
|
77 if l == 40:
|
|
78 return st[:20], st[20:40]
|
|
79 elif l > 0 and l < 40:
|
|
80 raise util.Abort(_('working directory state appears damaged!'))
|
|
81 except IOError, err:
|
|
82 if err.errno != errno.ENOENT: raise
|
|
83 return [nullid, nullid]
|
|
84
|
|
85 @propertycache
|
|
86 def _dirs(self):
|
|
87 dirs = {}
|
|
88 for f,s in self._map.iteritems():
|
|
89 if s[0] != 'r':
|
|
90 _incdirs(dirs, f)
|
|
91 return dirs
|
|
92
|
|
93 @propertycache
|
|
94 def _ignore(self):
|
|
95 files = [self._join('.hgignore')]
|
|
96 for name, path in self._ui.configitems("ui"):
|
|
97 if name == 'ignore' or name.startswith('ignore.'):
|
|
98 files.append(os.path.expanduser(path))
|
|
99 return ignore.ignore(self._root, files, self._ui.warn)
|
|
100
|
|
101 @propertycache
|
|
102 def _slash(self):
|
|
103 return self._ui.configbool('ui', 'slash') and os.sep != '/'
|
|
104
|
|
105 @propertycache
|
|
106 def _checklink(self):
|
|
107 return util.checklink(self._root)
|
|
108
|
|
109 @propertycache
|
|
110 def _checkexec(self):
|
|
111 return util.checkexec(self._root)
|
|
112
|
|
113 @propertycache
|
|
114 def _checkcase(self):
|
|
115 return not util.checkcase(self._join('.hg'))
|
|
116
|
|
117 def _join(self, f):
|
|
118 # much faster than os.path.join()
|
|
119 # it's safe because f is always a relative path
|
|
120 return self._rootdir + f
|
|
121
|
|
122 def flagfunc(self, fallback):
|
|
123 if self._checklink:
|
|
124 if self._checkexec:
|
|
125 def f(x):
|
|
126 p = self._join(x)
|
|
127 if os.path.islink(p):
|
|
128 return 'l'
|
|
129 if util.is_exec(p):
|
|
130 return 'x'
|
|
131 return ''
|
|
132 return f
|
|
133 def f(x):
|
|
134 if os.path.islink(self._join(x)):
|
|
135 return 'l'
|
|
136 if 'x' in fallback(x):
|
|
137 return 'x'
|
|
138 return ''
|
|
139 return f
|
|
140 if self._checkexec:
|
|
141 def f(x):
|
|
142 if 'l' in fallback(x):
|
|
143 return 'l'
|
|
144 if util.is_exec(self._join(x)):
|
|
145 return 'x'
|
|
146 return ''
|
|
147 return f
|
|
148 return fallback
|
|
149
|
|
150 def getcwd(self):
|
|
151 cwd = os.getcwd()
|
|
152 if cwd == self._root: return ''
|
|
153 # self._root ends with a path separator if self._root is '/' or 'C:\'
|
|
154 rootsep = self._root
|
|
155 if not util.endswithsep(rootsep):
|
|
156 rootsep += os.sep
|
|
157 if cwd.startswith(rootsep):
|
|
158 return cwd[len(rootsep):]
|
|
159 else:
|
|
160 # we're outside the repo. return an absolute path.
|
|
161 return cwd
|
|
162
|
|
163 def pathto(self, f, cwd=None):
|
|
164 if cwd is None:
|
|
165 cwd = self.getcwd()
|
|
166 path = util.pathto(self._root, cwd, f)
|
|
167 if self._slash:
|
|
168 return util.normpath(path)
|
|
169 return path
|
|
170
|
|
171 def __getitem__(self, key):
|
|
172 ''' current states:
|
|
173 n normal
|
|
174 m needs merging
|
|
175 r marked for removal
|
|
176 a marked for addition
|
|
177 ? not tracked'''
|
|
178 return self._map.get(key, ("?",))[0]
|
|
179
|
|
180 def __contains__(self, key):
|
|
181 return key in self._map
|
|
182
|
|
183 def __iter__(self):
|
|
184 for x in sorted(self._map):
|
|
185 yield x
|
|
186
|
|
187 def parents(self):
|
|
188 return self._pl
|
|
189
|
|
190 def branch(self):
|
|
191 return self._branch
|
|
192
|
|
193 def setparents(self, p1, p2=nullid):
|
|
194 self._dirty = self._dirtypl = True
|
|
195 self._pl = p1, p2
|
|
196
|
|
197 def setbranch(self, branch):
|
|
198 self._branch = branch
|
|
199 self._opener("branch", "w").write(branch + '\n')
|
|
200
|
|
201 def _read(self):
|
|
202 self._map = {}
|
|
203 self._copymap = {}
|
|
204 try:
|
|
205 st = self._opener("dirstate").read()
|
|
206 except IOError, err:
|
|
207 if err.errno != errno.ENOENT: raise
|
|
208 return
|
|
209 if not st:
|
|
210 return
|
|
211
|
|
212 p = parsers.parse_dirstate(self._map, self._copymap, st)
|
|
213 if not self._dirtypl:
|
|
214 self._pl = p
|
|
215
|
|
216 def invalidate(self):
|
|
217 for a in "_map _copymap _foldmap _branch _pl _dirs _ignore".split():
|
|
218 if a in self.__dict__:
|
|
219 delattr(self, a)
|
|
220 self._dirty = False
|
|
221
|
|
222 def copy(self, source, dest):
|
|
223 """Mark dest as a copy of source. Unmark dest if source is None.
|
|
224 """
|
|
225 if source == dest:
|
|
226 return
|
|
227 self._dirty = True
|
|
228 if source is not None:
|
|
229 self._copymap[dest] = source
|
|
230 elif dest in self._copymap:
|
|
231 del self._copymap[dest]
|
|
232
|
|
233 def copied(self, file):
|
|
234 return self._copymap.get(file, None)
|
|
235
|
|
236 def copies(self):
|
|
237 return self._copymap
|
|
238
|
|
239 def _droppath(self, f):
|
|
240 if self[f] not in "?r" and "_dirs" in self.__dict__:
|
|
241 _decdirs(self._dirs, f)
|
|
242
|
|
243 def _addpath(self, f, check=False):
|
|
244 oldstate = self[f]
|
|
245 if check or oldstate == "r":
|
|
246 if '\r' in f or '\n' in f:
|
|
247 raise util.Abort(
|
|
248 _("'\\n' and '\\r' disallowed in filenames: %r") % f)
|
|
249 if f in self._dirs:
|
|
250 raise util.Abort(_('directory %r already in dirstate') % f)
|
|
251 # shadows
|
|
252 for d in _finddirs(f):
|
|
253 if d in self._dirs:
|
|
254 break
|
|
255 if d in self._map and self[d] != 'r':
|
|
256 raise util.Abort(
|
|
257 _('file %r in dirstate clashes with %r') % (d, f))
|
|
258 if oldstate in "?r" and "_dirs" in self.__dict__:
|
|
259 _incdirs(self._dirs, f)
|
|
260
|
|
261 def normal(self, f):
|
|
262 'mark a file normal and clean'
|
|
263 self._dirty = True
|
|
264 self._addpath(f)
|
|
265 s = os.lstat(self._join(f))
|
|
266 self._map[f] = ('n', s.st_mode, s.st_size, int(s.st_mtime))
|
|
267 if f in self._copymap:
|
|
268 del self._copymap[f]
|
|
269
|
|
270 def normallookup(self, f):
|
|
271 'mark a file normal, but possibly dirty'
|
|
272 if self._pl[1] != nullid and f in self._map:
|
|
273 # if there is a merge going on and the file was either
|
|
274 # in state 'm' or dirty before being removed, restore that state.
|
|
275 entry = self._map[f]
|
|
276 if entry[0] == 'r' and entry[2] in (-1, -2):
|
|
277 source = self._copymap.get(f)
|
|
278 if entry[2] == -1:
|
|
279 self.merge(f)
|
|
280 elif entry[2] == -2:
|
|
281 self.normaldirty(f)
|
|
282 if source:
|
|
283 self.copy(source, f)
|
|
284 return
|
|
285 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
|
|
286 return
|
|
287 self._dirty = True
|
|
288 self._addpath(f)
|
|
289 self._map[f] = ('n', 0, -1, -1)
|
|
290 if f in self._copymap:
|
|
291 del self._copymap[f]
|
|
292
|
|
293 def normaldirty(self, f):
|
|
294 'mark a file normal, but dirty'
|
|
295 self._dirty = True
|
|
296 self._addpath(f)
|
|
297 self._map[f] = ('n', 0, -2, -1)
|
|
298 if f in self._copymap:
|
|
299 del self._copymap[f]
|
|
300
|
|
301 def add(self, f):
|
|
302 'mark a file added'
|
|
303 self._dirty = True
|
|
304 self._addpath(f, True)
|
|
305 self._map[f] = ('a', 0, -1, -1)
|
|
306 if f in self._copymap:
|
|
307 del self._copymap[f]
|
|
308
|
|
309 def remove(self, f):
|
|
310 'mark a file removed'
|
|
311 self._dirty = True
|
|
312 self._droppath(f)
|
|
313 size = 0
|
|
314 if self._pl[1] != nullid and f in self._map:
|
|
315 entry = self._map[f]
|
|
316 if entry[0] == 'm':
|
|
317 size = -1
|
|
318 elif entry[0] == 'n' and entry[2] == -2:
|
|
319 size = -2
|
|
320 self._map[f] = ('r', 0, size, 0)
|
|
321 if size == 0 and f in self._copymap:
|
|
322 del self._copymap[f]
|
|
323
|
|
324 def merge(self, f):
|
|
325 'mark a file merged'
|
|
326 self._dirty = True
|
|
327 s = os.lstat(self._join(f))
|
|
328 self._addpath(f)
|
|
329 self._map[f] = ('m', s.st_mode, s.st_size, int(s.st_mtime))
|
|
330 if f in self._copymap:
|
|
331 del self._copymap[f]
|
|
332
|
|
333 def forget(self, f):
|
|
334 'forget a file'
|
|
335 self._dirty = True
|
|
336 try:
|
|
337 self._droppath(f)
|
|
338 del self._map[f]
|
|
339 except KeyError:
|
|
340 self._ui.warn(_("not in dirstate: %s\n") % f)
|
|
341
|
|
342 def _normalize(self, path, knownpath):
|
|
343 norm_path = os.path.normcase(path)
|
|
344 fold_path = self._foldmap.get(norm_path, None)
|
|
345 if fold_path is None:
|
|
346 if knownpath or not os.path.exists(os.path.join(self._root, path)):
|
|
347 fold_path = path
|
|
348 else:
|
|
349 fold_path = self._foldmap.setdefault(norm_path,
|
|
350 util.fspath(path, self._root))
|
|
351 return fold_path
|
|
352
|
|
353 def clear(self):
|
|
354 self._map = {}
|
|
355 if "_dirs" in self.__dict__:
|
|
356 delattr(self, "_dirs");
|
|
357 self._copymap = {}
|
|
358 self._pl = [nullid, nullid]
|
|
359 self._dirty = True
|
|
360
|
|
361 def rebuild(self, parent, files):
|
|
362 self.clear()
|
|
363 for f in files:
|
|
364 if 'x' in files.flags(f):
|
|
365 self._map[f] = ('n', 0777, -1, 0)
|
|
366 else:
|
|
367 self._map[f] = ('n', 0666, -1, 0)
|
|
368 self._pl = (parent, nullid)
|
|
369 self._dirty = True
|
|
370
|
|
371 def write(self):
|
|
372 if not self._dirty:
|
|
373 return
|
|
374 st = self._opener("dirstate", "w", atomictemp=True)
|
|
375
|
|
376 try:
|
|
377 gran = int(self._ui.config('dirstate', 'granularity', 1))
|
|
378 except ValueError:
|
|
379 gran = 1
|
|
380 limit = sys.maxint
|
|
381 if gran > 0:
|
|
382 limit = util.fstat(st).st_mtime - gran
|
|
383
|
|
384 cs = cStringIO.StringIO()
|
|
385 copymap = self._copymap
|
|
386 pack = struct.pack
|
|
387 write = cs.write
|
|
388 write("".join(self._pl))
|
|
389 for f, e in self._map.iteritems():
|
|
390 if f in copymap:
|
|
391 f = "%s\0%s" % (f, copymap[f])
|
|
392 if e[3] > limit and e[0] == 'n':
|
|
393 e = (e[0], 0, -1, -1)
|
|
394 e = pack(_format, e[0], e[1], e[2], e[3], len(f))
|
|
395 write(e)
|
|
396 write(f)
|
|
397 st.write(cs.getvalue())
|
|
398 st.rename()
|
|
399 self._dirty = self._dirtypl = False
|
|
400
|
|
401 def _dirignore(self, f):
|
|
402 if f == '.':
|
|
403 return False
|
|
404 if self._ignore(f):
|
|
405 return True
|
|
406 for p in _finddirs(f):
|
|
407 if self._ignore(p):
|
|
408 return True
|
|
409 return False
|
|
410
|
|
411 def walk(self, match, unknown, ignored):
|
|
412 '''
|
|
413 walk recursively through the directory tree, finding all files
|
|
414 matched by the match function
|
|
415
|
|
416 results are yielded in a tuple (filename, stat), where stat
|
|
417 and st is the stat result if the file was found in the directory.
|
|
418 '''
|
|
419
|
|
420 def fwarn(f, msg):
|
|
421 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
|
|
422 return False
|
|
423
|
|
424 def badtype(mode):
|
|
425 kind = _('unknown')
|
|
426 if stat.S_ISCHR(mode): kind = _('character device')
|
|
427 elif stat.S_ISBLK(mode): kind = _('block device')
|
|
428 elif stat.S_ISFIFO(mode): kind = _('fifo')
|
|
429 elif stat.S_ISSOCK(mode): kind = _('socket')
|
|
430 elif stat.S_ISDIR(mode): kind = _('directory')
|
|
431 return _('unsupported file type (type is %s)') % kind
|
|
432
|
|
433 ignore = self._ignore
|
|
434 dirignore = self._dirignore
|
|
435 if ignored:
|
|
436 ignore = util.never
|
|
437 dirignore = util.never
|
|
438 elif not unknown:
|
|
439 # if unknown and ignored are False, skip step 2
|
|
440 ignore = util.always
|
|
441 dirignore = util.always
|
|
442
|
|
443 matchfn = match.matchfn
|
|
444 badfn = match.bad
|
|
445 dmap = self._map
|
|
446 normpath = util.normpath
|
|
447 listdir = osutil.listdir
|
|
448 lstat = os.lstat
|
|
449 getkind = stat.S_IFMT
|
|
450 dirkind = stat.S_IFDIR
|
|
451 regkind = stat.S_IFREG
|
|
452 lnkkind = stat.S_IFLNK
|
|
453 join = self._join
|
|
454 work = []
|
|
455 wadd = work.append
|
|
456
|
|
457 if self._checkcase:
|
|
458 normalize = self._normalize
|
|
459 else:
|
|
460 normalize = lambda x, y: x
|
|
461
|
|
462 exact = skipstep3 = False
|
|
463 if matchfn == match.exact: # match.exact
|
|
464 exact = True
|
|
465 dirignore = util.always # skip step 2
|
|
466 elif match.files() and not match.anypats(): # match.match, no patterns
|
|
467 skipstep3 = True
|
|
468
|
|
469 files = set(match.files())
|
|
470 if not files or '.' in files:
|
|
471 files = ['']
|
|
472 results = {'.hg': None}
|
|
473
|
|
474 # step 1: find all explicit files
|
|
475 for ff in sorted(files):
|
|
476 nf = normalize(normpath(ff), False)
|
|
477 if nf in results:
|
|
478 continue
|
|
479
|
|
480 try:
|
|
481 st = lstat(join(nf))
|
|
482 kind = getkind(st.st_mode)
|
|
483 if kind == dirkind:
|
|
484 skipstep3 = False
|
|
485 if nf in dmap:
|
|
486 #file deleted on disk but still in dirstate
|
|
487 results[nf] = None
|
|
488 match.dir(nf)
|
|
489 if not dirignore(nf):
|
|
490 wadd(nf)
|
|
491 elif kind == regkind or kind == lnkkind:
|
|
492 results[nf] = st
|
|
493 else:
|
|
494 badfn(ff, badtype(kind))
|
|
495 if nf in dmap:
|
|
496 results[nf] = None
|
|
497 except OSError, inst:
|
|
498 if nf in dmap: # does it exactly match a file?
|
|
499 results[nf] = None
|
|
500 else: # does it match a directory?
|
|
501 prefix = nf + "/"
|
|
502 for fn in dmap:
|
|
503 if fn.startswith(prefix):
|
|
504 match.dir(nf)
|
|
505 skipstep3 = False
|
|
506 break
|
|
507 else:
|
|
508 badfn(ff, inst.strerror)
|
|
509
|
|
510 # step 2: visit subdirectories
|
|
511 while work:
|
|
512 nd = work.pop()
|
|
513 skip = None
|
|
514 if nd == '.':
|
|
515 nd = ''
|
|
516 else:
|
|
517 skip = '.hg'
|
|
518 try:
|
|
519 entries = listdir(join(nd), stat=True, skip=skip)
|
|
520 except OSError, inst:
|
|
521 if inst.errno == errno.EACCES:
|
|
522 fwarn(nd, inst.strerror)
|
|
523 continue
|
|
524 raise
|
|
525 for f, kind, st in entries:
|
|
526 nf = normalize(nd and (nd + "/" + f) or f, True)
|
|
527 if nf not in results:
|
|
528 if kind == dirkind:
|
|
529 if not ignore(nf):
|
|
530 match.dir(nf)
|
|
531 wadd(nf)
|
|
532 if nf in dmap and matchfn(nf):
|
|
533 results[nf] = None
|
|
534 elif kind == regkind or kind == lnkkind:
|
|
535 if nf in dmap:
|
|
536 if matchfn(nf):
|
|
537 results[nf] = st
|
|
538 elif matchfn(nf) and not ignore(nf):
|
|
539 results[nf] = st
|
|
540 elif nf in dmap and matchfn(nf):
|
|
541 results[nf] = None
|
|
542
|
|
543 # step 3: report unseen items in the dmap hash
|
|
544 if not skipstep3 and not exact:
|
|
545 visit = sorted([f for f in dmap if f not in results and matchfn(f)])
|
|
546 for nf, st in zip(visit, util.statfiles([join(i) for i in visit])):
|
|
547 if not st is None and not getkind(st.st_mode) in (regkind, lnkkind):
|
|
548 st = None
|
|
549 results[nf] = st
|
|
550
|
|
551 del results['.hg']
|
|
552 return results
|
|
553
|
|
554 def status(self, match, ignored, clean, unknown):
|
|
555 listignored, listclean, listunknown = ignored, clean, unknown
|
|
556 lookup, modified, added, unknown, ignored = [], [], [], [], []
|
|
557 removed, deleted, clean = [], [], []
|
|
558
|
|
559 dmap = self._map
|
|
560 ladd = lookup.append
|
|
561 madd = modified.append
|
|
562 aadd = added.append
|
|
563 uadd = unknown.append
|
|
564 iadd = ignored.append
|
|
565 radd = removed.append
|
|
566 dadd = deleted.append
|
|
567 cadd = clean.append
|
|
568
|
|
569 for fn, st in self.walk(match, listunknown, listignored).iteritems():
|
|
570 if fn not in dmap:
|
|
571 if (listignored or match.exact(fn)) and self._dirignore(fn):
|
|
572 if listignored:
|
|
573 iadd(fn)
|
|
574 elif listunknown:
|
|
575 uadd(fn)
|
|
576 continue
|
|
577
|
|
578 state, mode, size, time = dmap[fn]
|
|
579
|
|
580 if not st and state in "nma":
|
|
581 dadd(fn)
|
|
582 elif state == 'n':
|
|
583 if (size >= 0 and
|
|
584 (size != st.st_size
|
|
585 or ((mode ^ st.st_mode) & 0100 and self._checkexec))
|
|
586 or size == -2
|
|
587 or fn in self._copymap):
|
|
588 madd(fn)
|
|
589 elif time != int(st.st_mtime):
|
|
590 ladd(fn)
|
|
591 elif listclean:
|
|
592 cadd(fn)
|
|
593 elif state == 'm':
|
|
594 madd(fn)
|
|
595 elif state == 'a':
|
|
596 aadd(fn)
|
|
597 elif state == 'r':
|
|
598 radd(fn)
|
|
599
|
|
600 return (lookup, modified, added, removed, deleted, unknown, ignored,
|
|
601 clean)
|