dirstate.py
531 lines
| 17.5 KiB
| text/x-python
|
PythonLexer
/ mercurial / dirstate.py
|
r1089 | """ | ||
dirstate.py - working directory tracking for mercurial | ||||
|
r2859 | Copyright 2005, 2006 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 * | ||
|
r1400 | from i18n import gettext as _ | ||
|
r1089 | from demandload import * | ||
|
r2953 | demandload(globals(), "struct os time bisect stat strutil util re errno") | ||
|
r1089 | |||
|
r1559 | class dirstate(object): | ||
|
r2393 | format = ">cllll" | ||
|
r1089 | def __init__(self, opener, ui, root): | ||
self.opener = opener | ||||
self.root = root | ||||
self.dirty = 0 | ||||
self.ui = ui | ||||
self.map = None | ||||
self.pl = None | ||||
|
r2953 | self.dirs = None | ||
|
r3154 | self.copymap = {} | ||
|
r1089 | self.ignorefunc = None | ||
def wjoin(self, f): | ||||
return os.path.join(self.root, f) | ||||
def getcwd(self): | ||||
cwd = os.getcwd() | ||||
if cwd == self.root: return '' | ||||
|
r3665 | # self.root ends with a path separator if self.root is '/' or 'C:\' | ||
common_prefix_len = len(self.root) | ||||
if not self.root.endswith(os.sep): | ||||
common_prefix_len += 1 | ||||
return cwd[common_prefix_len:] | ||||
|
r1089 | |||
|
r1270 | def hgignore(self): | ||
|
r2003 | '''return the contents of .hgignore files as a list of patterns. | ||
the files parsed for patterns include: | ||||
.hgignore in the repository root | ||||
any additional files specified in the [ui] section of ~/.hgrc | ||||
|
r1270 | |||
trailing white space is dropped. | ||||
the escape character is backslash. | ||||
comments start with #. | ||||
empty lines are skipped. | ||||
lines can be of the following formats: | ||||
syntax: regexp # defaults following lines to non-rooted regexps | ||||
syntax: glob # defaults following lines to non-rooted globs | ||||
re:pattern # non-rooted regular expression | ||||
glob:pattern # non-rooted glob | ||||
pattern # pattern of the current default type''' | ||||
syntaxes = {'re': 'relre:', 'regexp': 'relre:', 'glob': 'relglob:'} | ||||
def parselines(fp): | ||||
for line in fp: | ||||
escape = False | ||||
for i in xrange(len(line)): | ||||
if escape: escape = False | ||||
elif line[i] == '\\': escape = True | ||||
elif line[i] == '#': break | ||||
line = line[:i].rstrip() | ||||
if line: yield line | ||||
|
r2004 | repoignore = self.wjoin('.hgignore') | ||
files = [repoignore] | ||||
|
r2003 | files.extend(self.ui.hgignorefiles()) | ||
|
r2005 | pats = {} | ||
|
r2003 | for f in files: | ||
try: | ||||
|
r2005 | pats[f] = [] | ||
|
r2003 | fp = open(f) | ||
syntax = 'relre:' | ||||
for line in parselines(fp): | ||||
if line.startswith('syntax:'): | ||||
s = line[7:].strip() | ||||
try: | ||||
syntax = syntaxes[s] | ||||
except KeyError: | ||||
self.ui.warn(_("%s: ignoring invalid " | ||||
"syntax '%s'\n") % (f, s)) | ||||
continue | ||||
pat = syntax + line | ||||
for s in syntaxes.values(): | ||||
if line.startswith(s): | ||||
pat = line | ||||
break | ||||
|
r2005 | pats[f].append(pat) | ||
|
r2006 | except IOError, inst: | ||
|
r2004 | if f != repoignore: | ||
|
r2006 | self.ui.warn(_("skipping unreadable ignore file" | ||
" '%s': %s\n") % (f, inst.strerror)) | ||||
|
r1270 | return pats | ||
def ignore(self, fn): | ||||
|
r2003 | '''default match function used by dirstate and | ||
localrepository. this honours the repository .hgignore file | ||||
and any other files specified in the [ui] section of .hgrc.''' | ||||
|
r1089 | if not self.ignorefunc: | ||
|
r1271 | ignore = self.hgignore() | ||
|
r2008 | allpats = [] | ||
[allpats.extend(patlist) for patlist in ignore.values()] | ||||
if allpats: | ||||
|
r2005 | try: | ||
files, self.ignorefunc, anypats = ( | ||||
util.matcher(self.root, inc=allpats, src='.hgignore')) | ||||
except util.Abort: | ||||
# Re-raise an exception where the src is the right file | ||||
for f, patlist in ignore.items(): | ||||
files, self.ignorefunc, anypats = ( | ||||
util.matcher(self.root, inc=patlist, src=f)) | ||||
|
r1271 | else: | ||
self.ignorefunc = util.never | ||||
|
r1270 | return self.ignorefunc(fn) | ||
|
r1089 | |||
def __del__(self): | ||||
if self.dirty: | ||||
self.write() | ||||
def __getitem__(self, key): | ||||
try: | ||||
return self.map[key] | ||||
except TypeError: | ||||
|
r1529 | self.lazyread() | ||
|
r1089 | return self[key] | ||
def __contains__(self, key): | ||||
|
r1529 | self.lazyread() | ||
|
r1089 | return key in self.map | ||
def parents(self): | ||||
|
r1529 | self.lazyread() | ||
|
r1089 | return self.pl | ||
def markdirty(self): | ||||
if not self.dirty: | ||||
self.dirty = 1 | ||||
def setparents(self, p1, p2=nullid): | ||||
|
r1529 | self.lazyread() | ||
|
r1089 | self.markdirty() | ||
self.pl = p1, p2 | ||||
def state(self, key): | ||||
try: | ||||
return self[key][0] | ||||
except KeyError: | ||||
return "?" | ||||
|
r1529 | def lazyread(self): | ||
if self.map is None: | ||||
self.read() | ||||
|
r2427 | def parse(self, st): | ||
|
r1089 | self.pl = [st[:20], st[20: 40]] | ||
|
r2427 | # deref fields so they will be local in loop | ||
map = self.map | ||||
|
r3154 | copymap = self.copymap | ||
|
r2427 | format = self.format | ||
unpack = struct.unpack | ||||
|
r1089 | pos = 40 | ||
|
r2427 | e_size = struct.calcsize(format) | ||
|
r1089 | while pos < len(st): | ||
|
r2425 | newpos = pos + e_size | ||
|
r2427 | 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 | ||
|
r2427 | map[f] = e[:4] | ||
|
r2425 | pos = newpos | ||
|
r1089 | |||
|
r2427 | def read(self): | ||
self.map = {} | ||||
self.pl = [nullid, nullid] | ||||
try: | ||||
st = self.opener("dirstate").read() | ||||
if st: | ||||
self.parse(st) | ||||
except IOError, err: | ||||
if err.errno != errno.ENOENT: raise | ||||
|
r1089 | def copy(self, source, dest): | ||
|
r1529 | self.lazyread() | ||
|
r1089 | self.markdirty() | ||
|
r3154 | self.copymap[dest] = source | ||
|
r1089 | |||
def copied(self, file): | ||||
|
r3154 | return self.copymap.get(file, None) | ||
def copies(self): | ||||
return self.copymap | ||||
|
r1089 | |||
|
r2953 | def initdirs(self): | ||
if self.dirs is None: | ||||
self.dirs = {} | ||||
for f in self.map: | ||||
self.updatedirs(f, 1) | ||||
|
r3223 | |||
|
r2953 | def updatedirs(self, path, delta): | ||
if self.dirs is not None: | ||||
for c in strutil.findall(path, '/'): | ||||
pc = path[:c] | ||||
self.dirs.setdefault(pc, 0) | ||||
self.dirs[pc] += delta | ||||
|
r3607 | def checkinterfering(self, files): | ||
|
r2953 | def prefixes(f): | ||
for c in strutil.rfindall(f, '/'): | ||||
yield f[:c] | ||||
self.lazyread() | ||||
self.initdirs() | ||||
seendirs = {} | ||||
for f in files: | ||||
|
r3607 | # shadows | ||
|
r2953 | if self.dirs.get(f): | ||
raise util.Abort(_('directory named %r already in dirstate') % | ||||
f) | ||||
for d in prefixes(f): | ||||
if d in seendirs: | ||||
break | ||||
if d in self.map: | ||||
raise util.Abort(_('file named %r already in dirstate') % | ||||
d) | ||||
seendirs[d] = True | ||||
|
r3607 | # disallowed | ||
if '\r' in f or '\n' in f: | ||||
raise util.Abort(_("'\\n' and '\\r' disallowed in filenames")) | ||||
|
r2953 | |||
|
r1089 | def update(self, files, state, **kw): | ||
''' current states: | ||||
n normal | ||||
m needs merging | ||||
r marked for removal | ||||
a marked for addition''' | ||||
if not files: return | ||||
|
r1529 | self.lazyread() | ||
|
r1089 | self.markdirty() | ||
|
r2953 | if state == "a": | ||
self.initdirs() | ||||
|
r3607 | self.checkinterfering(files) | ||
|
r1089 | for f in files: | ||
if state == "r": | ||||
self.map[f] = ('r', 0, 0, 0) | ||||
|
r2953 | self.updatedirs(f, -1) | ||
|
r1089 | else: | ||
|
r2953 | if state == "a": | ||
self.updatedirs(f, 1) | ||||
|
r1510 | s = os.lstat(self.wjoin(f)) | ||
|
r1089 | st_size = kw.get('st_size', s.st_size) | ||
st_mtime = kw.get('st_mtime', s.st_mtime) | ||||
self.map[f] = (state, s.st_mode, st_size, st_mtime) | ||||
|
r3154 | if self.copymap.has_key(f): | ||
del self.copymap[f] | ||||
|
r1089 | |||
def forget(self, files): | ||||
if not files: return | ||||
|
r1529 | self.lazyread() | ||
|
r1089 | self.markdirty() | ||
|
r2953 | self.initdirs() | ||
|
r1089 | for f in files: | ||
try: | ||||
del self.map[f] | ||||
|
r2953 | self.updatedirs(f, -1) | ||
|
r1089 | except KeyError: | ||
|
r1402 | self.ui.warn(_("not in dirstate: %s!\n") % f) | ||
|
r1089 | pass | ||
def clear(self): | ||||
self.map = {} | ||||
|
r3154 | self.copymap = {} | ||
|
r2953 | self.dirs = None | ||
|
r1755 | self.markdirty() | ||
def rebuild(self, parent, files): | ||||
self.clear() | ||||
|
r2832 | for f in files: | ||
if files.execf(f): | ||||
|
r3842 | self.map[f] = ('n', 0777, -1, 0) | ||
|
r1755 | else: | ||
|
r3842 | self.map[f] = ('n', 0666, -1, 0) | ||
|
r1755 | self.pl = (parent, nullid) | ||
|
r1089 | self.markdirty() | ||
def write(self): | ||||
|
r1794 | if not self.dirty: | ||
return | ||||
|
r1529 | st = self.opener("dirstate", "w", atomic=True) | ||
|
r1089 | st.write("".join(self.pl)) | ||
for f, e in self.map.items(): | ||||
c = self.copied(f) | ||||
if c: | ||||
f = f + "\0" + c | ||||
|
r2393 | e = struct.pack(self.format, e[0], e[1], e[2], e[3], len(f)) | ||
|
r1089 | st.write(e + f) | ||
self.dirty = 0 | ||||
def filterfiles(self, files): | ||||
ret = {} | ||||
unknown = [] | ||||
for x in files: | ||||
|
r1541 | if x == '.': | ||
|
r1089 | return self.map.copy() | ||
if x not in self.map: | ||||
unknown.append(x) | ||||
else: | ||||
ret[x] = self.map[x] | ||||
if not unknown: | ||||
return ret | ||||
b = self.map.keys() | ||||
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): | ||
|
r2486 | ret[s] = self.map[s] | ||
|
r1089 | else: | ||
break | ||||
bs += 1 | ||||
return ret | ||||
|
r1527 | def supported_type(self, f, st, verbose=False): | ||
|
r1487 | if stat.S_ISREG(st.st_mode): | ||
return True | ||||
if verbose: | ||||
kind = 'unknown' | ||||
if stat.S_ISCHR(st.st_mode): kind = _('character device') | ||||
elif stat.S_ISBLK(st.st_mode): kind = _('block device') | ||||
elif stat.S_ISFIFO(st.st_mode): kind = _('fifo') | ||||
elif stat.S_ISLNK(st.st_mode): kind = _('symbolic link') | ||||
elif stat.S_ISSOCK(st.st_mode): kind = _('socket') | ||||
elif stat.S_ISDIR(st.st_mode): kind = _('directory') | ||||
self.ui.warn(_('%s: unsupported file type (type is %s)\n') % ( | ||||
util.pathto(self.getcwd(), f), | ||||
kind)) | ||||
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, | ||||
|
r2042 | badmatch=None): | ||
|
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 | ||||
'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. | ||
''' | ||||
|
r1529 | self.lazyread() | ||
|
r1089 | |||
# walk all files by default | ||||
if not files: | ||||
|
r4172 | files = ['.'] | ||
|
r3529 | dc = self.map.copy() | ||
else: | ||||
|
r3536 | files = util.unique(files) | ||
|
r1089 | dc = self.filterfiles(files) | ||
|
r3529 | def imatch(file_): | ||
|
r3534 | if file_ not in dc and self.ignore(file_): | ||
|
r1183 | return False | ||
|
r1749 | return match(file_) | ||
|
r1224 | |||
|
r3534 | if ignored: imatch = match | ||
|
r2671 | # self.root may end with a path separator when self.root == '/' | ||
common_prefix_len = len(self.root) | ||||
|
r4075 | if not self.root.endswith(os.sep): | ||
|
r2671 | common_prefix_len += 1 | ||
|
r1183 | # recursion free walker, faster than os.walk. | ||
def findfiles(s): | ||||
work = [s] | ||||
while work: | ||||
top = work.pop() | ||||
names = os.listdir(top) | ||||
names.sort() | ||||
# nd is the top of the repository dir tree | ||||
|
r2671 | nd = util.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. | ||||
|
r2061 | hg = bisect.bisect_left(names, '.hg') | ||
if hg < len(names) and names[hg] == '.hg': | ||||
if os.path.isdir(os.path.join(top, '.hg')): | ||||
continue | ||||
|
r1183 | for f in names: | ||
|
r1562 | np = util.pconvert(os.path.join(nd, f)) | ||
|
r1183 | if seen(np): | ||
continue | ||||
p = os.path.join(top, f) | ||||
|
r1228 | # don't trip over symlinks | ||
st = os.lstat(p) | ||||
|
r1183 | if stat.S_ISDIR(st.st_mode): | ||
|
r3534 | ds = util.pconvert(os.path.join(nd, f +'/')) | ||
|
r3529 | if imatch(ds): | ||
|
r1183 | work.append(p) | ||
|
r3529 | if imatch(np) and np in dc: | ||
|
r1562 | yield 'm', np, st | ||
|
r3529 | elif imatch(np): | ||
|
r1487 | if self.supported_type(np, st): | ||
|
r1562 | yield 'f', np, st | ||
|
r1487 | elif np in dc: | ||
|
r1562 | yield 'm', np, st | ||
|
r1392 | |||
|
r1089 | known = {'.hg': 1} | ||
def seen(fn): | ||||
if fn in known: return True | ||||
known[fn] = 1 | ||||
|
r1183 | |||
# step one, find all files that match our criteria | ||||
files.sort() | ||||
|
r3536 | for ff in files: | ||
nf = util.normpath(ff) | ||||
|
r1510 | f = self.wjoin(ff) | ||
|
r1183 | try: | ||
|
r1230 | st = os.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: | ||
self.ui.warn('%s: %s\n' % ( | ||||
util.pathto(self.getcwd(), ff), | ||||
inst.strerror)) | ||||
|
r3534 | elif badmatch and badmatch(ff) and imatch(nf): | ||
|
r2042 | yield 'b', ff, None | ||
|
r1183 | continue | ||
if stat.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: | ||
|
r3536 | if not seen(nf) and match(nf): | ||
|
r1527 | if self.supported_type(ff, st, 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: | ||||
|
r3529 | if not seen(k) and imatch(k): | ||
|
r1471 | yield 'm', k, None | ||
|
r1089 | |||
|
r2661 | def status(self, files=None, match=util.always, list_ignored=False, | ||
list_clean=False): | ||||
|
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: | ||
|
r1749 | type_, mode, size, time = self[fn] | ||
|
r1471 | except KeyError: | ||
|
r2661 | 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: | ||||
|
r2429 | st = os.lstat(self.wjoin(fn)) | ||
|
r1487 | except OSError, inst: | ||
if inst.errno != errno.ENOENT: | ||||
raise | ||||
st = None | ||||
# We need to re-check that it is a valid file | ||||
if st and self.supported_type(fn, st): | ||||
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: | ||
|
r2448 | st = os.lstat(self.wjoin(fn)) | ||
|
r1755 | if size >= 0 and (size != st.st_size | ||
or (mode ^ st.st_mode) & 0100): | ||||
|
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) | ||||