diff --git a/mercurial/hg.py b/mercurial/localrepo.py copy from mercurial/hg.py copy to mercurial/localrepo.py --- a/mercurial/hg.py +++ b/mercurial/localrepo.py @@ -1,627 +1,22 @@ -# hg.py - repository classes for mercurial +# localrepo.py - read/write repository class for mercurial # # Copyright 2005 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -import sys, struct, os -import util +import sys, struct, os, util +from repo import * from revlog import * +from filelog import * +from manifest import * +from changelog import * from demandload import * -demandload(globals(), "re lock urllib urllib2 transaction time socket") -demandload(globals(), "tempfile httprangereader bdiff urlparse") -demandload(globals(), "bisect errno select stat") - -class filelog(revlog): - def __init__(self, opener, path): - revlog.__init__(self, opener, - os.path.join("data", self.encodedir(path + ".i")), - os.path.join("data", self.encodedir(path + ".d"))) - - # This avoids a collision between a file named foo and a dir named - # foo.i or foo.d - def encodedir(self, path): - return (path - .replace(".hg/", ".hg.hg/") - .replace(".i/", ".i.hg/") - .replace(".d/", ".d.hg/")) - - def decodedir(self, path): - return (path - .replace(".d.hg/", ".d/") - .replace(".i.hg/", ".i/") - .replace(".hg.hg/", ".hg/")) - - def read(self, node): - t = self.revision(node) - if not t.startswith('\1\n'): - return t - s = t.find('\1\n', 2) - return t[s+2:] - - def readmeta(self, node): - t = self.revision(node) - if not t.startswith('\1\n'): - return t - s = t.find('\1\n', 2) - mt = t[2:s] - for l in mt.splitlines(): - k, v = l.split(": ", 1) - m[k] = v - return m - - def add(self, text, meta, transaction, link, p1=None, p2=None): - if meta or text.startswith('\1\n'): - mt = "" - if meta: - mt = [ "%s: %s\n" % (k, v) for k,v in meta.items() ] - text = "\1\n" + "".join(mt) + "\1\n" + text - return self.addrevision(text, transaction, link, p1, p2) - - def annotate(self, node): - - def decorate(text, rev): - return ([rev] * len(text.splitlines()), text) - - def pair(parent, child): - for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]): - child[0][b1:b2] = parent[0][a1:a2] - return child - - # find all ancestors - needed = {node:1} - visit = [node] - while visit: - n = visit.pop(0) - for p in self.parents(n): - if p not in needed: - needed[p] = 1 - visit.append(p) - else: - # count how many times we'll use this - needed[p] += 1 - - # sort by revision which is a topological order - visit = [ (self.rev(n), n) for n in needed.keys() ] - visit.sort() - hist = {} - - for r,n in visit: - curr = decorate(self.read(n), self.linkrev(n)) - for p in self.parents(n): - if p != nullid: - curr = pair(hist[p], curr) - # trim the history of unneeded revs - needed[p] -= 1 - if not needed[p]: - del hist[p] - hist[n] = curr - - return zip(hist[n][0], hist[n][1].splitlines(1)) - -class manifest(revlog): - def __init__(self, opener): - self.mapcache = None - self.listcache = None - self.addlist = None - revlog.__init__(self, opener, "00manifest.i", "00manifest.d") - - def read(self, node): - if node == nullid: return {} # don't upset local cache - if self.mapcache and self.mapcache[0] == node: - return self.mapcache[1] - text = self.revision(node) - map = {} - flag = {} - self.listcache = (text, text.splitlines(1)) - for l in self.listcache[1]: - (f, n) = l.split('\0') - map[f] = bin(n[:40]) - flag[f] = (n[40:-1] == "x") - self.mapcache = (node, map, flag) - return map - - def readflags(self, node): - if node == nullid: return {} # don't upset local cache - if not self.mapcache or self.mapcache[0] != node: - self.read(node) - return self.mapcache[2] - - def diff(self, a, b): - # this is sneaky, as we're not actually using a and b - if self.listcache and self.addlist and self.listcache[0] == a: - d = mdiff.diff(self.listcache[1], self.addlist, 1) - if mdiff.patch(a, d) != b: - sys.stderr.write("*** sortdiff failed, falling back ***\n") - return mdiff.textdiff(a, b) - return d - else: - return mdiff.textdiff(a, b) - - def add(self, map, flags, transaction, link, p1=None, p2=None, - changed=None): - # directly generate the mdiff delta from the data collected during - # the bisect loop below - def gendelta(delta): - i = 0 - result = [] - while i < len(delta): - start = delta[i][2] - end = delta[i][3] - l = delta[i][4] - if l == None: - l = "" - while i < len(delta) - 1 and start <= delta[i+1][2] \ - and end >= delta[i+1][2]: - if delta[i+1][3] > end: - end = delta[i+1][3] - if delta[i+1][4]: - l += delta[i+1][4] - i += 1 - result.append(struct.pack(">lll", start, end, len(l)) + l) - i += 1 - return result - - # apply the changes collected during the bisect loop to our addlist - def addlistdelta(addlist, delta): - # apply the deltas to the addlist. start from the bottom up - # so changes to the offsets don't mess things up. - i = len(delta) - while i > 0: - i -= 1 - start = delta[i][0] - end = delta[i][1] - if delta[i][4]: - addlist[start:end] = [delta[i][4]] - else: - del addlist[start:end] - return addlist - - # calculate the byte offset of the start of each line in the - # manifest - def calcoffsets(addlist): - offsets = [0] * (len(addlist) + 1) - offset = 0 - i = 0 - while i < len(addlist): - offsets[i] = offset - offset += len(addlist[i]) - i += 1 - offsets[i] = offset - return offsets - - # if we're using the listcache, make sure it is valid and - # parented by the same node we're diffing against - if not changed or not self.listcache or not p1 or \ - self.mapcache[0] != p1: - files = map.keys() - files.sort() - - self.addlist = ["%s\000%s%s\n" % - (f, hex(map[f]), flags[f] and "x" or '') - for f in files] - cachedelta = None - else: - addlist = self.listcache[1] - - # find the starting offset for each line in the add list - offsets = calcoffsets(addlist) - - # combine the changed lists into one list for sorting - work = [[x, 0] for x in changed[0]] - work[len(work):] = [[x, 1] for x in changed[1]] - work.sort() - - delta = [] - bs = 0 - - for w in work: - f = w[0] - # bs will either be the index of the item or the insert point - bs = bisect.bisect(addlist, f, bs) - if bs < len(addlist): - fn = addlist[bs][:addlist[bs].index('\0')] - else: - fn = None - if w[1] == 0: - l = "%s\000%s%s\n" % (f, hex(map[f]), - flags[f] and "x" or '') - else: - l = None - start = bs - if fn != f: - # item not found, insert a new one - end = bs - if w[1] == 1: - sys.stderr.write("failed to remove %s from manifest\n" - % f) - sys.exit(1) - else: - # item is found, replace/delete the existing line - end = bs + 1 - delta.append([start, end, offsets[start], offsets[end], l]) - - self.addlist = addlistdelta(addlist, delta) - if self.mapcache[0] == self.tip(): - cachedelta = "".join(gendelta(delta)) - else: - cachedelta = None - - text = "".join(self.addlist) - if cachedelta and mdiff.patch(self.listcache[0], cachedelta) != text: - sys.stderr.write("manifest delta failure\n") - sys.exit(1) - n = self.addrevision(text, transaction, link, p1, p2, cachedelta) - self.mapcache = (n, map, flags) - self.listcache = (text, self.addlist) - self.addlist = None - - return n - -class changelog(revlog): - def __init__(self, opener): - revlog.__init__(self, opener, "00changelog.i", "00changelog.d") - - def extract(self, text): - if not text: - return (nullid, "", "0", [], "") - last = text.index("\n\n") - desc = text[last + 2:] - l = text[:last].splitlines() - manifest = bin(l[0]) - user = l[1] - date = l[2] - if " " not in date: - date += " 0" # some tools used -d without a timezone - files = l[3:] - return (manifest, user, date, files, desc) - - def read(self, node): - return self.extract(self.revision(node)) - - def add(self, manifest, list, desc, transaction, p1=None, p2=None, - user=None, date=None): - if not date: - if time.daylight: offset = time.altzone - else: offset = time.timezone - date = "%d %d" % (time.time(), offset) - list.sort() - l = [hex(manifest), user, date] + list + ["", desc] - text = "\n".join(l) - return self.addrevision(text, transaction, self.count(), p1, p2) - -class dirstate: - 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 - - 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:] - - def ignore(self, f): - if not self.ignorefunc: - bigpat = [] - try: - l = file(self.wjoin(".hgignore")) - for pat in l: - p = pat.rstrip() - if p: - try: - re.compile(p) - except: - self.ui.warn("ignoring invalid ignore" - + " regular expression '%s'\n" % p) - else: - bigpat.append(p) - except IOError: pass - - if bigpat: - s = "(?:%s)" % (")|(?:".join(bigpat)) - r = re.compile(s) - self.ignorefunc = r.search - else: - self.ignorefunc = util.never - - return self.ignorefunc(f) - - def __del__(self): - if self.dirty: - self.write() - - def __getitem__(self, key): - try: - return self.map[key] - except TypeError: - self.read() - return self[key] - - def __contains__(self, key): - if not self.map: self.read() - return key in self.map - - def parents(self): - if not self.pl: - self.read() - return self.pl - - def markdirty(self): - if not self.dirty: - self.dirty = 1 - - def setparents(self, p1, p2=nullid): - self.markdirty() - self.pl = p1, p2 - - def state(self, key): - try: - return self[key][0] - except KeyError: - return "?" - - def read(self): - if self.map is not None: return self.map - - 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): - self.read() - 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 - self.read() - self.markdirty() - for f in files: - if state == "r": - self.map[f] = ('r', 0, 0, 0) - else: - s = os.stat(os.path.join(self.root, f)) - 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) - - def forget(self, files): - if not files: return - self.read() - self.markdirty() - for f in files: - try: - del self.map[f] - except KeyError: - self.ui.warn("not in dirstate: %s!\n" % f) - pass - - def clear(self): - self.map = {} - self.markdirty() - - def write(self): - st = self.opener("dirstate", "w") - 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: - if x is '.': - 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 - - def walk(self, files=None, match=util.always, dc=None): - self.read() - - # 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) - - known = {'.hg': 1} - def seen(fn): - if fn in known: return True - known[fn] = 1 - def traverse(): - for ff in util.unique(files): - f = os.path.join(self.root, ff) - try: - st = os.stat(f) - except OSError, inst: - if ff not in dc: self.ui.warn('%s: %s\n' % ( - util.pathto(self.getcwd(), ff), - inst.strerror)) - continue - if stat.S_ISDIR(st.st_mode): - for dir, subdirs, fl in os.walk(f): - d = dir[len(self.root) + 1:] - nd = util.normpath(d) - if nd == '.': nd = '' - if seen(nd): - subdirs[:] = [] - continue - for sd in subdirs: - ds = os.path.join(nd, sd +'/') - if self.ignore(ds) or not match(ds): - subdirs.remove(sd) - subdirs.sort() - fl.sort() - for fn in fl: - fn = util.pconvert(os.path.join(d, fn)) - yield 'f', fn - elif stat.S_ISREG(st.st_mode): - yield 'f', ff - else: - 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' - self.ui.warn('%s: unsupported file type (type is %s)\n' % ( - util.pathto(self.getcwd(), ff), - kind)) - - ks = dc.keys() - ks.sort() - for k in ks: - yield 'm', k - - # yield only files that match: all in dirstate, others only if - # not in .hgignore - - for src, fn in util.unique(traverse()): - fn = util.normpath(fn) - if seen(fn): continue - if fn not in dc and self.ignore(fn): - continue - if match(fn): - yield src, fn - - def changes(self, files=None, match=util.always): - self.read() - if not files: - dc = self.map.copy() - else: - dc = self.filterfiles(files) - lookup, modified, added, unknown = [], [], [], [] - removed, deleted = [], [] - - for src, fn in self.walk(files, match, dc=dc): - try: - s = os.stat(os.path.join(self.root, fn)) - except OSError: - continue - if not stat.S_ISREG(s.st_mode): - continue - c = dc.get(fn) - if c: - del dc[fn] - if c[0] == 'm': - modified.append(fn) - elif c[0] == 'a': - added.append(fn) - elif c[0] == 'r': - unknown.append(fn) - elif c[2] != s.st_size or (c[1] ^ s.st_mode) & 0100: - modified.append(fn) - elif c[3] != s.st_mtime: - lookup.append(fn) - else: - unknown.append(fn) - - for fn, c in [(fn, c) for fn, c in dc.items() if match(fn)]: - if c[0] == 'r': - removed.append(fn) - else: - deleted.append(fn) - return (lookup, modified, added, removed + deleted, unknown) - -# used to avoid circular references so destructors work -def opener(base): - p = base - def o(path, mode="r"): - if p.startswith("http://"): - f = os.path.join(p, urllib.quote(path)) - return httprangereader.httprangereader(f) - - f = os.path.join(p, path) - - mode += "b" # for that other OS - - if mode[0] != "r": - try: - s = os.stat(f) - except OSError: - d = os.path.dirname(f) - if not os.path.isdir(d): - os.makedirs(d) - else: - if s.st_nlink > 1: - file(f + ".tmp", "wb").write(file(f, "rb").read()) - util.rename(f+".tmp", f) - - return file(f, mode) - - return o - -class RepoError(Exception): pass +from dirstate import * +demandload(globals(), "re lock transaction tempfile stat") class localrepository: - def __init__(self, ui, path=None, create=0): + def __init__(self, ui, opener, path=None, create=0): self.remote = 0 if path and path.startswith("http://"): self.remote = 1 @@ -2014,283 +1409,3 @@ class localrepository: if errors: self.ui.warn("%d integrity errors encountered!\n" % errors) return 1 - -class remoterepository: - def local(self): - return False - -class httprepository(remoterepository): - def __init__(self, ui, path): - # fix missing / after hostname - s = urlparse.urlsplit(path) - partial = s[2] - if not partial: partial = "/" - self.url = urlparse.urlunsplit((s[0], s[1], partial, '', '')) - self.ui = ui - no_list = [ "localhost", "127.0.0.1" ] - host = ui.config("http_proxy", "host") - if host is None: - host = os.environ.get("http_proxy") - if host and host.startswith('http://'): - host = host[7:] - user = ui.config("http_proxy", "user") - passwd = ui.config("http_proxy", "passwd") - no = ui.config("http_proxy", "no") - if no is None: - no = os.environ.get("no_proxy") - if no: - no_list = no_list + no.split(",") - - no_proxy = 0 - for h in no_list: - if (path.startswith("http://" + h + "/") or - path.startswith("http://" + h + ":") or - path == "http://" + h): - no_proxy = 1 - - # Note: urllib2 takes proxy values from the environment and those will - # take precedence - for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]: - try: - if os.environ.has_key(env): - del os.environ[env] - except OSError: - pass - - proxy_handler = urllib2.BaseHandler() - if host and not no_proxy: - proxy_handler = urllib2.ProxyHandler({"http" : "http://" + host}) - - authinfo = None - if user and passwd: - passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm() - passmgr.add_password(None, host, user, passwd) - authinfo = urllib2.ProxyBasicAuthHandler(passmgr) - - opener = urllib2.build_opener(proxy_handler, authinfo) - urllib2.install_opener(opener) - - def dev(self): - return -1 - - def do_cmd(self, cmd, **args): - self.ui.debug("sending %s command\n" % cmd) - q = {"cmd": cmd} - q.update(args) - qs = urllib.urlencode(q) - cu = "%s?%s" % (self.url, qs) - resp = urllib2.urlopen(cu) - proto = resp.headers['content-type'] - - # accept old "text/plain" and "application/hg-changegroup" for now - if not proto.startswith('application/mercurial') and \ - not proto.startswith('text/plain') and \ - not proto.startswith('application/hg-changegroup'): - raise RepoError("'%s' does not appear to be an hg repository" - % self.url) - - if proto.startswith('application/mercurial'): - version = proto[22:] - if float(version) > 0.1: - raise RepoError("'%s' uses newer protocol %s" % - (self.url, version)) - - return resp - - def heads(self): - d = self.do_cmd("heads").read() - try: - return map(bin, d[:-1].split(" ")) - except: - self.ui.warn("unexpected response:\n" + d[:400] + "\n...\n") - raise - - def branches(self, nodes): - n = " ".join(map(hex, nodes)) - d = self.do_cmd("branches", nodes=n).read() - try: - br = [ tuple(map(bin, b.split(" "))) for b in d.splitlines() ] - return br - except: - self.ui.warn("unexpected response:\n" + d[:400] + "\n...\n") - raise - - def between(self, pairs): - n = "\n".join(["-".join(map(hex, p)) for p in pairs]) - d = self.do_cmd("between", pairs=n).read() - try: - p = [ l and map(bin, l.split(" ")) or [] for l in d.splitlines() ] - return p - except: - self.ui.warn("unexpected response:\n" + d[:400] + "\n...\n") - raise - - def changegroup(self, nodes): - n = " ".join(map(hex, nodes)) - f = self.do_cmd("changegroup", roots=n) - bytes = 0 - - class zread: - def __init__(self, f): - self.zd = zlib.decompressobj() - self.f = f - self.buf = "" - def read(self, l): - while l > len(self.buf): - r = self.f.read(4096) - if r: - self.buf += self.zd.decompress(r) - else: - self.buf += self.zd.flush() - break - d, self.buf = self.buf[:l], self.buf[l:] - return d - - return zread(f) - -class remotelock: - def __init__(self, repo): - self.repo = repo - def release(self): - self.repo.unlock() - self.repo = None - def __del__(self): - if self.repo: - self.release() - -class sshrepository(remoterepository): - def __init__(self, ui, path): - self.url = path - self.ui = ui - - m = re.match(r'ssh://(([^@]+)@)?([^:/]+)(:(\d+))?(/(.*))?', path) - if not m: - raise RepoError("couldn't parse destination %s" % path) - - self.user = m.group(2) - self.host = m.group(3) - self.port = m.group(5) - self.path = m.group(7) or "." - - args = self.user and ("%s@%s" % (self.user, self.host)) or self.host - args = self.port and ("%s -p %s") % (args, self.port) or args - - sshcmd = self.ui.config("ui", "ssh", "ssh") - remotecmd = self.ui.config("ui", "remotecmd", "hg") - cmd = "%s %s '%s -R %s serve --stdio'" - cmd = cmd % (sshcmd, args, remotecmd, self.path) - - self.pipeo, self.pipei, self.pipee = os.popen3(cmd) - - def readerr(self): - while 1: - r,w,x = select.select([self.pipee], [], [], 0) - if not r: break - l = self.pipee.readline() - if not l: break - self.ui.status("remote: ", l) - - def __del__(self): - try: - self.pipeo.close() - self.pipei.close() - for l in self.pipee: - self.ui.status("remote: ", l) - self.pipee.close() - except: - pass - - def dev(self): - return -1 - - def do_cmd(self, cmd, **args): - self.ui.debug("sending %s command\n" % cmd) - self.pipeo.write("%s\n" % cmd) - for k, v in args.items(): - self.pipeo.write("%s %d\n" % (k, len(v))) - self.pipeo.write(v) - self.pipeo.flush() - - return self.pipei - - def call(self, cmd, **args): - r = self.do_cmd(cmd, **args) - l = r.readline() - self.readerr() - try: - l = int(l) - except: - raise RepoError("unexpected response '%s'" % l) - return r.read(l) - - def lock(self): - self.call("lock") - return remotelock(self) - - def unlock(self): - self.call("unlock") - - def heads(self): - d = self.call("heads") - try: - return map(bin, d[:-1].split(" ")) - except: - raise RepoError("unexpected response '%s'" % (d[:400] + "...")) - - def branches(self, nodes): - n = " ".join(map(hex, nodes)) - d = self.call("branches", nodes=n) - try: - br = [ tuple(map(bin, b.split(" "))) for b in d.splitlines() ] - return br - except: - raise RepoError("unexpected response '%s'" % (d[:400] + "...")) - - def between(self, pairs): - n = "\n".join(["-".join(map(hex, p)) for p in pairs]) - d = self.call("between", pairs=n) - try: - p = [ l and map(bin, l.split(" ")) or [] for l in d.splitlines() ] - return p - except: - raise RepoError("unexpected response '%s'" % (d[:400] + "...")) - - def changegroup(self, nodes): - n = " ".join(map(hex, nodes)) - f = self.do_cmd("changegroup", roots=n) - return self.pipei - - def addchangegroup(self, cg): - d = self.call("addchangegroup") - if d: - raise RepoError("push refused: %s", d) - - while 1: - d = cg.read(4096) - if not d: break - self.pipeo.write(d) - self.readerr() - - self.pipeo.flush() - - self.readerr() - l = int(self.pipei.readline()) - return self.pipei.read(l) != "" - -class httpsrepository(httprepository): - pass - -def repository(ui, path=None, create=0): - if path: - if path.startswith("http://"): - return httprepository(ui, path) - if path.startswith("https://"): - return httpsrepository(ui, path) - if path.startswith("hg://"): - return httprepository(ui, path.replace("hg://", "http://")) - if path.startswith("old-http://"): - return localrepository(ui, path.replace("old-http://", "http://")) - if path.startswith("ssh://"): - return sshrepository(ui, path) - - return localrepository(ui, path, create)