diff hgext/mq.py @ 7414:040484030491

Merge with crew
author Matt Mackall <mpm@selenic.com>
date Tue, 25 Nov 2008 16:24:22 -0600
parents 7e9a15fa6c8f 41e87b4d0c9d
children 4c4324476be6
line wrap: on
line diff
--- a/hgext/mq.py	Tue Nov 18 16:02:14 2008 -0600
+++ b/hgext/mq.py	Tue Nov 25 16:24:22 2008 -0600
@@ -56,6 +56,67 @@
     def __str__(self):
         return self.rev + ':' + self.name
 
+class patchheader(object):
+    def __init__(self, message, comments, user, date, haspatch):
+        self.message = message
+        self.comments = comments
+        self.user = user
+        self.date = date
+        self.haspatch = haspatch
+
+    def setuser(self, user):
+        if not self.setheader(['From: ', '# User '], user):
+            try:
+                patchheaderat = self.comments.index('# HG changeset patch')
+                self.comments.insert(patchheaderat + 1,'# User ' + user)
+            except ValueError:
+                self.comments = ['From: ' + user, ''] + self.comments
+        self.user = user
+
+    def setdate(self, date):
+        if self.setheader(['# Date '], date):
+            self.date = date
+
+    def setmessage(self, message):
+        if self.comments:
+            self._delmsg()
+        self.message = [message]
+        self.comments += self.message
+
+    def setheader(self, prefixes, new):
+        '''Update all references to a field in the patch header.
+        If none found, add it email style.'''
+        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 __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 xrange(len(self.message)):
+            while self.message[mi] != self.comments[ci]:
+                ci += 1
+            del self.comments[ci]
+
 class queue:
     def __init__(self, ui, path, patchdir=None):
         self.basepath = path
@@ -307,7 +368,7 @@
         if format and format.startswith("tag") and subject:
             message.insert(0, "")
             message.insert(0, subject)
-        return (message, comments, user, date, diffstart > 1)
+        return patchheader(message, comments, user, date, diffstart > 1)
 
     def removeundo(self, repo):
         undo = repo.sjoin('undo')
@@ -351,13 +412,13 @@
         if n == None:
             raise util.Abort(_("repo commit failed"))
         try:
-            message, comments, user, date, patchfound = mergeq.readheaders(patch)
+            ph = mergeq.readheaders(patch)
         except:
             raise util.Abort(_("unable to read %s") % patch)
 
         patchf = self.opener(patch, "w")
+        comments = str(ph)
         if comments:
-            comments = "\n".join(comments) + '\n\n'
             patchf.write(comments)
         self.printdiff(repo, head, n, fp=patchf)
         patchf.close()
@@ -477,12 +538,13 @@
             pf = os.path.join(patchdir, patchname)
 
             try:
-                message, comments, user, date, patchfound = self.readheaders(patchname)
+                ph = self.readheaders(patchname)
             except:
                 self.ui.warn(_("Unable to read %s\n") % patchname)
                 err = 1
                 break
 
+            message = ph.message
             if not message:
                 message = _("imported patch %s\n") % patchname
             else:
@@ -512,7 +574,7 @@
 
             files = patch.updatedir(self.ui, repo, files)
             match = cmdutil.matchfiles(repo, files or [])
-            n = repo.commit(files, message, user, date, match=match,
+            n = repo.commit(files, message, ph.user, ph.date, match=match,
                             force=True)
 
             if n == None:
@@ -522,7 +584,7 @@
                 self.applied.append(statusentry(revlog.hex(n), patchname))
 
             if patcherr:
-                if not patchfound:
+                if not ph.haspatch:
                     self.ui.warn(_("patch %s is empty\n") % patchname)
                     err = 0
                 else:
@@ -824,11 +886,15 @@
         raise util.Abort(_("patch %s not in series") % patch)
 
     def push(self, repo, patch=None, force=False, list=False,
-             mergeq=None):
+             mergeq=None, all=False):
         wlock = repo.wlock()
         if repo.dirstate.parents()[0] != repo.changelog.tip():
             self.ui.status(_("(working directory not at tip)\n"))
 
+        if not self.series:
+            self.ui.warn(_('no patches in series\n'))
+            return 0
+
         try:
             patch = self.lookup(patch)
             # Suppose our series file is: A B C and the current 'top'
