dirstate.py
420 lines
| 13.5 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): | ||
'''return the contents of .hgignore as a list of patterns. | ||||
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 | ||||
pats = [] | ||||
try: | ||||
fp = open(self.wjoin('.hgignore')) | ||||
syntax = 'relre:' | ||||
for line in parselines(fp): | ||||
if line.startswith('syntax:'): | ||||
s = line[7:].strip() | ||||
try: | ||||
syntax = syntaxes[s] | ||||
except KeyError: | ||||
|
r1610 | self.ui.warn(_(".hgignore: ignoring invalid " | ||
"syntax '%s'\n") % s) | ||||
|
r1270 | continue | ||
pat = syntax + line | ||||
for s in syntaxes.values(): | ||||
if line.startswith(s): | ||||
pat = line | ||||
break | ||||
pats.append(pat) | ||||
except IOError: pass | ||||
return pats | ||||
def ignore(self, fn): | ||||
'''default match function used by dirstate and localrepository. | ||||
this honours the .hgignore file, and nothing more.''' | ||||
|
r1183 | if self.blockignore: | ||
return False | ||||
|
r1089 | if not self.ignorefunc: | ||
|
r1271 | ignore = self.hgignore() | ||
if ignore: | ||||
files, self.ignorefunc, anypats = util.matcher(self.root, | ||||
|
r1610 | inc=ignore, | ||
src='.hgignore') | ||||
|
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 = {} | ||||
self.markdirty() | ||||
def write(self): | ||||
|
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 | ||||
|
r1471 | def statwalk(self, files=None, match=util.always, dc=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) | ||||
|
r1183 | def statmatch(file, stat): | ||
|
r1224 | file = util.pconvert(file) | ||
|
r1183 | if file not in dc and self.ignore(file): | ||
return False | ||||
return match(file) | ||||
|
r1224 | |||
|
r1183 | return self.walkhelper(files=files, statmatch=statmatch, dc=dc) | ||
|
r1471 | def walk(self, files=None, match=util.always, dc=None): | ||
# filter out the stat | ||||
for src, f, st in self.statwalk(files, match, dc): | ||||
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. | ||||
# | ||||
def walkhelper(self, files, statmatch, dc): | ||||
# 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:]) | ||||
if nd == '.': nd = '' | ||||
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: | ||||
self.ui.warn('%s: %s\n' % ( | ||||
util.pathto(self.getcwd(), ff), | ||||
inst.strerror)) | ||||
|
r1183 | continue | ||
if stat.S_ISDIR(st.st_mode): | ||||
|
r1487 | cmp1 = (lambda x, y: cmp(x[1], y[1])) | ||
|
r1183 | sorted = [ x for x in findfiles(f) ] | ||
|
r1487 | sorted.sort(cmp1) | ||
for e in sorted: | ||||
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 | |||
def changes(self, files=None, match=util.always): | ||||
lookup, modified, added, unknown = [], [], [], [] | ||||
removed, deleted = [], [] | ||||
|
r1471 | for src, fn, st in self.statwalk(files, match): | ||
try: | ||||
type, mode, size, time = self[fn] | ||||
except KeyError: | ||||
|
r1270 | 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 ? | ||||
|
r1488 | if nonexistent and type in "nm": | ||
|
r1476 | deleted.append(fn) | ||
continue | ||||
|
r1471 | # check the common case first | ||
if type == 'n': | ||||
if not st: | ||||
st = os.stat(fn) | ||||
if size != st.st_size or (mode ^ st.st_mode) & 0100: | ||||
modified.append(fn) | ||||
elif time != st.st_mtime: | ||||
lookup.append(fn) | ||||
elif type == 'm': | ||||
modified.append(fn) | ||||
elif type == 'a': | ||||
added.append(fn) | ||||
elif type == 'r': | ||||
removed.append(fn) | ||||
|
r1183 | |||
|
r1617 | return (lookup, modified, added, removed, deleted, unknown) | ||