dirstate.py
518 lines
| 16.5 KiB
| text/x-python
|
PythonLexer
/ mercurial / dirstate.py
|
r1089 | """ | ||
dirstate.py - working directory tracking for mercurial | ||||
|
r4635 | Copyright 2005-2007 Matt Mackall <mpm@selenic.com> | ||
|
r1089 | |||
This software may be used and distributed according to the terms | ||||
of the GNU General Public License, incorporated herein by reference. | ||||
""" | ||||
|
r1094 | from node import * | ||
|
r3891 | from i18n import _ | ||
|
r4609 | import struct, os, time, bisect, stat, strutil, util, re, errno, ignore | ||
|
r4374 | import cStringIO | ||
|
r1089 | |||
|
r4610 | _unknown = ('?', 0, 0, 0) | ||
_format = ">cllll" | ||||
|
r1559 | class dirstate(object): | ||
|
r2393 | |||
|
r1089 | def __init__(self, opener, ui, root): | ||
|
r4614 | self._opener = opener | ||
self._root = root | ||||
|
r4903 | self._dirty = False | ||
|
r4965 | self._dirtypl = False | ||
|
r4614 | self._ui = ui | ||
|
r1089 | |||
|
r4603 | def __getattr__(self, name): | ||
|
r4614 | if name == '_map': | ||
|
r4615 | self._read() | ||
|
r4614 | return self._map | ||
elif name == '_copymap': | ||||
|
r4615 | self._read() | ||
|
r4614 | return self._copymap | ||
|
r4604 | elif name == '_branch': | ||
try: | ||||
|
r4633 | self._branch = (self._opener("branch").read().strip() | ||
or "default") | ||||
|
r4604 | except IOError: | ||
self._branch = "default" | ||||
return self._branch | ||||
|
r4614 | elif name == '_pl': | ||
self._pl = [nullid, nullid] | ||||
|
r4604 | try: | ||
|
r4614 | st = self._opener("dirstate").read(40) | ||
|
r4604 | if len(st) == 40: | ||
|
r4614 | self._pl = st[:20], st[20:40] | ||
|
r4604 | except IOError, err: | ||
if err.errno != errno.ENOENT: raise | ||||
|
r4614 | return self._pl | ||
|
r4615 | elif name == '_dirs': | ||
self._dirs = {} | ||||
|
r4614 | for f in self._map: | ||
|
r4615 | self._incpath(f) | ||
return self._dirs | ||||
|
r4609 | elif name == '_ignore': | ||
|
r4905 | files = [self._join('.hgignore')] | ||
|
r4620 | for name, path in self._ui.configitems("ui"): | ||
if name == 'ignore' or name.startswith('ignore.'): | ||||
files.append(os.path.expanduser(path)) | ||||
|
r4614 | self._ignore = ignore.ignore(self._root, files, self._ui.warn) | ||
|
r4609 | return self._ignore | ||
|
r4611 | elif name == '_slash': | ||
|
r4614 | self._slash = self._ui.configbool('ui', 'slash') and os.sep != '/' | ||
|
r4611 | return self._slash | ||
|
r4603 | else: | ||
raise AttributeError, name | ||||
|
r4905 | def _join(self, f): | ||
|
r4614 | return os.path.join(self._root, f) | ||
|
r1089 | |||
def getcwd(self): | ||||
cwd = os.getcwd() | ||||
|
r4614 | if cwd == self._root: return '' | ||
# self._root ends with a path separator if self._root is '/' or 'C:\' | ||||
rootsep = self._root | ||||
|
r4230 | if not rootsep.endswith(os.sep): | ||
rootsep += os.sep | ||||
if cwd.startswith(rootsep): | ||||
return cwd[len(rootsep):] | ||||
else: | ||||
# we're outside the repo. return an absolute path. | ||||
return cwd | ||||
|
r1089 | |||
|
r4525 | def pathto(self, f, cwd=None): | ||
if cwd is None: | ||||
cwd = self.getcwd() | ||||
|
r4614 | path = util.pathto(self._root, cwd, f) | ||
|
r4527 | if self._slash: | ||
|
r4611 | return path.replace(os.sep, '/') | ||
|
r4527 | return path | ||
|
r4525 | |||
|
r1089 | def __getitem__(self, key): | ||
|
r4906 | ''' current states: | ||
n normal | ||||
m needs merging | ||||
r marked for removal | ||||
a marked for addition | ||||
? not tracked''' | ||||
return self._map.get(key, ("?",))[0] | ||||
|
r1089 | |||
def __contains__(self, key): | ||||
|
r4614 | return key in self._map | ||
def __iter__(self): | ||||
a = self._map.keys() | ||||
a.sort() | ||||
for x in a: | ||||
yield x | ||||
|
r1089 | |||
def parents(self): | ||||
|
r4614 | return self._pl | ||
|
r1089 | |||
|
r4179 | def branch(self): | ||
return self._branch | ||||
|
r1089 | def setparents(self, p1, p2=nullid): | ||
|
r4965 | self._dirty = self._dirtypl = True | ||
|
r4614 | self._pl = p1, p2 | ||
|
r1089 | |||
|
r4179 | def setbranch(self, branch): | ||
self._branch = branch | ||||
|
r4614 | self._opener("branch", "w").write(branch + '\n') | ||
|
r4179 | |||
|
r4615 | def _read(self): | ||
|
r4614 | self._map = {} | ||
self._copymap = {} | ||||
|
r4952 | if not self._dirtypl: | ||
self._pl = [nullid, nullid] | ||||
|
r4607 | try: | ||
|
r4614 | st = self._opener("dirstate").read() | ||
|
r4607 | except IOError, err: | ||
if err.errno != errno.ENOENT: raise | ||||
return | ||||
if not st: | ||||
return | ||||
|
r4952 | if not self._dirtypl: | ||
self._pl = [st[:20], st[20: 40]] | ||||
|
r1089 | |||
|
r2427 | # deref fields so they will be local in loop | ||
|
r4614 | dmap = self._map | ||
copymap = self._copymap | ||||
|
r2427 | unpack = struct.unpack | ||
|
r1089 | pos = 40 | ||
|
r4610 | e_size = struct.calcsize(_format) | ||
|
r2427 | |||
|
r1089 | while pos < len(st): | ||
|
r2425 | newpos = pos + e_size | ||
|
r4610 | e = unpack(_format, st[pos:newpos]) | ||
|
r1089 | l = e[4] | ||
|
r2425 | pos = newpos | ||
newpos = pos + l | ||||
f = st[pos:newpos] | ||||
|
r1089 | if '\0' in f: | ||
f, c = f.split('\0') | ||||
|
r3154 | copymap[f] = c | ||
|
r4607 | dmap[f] = e[:4] | ||
|
r2425 | pos = newpos | ||
|
r1089 | |||
|
r4613 | def invalidate(self): | ||
|
r4656 | for a in "_map _copymap _branch _pl _dirs _ignore".split(): | ||
|
r4953 | if a in self.__dict__: | ||
delattr(self, a) | ||||
|
r4903 | self._dirty = False | ||
|
r4375 | |||
|
r1089 | def copy(self, source, dest): | ||
|
r4903 | self._dirty = True | ||
|
r4614 | self._copymap[dest] = source | ||
|
r1089 | |||
def copied(self, file): | ||||
|
r4614 | return self._copymap.get(file, None) | ||
|
r3154 | |||
def copies(self): | ||||
|
r4614 | return self._copymap | ||
|
r1089 | |||
|
r4615 | def _incpath(self, path): | ||
|
r4605 | for c in strutil.findall(path, '/'): | ||
pc = path[:c] | ||||
|
r4615 | self._dirs.setdefault(pc, 0) | ||
self._dirs[pc] += 1 | ||||
def _decpath(self, path): | ||||
for c in strutil.findall(path, '/'): | ||||
pc = path[:c] | ||||
self._dirs.setdefault(pc, 0) | ||||
self._dirs[pc] -= 1 | ||||
|
r2953 | |||
|
r4616 | def _incpathcheck(self, f): | ||
if '\r' in f or '\n' in f: | ||||
raise util.Abort(_("'\\n' and '\\r' disallowed in filenames")) | ||||
# shadows | ||||
if f in self._dirs: | ||||
raise util.Abort(_('directory named %r already in dirstate') % f) | ||||
for c in strutil.rfindall(f, '/'): | ||||
d = f[:c] | ||||
if d in self._dirs: | ||||
break | ||||
if d in self._map: | ||||
raise util.Abort(_('file named %r already in dirstate') % d) | ||||
self._incpath(f) | ||||
|
r2953 | |||
|
r4904 | def normal(self, f): | ||
'mark a file normal' | ||||
self._dirty = True | ||||
|
r4905 | s = os.lstat(self._join(f)) | ||
|
r4904 | self._map[f] = ('n', s.st_mode, s.st_size, s.st_mtime) | ||
if self._copymap.has_key(f): | ||||
del self._copymap[f] | ||||
|
r1089 | |||
|
r4904 | def normaldirty(self, f): | ||
'mark a file normal, but possibly dirty' | ||||
|
r4903 | self._dirty = True | ||
|
r4905 | s = os.lstat(self._join(f)) | ||
|
r4904 | self._map[f] = ('n', s.st_mode, -1, -1) | ||
if f in self._copymap: | ||||
del self._copymap[f] | ||||
def add(self, f): | ||||
'mark a file added' | ||||
self._dirty = True | ||||
self._incpathcheck(f) | ||||
|
r4911 | self._map[f] = ('a', 0, -1, -1) | ||
|
r4904 | if f in self._copymap: | ||
del self._copymap[f] | ||||
|
r4616 | |||
|
r4904 | def remove(self, f): | ||
'mark a file removed' | ||||
self._dirty = True | ||||
self._map[f] = ('r', 0, 0, 0) | ||||
self._decpath(f) | ||||
if f in self._copymap: | ||||
del self._copymap[f] | ||||
|
r1089 | |||
|
r4904 | def merge(self, f): | ||
'mark a file merged' | ||||
|
r4903 | self._dirty = True | ||
|
r4905 | s = os.lstat(self._join(f)) | ||
|
r4904 | self._map[f] = ('m', s.st_mode, s.st_size, s.st_mtime) | ||
if f in self._copymap: | ||||
del self._copymap[f] | ||||
def forget(self, f): | ||||
'forget a file' | ||||
self._dirty = True | ||||
try: | ||||
del self._map[f] | ||||
self._decpath(f) | ||||
except KeyError: | ||||
self._ui.warn(_("not in dirstate: %s!\n") % f) | ||||
|
r1089 | |||
|
r1755 | def rebuild(self, parent, files): | ||
|
r4613 | self.invalidate() | ||
|
r2832 | for f in files: | ||
if files.execf(f): | ||||
|
r4614 | self._map[f] = ('n', 0777, -1, 0) | ||
|
r1755 | else: | ||
|
r4614 | self._map[f] = ('n', 0666, -1, 0) | ||
self._pl = (parent, nullid) | ||||
|
r4903 | self._dirty = True | ||
|
r1089 | |||
def write(self): | ||||
|
r4612 | if not self._dirty: | ||
|
r1794 | return | ||
|
r4374 | cs = cStringIO.StringIO() | ||
|
r4614 | cs.write("".join(self._pl)) | ||
for f, e in self._map.iteritems(): | ||||
|
r1089 | c = self.copied(f) | ||
if c: | ||||
f = f + "\0" + c | ||||
|
r4610 | e = struct.pack(_format, e[0], e[1], e[2], e[3], len(f)) | ||
|
r4374 | cs.write(e) | ||
cs.write(f) | ||||
|
r4614 | st = self._opener("dirstate", "w", atomictemp=True) | ||
|
r4374 | st.write(cs.getvalue()) | ||
|
r4507 | st.rename() | ||
|
r4965 | self._dirty = self._dirtypl = False | ||
|
r1089 | |||
|
r4907 | def _filter(self, files): | ||
|
r1089 | ret = {} | ||
unknown = [] | ||||
for x in files: | ||||
|
r1541 | if x == '.': | ||
|
r4614 | return self._map.copy() | ||
if x not in self._map: | ||||
|
r1089 | unknown.append(x) | ||
else: | ||||
|
r4614 | ret[x] = self._map[x] | ||
|
r1089 | |||
if not unknown: | ||||
return ret | ||||
|
r4614 | b = self._map.keys() | ||
|
r1089 | b.sort() | ||
blen = len(b) | ||||
for x in unknown: | ||||
|
r2486 | bs = bisect.bisect(b, "%s%s" % (x, '/')) | ||
|
r1089 | while bs < blen: | ||
s = b[bs] | ||||
|
r2485 | if len(s) > len(x) and s.startswith(x): | ||
|
r4614 | ret[s] = self._map[s] | ||
|
r1089 | else: | ||
break | ||||
bs += 1 | ||||
return ret | ||||
|
r5001 | def _supported(self, f, mode, verbose=False): | ||
if stat.S_ISREG(mode) or stat.S_ISLNK(mode): | ||||
|
r1487 | return True | ||
if verbose: | ||||
kind = 'unknown' | ||||
|
r5001 | if stat.S_ISCHR(mode): kind = _('character device') | ||
elif stat.S_ISBLK(mode): kind = _('block device') | ||||
elif stat.S_ISFIFO(mode): kind = _('fifo') | ||||
elif stat.S_ISSOCK(mode): kind = _('socket') | ||||
elif stat.S_ISDIR(mode): kind = _('directory') | ||||
|
r4614 | self._ui.warn(_('%s: unsupported file type (type is %s)\n') | ||
|
r4633 | % (self.pathto(f), kind)) | ||
|
r1487 | return False | ||
|
r3529 | def walk(self, files=None, match=util.always, badmatch=None): | ||
# filter out the stat | ||||
for src, f, st in self.statwalk(files, match, badmatch=badmatch): | ||||
yield src, f | ||||
def statwalk(self, files=None, match=util.always, ignored=False, | ||||
|
r4146 | badmatch=None, directories=False): | ||
|
r3529 | ''' | ||
walk recursively through the directory tree, finding all files | ||||
matched by the match function | ||||
results are yielded in a tuple (src, filename, st), where src | ||||
is one of: | ||||
'f' the file was found in the directory tree | ||||
|
r4146 | 'd' the file is a directory of the tree | ||
|
r3529 | 'm' the file was only in the dirstate and not in the tree | ||
|
r3532 | 'b' file was not found and matched badmatch | ||
|
r3529 | and st is the stat result if the file was found in the directory. | ||
''' | ||||
|
r1089 | |||
# walk all files by default | ||||
if not files: | ||||
|
r4172 | files = ['.'] | ||
|
r4614 | dc = self._map.copy() | ||
|
r3529 | else: | ||
|
r3536 | files = util.unique(files) | ||
|
r4907 | dc = self._filter(files) | ||
|
r1089 | |||
|
r3529 | def imatch(file_): | ||
|
r4609 | if file_ not in dc and self._ignore(file_): | ||
|
r1183 | return False | ||
|
r1749 | return match(file_) | ||
|
r1224 | |||
|
r4609 | ignore = self._ignore | ||
|
r4193 | if ignored: | ||
imatch = match | ||||
ignore = util.never | ||||
|
r3534 | |||
|
r4614 | # self._root may end with a path separator when self._root == '/' | ||
common_prefix_len = len(self._root) | ||||
if not self._root.endswith(os.sep): | ||||
|
r2671 | common_prefix_len += 1 | ||
|
r5000 | |||
normpath = util.normpath | ||||
listdir = os.listdir | ||||
lstat = os.lstat | ||||
bisect_left = bisect.bisect_left | ||||
isdir = os.path.isdir | ||||
pconvert = util.pconvert | ||||
join = os.path.join | ||||
s_isdir = stat.S_ISDIR | ||||
supported = self._supported | ||||
|
r5001 | _join = self._join | ||
known = {'.hg': 1} | ||||
|
r5000 | |||
|
r5001 | # recursion free walker, faster than os.walk. | ||
|
r1183 | def findfiles(s): | ||
work = [s] | ||||
|
r4146 | if directories: | ||
|
r5001 | yield 'd', normpath(s[common_prefix_len:]), lstat(s) | ||
|
r1183 | while work: | ||
top = work.pop() | ||||
|
r5000 | names = listdir(top) | ||
|
r1183 | names.sort() | ||
# nd is the top of the repository dir tree | ||||
|
r5000 | nd = normpath(top[common_prefix_len:]) | ||
|
r2061 | if nd == '.': | ||
nd = '' | ||||
else: | ||||
|
r2063 | # do not recurse into a repo contained in this | ||
# one. use bisect to find .hg directory so speed | ||||
# is good on big directory. | ||||
|
r5000 | hg = bisect_left(names, '.hg') | ||
|
r2061 | if hg < len(names) and names[hg] == '.hg': | ||
|
r5000 | if isdir(join(top, '.hg')): | ||
|
r2061 | continue | ||
|
r1183 | for f in names: | ||
|
r5001 | np = pconvert(join(nd, f)) | ||
if np in known: | ||||
|
r1183 | continue | ||
|
r5001 | known[np] = 1 | ||
|
r5000 | p = join(top, f) | ||
|
r1228 | # don't trip over symlinks | ||
|
r5000 | st = lstat(p) | ||
if s_isdir(st.st_mode): | ||||
|
r4254 | if not ignore(np): | ||
|
r1183 | work.append(p) | ||
|
r4146 | if directories: | ||
yield 'd', np, st | ||||
|
r5001 | if np in dc and match(np): | ||
|
r1562 | yield 'm', np, st | ||
|
r3529 | elif imatch(np): | ||
|
r5001 | if supported(np, st.st_mode): | ||
|
r1562 | yield 'f', np, st | ||
|
r1487 | elif np in dc: | ||
|
r1562 | yield 'm', np, st | ||
|
r1392 | |||
|
r1183 | # step one, find all files that match our criteria | ||
files.sort() | ||||
|
r3536 | for ff in files: | ||
|
r5000 | nf = normpath(ff) | ||
|
r5001 | f = _join(ff) | ||
|
r1183 | try: | ||
|
r5000 | st = lstat(f) | ||
|
r1183 | except OSError, inst: | ||
|
r1564 | found = False | ||
for fn in dc: | ||||
if nf == fn or (fn.startswith(nf) and fn[len(nf)] == '/'): | ||||
found = True | ||||
break | ||||
if not found: | ||||
|
r2042 | if inst.errno != errno.ENOENT or not badmatch: | ||
|
r4633 | self._ui.warn('%s: %s\n' % | ||
(self.pathto(ff), inst.strerror)) | ||||
|
r3534 | elif badmatch and badmatch(ff) and imatch(nf): | ||
|
r2042 | yield 'b', ff, None | ||
|
r1183 | continue | ||
|
r5000 | if s_isdir(st.st_mode): | ||
|
r1487 | cmp1 = (lambda x, y: cmp(x[1], y[1])) | ||
|
r1749 | sorted_ = [ x for x in findfiles(f) ] | ||
sorted_.sort(cmp1) | ||||
for e in sorted_: | ||||
|
r1487 | yield e | ||
|
r1392 | else: | ||
|
r5001 | if nf in known: | ||
continue | ||||
known[nf] = 1 | ||||
if match(nf): | ||||
if supported(ff, st.st_mode, verbose=True): | ||||
|
r3536 | yield 'f', nf, st | ||
|
r1487 | elif ff in dc: | ||
|
r3536 | yield 'm', nf, st | ||
|
r1089 | |||
|
r1183 | # step two run through anything left in the dc hash and yield | ||
# if we haven't already seen it | ||||
ks = dc.keys() | ||||
ks.sort() | ||||
for k in ks: | ||||
|
r5001 | if k in known: | ||
continue | ||||
known[k] = 1 | ||||
if imatch(k): | ||||
|
r1471 | yield 'm', k, None | ||
|
r1089 | |||
|
r4908 | def status(self, files, match, list_ignored, list_clean): | ||
|
r2022 | lookup, modified, added, unknown, ignored = [], [], [], [], [] | ||
|
r2661 | removed, deleted, clean = [], [], [] | ||
|
r1089 | |||
|
r2661 | for src, fn, st in self.statwalk(files, match, ignored=list_ignored): | ||
|
r1471 | try: | ||
|
r4906 | type_, mode, size, time = self._map[fn] | ||
|
r1471 | except KeyError: | ||
|
r4609 | if list_ignored and self._ignore(fn): | ||
|
r2022 | ignored.append(fn) | ||
else: | ||||
unknown.append(fn) | ||||
|
r1471 | continue | ||
|
r1476 | if src == 'm': | ||
|
r1487 | nonexistent = True | ||
if not st: | ||||
try: | ||||
|
r4905 | st = os.lstat(self._join(fn)) | ||
|
r1487 | except OSError, inst: | ||
if inst.errno != errno.ENOENT: | ||||
raise | ||||
st = None | ||||
# We need to re-check that it is a valid file | ||||
|
r5001 | if st and self._supported(fn, st.st_mode): | ||
|
r1487 | nonexistent = False | ||
|
r1476 | # XXX: what to do with file no longer present in the fs | ||
# who are not removed in the dirstate ? | ||||
|
r1749 | if nonexistent and type_ in "nm": | ||
|
r1476 | deleted.append(fn) | ||
continue | ||||
|
r1471 | # check the common case first | ||
|
r1749 | if type_ == 'n': | ||
|
r1471 | if not st: | ||
|
r4905 | st = os.lstat(self._join(fn)) | ||
|
r4677 | if (size >= 0 and (size != st.st_size | ||
or (mode ^ st.st_mode) & 0100) | ||||
or fn in self._copymap): | ||||
|
r1471 | modified.append(fn) | ||
|
r2962 | elif time != int(st.st_mtime): | ||
|
r1471 | lookup.append(fn) | ||
|
r2661 | elif list_clean: | ||
clean.append(fn) | ||||
|
r1749 | elif type_ == 'm': | ||
|
r1471 | modified.append(fn) | ||
|
r1749 | elif type_ == 'a': | ||
|
r1471 | added.append(fn) | ||
|
r1749 | elif type_ == 'r': | ||
|
r1471 | removed.append(fn) | ||
|
r1183 | |||
|
r2661 | return (lookup, modified, added, removed, deleted, unknown, ignored, | ||
clean) | ||||