@@ -841,26 +907,36 @@
                     if info[0] < len(self.applied) - 1:
                         raise util.Abort(
                             _("cannot push to a previous patch: %s") % patch)
-                    if info[0] < len(self.series) - 1:
-                        self.ui.warn(
-                            _('qpush: %s is already at the top\n') % patch)
+                    self.ui.warn(
+                        _('qpush: %s is already at the top\n') % patch)
+                    return
+                pushable, reason = self.pushable(patch)
+                if not pushable:
+                    if reason:
+                        reason = _('guarded by %r') % reason
                     else:
-                        self.ui.warn(_('all patches are currently applied\n'))
-                    return
+                        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
-            if self.series_end() == len(self.series):
+            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)
 
-            self.applied_dirty = 1;
-            start = self.series_end()
+            self.applied_dirty = 1
             if start > 0:
                 self.check_toppatch(repo)
             if not patch:
@@ -1001,6 +1077,8 @@
         if len(self.applied) == 0:
             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)
@@ -1013,9 +1091,9 @@
                 raise util.Abort(_("cannot refresh a revision with children"))
             cparents = repo.changelog.parents(top)
             patchparent = self.qparents(repo, top)
-            message, comments, user, date, patchfound = self.readheaders(patchfn)
+            ph = self.readheaders(patchfn)
 
-            patchf = self.opener(patchfn, 'r+')
+            patchf = self.opener(patchfn, 'r')
 
             # if the patch was a git patch, refresh it as a git patch
             for line in patchf:
@@ -1023,59 +1101,21 @@
                     self.diffopts().git = True
                     break
 
-            msg = opts.get('msg', '').rstrip()
-            if msg and comments:
-                # Remove existing message, keeping the rest of the comments
-                # fields.
-                # If comments contains 'subject: ', message will prepend
-                # the field and a blank line.
-                if message:
-                    subj = 'subject: ' + message[0].lower()
-                    for i in xrange(len(comments)):
-                        if subj == comments[i].lower():
-                            del comments[i]
-                            message = message[2:]
-                            break
-                ci = 0
-                for mi in xrange(len(message)):
-                    while message[mi] != comments[ci]:
-                        ci += 1
-                    del comments[ci]
+            if msg:
+                ph.setmessage(msg)
+            if newuser:
+                ph.setuser(newuser)
+            if newdate:
+                ph.setdate(newdate)
 
-            def setheaderfield(comments, prefixes, new):
-                # Update all references to a field in the patch header.
-                # If none found, add it email style.
-                res = False
-                for prefix in prefixes:
-                    for i in xrange(len(comments)):
-                        if comments[i].startswith(prefix):
-                            comments[i] = prefix + new
-                            res = True
-                            break
-                return res
-
-            newuser = opts.get('user')
-            if newuser:
-                if not setheaderfield(comments, ['From: ', '# User '], newuser):
-                    try:
-                        patchheaderat = comments.index('# HG changeset patch')
-                        comments.insert(patchheaderat + 1,'# User ' + newuser)
-                    except ValueError:
-                        comments = ['From: ' + newuser, ''] + comments
-                user = newuser
-
-            if newdate:
-                if setheaderfield(comments, ['# Date '], newdate):
-                    date = newdate
-
-            if msg:
-                comments.append(msg)
+            # only commit new patch when write is complete
+            patchf = self.opener(patchfn, 'w', atomictemp=True)
 
             patchf.seek(0)
             patchf.truncate()
 
+            comments = str(ph)
             if comments:
-                comments = "\n".join(comments) + '\n\n'
                 patchf.write(comments)
 
             if opts.get('git'):
@@ -1148,69 +1188,82 @@
                                     changes=c, opts=self.diffopts())
                 for chunk in chunks:
                     patchf.write(chunk)
-                patchf.close()
 
-                repo.dirstate.setparents(*cparents)
-                copies = {}
-                for dst in a:
-                    src = repo.dirstate.copied(dst)
-                    if src is not None:
-                        copies.setdefault(src, []).append(dst)
-                    repo.dirstate.add(dst)
-                # remember the copies between patchparent and tip
-                # this may be slow, so don't do it if we're not tracking copies
-                if self.diffopts().git:
-                    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)
-                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)
+                try:
+                    copies = {}
+                    for dst in a:
+                        src = repo.dirstate.copied(dst)
+                        if src is not None:
+                            copies.setdefault(src, []).append(dst)
+                        repo.dirstate.add(dst)
+                    # remember the copies between patchparent and tip
+                    # this may be slow, so don't do it if we're not tracking copies
+                    if self.diffopts().git:
+                        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)
+                    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 message:
-                        message = "[mq]: %s\n" % patchfn
+                    if not msg:
+                        if not ph.message:
+                            message = "[mq]: %s\n" % patchfn
+                        else:
+                            message = "\n".join(ph.message)
                     else:
