Mercurial > hg
changeset 1808:7036cd7f770d
Add mq extension
author | mason@suse.com |
---|---|
date | Tue, 28 Feb 2006 12:25:26 -0600 |
parents | f1f43ea22cbf |
children | ef53d200df3d |
files | hgext/mq.py |
diffstat | 1 files changed, 1286 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/mq.py Tue Feb 28 12:25:26 2006 -0600 @@ -0,0 +1,1286 @@ +#!/usr/bin/env python +# queue.py - patch queues for mercurial +# +# Copyright 2005 Chris Mason <mason@suse.com> +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +from mercurial.demandload import * +demandload(globals(), "os sys re struct traceback errno bz2") +from mercurial.i18n import gettext as _ +from mercurial import ui, hg, revlog, commands, util + +versionstr = "0.45" + +repomap = {} + +class queue: + def __init__(self, ui, path, patchdir=None): + self.opener = util.opener(path) + self.basepath = path + if patchdir: + self.path = patchdir + else: + self.path = os.path.join(path, "patches") + self.ui = ui + self.applied = [] + self.full_series = [] + self.applied_dirty = 0 + self.series_dirty = 0 + self.series_path = os.path.join(self.path, "series") + self.status_path = os.path.join(self.path, "status") + + s = self.series_path + if os.path.exists(s): + self.full_series = self.opener(s).read().splitlines() + self.read_series(self.full_series) + + s = self.status_path + if os.path.exists(s): + self.applied = self.opener(s).read().splitlines() + + def find_series(self, patch): + pre = re.compile("(\s*)([^#]+)") + index = 0 + for l in self.full_series: + m = pre.match(l) + if m: + s = m.group(2) + s = s.rstrip() + if s == patch: + return index + index += 1 + return None + + def read_series(self, list): + def matcher(list): + pre = re.compile("(\s*)([^#]+)") + for l in list: + m = pre.match(l) + if m: + s = m.group(2) + s = s.rstrip() + if len(s) > 0: + yield s + self.series = [] + self.series = [ x for x in matcher(list) ] + + def save_dirty(self): + if self.applied_dirty: + if len(self.applied) > 0: + nl = "\n" + else: + nl = "" + f = self.opener(self.status_path, "w") + f.write("\n".join(self.applied) + nl) + if self.series_dirty: + if len(self.full_series) > 0: + nl = "\n" + else: + nl = "" + f = self.opener(self.series_path, "w") + f.write("\n".join(self.full_series) + nl) + + def readheaders(self, patch): + def eatdiff(lines): + while lines: + l = lines[-1] + if l.startswith("diff -") or \ + l.startswith("Index:") or \ + l.startswith("==========="): + del lines[-1] + else: + break + def eatempty(lines): + while lines: + l = lines[-1] + if re.match('\s*$', l): + del lines[-1] + else: + break + + pf = os.path.join(self.path, patch) + message = [] + comments = [] + user = None + format = None + subject = None + diffstart = 0 + + for line in file(pf): + line = line.rstrip() + if diffstart: + if line.startswith('+++ '): + diffstart = 2 + break + if line.startswith("--- "): + diffstart = 1 + continue + elif format == "hgpatch": + # parse values when importing the result of an hg export + if line.startswith("# User "): + user = line[7:] + elif not line.startswith("# ") and line: + message.append(line) + format = None + elif line == '# HG changeset patch': + format = "hgpatch" + elif format != "tagdone" and \ + (line.startswith("Subject: ") or \ + line.startswith("subject: ")): + subject = line[9:] + format = "tag" + elif format != "tagdone" and \ + (line.startswith("From: ") or \ + line.startswith("from: ")): + user = line[6:] + format = "tag" + elif format == "tag" and line == "": + # when looking for tags (subject: from: etc) they + # end once you find a blank line in the source + format = "tagdone" + else: + message.append(line) + comments.append(line) + + eatdiff(message) + eatdiff(comments) + eatempty(message) + eatempty(comments) + + # make sure message isn't empty + if format and format.startswith("tag") and subject: + message.insert(0, "") + message.insert(0, subject) + return (message, comments, user, diffstart > 1) + + def mergeone(self, repo, mergeq, head, patch, rev, wlock): + # first try just applying the patch + (err, n) = self.apply(repo, [ patch ], update_status=False, + strict=True, merge=rev, wlock=wlock) + + if err == 0: + return (err, n) + + if n is None: + self.ui.warn("apply failed for patch %s\n" % patch) + sys.exit(1) + + self.ui.warn("patch didn't work out, merging %s\n" % patch) + + # apply failed, strip away that rev and merge. + repo.update(head, allow=False, force=True, wlock=wlock) + self.strip(repo, n, update=False, backup='strip', wlock=wlock) + + c = repo.changelog.read(rev) + ret = repo.update(rev, allow=True, wlock=wlock) + if ret: + self.ui.warn("update returned %d\n" % ret) + sys.exit(1) + n = repo.commit(None, c[4], c[1], force=1, wlock=wlock) + if n == None: + self.ui.warn("repo commit failed\n") + sys.exit(1) + try: + message, comments, user, patchfound = mergeq.readheaders(patch) + except: + self.ui.warn("Unable to read %s\n" % patch) + sys.exit(1) + + patchf = self.opener(os.path.join(self.path, patch), "w") + if comments: + comments = "\n".join(comments) + '\n\n' + patchf.write(comments) + commands.dodiff(patchf, self.ui, repo, head, n) + patchf.close() + return (0, n) + + def qparents(self, repo, rev=None): + if rev is None: + (p1, p2) = repo.dirstate.parents() + if p2 == revlog.nullid: + return p1 + if len(self.applied) == 0: + return None + (top, patch) = self.applied[-1].split(':') + top = revlog.bin(top) + return top + pp = repo.changelog.parents(rev) + if pp[1] != revlog.nullid: + arevs = [ x.split(':')[0] for x in self.applied ] + p0 = revlog.hex(pp[0]) + p1 = revlog.hex(pp[1]) + if p0 in arevs: + return pp[0] + if p1 in arevs: + return pp[1] + return None + return pp[0] + + def mergepatch(self, repo, mergeq, series, wlock): + if len(self.applied) == 0: + # each of the patches merged in will have two parents. This + # can confuse the qrefresh, qdiff, and strip code because it + # needs to know which parent is actually in the patch queue. + # so, we insert a merge marker with only one parent. This way + # the first patch in the queue is never a merge patch + # + pname = ".hg.patches.merge.marker" + n = repo.commit(None, '[mq]: merge marker', user=None, force=1, + wlock=wlock) + self.applied.append(revlog.hex(n) + ":" + pname) + self.applied_dirty = 1 + + head = self.qparents(repo) + + for patch in series: + patch = mergeq.lookup(patch) + if not patch: + self.ui.warn("patch %s does not exist\n" % patch) + return (1, None) + + info = mergeq.isapplied(patch) + if not info: + self.ui.warn("patch %s is not applied\n" % patch) + return (1, None) + rev = revlog.bin(info[1]) + (err, head) = self.mergeone(repo, mergeq, head, patch, rev, wlock) + if head: + self.applied.append(revlog.hex(head) + ":" + patch) + self.applied_dirty = 1 + if err: + return (err, head) + return (0, head) + + def apply(self, repo, series, list=False, update_status=True, + strict=False, patchdir=None, merge=None, wlock=None): + # TODO unify with commands.py + if not patchdir: + patchdir = self.path + pwd = os.getcwd() + os.chdir(repo.root) + err = 0 + if not wlock: + wlock = repo.wlock() + lock = repo.lock() + tr = repo.transaction() + n = None + for patch in series: + self.ui.warn("applying %s\n" % patch) + pf = os.path.join(patchdir, patch) + + try: + message, comments, user, patchfound = self.readheaders(patch) + except: + self.ui.warn("Unable to read %s\n" % pf) + err = 1 + break + + if not message: + message = "imported patch %s\n" % patch + else: + if list: + message.append("\nimported patch %s" % patch) + message = '\n'.join(message) + + try: + f = os.popen("patch -p1 --no-backup-if-mismatch < '%s'" % + (pf)) + except: + self.ui.warn("patch failed, unable to continue (try -v)\n") + err = 1 + break + files = [] + fuzz = False + for l in f: + l = l.rstrip('\r\n'); + if self.ui.verbose: + self.ui.warn(l + "\n") + if l[:14] == 'patching file ': + pf = os.path.normpath(l[14:]) + # when patch finds a space in the file name, it puts + # single quotes around the filename. strip them off + if pf[0] == "'" and pf[-1] == "'": + pf = pf[1:-1] + if pf not in files: + files.append(pf) + printed_file = False + file_str = l + elif l.find('with fuzz') >= 0: + if not printed_file: + self.ui.warn(file_str + '\n') + printed_file = True + self.ui.warn(l + '\n') + fuzz = True + elif l.find('saving rejects to file') >= 0: + self.ui.warn(l + '\n') + elif l.find('FAILED') >= 0: + if not printed_file: + self.ui.warn(file_str + '\n') + printed_file = True + self.ui.warn(l + '\n') + patcherr = f.close() + + if merge and len(files) > 0: + # Mark as merged and update dirstate parent info + repo.dirstate.update(repo.dirstate.filterfiles(files), 'm') + p1, p2 = repo.dirstate.parents() + repo.dirstate.setparents(p1, merge) + if len(files) > 0: + commands.addremove_lock(self.ui, repo, files, + opts={}, wlock=wlock) + n = repo.commit(files, message, user, force=1, lock=lock, + wlock=wlock) + + if n == None: + self.ui.warn("repo commit failed\n") + sys.exit(1) + + if update_status: + self.applied.append(revlog.hex(n) + ":" + patch) + + if patcherr: + if not patchfound: + self.ui.warn("patch %s is empty\n" % patch) + err = 0 + else: + self.ui.warn("patch failed, rejects left in working dir\n") + err = 1 + break + + if fuzz and strict: + self.ui.warn("fuzz found when applying patch, stopping\n") + err = 1 + break + tr.close() + os.chdir(pwd) + return (err, n) + + def delete(self, repo, patch): + patch = self.lookup(patch) + info = self.isapplied(patch) + if info: + self.ui.warn("cannot delete applied patch %s\n" % patch) + sys.exit(1) + if patch not in self.series: + self.ui.warn("patch %s not in series file\n" % patch) + sys.exit(1) + i = self.find_series(patch) + del self.full_series[i] + self.read_series(self.full_series) + self.series_dirty = 1 + + def check_toppatch(self, repo): + if len(self.applied) > 0: + (top, patch) = self.applied[-1].split(':') + top = revlog.bin(top) + pp = repo.dirstate.parents() + if top not in pp: + self.ui.warn("queue top not at dirstate parents. top %s dirstate %s %s\n" %( revlog.short(top), revlog.short(pp[0]), revlog.short(pp[1]))) + sys.exit(1) + return top + return None + def check_localchanges(self, repo): + (c, a, r, d, u) = repo.changes(None, None) + if c or a or d or r: + self.ui.write("Local changes found, refresh first\n") + sys.exit(1) + def new(self, repo, patch, msg=None, force=None): + if not force: + self.check_localchanges(repo) + self.check_toppatch(repo) + wlock = repo.wlock() + insert = self.series_end() + if msg: + n = repo.commit([], "[mq]: %s" % msg, force=True, wlock=wlock) + else: + n = repo.commit([], "New patch: %s" % patch, force=True, wlock=wlock) + if n == None: + self.ui.warn("repo commit failed\n") + sys.exit(1) + self.full_series[insert:insert] = [patch] + self.applied.append(revlog.hex(n) + ":" + patch) + self.read_series(self.full_series) + self.series_dirty = 1 + self.applied_dirty = 1 + p = self.opener(os.path.join(self.path, patch), "w") + if msg: + msg = msg + "\n" + p.write(msg) + p.close() + wlock = None + r = self.qrepo() + if r: r.add([patch]) + + def strip(self, repo, rev, update=True, backup="all", wlock=None): + def limitheads(chlog, stop): + """return the list of all nodes that have no children""" + p = {} + h = [] + stoprev = 0 + if stop in chlog.nodemap: + stoprev = chlog.rev(stop) + + for r in range(chlog.count() - 1, -1, -1): + n = chlog.node(r) + if n not in p: + h.append(n) + if n == stop: + break + if r < stoprev: + break + for pn in chlog.parents(n): + p[pn] = 1 + return h + + def bundle(cg): + backupdir = repo.join("strip-backup") + if not os.path.isdir(backupdir): + os.mkdir(backupdir) + name = os.path.join(backupdir, "%s" % revlog.short(rev)) + name = savename(name) + self.ui.warn("saving bundle to %s\n" % name) + # TODO, exclusive open + f = open(name, "wb") + try: + f.write("HG10") + z = bz2.BZ2Compressor(9) + while 1: + chunk = cg.read(4096) + if not chunk: + break + f.write(z.compress(chunk)) + f.write(z.flush()) + except: + os.unlink(name) + raise + f.close() + return name + + def stripall(rev, revnum): + cl = repo.changelog + c = cl.read(rev) + mm = repo.manifest.read(c[0]) + seen = {} + + for x in xrange(revnum, cl.count()): + c = cl.read(cl.node(x)) + for f in c[3]: + if f in seen: + continue + seen[f] = 1 + if f in mm: + filerev = mm[f] + else: + filerev = 0 + seen[f] = filerev + # we go in two steps here so the strip loop happens in a + # sensible order. When stripping many files, this helps keep + # our disk access patterns under control. + list = seen.keys() + list.sort() + for f in list: + ff = repo.file(f) + filerev = seen[f] + if filerev != 0: + if filerev in ff.nodemap: + filerev = ff.rev(filerev) + else: + filerev = 0 + ff.strip(filerev, revnum) + + if not wlock: + wlock = repo.wlock() + lock = repo.lock() + chlog = repo.changelog + # TODO delete the undo files, and handle undo of merge sets + pp = chlog.parents(rev) + revnum = chlog.rev(rev) + + if update: + urev = self.qparents(repo, rev) + repo.update(urev, allow=False, force=True, wlock=wlock) + repo.dirstate.write() + + # save is a list of all the branches we are truncating away + # that we actually want to keep. changegroup will be used + # to preserve them and add them back after the truncate + saveheads = [] + savebases = {} + + tip = chlog.tip() + heads = limitheads(chlog, rev) + seen = {} + + # search through all the heads, finding those where the revision + # we want to strip away is an ancestor. Also look for merges + # that might be turned into new heads by the strip. + while heads: + h = heads.pop() + n = h + while True: + seen[n] = 1 + pp = chlog.parents(n) + if pp[1] != revlog.nullid and chlog.rev(pp[1]) > revnum: + if pp[1] not in seen: + heads.append(pp[1]) + if pp[0] == revlog.nullid: + break + if chlog.rev(pp[0]) < revnum: + break + n = pp[0] + if n == rev: + break + r = chlog.reachable(h, rev) + if rev not in r: + saveheads.append(h) + for x in r: + if chlog.rev(x) > revnum: + savebases[x] = 1 + + # create a changegroup for all the branches we need to keep + if backup is "all": + backupch = repo.changegroupsubset([rev], chlog.heads(), 'strip') + bundle(backupch) + if saveheads: + backupch = repo.changegroupsubset(savebases.keys(), saveheads, 'strip') + chgrpfile = bundle(backupch) + + stripall(rev, revnum) + + change = chlog.read(rev) + repo.manifest.strip(repo.manifest.rev(change[0]), revnum) + chlog.strip(revnum, revnum) + if saveheads: + self.ui.status("adding branch\n") + commands.unbundle(self.ui, repo, chgrpfile, update=False) + if backup is not "strip": + os.unlink(chgrpfile) + + def isapplied(self, patch): + """returns (index, rev, patch)""" + for i in xrange(len(self.applied)): + p = self.applied[i] + a = p.split(':') + if a[1] == patch: + return (i, a[0], a[1]) + return None + + def lookup(self, patch): + if patch == None: + return None + if patch in self.series: + return patch + if not os.path.isfile(os.path.join(self.path, patch)): + try: + sno = int(patch) + except(ValueError, OverflowError): + self.ui.warn("patch %s not in series\n" % patch) + sys.exit(1) + if sno >= len(self.series): + self.ui.warn("patch number %d is out of range\n" % sno) + sys.exit(1) + patch = self.series[sno] + else: + self.ui.warn("patch %s not in series\n" % patch) + sys.exit(1) + return patch + + def push(self, repo, patch=None, force=False, list=False, + mergeq=None, wlock=None): + if not wlock: + wlock = repo.wlock() + patch = self.lookup(patch) + if patch and self.isapplied(patch): + self.ui.warn("patch %s is already applied\n" % patch) + sys.exit(1) + if self.series_end() == len(self.series): + self.ui.warn("File series fully applied\n") + sys.exit(1) + if not force: + self.check_localchanges(repo) + + self.applied_dirty = 1; + start = self.series_end() + if start > 0: + self.check_toppatch(repo) + if not patch: + patch = self.series[start] + end = start + 1 + else: + end = self.series.index(patch, start) + 1 + s = self.series[start:end] + if mergeq: + ret = self.mergepatch(repo, mergeq, s, wlock) + else: + ret = self.apply(repo, s, list, wlock=wlock) + top = self.applied[-1].split(':')[1] + if ret[0]: + self.ui.write("Errors during apply, please fix and refresh %s\n" % + top) + else: + self.ui.write("Now at: %s\n" % top) + return ret[0] + + def pop(self, repo, patch=None, force=False, update=True, wlock=None): + def getfile(f, rev): + t = repo.file(f).read(rev) + try: + repo.wfile(f, "w").write(t) + except IOError: + os.makedirs(os.path.dirname(repo.wjoin(f))) + repo.wfile(f, "w").write(t) + + if not wlock: + wlock = repo.wlock() + if patch: + # index, rev, patch + info = self.isapplied(patch) + if not info: + patch = self.lookup(patch) + info = self.isapplied(patch) + if not info: + self.ui.warn("patch %s is not applied\n" % patch) + sys.exit(1) + if len(self.applied) == 0: + self.ui.warn("No patches applied\n") + sys.exit(1) + + if not update: + parents = repo.dirstate.parents() + rr = [ revlog.bin(x.split(':')[0]) for x in self.applied ] + for p in parents: + if p in rr: + self.ui.warn("qpop: forcing dirstate update\n") + update = True + + if not force and update: + self.check_localchanges(repo) + + self.applied_dirty = 1; + end = len(self.applied) + if not patch: + info = [len(self.applied) - 1] + self.applied[-1].split(':') + start = info[0] + rev = revlog.bin(info[1]) + + # we know there are no local changes, so we can make a simplified + # form of hg.update. + if update: + top = self.check_toppatch(repo) + qp = self.qparents(repo, rev) + changes = repo.changelog.read(qp) + mf1 = repo.manifest.readflags(changes[0]) + mmap = repo.manifest.read(changes[0]) + (c, a, r, d, u) = repo.changes(qp, top) + if d: + raise util.Abort("deletions found between repo revs") + for f in c: + getfile(f, mmap[f]) + for f in r: + getfile(f, mmap[f]) + util.set_exec(repo.wjoin(f), mf1[f]) + repo.dirstate.update(c + r, 'n') + for f in a: + try: os.unlink(repo.wjoin(f)) + except: raise + try: os.removedirs(os.path.dirname(repo.wjoin(f))) + except: pass + if a: + repo.dirstate.forget(a) + repo.dirstate.setparents(qp, revlog.nullid) + self.strip(repo, rev, update=False, backup='strip', wlock=wlock) + del self.applied[start:end] + if len(self.applied): + self.ui.write("Now at: %s\n" % self.applied[-1].split(':')[1]) + else: + self.ui.write("Patch queue now empty\n") + + def diff(self, repo, files): + top = self.check_toppatch(repo) + if not top: + self.ui.write("No patches applied\n") + return + qp = self.qparents(repo, top) + commands.dodiff(sys.stdout, self.ui, repo, qp, None, files) + + def refresh(self, repo, short=False): + if len(self.applied) == 0: + self.ui.write("No patches applied\n") + return + wlock = repo.wlock() + self.check_toppatch(repo) + qp = self.qparents(repo) + (top, patch) = self.applied[-1].split(':') + top = revlog.bin(top) + cparents = repo.changelog.parents(top) + patchparent = self.qparents(repo, top) + message, comments, user, patchfound = self.readheaders(patch) + + patchf = self.opener(os.path.join(self.path, patch), "w") + if comments: + comments = "\n".join(comments) + '\n\n' + patchf.write(comments) + + tip = repo.changelog.tip() + if top == tip: + # if the top of our patch queue is also the tip, there is an + # optimization here. We update the dirstate in place and strip + # off the tip commit. Then just commit the current directory + # tree. We can also send repo.commit the list of files + # changed to speed up the diff + # + # in short mode, we only diff the files included in the + # patch already + # + # this should really read: + #(cc, dd, aa, aa2, uu) = repo.changes(tip, patchparent) + # but we do it backwards to take advantage of manifest/chlog + # caching against the next repo.changes call + # + (cc, aa, dd, aa2, uu) = repo.changes(patchparent, tip) + if short: + filelist = cc + aa + dd + else: + filelist = None + (c, a, r, d, u) = repo.changes(None, None, filelist) + + # we might end up with files that were added between tip and + # the dirstate parent, but then changed in the local dirstate. + # in this case, we want them to only show up in the added section + for x in c: + if x not in aa: + cc.append(x) + # we might end up with files added by the local dirstate that + # were deleted by the patch. In this case, they should only + # show up in the changed section. + for x in a: + if x in dd: + del dd[dd.index(x)] + cc.append(x) + else: + aa.append(x) + # make sure any files deleted in the local dirstate + # are not in the add or change column of the patch + forget = [] + for x in d + r: + if x in aa: + del aa[aa.index(x)] + forget.append(x) + continue + elif x in cc: + del cc[cc.index(x)] + dd.append(x) + + c = list(util.unique(cc)) + r = list(util.unique(dd)) + a = list(util.unique(aa)) + filelist = list(util.unique(c + r + a )) + commands.dodiff(patchf, self.ui, repo, patchparent, None, + filelist, changes=(c, a, r, [], u)) + patchf.close() + + changes = repo.changelog.read(tip) + repo.dirstate.setparents(*cparents) + repo.dirstate.update(a, 'a') + repo.dirstate.update(r, 'r') + repo.dirstate.update(c, 'n') + repo.dirstate.forget(forget) + + if not message: + message = "patch queue: %s\n" % patch + else: + message = "\n".join(message) + self.strip(repo, top, update=False, backup='strip', wlock=wlock) + n = repo.commit(filelist, message, changes[1], force=1, wlock=wlock) + self.applied[-1] = revlog.hex(n) + ':' + patch + self.applied_dirty = 1 + else: + commands.dodiff(patchf, self.ui, repo, patchparent, None) + patchf.close() + self.pop(repo, force=True, wlock=wlock) + self.push(repo, force=True, wlock=wlock) + + def init(self, repo, create=False): + if os.path.isdir(self.path): + raise util.Abort("patch queue directory already exists") + os.mkdir(self.path) + if create: + return self.qrepo(create=True) + + def unapplied(self, repo, patch=None): + if patch and patch not in self.series: + self.ui.warn("%s not in the series file\n" % patch) + sys.exit(1) + if not patch: + start = self.series_end() + else: + start = self.series.index(patch) + 1 + for p in self.series[start:]: + self.ui.write("%s\n" % p) + + def qseries(self, repo, missing=None): + start = self.series_end() + if not missing: + for p in self.series[:start]: + if self.ui.verbose: + self.ui.write("%d A " % self.series.index(p)) + self.ui.write("%s\n" % p) + for p in self.series[start:]: + if self.ui.verbose: + self.ui.write("%d U " % self.series.index(p)) + self.ui.write("%s\n" % p) + else: + list = [] + for root, dirs, files in os.walk(self.path): + d = root[len(self.path) + 1:] + for f in files: + fl = os.path.join(d, f) + if (fl not in self.series and fl != "status" and + fl != "series" and not fl.startswith('.')): + list.append(fl) + list.sort() + if list: + for x in list: + if self.ui.verbose: + self.ui.write("D ") + self.ui.write("%s\n" % x) + + def issaveline(self, l): + name = l.split(':')[1] + if name == '.hg.patches.save.line': + return True + + def qrepo(self, create=False): + if create or os.path.isdir(os.path.join(self.path, ".hg")): + return hg.repository(ui=self.ui, path=self.path, create=create) + + def restore(self, repo, rev, delete=None, qupdate=None): + c = repo.changelog.read(rev) + desc = c[4].strip() + lines = desc.splitlines() + i = 0 + datastart = None + series = [] + applied = [] + qpp = None + for i in xrange(0, len(lines)): + if lines[i] == 'Patch Data:': + datastart = i + 1 + elif lines[i].startswith('Dirstate:'): + l = lines[i].rstrip() + l = l[10:].split(' ') + qpp = [ hg.bin(x) for x in l ] + elif datastart != None: + l = lines[i].rstrip() + index = l.index(':') + id = l[:index] + file = l[index + 1:] + if id: + applied.append(l) + series.append(file) + if datastart == None: + self.ui.warn("No saved patch data found\n") + return 1 + self.ui.warn("restoring status: %s\n" % lines[0]) + self.full_series = series + self.applied = applied + self.read_series(self.full_series) + self.series_dirty = 1 + self.applied_dirty = 1 + heads = repo.changelog.heads() + if delete: + if rev not in heads: + self.ui.warn("save entry has children, leaving it alone\n") + else: + self.ui.warn("removing save entry %s\n" % hg.short(rev)) + pp = repo.dirstate.parents() + if rev in pp: + update = True + else: + update = False + self.strip(repo, rev, update=update, backup='strip') + if qpp: + self.ui.warn("saved queue repository parents: %s %s\n" % + (hg.short(qpp[0]), hg.short(qpp[1]))) + if qupdate: + print "queue directory updating" + r = self.qrepo() + if not r: + self.ui.warn("Unable to load queue repository\n") + return 1 + r.update(qpp[0], allow=False, force=True) + + def save(self, repo, msg=None): + if len(self.applied) == 0: + self.ui.warn("save: no patches applied, exiting\n") + return 1 + if self.issaveline(self.applied[-1]): + self.ui.warn("status is already saved\n") + return 1 + + ar = [ ':' + x for x in self.full_series ] + if not msg: + msg = "hg patches saved state" + else: + msg = "hg patches: " + msg.rstrip('\r\n') + r = self.qrepo() + if r: + pp = r.dirstate.parents() + msg += "\nDirstate: %s %s" % (hg.hex(pp[0]), hg.hex(pp[1])) + msg += "\n\nPatch Data:\n" + text = msg + "\n".join(self.applied) + '\n' + (ar and "\n".join(ar) + + '\n' or "") + n = repo.commit(None, text, user=None, force=1) + if not n: + self.ui.warn("repo commit failed\n") + return 1 + self.applied.append(revlog.hex(n) + ":" + '.hg.patches.save.line') + self.applied_dirty = 1 + + def series_end(self): + end = 0 + if len(self.applied) > 0: + (top, p) = self.applied[-1].split(':') + try: + end = self.series.index(p) + except ValueError: + return 0 + return end + 1 + return end + + def qapplied(self, repo, patch=None): + if patch and patch not in self.series: + self.ui.warn("%s not in the series file\n" % patch) + sys.exit(1) + if not patch: + end = len(self.applied) + else: + end = self.series.index(patch) + 1 + for x in xrange(end): + p = self.appliedname(x) + self.ui.write("%s\n" % p) + + def appliedname(self, index): + p = self.applied[index] + if not self.ui.verbose: + p = p.split(':')[1] + return p + + def top(self, repo): + if len(self.applied): + p = self.appliedname(-1) + self.ui.write(p + '\n') + else: + self.ui.write("No patches applied\n") + + def next(self, repo): + end = self.series_end() + if end == len(self.series): + self.ui.write("All patches applied\n") + else: + self.ui.write(self.series[end] + '\n') + + def prev(self, repo): + if len(self.applied) > 1: + p = self.appliedname(-2) + self.ui.write(p + '\n') + elif len(self.applied) == 1: + self.ui.write("Only one patch applied\n") + else: + self.ui.write("No patches applied\n") + + def qimport(self, repo, files, patch=None, existing=None, force=None): + if len(files) > 1 and patch: + self.ui.warn("-n option not valid when importing multiple files\n") + sys.exit(1) + i = 0 + for filename in files: + if existing: + if not patch: + patch = filename + if not os.path.isfile(os.path.join(self.path, patch)): + self.ui.warn("patch %s does not exist\n" % patch) + sys.exit(1) + else: + try: + text = file(filename).read() + except IOError: + self.ui.warn("Unable to read %s\n" % patch) + sys.exit(1) + if not patch: + patch = os.path.split(filename)[1] + if not force and os.path.isfile(os.path.join(self.path, patch)): + self.ui.warn("patch %s already exists\n" % patch) + sys.exit(1) + patchf = self.opener(os.path.join(self.path, patch), "w") + patchf.write(text) + if patch in self.series: + self.ui.warn("patch %s is already in the series file\n" % patch) + sys.exit(1) + index = self.series_end() + i + self.full_series[index:index] = [patch] + self.read_series(self.full_series) + self.ui.warn("adding %s to series file\n" % patch) + i += 1 + patch = None + self.series_dirty = 1 + +def delete(ui, repo, patch, **opts): + """remove a patch from the series file""" + q = repomap[repo] + q.delete(repo, patch) + q.save_dirty() + return 0 + +def applied(ui, repo, patch=None, **opts): + """print the patches already applied""" + repomap[repo].qapplied(repo, patch) + return 0 + +def unapplied(ui, repo, patch=None, **opts): + """print the patches not yet applied""" + repomap[repo].unapplied(repo, patch) + return 0 + +def qimport(ui, repo, *filename, **opts): + """import a patch""" + q = repomap[repo] + q.qimport(repo, filename, patch=opts['name'], + existing=opts['existing'], force=opts['force']) + q.save_dirty() + return 0 + +def init(ui, repo, **opts): + """init a new queue repository""" + q = repomap[repo] + r = q.init(repo, create=opts['create_repo']) + q.save_dirty() + if r: + fp = r.wopener('.hgignore', 'w') + print >> fp, 'syntax: glob' + print >> fp, 'status' + fp.close() + r.wopener('series', 'w').close() + r.add(['.hgignore', 'series']) + return 0 + +def commit(ui, repo, *pats, **opts): + q = repomap[repo] + r = q.qrepo() + if not r: raise util.Abort('no queue repository') + commands.commit(r.ui, r, *pats, **opts) + +def series(ui, repo, **opts): + """print the entire series file""" + repomap[repo].qseries(repo, missing=opts['missing']) + return 0 + +def top(ui, repo, **opts): + """print the name of the current patch""" + repomap[repo].top(repo) + return 0 + +def next(ui, repo, **opts): + """print the name of the next patch""" + repomap[repo].next(repo) + return 0 + +def prev(ui, repo, **opts): + """print the name of the previous patch""" + repomap[repo].prev(repo) + return 0 + +def new(ui, repo, patch, **opts): + """create a new patch""" + q = repomap[repo] + q.new(repo, patch, msg=opts['message'], force=opts['force']) + q.save_dirty() + return 0 + +def refresh(ui, repo, **opts): + """update the current patch""" + q = repomap[repo] + q.refresh(repo, short=opts['short']) + q.save_dirty() + return 0 + +def diff(ui, repo, *files, **opts): + """diff of the current patch""" + repomap[repo].diff(repo, files) + return 0 + +def lastsavename(path): + (dir, base) = os.path.split(path) + names = os.listdir(dir) + namere = re.compile("%s.([0-9]+)" % base) + max = None + maxname = None + for f in names: + m = namere.match(f) + if m: + index = int(m.group(1)) + if max == None or index > max: + max = index + maxname = f + if maxname: + return (os.path.join(dir, maxname), max) + return (None, None) + +def savename(path): + (last, index) = lastsavename(path) + if last is None: + index = 0 + newpath = path + ".%d" % (index + 1) + return newpath + +def push(ui, repo, patch=None, **opts): + """push the next patch onto the stack""" + q = repomap[repo] + mergeq = None + + if opts['all']: + patch = q.series[-1] + if opts['merge']: + if opts['name']: + newpath = opts['name'] + else: + newpath,i = lastsavename(q.path) + if not newpath: + ui.warn("no saved queues found, please use -n\n") + return 1 + mergeq = queue(ui, repo.join(""), newpath) + ui.warn("merging with queue at: %s\n" % mergeq.path) + ret = q.push(repo, patch, force=opts['force'], list=opts['list'], + mergeq=mergeq) + q.save_dirty() + return ret + +def pop(ui, repo, patch=None, **opts): + """pop the current patch off the stack""" + localupdate = True + if opts['name']: + q = queue(ui, repo.join(""), repo.join(opts['name'])) + ui.warn('using patch queue: %s\n' % q.path) + localupdate = False + else: + q = repomap[repo] + if opts['all'] and len(q.applied) > 0: + patch = q.applied[0].split(':')[1] + q.pop(repo, patch, force=opts['force'], update=localupdate) + q.save_dirty() + return 0 + +def restore(ui, repo, rev, **opts): + """restore the queue state saved by a rev""" + rev = repo.lookup(rev) + q = repomap[repo] + q.restore(repo, rev, delete=opts['delete'], + qupdate=opts['update']) + q.save_dirty() + return 0 + +def save(ui, repo, **opts): + """save current queue state""" + q = repomap[repo] + ret = q.save(repo, msg=opts['message']) + if ret: + return ret + q.save_dirty() + if opts['copy']: + path = q.path + if opts['name']: + newpath = os.path.join(q.basepath, opts['name']) + if os.path.exists(newpath): + if not os.path.isdir(newpath): + ui.warn("destination %s exists and is not a directory\n" % + newpath) + sys.exit(1) + if not opts['force']: + ui.warn("destination %s exists, use -f to force\n" % + newpath) + sys.exit(1) + else: + newpath = savename(path) + ui.warn("copy %s to %s\n" % (path, newpath)) + util.copyfiles(path, newpath) + if opts['empty']: + try: + os.unlink(q.status_path) + except: + pass + return 0 + +def strip(ui, repo, rev, **opts): + """strip a revision and all later revs on the same branch""" + rev = repo.lookup(rev) + backup = 'all' + if opts['backup']: + backup = 'strip' + elif opts['nobackup']: + backup = 'none' + repomap[repo].strip(repo, rev, backup=backup) + return 0 + +def version(ui, q=None): + """print the version number""" + ui.write("mq version %s\n" % versionstr) + return 0 + +def reposetup(ui, repo): + repomap[repo] = queue(ui, repo.join("")) + +cmdtable = { + "qapplied": (applied, [], "hg qapplied [patch]"), + "qcommit|qci": (commit, + [('A', 'addremove', None, _('run addremove during commit')), + ('I', 'include', [], _('include names matching the given patterns')), + ('X', 'exclude', [], _('exclude names matching the given patterns')), + ('m', 'message', "", _('use <text> as commit message')), + ('l', 'logfile', "", _('read the commit message from <file>')), + ('d', 'date', "", _('record datecode as commit date')), + ('u', 'user', "", _('record user as commiter'))], + "hg qcommit [options] [files]"), + "^qdiff": (diff, [], "hg qdiff [files]"), + "qdelete": (delete, [], "hg qdelete [patch]"), + "^qimport": (qimport, [('e', 'existing', None, 'import file in patch dir'), + ('n', 'name', "", 'patch file name'), + ('f', 'force', None, 'overwrite existing files')], + "hg qimport"), + "^qinit": (init, [('c', 'create-repo', None, 'create patch repository')], + "hg [-c] qinit"), + "qnew": (new, [('m', 'message', "", 'commit message'), + ('f', 'force', None, 'force')], + "hg qnew [-m message ] patch"), + "qnext": (next, [], "hg qnext"), + "qprev": (prev, [], "hg qprev"), + "^qpop": (pop, [('a', 'all', None, 'pop all patches'), + ('n', 'name', "", 'queue name to pop'), + ('f', 'force', None, 'forget any local changes')], + 'hg qpop [options] [patch/index]'), + "^qpush": (push, [('f', 'force', None, 'apply if the patch has rejects'), + ('l', 'list', None, 'list patch name in commit text'), + ('a', 'all', None, 'apply all patches'), + ('m', 'merge', None, 'merge from another queue'), + ('n', 'name', "", 'merge queue name')], + 'hg qpush [options] [patch/index]'), + "^qrefresh": (refresh, [('s', 'short', None, 'short refresh')],"hg qrefresh"), + "qrestore": (restore, [('d', 'delete', None, 'delete save entry'), + ('u', 'update', None, 'update queue working dir')], + 'hg qrestore rev'), + "qsave": (save, [('m', 'message', "", 'commit message'), + ('c', 'copy', None, 'copy patch directory'), + ('n', 'name', "", 'copy directory name'), + ('e', 'empty', None, 'clear queue status file'), + ('f', 'force', None, 'force copy')], 'hg qsave'), + "qseries": (series, [('m', 'missing', None, 'print patches not in series')], + "hg qseries"), + "^strip": (strip, [('f', 'force', None, 'force multi-head removal'), + ('b', 'backup', None, 'bundle unrelated changesets'), + ('n', 'nobackup', None, 'no backups')], "hg strip rev"), + "qtop": (top, [], "hg qtop"), + "qunapplied": (unapplied, [], "hg qunapplied [patch]"), + "qversion": (version, [], "hg qversion") +} +