/hgext/mq.py
Python | 3462 lines | 3340 code | 48 blank | 74 comment | 230 complexity | 35a077832e2917af08572e7323cb1c6d MD5 | raw file
Possible License(s): GPL-2.0
Large files files are truncated, but you can click here to view the full file
- # 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.
- It may be desirable for mq changesets to be kept in the secret phase (see
- :hg:`help phases`), which can be enabled with the following setting::
- [mq]
- secret = True
- You will by default be managing a patch queue named "patches". You can
- create other, independent patch queues with the :hg:`qqueue` command.
- If the working directory contains uncommitted files, qpush, qpop and
- qgoto abort immediately. If -f/--force is used, the changes are
- discarded. Setting::
- [mq]
- keepchanges = True
- make them behave as if --keep-changes were passed, and non-conflicting
- local changes will be tolerated and preserved. If incompatible options
- such as -f/--force or --exact are passed, this setting is ignored.
- This extension used to provide a strip command. This command now lives
- in the strip extension.
- '''
- from mercurial.i18n import _
- from mercurial.node import bin, hex, short, nullid, nullrev
- from mercurial.lock import release
- from mercurial import commands, cmdutil, hg, scmutil, util, revset
- from mercurial import extensions, error, phases
- from mercurial import patch as patchmod
- from mercurial import localrepo
- from mercurial import subrepo
- import os, re, errno, shutil
- seriesopts = [('s', 'summary', None, _('print first line of patch header'))]
- cmdtable = {}
- command = cmdutil.command(cmdtable)
- testedwith = 'internal'
- # force load strip extension formerly included in mq and import some utility
- try:
- stripext = extensions.find('strip')
- except KeyError:
- # note: load is lazy so we could avoid the try-except,
- # but I (marmoute) prefer this explicit code.
- class dummyui(object):
- def debug(self, msg):
- pass
- stripext = extensions.load(dummyui(), 'strip', '')
- strip = stripext.strip
- checksubstate = stripext.checksubstate
- checklocalchanges = stripext.checklocalchanges
- # 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
- branch = None
- nodeid = 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:].lstrip()
- elif line.startswith("# Branch "):
- branch = line[9:]
- elif line.startswith("# Node ID "):
- nodeid = line[10:]
- 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)
- # Remember the exact starting line of the patch diffs before consuming
- # empty lines, for external use by TortoiseHg and others
- self.diffstartline = len(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
- # nodeid and branch are for external use by TortoiseHg and others
- self.nodeid = nodeid
- self.branch = branch
- 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]
- def newcommit(repo, phase, *args, **kwargs):
- """helper dedicated to ensure a commit respect mq.secret setting
- It should be used instead of repo.commit inside the mq source for operation
- creating new changeset.
- """
- repo = repo.unfiltered()
- if phase is None:
- if repo.ui.configbool('mq', 'secret', False):
- phase = phases.secret
- if phase is not None:
- backup = repo.ui.backupconfig('phases', 'new-commit')
- try:
- if phase is not None:
- repo.ui.setconfig('phases', 'new-commit', phase, 'mq')
- return repo.commit(*args, **kwargs)
- finally:
- if phase is not None:
- repo.ui.restoreconfig(backup)
- class AbortNoCleanup(error.Abort):
- pass
- class queue(object):
- def __init__(self, ui, baseui, path, patchdir=None):
- self.basepath = path
- try:
- fh = open(os.path.join(path, 'patches.queue'))
- cur = fh.read().rstrip()
- fh.close()
- 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 = scmutil.opener(self.path)
- self.ui = ui
- self.baseui = baseui
- self.applieddirty = False
- self.seriesdirty = False
- self.added = []
- self.seriespath = "series"
- self.statuspath = "status"
- self.guardspath = "guards"
- self.activeguards = None
- self.guardsdirty = 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)
- self.checkapplied = True
- @util.propertycache
- def applied(self):
- def parselines(lines):
- for l in lines:
- entry = l.split(':', 1)
- if len(entry) > 1:
- n, name = entry
- yield statusentry(bin(n), name)
- elif l.strip():
- self.ui.warn(_('malformated mq status line: %s\n') % entry)
- # else we ignore empty lines
- try:
- lines = self.opener.read(self.statuspath).splitlines()
- return list(parselines(lines))
- except IOError, e:
- if e.errno == errno.ENOENT:
- return []
- raise
- @util.propertycache
- def fullseries(self):
- try:
- return self.opener.read(self.seriespath).splitlines()
- except IOError, e:
- if e.errno == errno.ENOENT:
- return []
- raise
- @util.propertycache
- def series(self):
- self.parseseries()
- return self.series
- @util.propertycache
- def seriesguards(self):
- self.parseseries()
- return self.seriesguards
- def invalidate(self):
- for a in 'applied fullseries series seriesguards'.split():
- if a in self.__dict__:
- delattr(self, a)
- self.applieddirty = False
- self.seriesdirty = False
- self.guardsdirty = False
- self.activeguards = None
- def diffopts(self, opts={}, patchfn=None):
- diffopts = patchmod.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 findseries(self, patch):
- def matchpatch(l):
- l = l.split('#', 1)[0]
- return l.strip() == patch
- for index, l in enumerate(self.fullseries):
- if matchpatch(l):
- return index
- return None
- guard_re = re.compile(r'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
- def parseseries(self):
- self.series = []
- self.seriesguards = []
- for l in self.fullseries:
- 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.seriespath)))
- self.series.append(patch)
- self.seriesguards.append(self.guard_re.findall(comment))
- def checkguard(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 setactive(self, guards):
- for guard in guards:
- bad = self.checkguard(guard)
- if bad:
- raise util.Abort(bad)
- guards = sorted(set(guards))
- self.ui.debug('active guards: %s\n' % ' '.join(guards))
- self.activeguards = guards
- self.guardsdirty = True
- def active(self):
- if self.activeguards is None:
- self.activeguards = []
- try:
- guards = self.opener.read(self.guardspath).split()
- except IOError, err:
- if err.errno != errno.ENOENT:
- raise
- guards = []
- for i, guard in enumerate(guards):
- bad = self.checkguard(guard)
- if bad:
- self.ui.warn('%s:%d: %s\n' %
- (self.join(self.guardspath), i + 1, bad))
- else:
- self.activeguards.append(guard)
- return self.activeguards
- def setguards(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.checkguard(g[1:])
- if bad:
- raise util.Abort(bad)
- drop = self.guard_re.sub('', self.fullseries[idx])
- self.fullseries[idx] = drop + ''.join([' #' + g for g in guards])
- self.parseseries()
- self.seriesdirty = True
- def pushable(self, idx):
- if isinstance(idx, str):
- idx = self.series.index(idx)
- patchguards = self.seriesguards[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, repr(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, repr(exactpos[0])
- return False, ' '.join(map(repr, pos))
- return True, ''
- def explainpushable(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 %s\n') %
- (self.series[idx], why))
- if not pushable:
- if why:
- write(_('skipping %s - guarded by %s\n') %
- (self.series[idx], why))
- else:
- write(_('skipping %s - no matching guards\n') %
- self.series[idx])
- def savedirty(self):
- def writelist(items, path):
- fp = self.opener(path, 'w')
- for i in items:
- fp.write("%s\n" % i)
- fp.close()
- if self.applieddirty:
- writelist(map(str, self.applied), self.statuspath)
- self.applieddirty = False
- if self.seriesdirty:
- writelist(self.fullseries, self.seriespath)
- self.seriesdirty = False
- if self.guardsdirty:
- writelist(self.activeguards, self.guardspath)
- self.guardsdirty = False
- 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 backup(self, repo, files, copy=False):
- # backup local changes in --force case
- for f in sorted(files):
- absf = repo.wjoin(f)
- if os.path.lexists(absf):
- self.ui.note(_('saving current version of %s as %s\n') %
- (f, f + '.orig'))
- if copy:
- util.copyfile(absf, absf + '.orig')
- else:
- util.rename(absf, absf + '.orig')
- def printdiff(self, repo, diffopts, node1, node2=None, files=None,
- fp=None, changes=None, opts={}):
- stat = opts.get('stat')
- m = scmutil.match(repo[node1], 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)
- strip(self.ui, repo, [n], update=False, backup='strip')
- ctx = repo[rev]
- ret = hg.merge(repo, rev)
- if ret:
- raise util.Abort(_("update returned %d") % ret)
- n = newcommit(repo, None, 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 Exception:
- 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):
- """return the mq handled parent or p1
- In some case where mq get himself in being the parent of a merge the
- appropriate parent may be p2.
- (eg: an in progress merge started with mq disabled)
- If no parent are managed by mq, p1 is returned.
- """
- 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 = newcommit(repo, None, '[mq]: merge marker', force=True)
- self.removeundo(repo)
- self.applied.append(statusentry(n, pname))
- self.applieddirty = True
- 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.explainpushable(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.applieddirty = True
- if err:
- return (err, head)
- self.savedirty()
- return (0, head)
- def patch(self, repo, patchfile):
- '''Apply patchfile to the working directory.
- patchfile: name of patch file'''
- files = set()
- try:
- fuzz = patchmod.patch(self.ui, repo, patchfile, strip=1,
- files=files, eolmode=None)
- return (True, list(files), fuzz)
- 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"))
- self.ui.traceback()
- return (False, list(files), False)
- def apply(self, repo, series, list=False, update_status=True,
- strict=False, patchdir=None, merge=None, all_files=None,
- tobackup=None, keepchanges=False):
- 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,
- tobackup=tobackup, keepchanges=keepchanges)
- tr.close()
- self.savedirty()
- return ret
- except AbortNoCleanup:
- tr.close()
- self.savedirty()
- return 2, repo.dirstate.p1()
- except: # re-raises
- try:
- tr.abort()
- finally:
- repo.invalidate()
- repo.dirstate.invalidate()
- self.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,
- tobackup=None, keepchanges=False):
- """returns (error, hash)
- error = 1 for unable to read, 2 for patch failed, 3 for patch
- fuzz. tobackup is None or a set of files to backup before they
- are modified by a patch.
- """
- # 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.explainpushable(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 IOError:
- 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:
- if tobackup:
- touched = patchmod.changedfiles(self.ui, repo, pf)
- touched = set(touched) & tobackup
- if touched and keepchanges:
- raise AbortNoCleanup(
- _("local changes found, refresh first"))
- self.backup(repo, touched, copy=True)
- tobackup = tobackup - touched
- (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.setparents(p1, merge)
- if all_files and '.hgsubstate' in all_files:
- wctx = repo[None]
- pctx = repo['.']
- overwrite = False
- mergedsubstate = subrepo.submerge(repo, pctx, wctx, wctx,
- overwrite)
- files += mergedsubstate.keys()
- match = scmutil.matchfiles(repo, files or [])
- oldtip = repo['tip']
- n = newcommit(repo, None, message, ph.user, ph.date, match=match,
- force=True)
- if repo['tip'] == oldtip:
- raise util.Abort(_("qpush exactly duplicates child changeset"))
- 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].forget(patches)
- for p in patches:
- try:
- os.unlink(self.join(p))
- except OSError, inst:
- if inst.errno != errno.ENOENT:
- raise
- qfinished = []
- if numrevs:
- qfinished = self.applied[:numrevs]
- del self.applied[:numrevs]
- self.applieddirty = True
- unknown = []
- for (i, p) in sorted([(self.findseries(p), p) for p in patches],
- reverse=True):
- if i is not None:
- del self.fullseries[i]
- else:
- unknown.append(p)
- if unknown:
- if numrevs:
- rev = dict((entry.name, entry.node) for entry in qfinished)
- for p in unknown:
- msg = _('revision %s refers to unknown patches: %s\n')
- self.ui.warn(msg % (short(rev[p]), p))
- else:
- msg = _('unknown patches: %s\n')
- raise util.Abort(''.join(msg % p for p in unknown))
- self.parseseries()
- self.seriesdirty = True
- return [entry.node for entry in qfinished]
- 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):
- # Manually trigger phase computation to ensure phasedefaults is
- # executed before we remove the patches.
- repo._phasecache
- patches = self._revpatches(repo, sorted(revs))
- qfinished = self._cleanup(patches, len(patches))
- if qfinished and repo.ui.configbool('mq', 'secret', False):
- # only use this logic when the secret option is added
- oldqbase = repo[qfinished[0]]
- tphase = repo.ui.config('phases', 'new-commit', phases.draft)
- if oldqbase.phase() > tphase and oldqbase.p1().phase() <= tphase:
- phases.advanceboundary(repo, tphase, qfinished)
- 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 = scmutil.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 checktoppatch(self, repo):
- '''check that working directory is at qtip'''
- if self.applied:
- top = self.applied[-1].node
- patch = self.applied[-1].name
- if repo.dirstate.p1() != top:
- raise util.Abort(_("working directory revision is not qtip"))
- return top, patch
- return None, None
- def putsubstate2changes(self, substatestate, changes):
- for files in changes[:3]:
- if '.hgsubstate' in files:
- return # already listed up
- # not yet listed up
- if substatestate in 'a?':
- changes[1].append('.hgsubstate')
- elif substatestate in 'r':
- changes[2].append('.hgsubstate')
- else: # modified
- changes[0].append('.hgsubstate')
- def checklocalchanges(self, repo, force=False, refresh=True):
- excsuffix = ''
- if refresh:
- excsuffix = ', refresh first'
- # plain versions for i18n tool to detect them
- _("local changes found, refresh first")
- _("local changed subrepos found, refresh first")
- return checklocalchanges(repo, force, excsuffix)
- _reserved = ('series', 'status', 'guards', '.', '..')
- def checkreservedname(self, name):
- if name in self._reserved:
- raise util.Abort(_('"%s" cannot be used as the name of a patch')
- % name)
- for prefix in ('.hg', '.mq'):
- if name.startswith(prefix):
- raise util.Abort(_('patch name cannot begin with "%s"')
- % prefix)
- for c in ('#', ':'):
- if c in name:
- raise util.Abort(_('"%s" cannot be used in the name of a patch')
- % c)
- def checkpatchname(self, name, force=False):
- self.checkreservedname(name)
- if not force and os.path.exists(self.join(name)):
- if os.path.isdir(self.join(name)):
- raise util.Abort(_('"%s" already exists as a directory')
- % name)
- else:
- raise util.Abort(_('patch "%s" already exists') % name)
- def checkkeepchanges(self, keepchanges, force):
- if force and keepchanges:
- raise util.Abort(_('cannot use both --force and --keep-changes'))
- def new(self, repo, patchfn, *pats, **opts):
- """options:
- msg: a string or a no-argument function returning a string
- """
- msg = opts.get('msg')
- edit = opts.get('edit')
- user = opts.get('user')
- date = opts.get('date')
- if date:
- date = util.parsedate(date)
- diffopts = self.diffopts({'git': opts.get('git')})
- if opts.get('checkname', True):
- self.checkpatchname(patchfn)
- inclsubs = checksubstate(repo)
- if inclsubs:
- substatestate = repo.dirstate['.hgsubstate']
- if opts.get('include') or opts.get('exclude') or pats:
- match = scmutil.match(repo[None], pats, opts)
- # detect missing files in pats
- def badfn(f, msg):
- if f != '.hgsubstate': # .hgsubstate is auto-created
- raise util.Abort('%s: %s' % (f, msg))
- match.bad = badfn
- changes = repo.status(match=match)
- else:
- changes = self.checklocalchanges(repo, force=True)
- commitfiles = list(inclsubs)
- for files in changes[:3]:
- commitfiles.extend(files)
- match = scmutil.matchfiles(repo, commitfiles)
- if len(repo[None].parents()) > 1:
- raise util.Abort(_('cannot manage merge changesets'))
- self.checktoppatch(repo)
- insert = self.fullseriesend()
- 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].p1().node()) + "\n")
- if user:
- p.write("# User " + user + "\n")
- if date:
- p.write("# Date %s %s\n\n" % date)
- defaultmsg = "[mq]: %s" % patchfn
- editor = cmdutil.getcommiteditor()
- if edit:
- def finishdesc(desc):
- if desc.rstrip():
- return desc
- else:
- return defaultmsg
- # i18n: this message is shown in editor with "HG: " prefix
- extramsg = _('Leave message empty to use default message.')
- editor = cmdutil.getcommiteditor(finishdesc=finishdesc,
- extramsg=extramsg)
- commitmsg = msg
- else:
- commitmsg = msg or defaultmsg
- n = newcommit(repo, None, commitmsg, user, date, match=match,
- force=True, editor=editor)
- if n is None:
- raise util.Abort(_("repo commit failed"))
- try:
- self.fullseries[insert:insert] = [patchfn]
- self.applied.append(statusentry(n, patchfn))
- self.parseseries()
- self.seriesdirty = True
- self.applieddirty = True
- nctx = repo[n]
- if nctx.description() != defaultmsg.rstrip():
- msg = nctx.description() + "\n\n"
- p.write(msg)
- if commitfiles:
- parent = self.qparents(repo, n)
- if inclsubs:
- self.putsubstate2changes(substatestate, changes)
- chunks = patchmod.diff(repo, node1=parent, node2=n,
- changes=changes, opts=diffopts)
- for chunk in chunks:
- p.write(chunk)
- p.close()
- r = self.qrepo()
- if r:
- r[None].add([patchfn])
- except: # re-raises
- repo.rollback()
- raise
- except Exception:
- patchpath = self.join(patchfn)
- try:
- os.unlink(patchpath)
- except OSError:
- self.ui.warn(_('error unlinking %s\n') % patchpath)
- raise
- self.removeundo(repo)
- finally:
- release(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 (as string) 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):
- def partialname(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.seriesend(True) - 1]
- if s == 'qbase':
- return self.series[0]
- 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 = partialname(patch)
- if res:
- return res
- minus = patch.rfind('-')
- if minus >= 0:
- res = partialname(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 = partialname(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, exact=False, nobackup=False,
- keepchanges=False):
- self.checkkeepchanges(keepchanges, force)
- diffopts = self.diffopts()
- wlock = repo.wlock()
- try:
- heads = []
- for hs in repo.branchmap().itervalues():
- heads.extend(hs)
- if not heads:
- heads = [nullid]
- if repo.dirstate.p1() not in heads and not exact:
- self.ui.status(_("(working directory not at a head)\n"))
- if not self.series:
- self.ui.warn(_('no patches in series\n'))
- return 0
- # 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:
- patch = self.lookup(patch)
- info = self.isapplied(patch)
- if info and info[0] >= len(self.applied) - 1:
- self.ui.warn(
- _('qpush: %s is already at the top\n') % patch)
- return 0
- pushable, reason = self.pushable(patch)
- if pushable:
- if self.series.index(patch) < self.seriesend():
- raise util.Abort(
- _("cannot push to a previous patch: %s") % patch)
- else:
- if reason:
- reason = _('guarded by %s') % 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.seriesend()
- if start == len(self.series):
- self.ui.warn(_('patch series already fully applied\n'))
- return 1
- if not force and not keepchanges:
- self.checklocalchanges(repo, refresh=self.applied)
- if exact:
- if keepchanges:
- raise util.Abort(
- _("cannot use --exact and --keep-changes together"))
- if move:
- raise util.Abort(_('cannot use --exact and --move '
- 'together'))
- if self.applied:
- raise util.Abort(_('cannot push --exact with applied '
- 'patches'))
- root = self.series[start]
- target = patchheader(self.join(root), self.plainmode).parent
- if not target:
- raise util.Abort(
- _("%s does not have a parent recorded") % root)
- if not repo[target] == repo['.']:
- hg.update(repo, target)
- if move:
- if not patch:
- raise util.Abort(_("please specify the patch to move"))
- for fullstart, rpn in enumerate(self.fullseries):
- # strip markers for patch guards
- if self.guard_re.split(rpn, 1)[0] == self.series[start]:
- break
- for i, rpn in enumerate(self.fullseries[fullstart:]):
- # strip markers for patch guards
- if self.guard_re.split(rpn, 1)[0] == patch:
- break
- index = fullstart + i
- assert index < len(self.fullseries)
- fullpatch = self.fullseries[index]
- del self.fullseries[index]
- self.fullseries.insert(fullstart, fullpatch)
- self.parseseries()
- self.seriesdirty = True
- self.applieddirty = True
- if start > 0:
- self.checktoppatch(repo)
- if not patch:
- …
Large files files are truncated, but you can click here to view the full file