-                        message = "\n".join(message)
-                else:
-                    message = msg
+                        message = msg
+
+                    user = ph.user or changes[1]
 
-                if not user:
-                    user = 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
 
-                self.applied.pop()
-                self.applied_dirty = 1
-                self.strip(repo, top, update=False,
-                           backup='strip')
-                n = repo.commit(match.files(), message, user, date, match=match,
-                                force=1)
-                self.applied.append(statusentry(revlog.hex(n), patchfn))
-                self.removeundo(repo)
+                try:
+                    # might be nice to attempt to roll back strip after this
+                    patchf.rename()
+                    n = repo.commit(match.files(), message, user, ph.date,
+                                    match=match, force=1)
+                    self.applied.append(statusentry(revlog.hex(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
             else:
                 self.printdiff(repo, patchparent, fp=patchf)
-                patchf.close()
+                patchf.rename()
                 added = repo.status()[1]
                 for a in added:
                     f = repo.wjoin(a)
@@ -1228,6 +1281,7 @@
                 self.push(repo, force=True)
         finally:
             del wlock
+            self.removeundo(repo)
 
     def init(self, repo, create=False):
         if not create and os.path.isdir(self.path):
@@ -1259,7 +1313,8 @@
                 summary=False):
         def displayname(patchname):
             if summary:
-                msg = self.readheaders(patchname)[0]
+                ph = self.readheaders(patchname)
+                msg = ph.message
                 msg = msg and ': ' + msg[0] or ': '
             else:
                 msg = ''
@@ -1815,8 +1870,8 @@
         if message:
             raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
         patch = q.applied[-1].name
-        (message, comment, user, date, hasdiff) = q.readheaders(patch)
-        message = ui.edit('\n'.join(message), user or ui.username())
+        ph = q.readheaders(patch)
+        message = ui.edit('\n'.join(ph.message), ph.user or ui.username())
     setupheaderopts(ui, opts)
     ret = q.refresh(repo, pats, msg=message, **opts)
     q.save_dirty()
@@ -1874,7 +1929,8 @@
 
     for p in patches:
         if not message:
-            messages.append(q.readheaders(p)[0])
+            ph = q.readheaders(p)
+            messages.append(ph.message)
         pf = q.join(p)
         (patchsuccess, files, fuzz) = q.patch(repo, pf)
         if not patchsuccess:
@@ -1882,7 +1938,8 @@
         patch.updatedir(ui, repo, files)
 
     if not message:
-        message, comments, user = q.readheaders(parent)[0:3]
+        ph = q.readheaders(parent)
+        message, user = ph.message, ph.user
         for msg in messages:
             message.append('* * *')
             message.extend(msg)
@@ -1965,9 +2022,9 @@
             ui.write('No patches applied\n')
             return 1
         patch = q.lookup('qtip')
-    message = repo.mq.readheaders(patch)[0]
+    ph = repo.mq.readheaders(patch)
 
-    ui.write('\n'.join(message) + '\n')
+    ui.write('\n'.join(ph.message) + '\n')
 
 def lastsavename(path):
     (directory, base) = os.path.split(path)
@@ -2001,11 +2058,6 @@
     q = repo.mq
     mergeq = None
 
-    if opts['all']:
-        if not q.series:
-            ui.warn(_('no patches in series\n'))
-            return 0
-        patch = q.series[-1]
     if opts['merge']:
         if opts['name']:
             newpath = repo.join(opts['name'])
@@ -2017,7 +2069,7 @@
         mergeq = queue(ui, repo.join(""), newpath)
         ui.warn(_("merging with queue at: %s\n") % mergeq.path)
     ret = q.push(repo, patch, force=opts['force'], list=opts['list'],
-                 mergeq=mergeq)
+                 mergeq=mergeq, all=opts.get('all'))
     return ret
 
 def pop(ui, repo, patch=None, **opts):