dirstate.py
473 lines
| 15.8 KiB
| text/x-python
|
PythonLexer
/ mercurial / dirstate.py
|
r1089 | """ | ||
dirstate.py - working directory tracking for mercurial | ||||
Copyright 2005 Matt Mackall <mpm@selenic.com> | ||||
This software may be used and distributed according to the terms | ||||
of the GNU General Public License, incorporated herein by reference. | ||||
""" | ||||
|
r1094 | import struct, os | ||
from node import * | ||||
|
r1400 | from i18n import gettext as _ | ||
|
r1089 | from demandload import * | ||
|
r1476 | demandload(globals(), "time bisect stat util re errno") | ||
|
r1089 | |||
|
r1559 | class dirstate(object): | ||
|
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 | ||||
self.copies = {} | ||||
self.ignorefunc = None | ||||
|
r1183 | self.blockignore = False | ||
|
r1089 | |||
def wjoin(self, f): | ||||
return os.path.join(self.root, f) | ||||
def getcwd(self): | ||||
cwd = os.getcwd() | ||||
if cwd == self.root: return '' | ||||
return cwd[len(self.root) + 1:] | ||||
|
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.''' | ||||
|
r1183 | if self.blockignore: | ||
return False | ||||
|
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() | ||||
|
r1089 | def read(self): | ||
self.map = {} | ||||
self.pl = [nullid, nullid] | ||||
try: | ||||
st = self.opener("dirstate").read() | ||||
if not st: return | ||||
except: return | ||||
self.pl = [st[:20], st[20: 40]] | ||||
pos = 40 | ||||
while pos < len(st): | ||||
e = struct.unpack(">cllll", st[pos:pos+17]) | ||||
l = e[4] | ||||
pos += 17 | ||||
f = st[pos:pos + l] | ||||
if '\0' in f: | ||||
f, c = f.split('\0') | ||||
self.copies[f] = c | ||||
self.map[f] = e[:4] | ||||
pos += l | ||||
def copy(self, source, dest): | ||||
|
r1529 | self.lazyread() | ||
|
r1089 | self.markdirty() | ||
self.copies[dest] = source | ||||
def copied(self, file): | ||||
return self.copies.get(file, None) | ||||
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() | ||
for f in files: | ||||
if state == "r": | ||||
self.map[f] = ('r', 0, 0, 0) | ||||
else: | ||||
|
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) | ||||
|
r1117 | if self.copies.has_key(f): | ||
del self.copies[f] | ||||
|
r1089 | |||
def forget(self, files): | ||||
if not files: return | ||||
|
r1529 | self.lazyread() | ||
|
r1089 | self.markdirty() | ||
for f in files: | ||||
try: | ||||
del self.map[f] | ||||
except KeyError: | ||||
|
r1402 | self.ui.warn(_("not in dirstate: %s!\n") % f) | ||
|
r1089 | pass | ||
def clear(self): | ||||
self.map = {} | ||||
|
r1755 | self.copies = {} | ||
self.markdirty() | ||||
def rebuild(self, parent, files): | ||||
self.clear() | ||||
umask = os.umask(0) | ||||
os.umask(umask) | ||||
for f, mode in files: | ||||
if mode: | ||||
self.map[f] = ('n', ~umask, -1, 0) | ||||
else: | ||||
self.map[f] = ('n', ~umask & 0666, -1, 0) | ||||
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 | ||||
e = struct.pack(">cllll", e[0], e[1], e[2], e[3], len(f)) | ||||
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: | ||||
bs = bisect.bisect(b, x) | ||||
if bs != 0 and b[bs-1] == x: | ||||
ret[x] = self.map[x] | ||||
continue | ||||
while bs < blen: | ||||
s = b[bs] | ||||
if len(s) > len(x) and s.startswith(x) and s[len(x)] == '/': | ||||
ret[s] = self.map[s] | ||||
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 | ||||
|
r2042 | def statwalk(self, files=None, match=util.always, dc=None, ignored=False, | ||
badmatch=None): | ||||
|
r1529 | self.lazyread() | ||
|
r1089 | |||
# walk all files by default | ||||
if not files: | ||||
files = [self.root] | ||||
if not dc: | ||||
dc = self.map.copy() | ||||
elif not dc: | ||||
dc = self.filterfiles(files) | ||||
|
r1749 | def statmatch(file_, stat): | ||
file_ = util.pconvert(file_) | ||||
|
r2022 | if not ignored and file_ not in dc and self.ignore(file_): | ||
|
r1183 | return False | ||
|
r1749 | return match(file_) | ||
|
r1224 | |||
|
r2042 | return self.walkhelper(files=files, statmatch=statmatch, dc=dc, | ||
badmatch=badmatch) | ||||
|
r1183 | |||
|
r2042 | def walk(self, files=None, match=util.always, dc=None, badmatch=None): | ||
|
r1471 | # filter out the stat | ||
|
r2042 | for src, f, st in self.statwalk(files, match, dc, badmatch=badmatch): | ||
|
r1471 | yield src, f | ||
|
r1183 | # walk recursively through the directory tree, finding all files | ||
# matched by the statmatch function | ||||
|
r1224 | # | ||
|
r1471 | # results are yielded in a tuple (src, filename, st), where src | ||
# is one of: | ||||
|
r1183 | # 'f' the file was found in the directory tree | ||
# 'm' the file was only in the dirstate and not in the tree | ||||
|
r1471 | # and st is the stat result if the file was found in the directory. | ||
|
r1183 | # | ||
# dc is an optional arg for the current dirstate. dc is not modified | ||||
# directly by this function, but might be modified by your statmatch call. | ||||
# | ||||
|
r2042 | def walkhelper(self, files, statmatch, dc, badmatch=None): | ||
|
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 | ||||
nd = util.normpath(top[len(self.root) + 1:]) | ||||
|
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): | ||
ds = os.path.join(nd, f +'/') | ||||
if statmatch(ds, st): | ||||
work.append(p) | ||||
|
r1487 | if statmatch(np, st) and np in dc: | ||
|
r1562 | yield 'm', np, st | ||
|
r1487 | elif statmatch(np, st): | ||
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() | ||||
for ff in util.unique(files): | ||||
|
r1510 | f = self.wjoin(ff) | ||
|
r1183 | try: | ||
|
r1230 | st = os.lstat(f) | ||
|
r1183 | except OSError, inst: | ||
|
r1564 | nf = util.normpath(ff) | ||
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)) | ||||
elif badmatch and badmatch(ff) and statmatch(ff, None): | ||||
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: | ||
|
r1183 | ff = util.normpath(ff) | ||
if seen(ff): | ||||
|
r1089 | continue | ||
|
r1183 | self.blockignore = True | ||
|
r1487 | if statmatch(ff, st): | ||
|
r1527 | if self.supported_type(ff, st, verbose=True): | ||
|
r1487 | yield 'f', ff, st | ||
elif ff in dc: | ||||
yield 'm', ff, st | ||||
|
r1183 | self.blockignore = False | ||
|
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: | ||||
if not seen(k) and (statmatch(k, None)): | ||||
|
r1471 | yield 'm', k, None | ||
|
r1089 | |||
|
r2022 | def changes(self, files=None, match=util.always, show_ignored=None): | ||
lookup, modified, added, unknown, ignored = [], [], [], [], [] | ||||
|
r1089 | removed, deleted = [], [] | ||
|
r2022 | for src, fn, st in self.statwalk(files, match, ignored=show_ignored): | ||
|
r1471 | try: | ||
|
r1749 | type_, mode, size, time = self[fn] | ||
|
r1471 | except KeyError: | ||
|
r2022 | if show_ignored and self.ignore(fn): | ||
ignored.append(fn) | ||||
else: | ||||
unknown.append(fn) | ||||
|
r1471 | continue | ||
|
r1476 | if src == 'm': | ||
|
r1487 | nonexistent = True | ||
if not st: | ||||
try: | ||||
|
r1510 | f = self.wjoin(fn) | ||
|
r1491 | st = os.lstat(f) | ||
|
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: | ||
st = os.stat(fn) | ||||
|
r1755 | if size >= 0 and (size != st.st_size | ||
or (mode ^ st.st_mode) & 0100): | ||||
|
r1471 | modified.append(fn) | ||
elif time != st.st_mtime: | ||||
lookup.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 | |||
|
r2022 | return (lookup, modified, added, removed, deleted, unknown, ignored) | ||