diff options
Diffstat (limited to 'eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/mq.py')
-rw-r--r-- | eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/mq.py | 3211 |
1 files changed, 3211 insertions, 0 deletions
diff --git a/eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/mq.py b/eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/mq.py new file mode 100644 index 0000000..5137089 --- /dev/null +++ b/eggs/mercurial-1.7.3-py2.6-linux-x86_64.egg/hgext/mq.py @@ -0,0 +1,3211 @@ +# mq.py - patch queues for mercurial +# +# Copyright 2005, 2006 Chris Mason <mason@suse.com> +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''manage a stack of patches + +This extension lets you work with a stack of patches in a Mercurial +repository. It manages two stacks of patches - all known patches, and +applied patches (subset of known patches). + +Known patches are represented as patch files in the .hg/patches +directory. Applied patches are both patch files and changesets. + +Common tasks (use :hg:`help command` for more details):: + + create new patch qnew + import existing patch qimport + + print patch series qseries + print applied patches qapplied + + add known patch to applied stack qpush + remove patch from applied stack qpop + refresh contents of top applied patch qrefresh + +By default, mq will automatically use git patches when required to +avoid losing file mode changes, copy records, binary files or empty +files creations or deletions. This behaviour can be configured with:: + + [mq] + git = auto/keep/yes/no + +If set to 'keep', mq will obey the [diff] section configuration while +preserving existing git patches upon qrefresh. If set to 'yes' or +'no', mq will override the [diff] section and always generate git or +regular patches, possibly losing data in the second case. + +You will by default be managing a patch queue named "patches". You can +create other, independent patch queues with the :hg:`qqueue` command. +''' + +from mercurial.i18n import _ +from mercurial.node import bin, hex, short, nullid, nullrev +from mercurial.lock import release +from mercurial import commands, cmdutil, hg, patch, util +from mercurial import repair, extensions, url, error +import os, sys, re, errno, shutil + +commands.norepo += " qclone" + +# Patch names looks like unix-file names. +# They must be joinable with queue directory and result in the patch path. +normname = util.normpath + +class statusentry(object): + def __init__(self, node, name): + self.node, self.name = node, name + def __repr__(self): + return hex(self.node) + ':' + self.name + +class patchheader(object): + def __init__(self, pf, plainmode=False): + 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: + if not lines[-1].strip(): + del lines[-1] + else: + break + + message = [] + comments = [] + user = None + date = None + parent = None + format = None + subject = None + diffstart = 0 + + for line in file(pf): + line = line.rstrip() + if (line.startswith('diff --git') + or (diffstart and line.startswith('+++ '))): + diffstart = 2 + break + diffstart = 0 # reset + 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 line.startswith("# Date "): + date = line[7:] + elif line.startswith("# Parent "): + parent = line[9:] + elif not line.startswith("# ") and line: + message.append(line) + format = None + elif line == '# HG changeset patch': + message = [] + 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 != "tagdone" and (line.startswith("Date: ") or + line.startswith("date: "))): + date = 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" + elif message or line: + 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) + + self.message = message + self.comments = comments + self.user = user + self.date = date + self.parent = parent + self.haspatch = diffstart > 1 + self.plainmode = plainmode + + def setuser(self, user): + if not self.updateheader(['From: ', '# User '], user): + try: + patchheaderat = self.comments.index('# HG changeset patch') + self.comments.insert(patchheaderat + 1, '# User ' + user) + except ValueError: + if self.plainmode or self._hasheader(['Date: ']): + self.comments = ['From: ' + user] + self.comments + else: + tmp = ['# HG changeset patch', '# User ' + user, ''] + self.comments = tmp + self.comments + self.user = user + + def setdate(self, date): + if not self.updateheader(['Date: ', '# Date '], date): + try: + patchheaderat = self.comments.index('# HG changeset patch') + self.comments.insert(patchheaderat + 1, '# Date ' + date) + except ValueError: + if self.plainmode or self._hasheader(['From: ']): + self.comments = ['Date: ' + date] + self.comments + else: + tmp = ['# HG changeset patch', '# Date ' + date, ''] + self.comments = tmp + self.comments + self.date = date + + def setparent(self, parent): + if not self.updateheader(['# Parent '], parent): + try: + patchheaderat = self.comments.index('# HG changeset patch') + self.comments.insert(patchheaderat + 1, '# Parent ' + parent) + except ValueError: + pass + self.parent = parent + + def setmessage(self, message): + if self.comments: + self._delmsg() + self.message = [message] + self.comments += self.message + + def updateheader(self, prefixes, new): + '''Update all references to a field in the patch header. + Return whether the field is present.''' + res = False + for prefix in prefixes: + for i in xrange(len(self.comments)): + if self.comments[i].startswith(prefix): + self.comments[i] = prefix + new + res = True + break + return res + + def _hasheader(self, prefixes): + '''Check if a header starts with any of the given prefixes.''' + for prefix in prefixes: + for comment in self.comments: + if comment.startswith(prefix): + return True + return False + + def __str__(self): + if not self.comments: + return '' + return '\n'.join(self.comments) + '\n\n' + + def _delmsg(self): + '''Remove existing message, keeping the rest of the comments fields. + If comments contains 'subject: ', message will prepend + the field and a blank line.''' + if self.message: + subj = 'subject: ' + self.message[0].lower() + for i in xrange(len(self.comments)): + if subj == self.comments[i].lower(): + del self.comments[i] + self.message = self.message[2:] + break + ci = 0 + for mi in self.message: + while mi != self.comments[ci]: + ci += 1 + del self.comments[ci] + +class queue(object): + def __init__(self, ui, path, patchdir=None): + self.basepath = path + try: + fh = open(os.path.join(path, 'patches.queue')) + cur = fh.read().rstrip() + if not cur: + curpath = os.path.join(path, 'patches') + else: + curpath = os.path.join(path, 'patches-' + cur) + except IOError: + curpath = os.path.join(path, 'patches') + self.path = patchdir or curpath + self.opener = util.opener(self.path) + self.ui = ui + self.applied_dirty = 0 + self.series_dirty = 0 + self.added = [] + self.series_path = "series" + self.status_path = "status" + self.guards_path = "guards" + self.active_guards = None + self.guards_dirty = False + # Handle mq.git as a bool with extended values + try: + gitmode = ui.configbool('mq', 'git', None) + if gitmode is None: + raise error.ConfigError() + self.gitmode = gitmode and 'yes' or 'no' + except error.ConfigError: + self.gitmode = ui.config('mq', 'git', 'auto').lower() + self.plainmode = ui.configbool('mq', 'plain', False) + + @util.propertycache + def applied(self): + if os.path.exists(self.join(self.status_path)): + def parse(l): + n, name = l.split(':', 1) + return statusentry(bin(n), name) + lines = self.opener(self.status_path).read().splitlines() + return [parse(l) for l in lines] + return [] + + @util.propertycache + def full_series(self): + if os.path.exists(self.join(self.series_path)): + return self.opener(self.series_path).read().splitlines() + return [] + + @util.propertycache + def series(self): + self.parse_series() + return self.series + + @util.propertycache + def series_guards(self): + self.parse_series() + return self.series_guards + + def invalidate(self): + for a in 'applied full_series series series_guards'.split(): + if a in self.__dict__: + delattr(self, a) + self.applied_dirty = 0 + self.series_dirty = 0 + self.guards_dirty = False + self.active_guards = None + + def diffopts(self, opts={}, patchfn=None): + diffopts = patch.diffopts(self.ui, opts) + if self.gitmode == 'auto': + diffopts.upgrade = True + elif self.gitmode == 'keep': + pass + elif self.gitmode in ('yes', 'no'): + diffopts.git = self.gitmode == 'yes' + else: + raise util.Abort(_('mq.git option can be auto/keep/yes/no' + ' got %s') % self.gitmode) + if patchfn: + diffopts = self.patchopts(diffopts, patchfn) + return diffopts + + def patchopts(self, diffopts, *patches): + """Return a copy of input diff options with git set to true if + referenced patch is a git patch and should be preserved as such. + """ + diffopts = diffopts.copy() + if not diffopts.git and self.gitmode == 'keep': + for patchfn in patches: + patchf = self.opener(patchfn, 'r') + # if the patch was a git patch, refresh it as a git patch + for line in patchf: + if line.startswith('diff --git'): + diffopts.git = True + break + patchf.close() + return diffopts + + def join(self, *p): + return os.path.join(self.path, *p) + + def find_series(self, patch): + def matchpatch(l): + l = l.split('#', 1)[0] + return l.strip() == patch + for index, l in enumerate(self.full_series): + if matchpatch(l): + return index + return None + + guard_re = re.compile(r'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)') + + def parse_series(self): + self.series = [] + self.series_guards = [] + for l in self.full_series: + h = l.find('#') + if h == -1: + patch = l + comment = '' + elif h == 0: + continue + else: + patch = l[:h] + comment = l[h:] + patch = patch.strip() + if patch: + if patch in self.series: + raise util.Abort(_('%s appears more than once in %s') % + (patch, self.join(self.series_path))) + self.series.append(patch) + self.series_guards.append(self.guard_re.findall(comment)) + + def check_guard(self, guard): + if not guard: + return _('guard cannot be an empty string') + bad_chars = '# \t\r\n\f' + first = guard[0] + if first in '-+': + return (_('guard %r starts with invalid character: %r') % + (guard, first)) + for c in bad_chars: + if c in guard: + return _('invalid character in guard %r: %r') % (guard, c) + + def set_active(self, guards): + for guard in guards: + bad = self.check_guard(guard) + if bad: + raise util.Abort(bad) + guards = sorted(set(guards)) + self.ui.debug('active guards: %s\n' % ' '.join(guards)) + self.active_guards = guards + self.guards_dirty = True + + def active(self): + if self.active_guards is None: + self.active_guards = [] + try: + guards = self.opener(self.guards_path).read().split() + except IOError, err: + if err.errno != errno.ENOENT: + raise + guards = [] + for i, guard in enumerate(guards): + bad = self.check_guard(guard) + if bad: + self.ui.warn('%s:%d: %s\n' % + (self.join(self.guards_path), i + 1, bad)) + else: + self.active_guards.append(guard) + return self.active_guards + + def set_guards(self, idx, guards): + for g in guards: + if len(g) < 2: + raise util.Abort(_('guard %r too short') % g) + if g[0] not in '-+': + raise util.Abort(_('guard %r starts with invalid char') % g) + bad = self.check_guard(g[1:]) + if bad: + raise util.Abort(bad) + drop = self.guard_re.sub('', self.full_series[idx]) + self.full_series[idx] = drop + ''.join([' #' + g for g in guards]) + self.parse_series() + self.series_dirty = True + + def pushable(self, idx): + if isinstance(idx, str): + idx = self.series.index(idx) + patchguards = self.series_guards[idx] + if not patchguards: + return True, None + guards = self.active() + exactneg = [g for g in patchguards if g[0] == '-' and g[1:] in guards] + if exactneg: + return False, exactneg[0] + pos = [g for g in patchguards if g[0] == '+'] + exactpos = [g for g in pos if g[1:] in guards] + if pos: + if exactpos: + return True, exactpos[0] + return False, pos + return True, '' + + def explain_pushable(self, idx, all_patches=False): + write = all_patches and self.ui.write or self.ui.warn + if all_patches or self.ui.verbose: + if isinstance(idx, str): + idx = self.series.index(idx) + pushable, why = self.pushable(idx) + if all_patches and pushable: + if why is None: + write(_('allowing %s - no guards in effect\n') % + self.series[idx]) + else: + if not why: + write(_('allowing %s - no matching negative guards\n') % + self.series[idx]) + else: + write(_('allowing %s - guarded by %r\n') % + (self.series[idx], why)) + if not pushable: + if why: + write(_('skipping %s - guarded by %r\n') % + (self.series[idx], why)) + else: + write(_('skipping %s - no matching guards\n') % + self.series[idx]) + + def save_dirty(self): + def write_list(items, path): + fp = self.opener(path, 'w') + for i in items: + fp.write("%s\n" % i) + fp.close() + if self.applied_dirty: + write_list(map(str, self.applied), self.status_path) + if self.series_dirty: + write_list(self.full_series, self.series_path) + if self.guards_dirty: + write_list(self.active_guards, self.guards_path) + if self.added: + qrepo = self.qrepo() + if qrepo: + qrepo[None].add(f for f in self.added if f not in qrepo[None]) + self.added = [] + + def removeundo(self, repo): + undo = repo.sjoin('undo') + if not os.path.exists(undo): + return + try: + os.unlink(undo) + except OSError, inst: + self.ui.warn(_('error removing undo: %s\n') % str(inst)) + + def printdiff(self, repo, diffopts, node1, node2=None, files=None, + fp=None, changes=None, opts={}): + stat = opts.get('stat') + m = cmdutil.match(repo, files, opts) + cmdutil.diffordiffstat(self.ui, repo, diffopts, node1, node2, m, + changes, stat, fp) + + def mergeone(self, repo, mergeq, head, patch, rev, diffopts): + # first try just applying the patch + (err, n) = self.apply(repo, [patch], update_status=False, + strict=True, merge=rev) + + if err == 0: + return (err, n) + + if n is None: + raise util.Abort(_("apply failed for patch %s") % patch) + + self.ui.warn(_("patch didn't work out, merging %s\n") % patch) + + # apply failed, strip away that rev and merge. + hg.clean(repo, head) + self.strip(repo, [n], update=False, backup='strip') + + ctx = repo[rev] + ret = hg.merge(repo, rev) + if ret: + raise util.Abort(_("update returned %d") % ret) + n = repo.commit(ctx.description(), ctx.user(), force=True) + if n is None: + raise util.Abort(_("repo commit failed")) + try: + ph = patchheader(mergeq.join(patch), self.plainmode) + except: + raise util.Abort(_("unable to read %s") % patch) + + diffopts = self.patchopts(diffopts, patch) + patchf = self.opener(patch, "w") + comments = str(ph) + if comments: + patchf.write(comments) + self.printdiff(repo, diffopts, head, n, fp=patchf) + patchf.close() + self.removeundo(repo) + return (0, n) + + def qparents(self, repo, rev=None): + if rev is None: + (p1, p2) = repo.dirstate.parents() + if p2 == nullid: + return p1 + if not self.applied: + return None + return self.applied[-1].node + p1, p2 = repo.changelog.parents(rev) + if p2 != nullid and p2 in [x.node for x in self.applied]: + return p2 + return p1 + + def mergepatch(self, repo, mergeq, series, diffopts): + if not self.applied: + # 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('[mq]: merge marker', force=True) + self.removeundo(repo) + self.applied.append(statusentry(n, pname)) + self.applied_dirty = 1 + + head = self.qparents(repo) + + for patch in series: + patch = mergeq.lookup(patch, strict=True) + if not patch: + self.ui.warn(_("patch %s does not exist\n") % patch) + return (1, None) + pushable, reason = self.pushable(patch) + if not pushable: + self.explain_pushable(patch, all_patches=True) + continue + info = mergeq.isapplied(patch) + if not info: + self.ui.warn(_("patch %s is not applied\n") % patch) + return (1, None) + rev = info[1] + err, head = self.mergeone(repo, mergeq, head, patch, rev, diffopts) + if head: + self.applied.append(statusentry(head, patch)) + self.applied_dirty = 1 + if err: + return (err, head) + self.save_dirty() + return (0, head) + + def patch(self, repo, patchfile): + '''Apply patchfile to the working directory. + patchfile: name of patch file''' + files = {} + try: + fuzz = patch.patch(patchfile, self.ui, strip=1, cwd=repo.root, + files=files, eolmode=None) + except Exception, inst: + self.ui.note(str(inst) + '\n') + if not self.ui.verbose: + self.ui.warn(_("patch failed, unable to continue (try -v)\n")) + return (False, files, False) + + return (True, files, fuzz) + + def apply(self, repo, series, list=False, update_status=True, + strict=False, patchdir=None, merge=None, all_files=None): + wlock = lock = tr = None + try: + wlock = repo.wlock() + lock = repo.lock() + tr = repo.transaction("qpush") + try: + ret = self._apply(repo, series, list, update_status, + strict, patchdir, merge, all_files=all_files) + tr.close() + self.save_dirty() + return ret + except: + try: + tr.abort() + finally: + repo.invalidate() + repo.dirstate.invalidate() + raise + finally: + release(tr, lock, wlock) + self.removeundo(repo) + + def _apply(self, repo, series, list=False, update_status=True, + strict=False, patchdir=None, merge=None, all_files=None): + '''returns (error, hash) + error = 1 for unable to read, 2 for patch failed, 3 for patch fuzz''' + # TODO unify with commands.py + if not patchdir: + patchdir = self.path + err = 0 + n = None + for patchname in series: + pushable, reason = self.pushable(patchname) + if not pushable: + self.explain_pushable(patchname, all_patches=True) + continue + self.ui.status(_("applying %s\n") % patchname) + pf = os.path.join(patchdir, patchname) + + try: + ph = patchheader(self.join(patchname), self.plainmode) + except: + self.ui.warn(_("unable to read %s\n") % patchname) + err = 1 + break + + message = ph.message + if not message: + # The commit message should not be translated + message = "imported patch %s\n" % patchname + else: + if list: + # The commit message should not be translated + message.append("\nimported patch %s" % patchname) + message = '\n'.join(message) + + if ph.haspatch: + (patcherr, files, fuzz) = self.patch(repo, pf) + if all_files is not None: + all_files.update(files) + patcherr = not patcherr + else: + self.ui.warn(_("patch %s is empty\n") % patchname) + patcherr, files, fuzz = 0, [], 0 + + if merge and files: + # Mark as removed/merged and update dirstate parent info + removed = [] + merged = [] + for f in files: + if os.path.lexists(repo.wjoin(f)): + merged.append(f) + else: + removed.append(f) + for f in removed: + repo.dirstate.remove(f) + for f in merged: + repo.dirstate.merge(f) + p1, p2 = repo.dirstate.parents() + repo.dirstate.setparents(p1, merge) + + files = cmdutil.updatedir(self.ui, repo, files) + match = cmdutil.matchfiles(repo, files or []) + n = repo.commit(message, ph.user, ph.date, match=match, force=True) + + if n is None: + raise util.Abort(_("repository commit failed")) + + if update_status: + self.applied.append(statusentry(n, patchname)) + + if patcherr: + self.ui.warn(_("patch failed, rejects left in working dir\n")) + err = 2 + break + + if fuzz and strict: + self.ui.warn(_("fuzz found when applying patch, stopping\n")) + err = 3 + break + return (err, n) + + def _cleanup(self, patches, numrevs, keep=False): + if not keep: + r = self.qrepo() + if r: + r[None].remove(patches, True) + else: + for p in patches: + os.unlink(self.join(p)) + + if numrevs: + del self.applied[:numrevs] + self.applied_dirty = 1 + + for i in sorted([self.find_series(p) for p in patches], reverse=True): + del self.full_series[i] + self.parse_series() + self.series_dirty = 1 + + def _revpatches(self, repo, revs): + firstrev = repo[self.applied[0].node].rev() + patches = [] + for i, rev in enumerate(revs): + + if rev < firstrev: + raise util.Abort(_('revision %d is not managed') % rev) + + ctx = repo[rev] + base = self.applied[i].node + if ctx.node() != base: + msg = _('cannot delete revision %d above applied patches') + raise util.Abort(msg % rev) + + patch = self.applied[i].name + for fmt in ('[mq]: %s', 'imported patch %s'): + if ctx.description() == fmt % patch: + msg = _('patch %s finalized without changeset message\n') + repo.ui.status(msg % patch) + break + + patches.append(patch) + return patches + + def finish(self, repo, revs): + patches = self._revpatches(repo, sorted(revs)) + self._cleanup(patches, len(patches)) + + def delete(self, repo, patches, opts): + if not patches and not opts.get('rev'): + raise util.Abort(_('qdelete requires at least one revision or ' + 'patch name')) + + realpatches = [] + for patch in patches: + patch = self.lookup(patch, strict=True) + info = self.isapplied(patch) + if info: + raise util.Abort(_("cannot delete applied patch %s") % patch) + if patch not in self.series: + raise util.Abort(_("patch %s not in series file") % patch) + if patch not in realpatches: + realpatches.append(patch) + + numrevs = 0 + if opts.get('rev'): + if not self.applied: + raise util.Abort(_('no patches applied')) + revs = cmdutil.revrange(repo, opts.get('rev')) + if len(revs) > 1 and revs[0] > revs[1]: + revs.reverse() + revpatches = self._revpatches(repo, revs) + realpatches += revpatches + numrevs = len(revpatches) + + self._cleanup(realpatches, numrevs, opts.get('keep')) + + def check_toppatch(self, repo): + if self.applied: + top = self.applied[-1].node + patch = self.applied[-1].name + pp = repo.dirstate.parents() + if top not in pp: + raise util.Abort(_("working directory revision is not qtip")) + return top, patch + return None, None + + def check_localchanges(self, repo, force=False, refresh=True): + m, a, r, d = repo.status()[:4] + if (m or a or r or d) and not force: + if refresh: + raise util.Abort(_("local changes found, refresh first")) + else: + raise util.Abort(_("local changes found")) + return m, a, r, d + + _reserved = ('series', 'status', 'guards') + def check_reserved_name(self, name): + if (name in self._reserved or name.startswith('.hg') + or name.startswith('.mq') or '#' in name or ':' in name): + raise util.Abort(_('"%s" cannot be used as the name of a patch') + % name) + + def new(self, repo, patchfn, *pats, **opts): + """options: + msg: a string or a no-argument function returning a string + """ + msg = opts.get('msg') + user = opts.get('user') + date = opts.get('date') + if date: + date = util.parsedate(date) + diffopts = self.diffopts({'git': opts.get('git')}) + self.check_reserved_name(patchfn) + if os.path.exists(self.join(patchfn)): + if os.path.isdir(self.join(patchfn)): + raise util.Abort(_('"%s" already exists as a directory') + % patchfn) + else: + raise util.Abort(_('patch "%s" already exists') % patchfn) + if opts.get('include') or opts.get('exclude') or pats: + match = cmdutil.match(repo, pats, opts) + # detect missing files in pats + def badfn(f, msg): + raise util.Abort('%s: %s' % (f, msg)) + match.bad = badfn + m, a, r, d = repo.status(match=match)[:4] + else: + m, a, r, d = self.check_localchanges(repo, force=True) + match = cmdutil.matchfiles(repo, m + a + r) + if len(repo[None].parents()) > 1: + raise util.Abort(_('cannot manage merge changesets')) + commitfiles = m + a + r + self.check_toppatch(repo) + insert = self.full_series_end() + wlock = repo.wlock() + try: + try: + # if patch file write fails, abort early + p = self.opener(patchfn, "w") + except IOError, e: + raise util.Abort(_('cannot write patch "%s": %s') + % (patchfn, e.strerror)) + try: + if self.plainmode: + if user: + p.write("From: " + user + "\n") + if not date: + p.write("\n") + if date: + p.write("Date: %d %d\n\n" % date) + else: + p.write("# HG changeset patch\n") + p.write("# Parent " + + hex(repo[None].parents()[0].node()) + "\n") + if user: + p.write("# User " + user + "\n") + if date: + p.write("# Date %s %s\n\n" % date) + if hasattr(msg, '__call__'): + msg = msg() + commitmsg = msg and msg or ("[mq]: %s" % patchfn) + n = repo.commit(commitmsg, user, date, match=match, force=True) + if n is None: + raise util.Abort(_("repo commit failed")) + try: + self.full_series[insert:insert] = [patchfn] + self.applied.append(statusentry(n, patchfn)) + self.parse_series() + self.series_dirty = 1 + self.applied_dirty = 1 + if msg: + msg = msg + "\n\n" + p.write(msg) + if commitfiles: + parent = self.qparents(repo, n) + chunks = patch.diff(repo, node1=parent, node2=n, + match=match, opts=diffopts) + for chunk in chunks: + p.write(chunk) + p.close() + wlock.release() + wlock = None + r = self.qrepo() + if r: + r[None].add([patchfn]) + except: + repo.rollback() + raise + except Exception: + patchpath = self.join(patchfn) + try: + os.unlink(patchpath) + except: + self.ui.warn(_('error unlinking %s\n') % patchpath) + raise + self.removeundo(repo) + finally: + release(wlock) + + def strip(self, repo, revs, update=True, backup="all", force=None): + wlock = lock = None + try: + wlock = repo.wlock() + lock = repo.lock() + + if update: + self.check_localchanges(repo, force=force, refresh=False) + urev = self.qparents(repo, revs[0]) + hg.clean(repo, urev) + repo.dirstate.write() + + self.removeundo(repo) + for rev in revs: + repair.strip(self.ui, repo, rev, backup) + # strip may have unbundled a set of backed up revisions after + # the actual strip + self.removeundo(repo) + finally: + release(lock, wlock) + + def isapplied(self, patch): + """returns (index, rev, patch)""" + for i, a in enumerate(self.applied): + if a.name == patch: + return (i, a.node, a.name) + return None + + # if the exact patch name does not exist, we try a few + # variations. If strict is passed, we try only #1 + # + # 1) a number to indicate an offset in the series file + # 2) a unique substring of the patch name was given + # 3) patchname[-+]num to indicate an offset in the series file + def lookup(self, patch, strict=False): + patch = patch and str(patch) + + def partial_name(s): + if s in self.series: + return s + matches = [x for x in self.series if s in x] + if len(matches) > 1: + self.ui.warn(_('patch name "%s" is ambiguous:\n') % s) + for m in matches: + self.ui.warn(' %s\n' % m) + return None + if matches: + return matches[0] + if self.series and self.applied: + if s == 'qtip': + return self.series[self.series_end(True)-1] + if s == 'qbase': + return self.series[0] + return None + + if patch is None: + return None + if patch in self.series: + return patch + + if not os.path.isfile(self.join(patch)): + try: + sno = int(patch) + except (ValueError, OverflowError): + pass + else: + if -len(self.series) <= sno < len(self.series): + return self.series[sno] + + if not strict: + res = partial_name(patch) + if res: + return res + minus = patch.rfind('-') + if minus >= 0: + res = partial_name(patch[:minus]) + if res: + i = self.series.index(res) + try: + off = int(patch[minus + 1:] or 1) + except (ValueError, OverflowError): + pass + else: + if i - off >= 0: + return self.series[i - off] + plus = patch.rfind('+') + if plus >= 0: + res = partial_name(patch[:plus]) + if res: + i = self.series.index(res) + try: + off = int(patch[plus + 1:] or 1) + except (ValueError, OverflowError): + pass + else: + if i + off < len(self.series): + return self.series[i + off] + raise util.Abort(_("patch %s not in series") % patch) + + def push(self, repo, patch=None, force=False, list=False, + mergeq=None, all=False, move=False): + diffopts = self.diffopts() + wlock = repo.wlock() + try: + heads = [] + for b, ls in repo.branchmap().iteritems(): + heads += ls + if not heads: + heads = [nullid] + if repo.dirstate.parents()[0] not in heads: + self.ui.status(_("(working directory not at a head)\n")) + + if not self.series: + self.ui.warn(_('no patches in series\n')) + return 0 + + patch = self.lookup(patch) + # Suppose our series file is: A B C and the current 'top' + # patch is B. qpush C should be performed (moving forward) + # qpush B is a NOP (no change) qpush A is an error (can't + # go backwards with qpush) + if patch: + info = self.isapplied(patch) + if info: + if info[0] < len(self.applied) - 1: + raise util.Abort( + _("cannot push to a previous patch: %s") % patch) + self.ui.warn( + _('qpush: %s is already at the top\n') % patch) + return 0 + pushable, reason = self.pushable(patch) + if not pushable: + if reason: + reason = _('guarded by %r') % reason + else: + reason = _('no matching guards') + self.ui.warn(_("cannot push '%s' - %s\n") % (patch, reason)) + return 1 + elif all: + patch = self.series[-1] + if self.isapplied(patch): + self.ui.warn(_('all patches are currently applied\n')) + return 0 + + # Following the above example, starting at 'top' of B: + # qpush should be performed (pushes C), but a subsequent + # qpush without an argument is an error (nothing to + # apply). This allows a loop of "...while hg qpush..." to + # work as it detects an error when done + start = self.series_end() + if start == len(self.series): + self.ui.warn(_('patch series already fully applied\n')) + return 1 + if not force: + self.check_localchanges(repo) + + if move: + if not patch: + raise util.Abort(_("please specify the patch to move")) + for i, rpn in enumerate(self.full_series[start:]): + # strip markers for patch guards + if self.guard_re.split(rpn, 1)[0] == patch: + break + index = start + i + assert index < len(self.full_series) + fullpatch = self.full_series[index] + del self.full_series[index] + self.full_series.insert(start, fullpatch) + self.parse_series() + self.series_dirty = 1 + + self.applied_dirty = 1 + 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] + all_files = set() + try: + if mergeq: + ret = self.mergepatch(repo, mergeq, s, diffopts) + else: + ret = self.apply(repo, s, list, all_files=all_files) + except: + self.ui.warn(_('cleaning up working directory...')) + node = repo.dirstate.parents()[0] + hg.revert(repo, node, None) + # only remove unknown files that we know we touched or + # created while patching + for f in all_files: + if f not in repo.dirstate: + try: + util.unlink(repo.wjoin(f)) + except OSError, inst: + if inst.errno != errno.ENOENT: + raise + self.ui.warn(_('done\n')) + raise + + if not self.applied: + return ret[0] + top = self.applied[-1].name + if ret[0] and ret[0] > 1: + msg = _("errors during apply, please fix and refresh %s\n") + self.ui.write(msg % top) + else: + self.ui.write(_("now at: %s\n") % top) + return ret[0] + + finally: + wlock.release() + + def pop(self, repo, patch=None, force=False, update=True, all=False): + wlock = repo.wlock() + try: + if patch: + # index, rev, patch + info = self.isapplied(patch) + if not info: + patch = self.lookup(patch) + info = self.isapplied(patch) + if not info: + raise util.Abort(_("patch %s is not applied") % patch) + + if not self.applied: + # Allow qpop -a to work repeatedly, + # but not qpop without an argument + self.ui.warn(_("no patches applied\n")) + return not all + + if all: + start = 0 + elif patch: + start = info[0] + 1 + else: + start = len(self.applied) - 1 + + if start >= len(self.applied): + self.ui.warn(_("qpop: %s is already at the top\n") % patch) + return + + if not update: + parents = repo.dirstate.parents() + rr = [x.node for x in self.applied] + for p in parents: + if p in rr: + self.ui.warn(_("qpop: forcing dirstate update\n")) + update = True + else: + parents = [p.node() for p in repo[None].parents()] + needupdate = False + for entry in self.applied[start:]: + if entry.node in parents: + needupdate = True + break + update = needupdate + + if not force and update: + self.check_localchanges(repo) + + self.applied_dirty = 1 + end = len(self.applied) + rev = self.applied[start].node + if update: + top = self.check_toppatch(repo)[0] + + try: + heads = repo.changelog.heads(rev) + except error.LookupError: + node = short(rev) + raise util.Abort(_('trying to pop unknown node %s') % node) + + if heads != [self.applied[-1].node]: + raise util.Abort(_("popping would remove a revision not " + "managed by this patch queue")) + + # we know there are no local changes, so we can make a simplified + # form of hg.update. + if update: + qp = self.qparents(repo, rev) + ctx = repo[qp] + m, a, r, d = repo.status(qp, top)[:4] + if d: + raise util.Abort(_("deletions found between repo revs")) + for f in a: + try: + util.unlink(repo.wjoin(f)) + except OSError, e: + if e.errno != errno.ENOENT: + raise + repo.dirstate.forget(f) + for f in m + r: + fctx = ctx[f] + repo.wwrite(f, fctx.data(), fctx.flags()) + repo.dirstate.normal(f) + repo.dirstate.setparents(qp, nullid) + for patch in reversed(self.applied[start:end]): + self.ui.status(_("popping %s\n") % patch.name) + del self.applied[start:end] + self.strip(repo, [rev], update=False, backup='strip') + if self.applied: + self.ui.write(_("now at: %s\n") % self.applied[-1].name) + else: + self.ui.write(_("patch queue now empty\n")) + finally: + wlock.release() + + def diff(self, repo, pats, opts): + top, patch = self.check_toppatch(repo) + if not top: + self.ui.write(_("no patches applied\n")) + return + qp = self.qparents(repo, top) + if opts.get('reverse'): + node1, node2 = None, qp + else: + node1, node2 = qp, None + diffopts = self.diffopts(opts, patch) + self.printdiff(repo, diffopts, node1, node2, files=pats, opts=opts) + + def refresh(self, repo, pats=None, **opts): + if not self.applied: + self.ui.write(_("no patches applied\n")) + return 1 + msg = opts.get('msg', '').rstrip() + newuser = opts.get('user') + newdate = opts.get('date') + if newdate: + newdate = '%d %d' % util.parsedate(newdate) + wlock = repo.wlock() + + try: + self.check_toppatch(repo) + (top, patchfn) = (self.applied[-1].node, self.applied[-1].name) + if repo.changelog.heads(top) != [top]: + raise util.Abort(_("cannot refresh a revision with children")) + + cparents = repo.changelog.parents(top) + patchparent = self.qparents(repo, top) + ph = patchheader(self.join(patchfn), self.plainmode) + diffopts = self.diffopts({'git': opts.get('git')}, patchfn) + if msg: + ph.setmessage(msg) + if newuser: + ph.setuser(newuser) + if newdate: + ph.setdate(newdate) + ph.setparent(hex(patchparent)) + + # only commit new patch when write is complete + patchf = self.opener(patchfn, 'w', atomictemp=True) + + comments = str(ph) + if comments: + patchf.write(comments) + + # update the dirstate in place, strip off the qtip commit + # and then commit. + # + # this should really read: + # mm, dd, aa, aa2 = repo.status(tip, patchparent)[:4] + # but we do it backwards to take advantage of manifest/chlog + # caching against the next repo.status call + mm, aa, dd, aa2 = repo.status(patchparent, top)[:4] + changes = repo.changelog.read(top) + man = repo.manifest.read(changes[0]) + aaa = aa[:] + matchfn = cmdutil.match(repo, pats, opts) + # in short mode, we only diff the files included in the + # patch already plus specified files + if opts.get('short'): + # if amending a patch, we start with existing + # files plus specified files - unfiltered + match = cmdutil.matchfiles(repo, mm + aa + dd + matchfn.files()) + # filter with inc/exl options + matchfn = cmdutil.match(repo, opts=opts) + else: + match = cmdutil.matchall(repo) + m, a, r, d = repo.status(match=match)[:4] + + # we might end up with files that were added between + # qtip 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 m: + if x == '.hgsub' or x == '.hgsubstate': + self.ui.warn(_('warning: not refreshing %s\n') % x) + continue + if x not in aa: + mm.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 == '.hgsub' or x == '.hgsubstate': + self.ui.warn(_('warning: not adding %s\n') % x) + continue + if x in dd: + del dd[dd.index(x)] + mm.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 == '.hgsub' or x == '.hgsubstate': + self.ui.warn(_('warning: not removing %s\n') % x) + continue + if x in aa: + del aa[aa.index(x)] + forget.append(x) + continue + elif x in mm: + del mm[mm.index(x)] + dd.append(x) + + m = list(set(mm)) + r = list(set(dd)) + a = list(set(aa)) + c = [filter(matchfn, l) for l in (m, a, r)] + match = cmdutil.matchfiles(repo, set(c[0] + c[1] + c[2])) + chunks = patch.diff(repo, patchparent, match=match, + changes=c, opts=diffopts) + for chunk in chunks: + patchf.write(chunk) + + try: + if diffopts.git or diffopts.upgrade: + copies = {} + for dst in a: + src = repo.dirstate.copied(dst) + # during qfold, the source file for copies may + # be removed. Treat this as a simple add. + if src is not None and src in repo.dirstate: + copies.setdefault(src, []).append(dst) + repo.dirstate.add(dst) + # remember the copies between patchparent and qtip + for dst in aaa: + f = repo.file(dst) + src = f.renamed(man[dst]) + if src: + copies.setdefault(src[0], []).extend( + copies.get(dst, [])) + if dst in a: + copies[src[0]].append(dst) + # we can't copy a file created by the patch itself + if dst in copies: + del copies[dst] + for src, dsts in copies.iteritems(): + for dst in dsts: + repo.dirstate.copy(src, dst) + else: + for dst in a: + repo.dirstate.add(dst) + # Drop useless copy information + for f in list(repo.dirstate.copies()): + repo.dirstate.copy(None, f) + for f in r: + repo.dirstate.remove(f) + # if the patch excludes a modified file, mark that + # file with mtime=0 so status can see it. + mm = [] + for i in xrange(len(m)-1, -1, -1): + if not matchfn(m[i]): + mm.append(m[i]) + del m[i] + for f in m: + repo.dirstate.normal(f) + for f in mm: + repo.dirstate.normallookup(f) + for f in forget: + repo.dirstate.forget(f) + + if not msg: + if not ph.message: + message = "[mq]: %s\n" % patchfn + else: + message = "\n".join(ph.message) + else: + message = msg + + user = ph.user or changes[1] + + # assumes strip can roll itself back if interrupted + repo.dirstate.setparents(*cparents) + self.applied.pop() + self.applied_dirty = 1 + self.strip(repo, [top], update=False, + backup='strip') + except: + repo.dirstate.invalidate() + raise + + try: + # might be nice to attempt to roll back strip after this + patchf.rename() + n = repo.commit(message, user, ph.date, match=match, + force=True) + self.applied.append(statusentry(n, patchfn)) + except: + ctx = repo[cparents[0]] + repo.dirstate.rebuild(ctx.node(), ctx.manifest()) + self.save_dirty() + self.ui.warn(_('refresh interrupted while patch was popped! ' + '(revert --all, qpush to recover)\n')) + raise + finally: + wlock.release() + self.removeundo(repo) + + def init(self, repo, create=False): + if not create and os.path.isdir(self.path): + raise util.Abort(_("patch queue directory already exists")) + try: + os.mkdir(self.path) + except OSError, inst: + if inst.errno != errno.EEXIST or not create: + raise + if create: + return self.qrepo(create=True) + + def unapplied(self, repo, patch=None): + if patch and patch not in self.series: + raise util.Abort(_("patch %s is not in series file") % patch) + if not patch: + start = self.series_end() + else: + start = self.series.index(patch) + 1 + unapplied = [] + for i in xrange(start, len(self.series)): + pushable, reason = self.pushable(i) + if pushable: + unapplied.append((i, self.series[i])) + self.explain_pushable(i) + return unapplied + + def qseries(self, repo, missing=None, start=0, length=None, status=None, + summary=False): + def displayname(pfx, patchname, state): + if pfx: + self.ui.write(pfx) + if summary: + ph = patchheader(self.join(patchname), self.plainmode) + msg = ph.message and ph.message[0] or '' + if self.ui.formatted(): + width = self.ui.termwidth() - len(pfx) - len(patchname) - 2 + if width > 0: + msg = util.ellipsis(msg, width) + else: + msg = '' + self.ui.write(patchname, label='qseries.' + state) + self.ui.write(': ') + self.ui.write(msg, label='qseries.message.' + state) + else: + self.ui.write(patchname, label='qseries.' + state) + self.ui.write('\n') + + applied = set([p.name for p in self.applied]) + if length is None: + length = len(self.series) - start + if not missing: + if self.ui.verbose: + idxwidth = len(str(start + length - 1)) + for i in xrange(start, start + length): + patch = self.series[i] + if patch in applied: + char, state = 'A', 'applied' + elif self.pushable(i)[0]: + char, state = 'U', 'unapplied' + else: + char, state = 'G', 'guarded' + pfx = '' + if self.ui.verbose: + pfx = '%*d %s ' % (idxwidth, i, char) + elif status and status != char: + continue + displayname(pfx, patch, state) + else: + msng_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 not in (self.status_path, self.series_path, + self.guards_path) + and not fl.startswith('.')): + msng_list.append(fl) + for x in sorted(msng_list): + pfx = self.ui.verbose and ('D ') or '' + displayname(pfx, x, 'missing') + + def issaveline(self, l): + if l.name == '.hg.patches.save.line': + return True + + def qrepo(self, create=False): + ui = self.ui.copy() + ui.setconfig('paths', 'default', '', overlay=False) + ui.setconfig('paths', 'default-push', '', overlay=False) + if create or os.path.isdir(self.join(".hg")): + return hg.repository(ui, path=self.path, create=create) + + def restore(self, repo, rev, delete=None, qupdate=None): + desc = repo[rev].description().strip() + lines = desc.splitlines() + i = 0 + datastart = None + series = [] + applied = [] + qpp = None + for i, line in enumerate(lines): + if line == 'Patch Data:': + datastart = i + 1 + elif line.startswith('Dirstate:'): + l = line.rstrip() + l = l[10:].split(' ') + qpp = [bin(x) for x in l] + elif datastart != None: + l = line.rstrip() + n, name = l.split(':', 1) + if n: + applied.append(statusentry(bin(n), name)) + else: + series.append(l) + if datastart is 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.parse_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") % 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") % + (short(qpp[0]), short(qpp[1]))) + if qupdate: + self.ui.status(_("updating queue directory\n")) + r = self.qrepo() + if not r: + self.ui.warn(_("Unable to load queue repository\n")) + return 1 + hg.clean(r, qpp[0]) + + def save(self, repo, msg=None): + if not self.applied: + 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 + + 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" % (hex(pp[0]), hex(pp[1])) + msg += "\n\nPatch Data:\n" + msg += ''.join('%s\n' % x for x in self.applied) + msg += ''.join(':%s\n' % x for x in self.full_series) + n = repo.commit(msg, force=True) + if not n: + self.ui.warn(_("repo commit failed\n")) + return 1 + self.applied.append(statusentry(n, '.hg.patches.save.line')) + self.applied_dirty = 1 + self.removeundo(repo) + + def full_series_end(self): + if self.applied: + p = self.applied[-1].name + end = self.find_series(p) + if end is None: + return len(self.full_series) + return end + 1 + return 0 + + def series_end(self, all_patches=False): + """If all_patches is False, return the index of the next pushable patch + in the series, or the series length. If all_patches is True, return the + index of the first patch past the last applied one. + """ + end = 0 + def next(start): + if all_patches or start >= len(self.series): + return start + for i in xrange(start, len(self.series)): + p, reason = self.pushable(i) + if p: + break + self.explain_pushable(i) + return i + if self.applied: + p = self.applied[-1].name + try: + end = self.series.index(p) + except ValueError: + return 0 + return next(end + 1) + return next(end) + + def appliedname(self, index): + pname = self.applied[index].name + if not self.ui.verbose: + p = pname + else: + p = str(self.series.index(pname)) + " " + pname + return p + + def qimport(self, repo, files, patchname=None, rev=None, existing=None, + force=None, git=False): + def checkseries(patchname): + if patchname in self.series: + raise util.Abort(_('patch %s is already in the series file') + % patchname) + def checkfile(patchname): + if not force and os.path.exists(self.join(patchname)): + raise util.Abort(_('patch "%s" already exists') + % patchname) + + if rev: + if files: + raise util.Abort(_('option "-r" not valid when importing ' + 'files')) + rev = cmdutil.revrange(repo, rev) + rev.sort(reverse=True) + if (len(files) > 1 or len(rev) > 1) and patchname: + raise util.Abort(_('option "-n" not valid when importing multiple ' + 'patches')) + if rev: + # If mq patches are applied, we can only import revisions + # that form a linear path to qbase. + # Otherwise, they should form a linear path to a head. + heads = repo.changelog.heads(repo.changelog.node(rev[-1])) + if len(heads) > 1: + raise util.Abort(_('revision %d is the root of more than one ' + 'branch') % rev[-1]) + if self.applied: + base = repo.changelog.node(rev[0]) + if base in [n.node for n in self.applied]: + raise util.Abort(_('revision %d is already managed') + % rev[0]) + if heads != [self.applied[-1].node]: + raise util.Abort(_('revision %d is not the parent of ' + 'the queue') % rev[0]) + base = repo.changelog.rev(self.applied[0].node) + lastparent = repo.changelog.parentrevs(base)[0] + else: + if heads != [repo.changelog.node(rev[0])]: + raise util.Abort(_('revision %d has unmanaged children') + % rev[0]) + lastparent = None + + diffopts = self.diffopts({'git': git}) + for r in rev: + p1, p2 = repo.changelog.parentrevs(r) + n = repo.changelog.node(r) + if p2 != nullrev: + raise util.Abort(_('cannot import merge revision %d') % r) + if lastparent and lastparent != r: + raise util.Abort(_('revision %d is not the parent of %d') + % (r, lastparent)) + lastparent = p1 + + if not patchname: + patchname = normname('%d.diff' % r) + self.check_reserved_name(patchname) + checkseries(patchname) + checkfile(patchname) + self.full_series.insert(0, patchname) + + patchf = self.opener(patchname, "w") + cmdutil.export(repo, [n], fp=patchf, opts=diffopts) + patchf.close() + + se = statusentry(n, patchname) + self.applied.insert(0, se) + + self.added.append(patchname) + patchname = None + self.parse_series() + self.applied_dirty = 1 + self.series_dirty = True + + for i, filename in enumerate(files): + if existing: + if filename == '-': + raise util.Abort(_('-e is incompatible with import from -')) + filename = normname(filename) + self.check_reserved_name(filename) + originpath = self.join(filename) + if not os.path.isfile(originpath): + raise util.Abort(_("patch %s does not exist") % filename) + + if patchname: + self.check_reserved_name(patchname) + checkfile(patchname) + + self.ui.write(_('renaming %s to %s\n') + % (filename, patchname)) + util.rename(originpath, self.join(patchname)) + else: + patchname = filename + + else: + try: + if filename == '-': + if not patchname: + raise util.Abort( + _('need --name to import a patch from -')) + text = sys.stdin.read() + else: + text = url.open(self.ui, filename).read() + except (OSError, IOError): + raise util.Abort(_("unable to read file %s") % filename) + if not patchname: + patchname = normname(os.path.basename(filename)) + self.check_reserved_name(patchname) + checkfile(patchname) + patchf = self.opener(patchname, "w") + patchf.write(text) + if not force: + checkseries(patchname) + if patchname not in self.series: + index = self.full_series_end() + i + self.full_series[index:index] = [patchname] + self.parse_series() + self.series_dirty = True + self.ui.warn(_("adding %s to series file\n") % patchname) + self.added.append(patchname) + patchname = None + +def delete(ui, repo, *patches, **opts): + """remove patches from queue + + The patches must not be applied, and at least one patch is required. With + -k/--keep, the patch files are preserved in the patch directory. + + To stop managing a patch and move it into permanent history, + use the :hg:`qfinish` command.""" + q = repo.mq + q.delete(repo, patches, opts) + q.save_dirty() + return 0 + +def applied(ui, repo, patch=None, **opts): + """print the patches already applied + + Returns 0 on success.""" + + q = repo.mq + + if patch: + if patch not in q.series: + raise util.Abort(_("patch %s is not in series file") % patch) + end = q.series.index(patch) + 1 + else: + end = q.series_end(True) + + if opts.get('last') and not end: + ui.write(_("no patches applied\n")) + return 1 + elif opts.get('last') and end == 1: + ui.write(_("only one patch applied\n")) + return 1 + elif opts.get('last'): + start = end - 2 + end = 1 + else: + start = 0 + + q.qseries(repo, length=end, start=start, status='A', + summary=opts.get('summary')) + + +def unapplied(ui, repo, patch=None, **opts): + """print the patches not yet applied + + Returns 0 on success.""" + + q = repo.mq + if patch: + if patch not in q.series: + raise util.Abort(_("patch %s is not in series file") % patch) + start = q.series.index(patch) + 1 + else: + start = q.series_end(True) + + if start == len(q.series) and opts.get('first'): + ui.write(_("all patches applied\n")) + return 1 + + length = opts.get('first') and 1 or None + q.qseries(repo, start=start, length=length, status='U', + summary=opts.get('summary')) + +def qimport(ui, repo, *filename, **opts): + """import a patch + + The patch is inserted into the series after the last applied + patch. If no patches have been applied, qimport prepends the patch + to the series. + + The patch will have the same name as its source file unless you + give it a new one with -n/--name. + + You can register an existing patch inside the patch directory with + the -e/--existing flag. + + With -f/--force, an existing patch of the same name will be + overwritten. + + An existing changeset may be placed under mq control with -r/--rev + (e.g. qimport --rev tip -n patch will place tip under mq control). + With -g/--git, patches imported with --rev will use the git diff + format. See the diffs help topic for information on why this is + important for preserving rename/copy information and permission + changes. + + To import a patch from standard input, pass - as the patch file. + When importing from standard input, a patch name must be specified + using the --name flag. + + To import an existing patch while renaming it:: + + hg qimport -e existing-patch -n new-name + + Returns 0 if import succeeded. + """ + q = repo.mq + try: + q.qimport(repo, filename, patchname=opts.get('name'), + existing=opts.get('existing'), force=opts.get('force'), + rev=opts.get('rev'), git=opts.get('git')) + finally: + q.save_dirty() + + if opts.get('push') and not opts.get('rev'): + return q.push(repo, None) + return 0 + +def qinit(ui, repo, create): + """initialize a new queue repository + + This command also creates a series file for ordering patches, and + an mq-specific .hgignore file in the queue repository, to exclude + the status and guards files (these contain mostly transient state). + + Returns 0 if initialization succeeded.""" + q = repo.mq + r = q.init(repo, create) + q.save_dirty() + if r: + if not os.path.exists(r.wjoin('.hgignore')): + fp = r.wopener('.hgignore', 'w') + fp.write('^\\.hg\n') + fp.write('^\\.mq\n') + fp.write('syntax: glob\n') + fp.write('status\n') + fp.write('guards\n') + fp.close() + if not os.path.exists(r.wjoin('series')): + r.wopener('series', 'w').close() + r[None].add(['.hgignore', 'series']) + commands.add(ui, r) + return 0 + +def init(ui, repo, **opts): + """init a new queue repository (DEPRECATED) + + The queue repository is unversioned by default. If + -c/--create-repo is specified, qinit will create a separate nested + repository for patches (qinit -c may also be run later to convert + an unversioned patch repository into a versioned one). You can use + qcommit to commit changes to this queue repository. + + This command is deprecated. Without -c, it's implied by other relevant + commands. With -c, use :hg:`init --mq` instead.""" + return qinit(ui, repo, create=opts.get('create_repo')) + +def clone(ui, source, dest=None, **opts): + '''clone main and patch repository at same time + + If source is local, destination will have no patches applied. If + source is remote, this command can not check if patches are + applied in source, so cannot guarantee that patches are not + applied in destination. If you clone remote repository, be sure + before that it has no patches applied. + + Source patch repository is looked for in <src>/.hg/patches by + default. Use -p <url> to change. + + The patch directory must be a nested Mercurial repository, as + would be created by :hg:`init --mq`. + + Return 0 on success. + ''' + def patchdir(repo): + url = repo.url() + if url.endswith('/'): + url = url[:-1] + return url + '/.hg/patches' + if dest is None: + dest = hg.defaultdest(source) + sr = hg.repository(hg.remoteui(ui, opts), ui.expandpath(source)) + if opts.get('patches'): + patchespath = ui.expandpath(opts.get('patches')) + else: + patchespath = patchdir(sr) + try: + hg.repository(ui, patchespath) + except error.RepoError: + raise util.Abort(_('versioned patch repository not found' + ' (see init --mq)')) + qbase, destrev = None, None + if sr.local(): + if sr.mq.applied: + qbase = sr.mq.applied[0].node + if not hg.islocal(dest): + heads = set(sr.heads()) + destrev = list(heads.difference(sr.heads(qbase))) + destrev.append(sr.changelog.parents(qbase)[0]) + elif sr.capable('lookup'): + try: + qbase = sr.lookup('qbase') + except error.RepoError: + pass + ui.note(_('cloning main repository\n')) + sr, dr = hg.clone(ui, sr.url(), dest, + pull=opts.get('pull'), + rev=destrev, + update=False, + stream=opts.get('uncompressed')) + ui.note(_('cloning patch repository\n')) + hg.clone(ui, opts.get('patches') or patchdir(sr), patchdir(dr), + pull=opts.get('pull'), update=not opts.get('noupdate'), + stream=opts.get('uncompressed')) + if dr.local(): + if qbase: + ui.note(_('stripping applied patches from destination ' + 'repository\n')) + dr.mq.strip(dr, [qbase], update=False, backup=None) + if not opts.get('noupdate'): + ui.note(_('updating destination repository\n')) + hg.update(dr, dr.changelog.tip()) + +def commit(ui, repo, *pats, **opts): + """commit changes in the queue repository (DEPRECATED) + + This command is deprecated; use :hg:`commit --mq` instead.""" + q = repo.mq + 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 + + Returns 0 on success.""" + repo.mq.qseries(repo, missing=opts.get('missing'), summary=opts.get('summary')) + return 0 + +def top(ui, repo, **opts): + """print the name of the current patch + + Returns 0 on success.""" + q = repo.mq + t = q.applied and q.series_end(True) or 0 + if t: + q.qseries(repo, start=t - 1, length=1, status='A', + summary=opts.get('summary')) + else: + ui.write(_("no patches applied\n")) + return 1 + +def next(ui, repo, **opts): + """print the name of the next patch + + Returns 0 on success.""" + q = repo.mq + end = q.series_end() + if end == len(q.series): + ui.write(_("all patches applied\n")) + return 1 + q.qseries(repo, start=end, length=1, summary=opts.get('summary')) + +def prev(ui, repo, **opts): + """print the name of the previous patch + + Returns 0 on success.""" + q = repo.mq + l = len(q.applied) + if l == 1: + ui.write(_("only one patch applied\n")) + return 1 + if not l: + ui.write(_("no patches applied\n")) + return 1 + q.qseries(repo, start=l - 2, length=1, status='A', + summary=opts.get('summary')) + +def setupheaderopts(ui, opts): + if not opts.get('user') and opts.get('currentuser'): + opts['user'] = ui.username() + if not opts.get('date') and opts.get('currentdate'): + opts['date'] = "%d %d" % util.makedate() + +def new(ui, repo, patch, *args, **opts): + """create a new patch + + qnew creates a new patch on top of the currently-applied patch (if + any). The patch will be initialized with any outstanding changes + in the working directory. You may also use -I/--include, + -X/--exclude, and/or a list of files after the patch name to add + only changes to matching files to the new patch, leaving the rest + as uncommitted modifications. + + -u/--user and -d/--date can be used to set the (given) user and + date, respectively. -U/--currentuser and -D/--currentdate set user + to current user and date to current date. + + -e/--edit, -m/--message or -l/--logfile set the patch header as + well as the commit message. If none is specified, the header is + empty and the commit message is '[mq]: PATCH'. + + Use the -g/--git option to keep the patch in the git extended diff + format. Read the diffs help topic for more information on why this + is important for preserving permission changes and copy/rename + information. + + Returns 0 on successful creation of a new patch. + """ + msg = cmdutil.logmessage(opts) + def getmsg(): + return ui.edit(msg, opts.get('user') or ui.username()) + q = repo.mq + opts['msg'] = msg + if opts.get('edit'): + opts['msg'] = getmsg + else: + opts['msg'] = msg + setupheaderopts(ui, opts) + q.new(repo, patch, *args, **opts) + q.save_dirty() + return 0 + +def refresh(ui, repo, *pats, **opts): + """update the current patch + + If any file patterns are provided, the refreshed patch will + contain only the modifications that match those patterns; the + remaining modifications will remain in the working directory. + + If -s/--short is specified, files currently included in the patch + will be refreshed just like matched files and remain in the patch. + + If -e/--edit is specified, Mercurial will start your configured editor for + you to enter a message. In case qrefresh fails, you will find a backup of + your message in ``.hg/last-message.txt``. + + hg add/remove/copy/rename work as usual, though you might want to + use git-style patches (-g/--git or [diff] git=1) to track copies + and renames. See the diffs help topic for more information on the + git diff format. + + Returns 0 on success. + """ + q = repo.mq + message = cmdutil.logmessage(opts) + if opts.get('edit'): + if not q.applied: + ui.write(_("no patches applied\n")) + return 1 + if message: + raise util.Abort(_('option "-e" incompatible with "-m" or "-l"')) + patch = q.applied[-1].name + ph = patchheader(q.join(patch), q.plainmode) + message = ui.edit('\n'.join(ph.message), ph.user or ui.username()) + # We don't want to lose the patch message if qrefresh fails (issue2062) + msgfile = repo.opener('last-message.txt', 'wb') + msgfile.write(message) + msgfile.close() + setupheaderopts(ui, opts) + ret = q.refresh(repo, pats, msg=message, **opts) + q.save_dirty() + return ret + +def diff(ui, repo, *pats, **opts): + """diff of the current patch and subsequent modifications + + Shows a diff which includes the current patch as well as any + changes which have been made in the working directory since the + last refresh (thus showing what the current patch would become + after a qrefresh). + + Use :hg:`diff` if you only want to see the changes made since the + last qrefresh, or :hg:`export qtip` if you want to see changes + made by the current patch without including changes made since the + qrefresh. + + Returns 0 on success. + """ + repo.mq.diff(repo, pats, opts) + return 0 + +def fold(ui, repo, *files, **opts): + """fold the named patches into the current patch + + Patches must not yet be applied. Each patch will be successively + applied to the current patch in the order given. If all the + patches apply successfully, the current patch will be refreshed + with the new cumulative patch, and the folded patches will be + deleted. With -k/--keep, the folded patch files will not be + removed afterwards. + + The header for each folded patch will be concatenated with the + current patch header, separated by a line of ``* * *``. + + Returns 0 on success.""" + + q = repo.mq + + if not files: + raise util.Abort(_('qfold requires at least one patch name')) + if not q.check_toppatch(repo)[0]: + raise util.Abort(_('no patches applied')) + q.check_localchanges(repo) + + message = cmdutil.logmessage(opts) + if opts.get('edit'): + if message: + raise util.Abort(_('option "-e" incompatible with "-m" or "-l"')) + + parent = q.lookup('qtip') + patches = [] + messages = [] + for f in files: + p = q.lookup(f) + if p in patches or p == parent: + ui.warn(_('Skipping already folded patch %s\n') % p) + if q.isapplied(p): + raise util.Abort(_('qfold cannot fold already applied patch %s') % p) + patches.append(p) + + for p in patches: + if not message: + ph = patchheader(q.join(p), q.plainmode) + if ph.message: + messages.append(ph.message) + pf = q.join(p) + (patchsuccess, files, fuzz) = q.patch(repo, pf) + if not patchsuccess: + raise util.Abort(_('error folding patch %s') % p) + cmdutil.updatedir(ui, repo, files) + + if not message: + ph = patchheader(q.join(parent), q.plainmode) + message, user = ph.message, ph.user + for msg in messages: + message.append('* * *') + message.extend(msg) + message = '\n'.join(message) + + if opts.get('edit'): + message = ui.edit(message, user or ui.username()) + + diffopts = q.patchopts(q.diffopts(), *patches) + q.refresh(repo, msg=message, git=diffopts.git) + q.delete(repo, patches, opts) + q.save_dirty() + +def goto(ui, repo, patch, **opts): + '''push or pop patches until named patch is at top of stack + + Returns 0 on success.''' + q = repo.mq + patch = q.lookup(patch) + if q.isapplied(patch): + ret = q.pop(repo, patch, force=opts.get('force')) + else: + ret = q.push(repo, patch, force=opts.get('force')) + q.save_dirty() + return ret + +def guard(ui, repo, *args, **opts): + '''set or print guards for a patch + + Guards control whether a patch can be pushed. A patch with no + guards is always pushed. A patch with a positive guard ("+foo") is + pushed only if the :hg:`qselect` command has activated it. A patch with + a negative guard ("-foo") is never pushed if the :hg:`qselect` command + has activated it. + + With no arguments, print the currently active guards. + With arguments, set guards for the named patch. + + .. note:: + Specifying negative guards now requires '--'. + + To set guards on another patch:: + + hg qguard other.patch -- +2.6.17 -stable + + Returns 0 on success. + ''' + def status(idx): + guards = q.series_guards[idx] or ['unguarded'] + if q.series[idx] in applied: + state = 'applied' + elif q.pushable(idx)[0]: + state = 'unapplied' + else: + state = 'guarded' + label = 'qguard.patch qguard.%s qseries.%s' % (state, state) + ui.write('%s: ' % ui.label(q.series[idx], label)) + + for i, guard in enumerate(guards): + if guard.startswith('+'): + ui.write(guard, label='qguard.positive') + elif guard.startswith('-'): + ui.write(guard, label='qguard.negative') + else: + ui.write(guard, label='qguard.unguarded') + if i != len(guards) - 1: + ui.write(' ') + ui.write('\n') + q = repo.mq + applied = set(p.name for p in q.applied) + patch = None + args = list(args) + if opts.get('list'): + if args or opts.get('none'): + raise util.Abort(_('cannot mix -l/--list with options or arguments')) + for i in xrange(len(q.series)): + status(i) + return + if not args or args[0][0:1] in '-+': + if not q.applied: + raise util.Abort(_('no patches applied')) + patch = q.applied[-1].name + if patch is None and args[0][0:1] not in '-+': + patch = args.pop(0) + if patch is None: + raise util.Abort(_('no patch to work with')) + if args or opts.get('none'): + idx = q.find_series(patch) + if idx is None: + raise util.Abort(_('no patch named %s') % patch) + q.set_guards(idx, args) + q.save_dirty() + else: + status(q.series.index(q.lookup(patch))) + +def header(ui, repo, patch=None): + """print the header of the topmost or specified patch + + Returns 0 on success.""" + q = repo.mq + + if patch: + patch = q.lookup(patch) + else: + if not q.applied: + ui.write(_('no patches applied\n')) + return 1 + patch = q.lookup('qtip') + ph = patchheader(q.join(patch), q.plainmode) + + ui.write('\n'.join(ph.message) + '\n') + +def lastsavename(path): + (directory, base) = os.path.split(path) + names = os.listdir(directory) + namere = re.compile("%s.([0-9]+)" % base) + maxindex = None + maxname = None + for f in names: + m = namere.match(f) + if m: + index = int(m.group(1)) + if maxindex is None or index > maxindex: + maxindex = index + maxname = f + if maxname: + return (os.path.join(directory, maxname), maxindex) + 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 + + When -f/--force is applied, all local changes in patched files + will be lost. + + Return 0 on succces. + """ + q = repo.mq + mergeq = None + + if opts.get('merge'): + if opts.get('name'): + newpath = repo.join(opts.get('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.get('force'), list=opts.get('list'), + mergeq=mergeq, all=opts.get('all'), move=opts.get('move')) + return ret + +def pop(ui, repo, patch=None, **opts): + """pop the current patch off the stack + + By default, pops off the top of the patch stack. If given a patch + name, keeps popping off patches until the named patch is at the + top of the stack. + + Return 0 on success. + """ + localupdate = True + if opts.get('name'): + q = queue(ui, repo.join(""), repo.join(opts.get('name'))) + ui.warn(_('using patch queue: %s\n') % q.path) + localupdate = False + else: + q = repo.mq + ret = q.pop(repo, patch, force=opts.get('force'), update=localupdate, + all=opts.get('all')) + q.save_dirty() + return ret + +def rename(ui, repo, patch, name=None, **opts): + """rename a patch + + With one argument, renames the current patch to PATCH1. + With two arguments, renames PATCH1 to PATCH2. + + Returns 0 on success.""" + + q = repo.mq + + if not name: + name = patch + patch = None + + if patch: + patch = q.lookup(patch) + else: + if not q.applied: + ui.write(_('no patches applied\n')) + return + patch = q.lookup('qtip') + absdest = q.join(name) + if os.path.isdir(absdest): + name = normname(os.path.join(name, os.path.basename(patch))) + absdest = q.join(name) + if os.path.exists(absdest): + raise util.Abort(_('%s already exists') % absdest) + + if name in q.series: + raise util.Abort( + _('A patch named %s already exists in the series file') % name) + + ui.note(_('renaming %s to %s\n') % (patch, name)) + i = q.find_series(patch) + guards = q.guard_re.findall(q.full_series[i]) + q.full_series[i] = name + ''.join([' #' + g for g in guards]) + q.parse_series() + q.series_dirty = 1 + + info = q.isapplied(patch) + if info: + q.applied[info[0]] = statusentry(info[1], name) + q.applied_dirty = 1 + + destdir = os.path.dirname(absdest) + if not os.path.isdir(destdir): + os.makedirs(destdir) + util.rename(q.join(patch), absdest) + r = q.qrepo() + if r and patch in r.dirstate: + wctx = r[None] + wlock = r.wlock() + try: + if r.dirstate[patch] == 'a': + r.dirstate.forget(patch) + r.dirstate.add(name) + else: + if r.dirstate[name] == 'r': + wctx.undelete([name]) + wctx.copy(patch, name) + wctx.remove([patch], False) + finally: + wlock.release() + + q.save_dirty() + +def restore(ui, repo, rev, **opts): + """restore the queue state saved by a revision (DEPRECATED) + + This command is deprecated, use :hg:`rebase` instead.""" + rev = repo.lookup(rev) + q = repo.mq + q.restore(repo, rev, delete=opts.get('delete'), + qupdate=opts.get('update')) + q.save_dirty() + return 0 + +def save(ui, repo, **opts): + """save current queue state (DEPRECATED) + + This command is deprecated, use :hg:`rebase` instead.""" + q = repo.mq + message = cmdutil.logmessage(opts) + ret = q.save(repo, msg=message) + if ret: + return ret + q.save_dirty() + if opts.get('copy'): + path = q.path + if opts.get('name'): + newpath = os.path.join(q.basepath, opts.get('name')) + if os.path.exists(newpath): + if not os.path.isdir(newpath): + raise util.Abort(_('destination %s exists and is not ' + 'a directory') % newpath) + if not opts.get('force'): + raise util.Abort(_('destination %s exists, ' + 'use -f to force') % newpath) + else: + newpath = savename(path) + ui.warn(_("copy %s to %s\n") % (path, newpath)) + util.copyfiles(path, newpath) + if opts.get('empty'): + try: + os.unlink(q.join(q.status_path)) + except: + pass + return 0 + +def strip(ui, repo, *revs, **opts): + """strip changesets and all their descendants from the repository + + The strip command removes the specified changesets and all their + descendants. If the working directory has uncommitted changes, + the operation is aborted unless the --force flag is supplied. + + If a parent of the working directory is stripped, then the working + directory will automatically be updated to the most recent + available ancestor of the stripped parent after the operation + completes. + + Any stripped changesets are stored in ``.hg/strip-backup`` as a + bundle (see :hg:`help bundle` and :hg:`help unbundle`). They can + be restored by running :hg:`unbundle .hg/strip-backup/BUNDLE`, + where BUNDLE is the bundle file created by the strip. Note that + the local revision numbers will in general be different after the + restore. + + Use the --no-backup option to discard the backup bundle once the + operation completes. + + Return 0 on success. + """ + backup = 'all' + if opts.get('backup'): + backup = 'strip' + elif opts.get('no_backup') or opts.get('nobackup'): + backup = 'none' + + cl = repo.changelog + revs = set(cmdutil.revrange(repo, revs)) + if not revs: + raise util.Abort(_('empty revision set')) + + descendants = set(cl.descendants(*revs)) + strippedrevs = revs.union(descendants) + roots = revs.difference(descendants) + + update = False + # if one of the wdir parent is stripped we'll need + # to update away to an earlier revision + for p in repo.dirstate.parents(): + if p != nullid and cl.rev(p) in strippedrevs: + update = True + break + + rootnodes = set(cl.node(r) for r in roots) + + q = repo.mq + if q.applied: + # refresh queue state if we're about to strip + # applied patches + if cl.rev(repo.lookup('qtip')) in strippedrevs: + q.applied_dirty = True + start = 0 + end = len(q.applied) + for i, statusentry in enumerate(q.applied): + if statusentry.node in rootnodes: + # if one of the stripped roots is an applied + # patch, only part of the queue is stripped + start = i + break + del q.applied[start:end] + q.save_dirty() + + revs = list(rootnodes) + if update and opts.get('keep'): + wlock = repo.wlock() + try: + urev = repo.mq.qparents(repo, revs[0]) + repo.dirstate.rebuild(urev, repo[urev].manifest()) + repo.dirstate.write() + update = False + finally: + wlock.release() + + repo.mq.strip(repo, revs, backup=backup, update=update, + force=opts.get('force')) + return 0 + +def select(ui, repo, *args, **opts): + '''set or print guarded patches to push + + Use the :hg:`qguard` command to set or print guards on patch, then use + qselect to tell mq which guards to use. A patch will be pushed if + it has no guards or any positive guards match the currently + selected guard, but will not be pushed if any negative guards + match the current guard. For example:: + + qguard foo.patch -stable (negative guard) + qguard bar.patch +stable (positive guard) + qselect stable + + This activates the "stable" guard. mq will skip foo.patch (because + it has a negative match) but push bar.patch (because it has a + positive match). + + With no arguments, prints the currently active guards. + With one argument, sets the active guard. + + Use -n/--none to deactivate guards (no other arguments needed). + When no guards are active, patches with positive guards are + skipped and patches with negative guards are pushed. + + qselect can change the guards on applied patches. It does not pop + guarded patches by default. Use --pop to pop back to the last + applied patch that is not guarded. Use --reapply (which implies + --pop) to push back to the current patch afterwards, but skip + guarded patches. + + Use -s/--series to print a list of all guards in the series file + (no other arguments needed). Use -v for more information. + + Returns 0 on success.''' + + q = repo.mq + guards = q.active() + if args or opts.get('none'): + old_unapplied = q.unapplied(repo) + old_guarded = [i for i in xrange(len(q.applied)) if + not q.pushable(i)[0]] + q.set_active(args) + q.save_dirty() + if not args: + ui.status(_('guards deactivated\n')) + if not opts.get('pop') and not opts.get('reapply'): + unapplied = q.unapplied(repo) + guarded = [i for i in xrange(len(q.applied)) + if not q.pushable(i)[0]] + if len(unapplied) != len(old_unapplied): + ui.status(_('number of unguarded, unapplied patches has ' + 'changed from %d to %d\n') % + (len(old_unapplied), len(unapplied))) + if len(guarded) != len(old_guarded): + ui.status(_('number of guarded, applied patches has changed ' + 'from %d to %d\n') % + (len(old_guarded), len(guarded))) + elif opts.get('series'): + guards = {} + noguards = 0 + for gs in q.series_guards: + if not gs: + noguards += 1 + for g in gs: + guards.setdefault(g, 0) + guards[g] += 1 + if ui.verbose: + guards['NONE'] = noguards + guards = guards.items() + guards.sort(key=lambda x: x[0][1:]) + if guards: + ui.note(_('guards in series file:\n')) + for guard, count in guards: + ui.note('%2d ' % count) + ui.write(guard, '\n') + else: + ui.note(_('no guards in series file\n')) + else: + if guards: + ui.note(_('active guards:\n')) + for g in guards: + ui.write(g, '\n') + else: + ui.write(_('no active guards\n')) + reapply = opts.get('reapply') and q.applied and q.appliedname(-1) + popped = False + if opts.get('pop') or opts.get('reapply'): + for i in xrange(len(q.applied)): + pushable, reason = q.pushable(i) + if not pushable: + ui.status(_('popping guarded patches\n')) + popped = True + if i == 0: + q.pop(repo, all=True) + else: + q.pop(repo, i - 1) + break + if popped: + try: + if reapply: + ui.status(_('reapplying unguarded patches\n')) + q.push(repo, reapply) + finally: + q.save_dirty() + +def finish(ui, repo, *revrange, **opts): + """move applied patches into repository history + + Finishes the specified revisions (corresponding to applied + patches) by moving them out of mq control into regular repository + history. + + Accepts a revision range or the -a/--applied option. If --applied + is specified, all applied mq revisions are removed from mq + control. Otherwise, the given revisions must be at the base of the + stack of applied patches. + + This can be especially useful if your changes have been applied to + an upstream repository, or if you are about to push your changes + to upstream. + + Returns 0 on success. + """ + if not opts.get('applied') and not revrange: + raise util.Abort(_('no revisions specified')) + elif opts.get('applied'): + revrange = ('qbase::qtip',) + revrange + + q = repo.mq + if not q.applied: + ui.status(_('no patches applied\n')) + return 0 + + revs = cmdutil.revrange(repo, revrange) + q.finish(repo, revs) + q.save_dirty() + return 0 + +def qqueue(ui, repo, name=None, **opts): + '''manage multiple patch queues + + Supports switching between different patch queues, as well as creating + new patch queues and deleting existing ones. + + Omitting a queue name or specifying -l/--list will show you the registered + queues - by default the "normal" patches queue is registered. The currently + active queue will be marked with "(active)". + + To create a new queue, use -c/--create. The queue is automatically made + active, except in the case where there are applied patches from the + currently active queue in the repository. Then the queue will only be + created and switching will fail. + + To delete an existing queue, use --delete. You cannot delete the currently + active queue. + + Returns 0 on success. + ''' + + q = repo.mq + + _defaultqueue = 'patches' + _allqueues = 'patches.queues' + _activequeue = 'patches.queue' + + def _getcurrent(): + cur = os.path.basename(q.path) + if cur.startswith('patches-'): + cur = cur[8:] + return cur + + def _noqueues(): + try: + fh = repo.opener(_allqueues, 'r') + fh.close() + except IOError: + return True + + return False + + def _getqueues(): + current = _getcurrent() + + try: + fh = repo.opener(_allqueues, 'r') + queues = [queue.strip() for queue in fh if queue.strip()] + if current not in queues: + queues.append(current) + except IOError: + queues = [_defaultqueue] + + return sorted(queues) + + def _setactive(name): + if q.applied: + raise util.Abort(_('patches applied - cannot set new queue active')) + _setactivenocheck(name) + + def _setactivenocheck(name): + fh = repo.opener(_activequeue, 'w') + if name != 'patches': + fh.write(name) + fh.close() + + def _addqueue(name): + fh = repo.opener(_allqueues, 'a') + fh.write('%s\n' % (name,)) + fh.close() + + def _queuedir(name): + if name == 'patches': + return repo.join('patches') + else: + return repo.join('patches-' + name) + + def _validname(name): + for n in name: + if n in ':\\/.': + return False + return True + + def _delete(name): + if name not in existing: + raise util.Abort(_('cannot delete queue that does not exist')) + + current = _getcurrent() + + if name == current: + raise util.Abort(_('cannot delete currently active queue')) + + fh = repo.opener('patches.queues.new', 'w') + for queue in existing: + if queue == name: + continue + fh.write('%s\n' % (queue,)) + fh.close() + util.rename(repo.join('patches.queues.new'), repo.join(_allqueues)) + + if not name or opts.get('list'): + current = _getcurrent() + for queue in _getqueues(): + ui.write('%s' % (queue,)) + if queue == current and not ui.quiet: + ui.write(_(' (active)\n')) + else: + ui.write('\n') + return + + if not _validname(name): + raise util.Abort( + _('invalid queue name, may not contain the characters ":\\/."')) + + existing = _getqueues() + + if opts.get('create'): + if name in existing: + raise util.Abort(_('queue "%s" already exists') % name) + if _noqueues(): + _addqueue(_defaultqueue) + _addqueue(name) + _setactive(name) + elif opts.get('rename'): + current = _getcurrent() + if name == current: + raise util.Abort(_('can\'t rename "%s" to its current name') % name) + if name in existing: + raise util.Abort(_('queue "%s" already exists') % name) + + olddir = _queuedir(current) + newdir = _queuedir(name) + + if os.path.exists(newdir): + raise util.Abort(_('non-queue directory "%s" already exists') % + newdir) + + fh = repo.opener('patches.queues.new', 'w') + for queue in existing: + if queue == current: + fh.write('%s\n' % (name,)) + if os.path.exists(olddir): + util.rename(olddir, newdir) + else: + fh.write('%s\n' % (queue,)) + fh.close() + util.rename(repo.join('patches.queues.new'), repo.join(_allqueues)) + _setactivenocheck(name) + elif opts.get('delete'): + _delete(name) + elif opts.get('purge'): + if name in existing: + _delete(name) + qdir = _queuedir(name) + if os.path.exists(qdir): + shutil.rmtree(qdir) + else: + if name not in existing: + raise util.Abort(_('use --create to create a new queue')) + _setactive(name) + +def reposetup(ui, repo): + class mqrepo(repo.__class__): + @util.propertycache + def mq(self): + return queue(self.ui, self.join("")) + + def abort_if_wdir_patched(self, errmsg, force=False): + if self.mq.applied and not force: + parent = self.dirstate.parents()[0] + if parent in [s.node for s in self.mq.applied]: + raise util.Abort(errmsg) + + def commit(self, text="", user=None, date=None, match=None, + force=False, editor=False, extra={}): + self.abort_if_wdir_patched( + _('cannot commit over an applied mq patch'), + force) + + return super(mqrepo, self).commit(text, user, date, match, force, + editor, extra) + + def push(self, remote, force=False, revs=None, newbranch=False): + if self.mq.applied and not force: + haspatches = True + if revs: + # Assume applied patches have no non-patch descendants + # and are not on remote already. If they appear in the + # set of resolved 'revs', bail out. + applied = set(e.node for e in self.mq.applied) + haspatches = bool([n for n in revs if n in applied]) + if haspatches: + raise util.Abort(_('source has mq patches applied')) + return super(mqrepo, self).push(remote, force, revs, newbranch) + + def _findtags(self): + '''augment tags from base class with patch tags''' + result = super(mqrepo, self)._findtags() + + q = self.mq + if not q.applied: + return result + + mqtags = [(patch.node, patch.name) for patch in q.applied] + + if mqtags[-1][0] not in self.changelog.nodemap: + self.ui.warn(_('mq status file refers to unknown node %s\n') + % short(mqtags[-1][0])) + return result + + mqtags.append((mqtags[-1][0], 'qtip')) + mqtags.append((mqtags[0][0], 'qbase')) + mqtags.append((self.changelog.parents(mqtags[0][0])[0], 'qparent')) + tags = result[0] + for patch in mqtags: + if patch[1] in tags: + self.ui.warn(_('Tag %s overrides mq patch of the same name\n') + % patch[1]) + else: + tags[patch[1]] = patch[0] + + return result + + def _branchtags(self, partial, lrev): + q = self.mq + if not q.applied: + return super(mqrepo, self)._branchtags(partial, lrev) + + cl = self.changelog + qbasenode = q.applied[0].node + if qbasenode not in cl.nodemap: + self.ui.warn(_('mq status file refers to unknown node %s\n') + % short(qbasenode)) + return super(mqrepo, self)._branchtags(partial, lrev) + + qbase = cl.rev(qbasenode) + start = lrev + 1 + if start < qbase: + # update the cache (excluding the patches) and save it + ctxgen = (self[r] for r in xrange(lrev + 1, qbase)) + self._updatebranchcache(partial, ctxgen) + self._writebranchcache(partial, cl.node(qbase - 1), qbase - 1) + start = qbase + # if start = qbase, the cache is as updated as it should be. + # if start > qbase, the cache includes (part of) the patches. + # we might as well use it, but we won't save it. + + # update the cache up to the tip + ctxgen = (self[r] for r in xrange(start, len(cl))) + self._updatebranchcache(partial, ctxgen) + + return partial + + if repo.local(): + repo.__class__ = mqrepo + +def mqimport(orig, ui, repo, *args, **kwargs): + if (hasattr(repo, 'abort_if_wdir_patched') + and not kwargs.get('no_commit', False)): + repo.abort_if_wdir_patched(_('cannot import over an applied patch'), + kwargs.get('force')) + return orig(ui, repo, *args, **kwargs) + +def mqinit(orig, ui, *args, **kwargs): + mq = kwargs.pop('mq', None) + + if not mq: + return orig(ui, *args, **kwargs) + + if args: + repopath = args[0] + if not hg.islocal(repopath): + raise util.Abort(_('only a local queue repository ' + 'may be initialized')) + else: + repopath = cmdutil.findrepo(os.getcwd()) + if not repopath: + raise util.Abort(_('there is no Mercurial repository here ' + '(.hg not found)')) + repo = hg.repository(ui, repopath) + return qinit(ui, repo, True) + +def mqcommand(orig, ui, repo, *args, **kwargs): + """Add --mq option to operate on patch repository instead of main""" + + # some commands do not like getting unknown options + mq = kwargs.pop('mq', None) + + if not mq: + return orig(ui, repo, *args, **kwargs) + + q = repo.mq + r = q.qrepo() + if not r: + raise util.Abort(_('no queue repository')) + return orig(r.ui, r, *args, **kwargs) + +def summary(orig, ui, repo, *args, **kwargs): + r = orig(ui, repo, *args, **kwargs) + q = repo.mq + m = [] + a, u = len(q.applied), len(q.unapplied(repo)) + if a: + m.append(ui.label(_("%d applied"), 'qseries.applied') % a) + if u: + m.append(ui.label(_("%d unapplied"), 'qseries.unapplied') % u) + if m: + ui.write("mq: %s\n" % ', '.join(m)) + else: + ui.note(_("mq: (empty queue)\n")) + return r + +def uisetup(ui): + mqopt = [('', 'mq', None, _("operate on patch repository"))] + + extensions.wrapcommand(commands.table, 'import', mqimport) + extensions.wrapcommand(commands.table, 'summary', summary) + + entry = extensions.wrapcommand(commands.table, 'init', mqinit) + entry[1].extend(mqopt) + + nowrap = set(commands.norepo.split(" ") + ['qrecord']) + + def dotable(cmdtable): + for cmd in cmdtable.keys(): + cmd = cmdutil.parsealiases(cmd)[0] + if cmd in nowrap: + continue + entry = extensions.wrapcommand(cmdtable, cmd, mqcommand) + entry[1].extend(mqopt) + + dotable(commands.table) + + for extname, extmodule in extensions.extensions(): + if extmodule.__file__ != __file__: + dotable(getattr(extmodule, 'cmdtable', {})) + +seriesopts = [('s', 'summary', None, _('print first line of patch header'))] + +cmdtable = { + "qapplied": + (applied, + [('1', 'last', None, _('show only the last patch'))] + seriesopts, + _('hg qapplied [-1] [-s] [PATCH]')), + "qclone": + (clone, + [('', 'pull', None, _('use pull protocol to copy metadata')), + ('U', 'noupdate', None, _('do not update the new working directories')), + ('', 'uncompressed', None, + _('use uncompressed transfer (fast over LAN)')), + ('p', 'patches', '', + _('location of source patch repository'), _('REPO')), + ] + commands.remoteopts, + _('hg qclone [OPTION]... SOURCE [DEST]')), + "qcommit|qci": + (commit, + commands.table["^commit|ci"][1], + _('hg qcommit [OPTION]... [FILE]...')), + "^qdiff": + (diff, + commands.diffopts + commands.diffopts2 + commands.walkopts, + _('hg qdiff [OPTION]... [FILE]...')), + "qdelete|qremove|qrm": + (delete, + [('k', 'keep', None, _('keep patch file')), + ('r', 'rev', [], + _('stop managing a revision (DEPRECATED)'), _('REV'))], + _('hg qdelete [-k] [PATCH]...')), + 'qfold': + (fold, + [('e', 'edit', None, _('edit patch header')), + ('k', 'keep', None, _('keep folded patch files')), + ] + commands.commitopts, + _('hg qfold [-e] [-k] [-m TEXT] [-l FILE] PATCH...')), + 'qgoto': + (goto, + [('f', 'force', None, _('overwrite any local changes'))], + _('hg qgoto [OPTION]... PATCH')), + 'qguard': + (guard, + [('l', 'list', None, _('list all patches and guards')), + ('n', 'none', None, _('drop all guards'))], + _('hg qguard [-l] [-n] [PATCH] [-- [+GUARD]... [-GUARD]...]')), + 'qheader': (header, [], _('hg qheader [PATCH]')), + "qimport": + (qimport, + [('e', 'existing', None, _('import file in patch directory')), + ('n', 'name', '', + _('name of patch file'), _('NAME')), + ('f', 'force', None, _('overwrite existing files')), + ('r', 'rev', [], + _('place existing revisions under mq control'), _('REV')), + ('g', 'git', None, _('use git extended diff format')), + ('P', 'push', None, _('qpush after importing'))], + _('hg qimport [-e] [-n NAME] [-f] [-g] [-P] [-r REV]... FILE...')), + "^qinit": + (init, + [('c', 'create-repo', None, _('create queue repository'))], + _('hg qinit [-c]')), + "^qnew": + (new, + [('e', 'edit', None, _('edit commit message')), + ('f', 'force', None, _('import uncommitted changes (DEPRECATED)')), + ('g', 'git', None, _('use git extended diff format')), + ('U', 'currentuser', None, _('add "From: <current user>" to patch')), + ('u', 'user', '', + _('add "From: <USER>" to patch'), _('USER')), + ('D', 'currentdate', None, _('add "Date: <current date>" to patch')), + ('d', 'date', '', + _('add "Date: <DATE>" to patch'), _('DATE')) + ] + commands.walkopts + commands.commitopts, + _('hg qnew [-e] [-m TEXT] [-l FILE] PATCH [FILE]...')), + "qnext": (next, [] + seriesopts, _('hg qnext [-s]')), + "qprev": (prev, [] + seriesopts, _('hg qprev [-s]')), + "^qpop": + (pop, + [('a', 'all', None, _('pop all patches')), + ('n', 'name', '', + _('queue name to pop (DEPRECATED)'), _('NAME')), + ('f', 'force', None, _('forget any local changes to patched files'))], + _('hg qpop [-a] [-f] [PATCH | INDEX]')), + "^qpush": + (push, + [('f', 'force', None, _('apply on top of local changes')), + ('l', 'list', None, _('list patch name in commit text')), + ('a', 'all', None, _('apply all patches')), + ('m', 'merge', None, _('merge from another queue (DEPRECATED)')), + ('n', 'name', '', + _('merge queue name (DEPRECATED)'), _('NAME')), + ('', 'move', None, _('reorder patch series and apply only the patch'))], + _('hg qpush [-f] [-l] [-a] [--move] [PATCH | INDEX]')), + "^qrefresh": + (refresh, + [('e', 'edit', None, _('edit commit message')), + ('g', 'git', None, _('use git extended diff format')), + ('s', 'short', None, + _('refresh only files already in the patch and specified files')), + ('U', 'currentuser', None, + _('add/update author field in patch with current user')), + ('u', 'user', '', + _('add/update author field in patch with given user'), _('USER')), + ('D', 'currentdate', None, + _('add/update date field in patch with current date')), + ('d', 'date', '', + _('add/update date field in patch with given date'), _('DATE')) + ] + commands.walkopts + commands.commitopts, + _('hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] [FILE]...')), + 'qrename|qmv': + (rename, [], _('hg qrename PATCH1 [PATCH2]')), + "qrestore": + (restore, + [('d', 'delete', None, _('delete save entry')), + ('u', 'update', None, _('update queue working directory'))], + _('hg qrestore [-d] [-u] REV')), + "qsave": + (save, + [('c', 'copy', None, _('copy patch directory')), + ('n', 'name', '', + _('copy directory name'), _('NAME')), + ('e', 'empty', None, _('clear queue status file')), + ('f', 'force', None, _('force copy'))] + commands.commitopts, + _('hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]')), + "qselect": + (select, + [('n', 'none', None, _('disable all guards')), + ('s', 'series', None, _('list all guards in series file')), + ('', 'pop', None, _('pop to before first guarded applied patch')), + ('', 'reapply', None, _('pop, then reapply patches'))], + _('hg qselect [OPTION]... [GUARD]...')), + "qseries": + (series, + [('m', 'missing', None, _('print patches not in series')), + ] + seriesopts, + _('hg qseries [-ms]')), + "strip": + (strip, + [('f', 'force', None, _('force removal of changesets even if the ' + 'working directory has uncommitted changes')), + ('b', 'backup', None, _('bundle only changesets with local revision' + ' number greater than REV which are not' + ' descendants of REV (DEPRECATED)')), + ('n', 'no-backup', None, _('no backups')), + ('', 'nobackup', None, _('no backups (DEPRECATED)')), + ('k', 'keep', None, _("do not modify working copy during strip"))], + _('hg strip [-k] [-f] [-n] REV...')), + "qtop": (top, [] + seriesopts, _('hg qtop [-s]')), + "qunapplied": + (unapplied, + [('1', 'first', None, _('show only the first patch'))] + seriesopts, + _('hg qunapplied [-1] [-s] [PATCH]')), + "qfinish": + (finish, + [('a', 'applied', None, _('finish all applied changesets'))], + _('hg qfinish [-a] [REV]...')), + 'qqueue': + (qqueue, + [ + ('l', 'list', False, _('list all available queues')), + ('c', 'create', False, _('create new queue')), + ('', 'rename', False, _('rename active queue')), + ('', 'delete', False, _('delete reference to queue')), + ('', 'purge', False, _('delete queue, and remove patch dir')), + ], + _('[OPTION] [QUEUE]')), +} + +colortable = {'qguard.negative': 'red', + 'qguard.positive': 'yellow', + 'qguard.unguarded': 'green', + 'qseries.applied': 'blue bold underline', + 'qseries.guarded': 'black bold', + 'qseries.missing': 'red bold', + 'qseries.unapplied': 'black bold'} |