hgext/mq.py
changeset 6042 2da5b19a6460
parent 6041 dd714452c26e
parent 6001 30d2fecaab76
child 6043 6283316bcfd4
--- a/hgext/mq.py	Thu Jul 26 07:56:27 2007 -0400
+++ b/hgext/mq.py	Wed Feb 06 19:57:52 2008 -0800
@@ -34,7 +34,7 @@
 from mercurial import repair
 import os, sys, re, errno
 
-commands.norepo += " qclone qversion"
+commands.norepo += " qclone"
 
 # Patch names looks like unix-file names.
 # They must be joinable with queue directory and result in the patch path.
@@ -224,7 +224,7 @@
         def write_list(items, path):
             fp = self.opener(path, 'w')
             for i in items:
-                print >> fp, i
+                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)
@@ -455,7 +455,8 @@
                     repo.dirstate.invalidate()
                 raise
         finally:
-            del lock, wlock, tr
+            del tr, lock, wlock
+            self.removeundo(repo)
 
     def _apply(self, repo, series, list=False, update_status=True,
                strict=False, patchdir=None, merge=None, all_files={}):
@@ -527,7 +528,6 @@
                 self.ui.warn("fuzz found when applying patch, stopping\n")
                 err = 1
                 break
-        self.removeundo(repo)
         return (err, n)
 
     def delete(self, repo, patches, opts):
@@ -587,7 +587,7 @@
             top = revlog.bin(self.applied[-1].rev)
             pp = repo.dirstate.parents()
             if top not in pp:
-                raise util.Abort(_("queue top not at same revision as working directory"))
+                raise util.Abort(_("working directory revision is not qtip"))
             return top
         return None
     def check_localchanges(self, repo, force=False, refresh=True):
@@ -600,9 +600,19 @@
                     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')):
+            raise util.Abort(_('"%s" cannot be used as the name of a patch')
+                             % name)
+
     def new(self, repo, patch, *pats, **opts):
         msg = opts.get('msg')
         force = opts.get('force')
+        user = opts.get('user')
+        date = opts.get('date')
+        self.check_reserved_name(patch)
         if os.path.exists(self.join(patch)):
             raise util.Abort(_('patch "%s" already exists') % patch)
         if opts.get('include') or opts.get('exclude') or pats:
@@ -610,15 +620,14 @@
             m, a, r, d = repo.status(files=fns, match=match)[:4]
         else:
             m, a, r, d = self.check_localchanges(repo, force)
+            fns, match, anypats = cmdutil.matchpats(repo, m + a + r)
         commitfiles = m + a + r
         self.check_toppatch(repo)
         wlock = repo.wlock()
         try:
             insert = self.full_series_end()
-            if msg:
-                n = repo.commit(commitfiles, msg, force=True)
-            else:
-                n = repo.commit(commitfiles, "[mq]: %s" % patch, force=True)
+            commitmsg = msg and msg or ("[mq]: %s" % patch)
+            n = repo.commit(commitfiles, commitmsg, user, date, match=match, force=True)
             if n == None:
                 raise util.Abort(_("repo commit failed"))
             self.full_series[insert:insert] = [patch]
@@ -627,6 +636,15 @@
             self.series_dirty = 1
             self.applied_dirty = 1
             p = self.opener(patch, "w")
+            if date:
+                p.write("# HG changeset patch\n")
+                if user:
+                    p.write("# User " + user + "\n")
+                p.write("# Date " + date + "\n")
+                p.write("\n")
+            elif user:
+                p.write("From: " + user + "\n")
+                p.write("\n")
             if msg:
                 msg = msg + "\n"
                 p.write(msg)
@@ -635,7 +653,7 @@
             r = self.qrepo()
             if r: r.add([patch])
             if commitfiles:
-                self.refresh(repo, short=True)
+                self.refresh(repo, short=True, git=opts.get('git'))
             self.removeundo(repo)
         finally:
             del wlock
@@ -654,6 +672,9 @@
 
             self.removeundo(repo)
             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:
             del lock, wlock
 
@@ -810,9 +831,9 @@
             del wlock
 
     def pop(self, repo, patch=None, force=False, update=True, all=False):
-        def getfile(f, rev):
+        def getfile(f, rev, flags):
             t = repo.file(f).read(rev)
-            repo.wfile(f, "w").write(t)
+            repo.wwrite(f, t, flags)
 
         wlock = repo.wlock()
         try:
@@ -859,10 +880,16 @@
             start = info[0]
             rev = revlog.bin(info[1])
 
+            if update:
+                top = self.check_toppatch(repo)
+
+            if repo.changelog.heads(rev) != [revlog.bin(self.applied[-1].rev)]:
+                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:
-                top = self.check_toppatch(repo)
                 qp = self.qparents(repo, rev)
                 changes = repo.changelog.read(qp)
                 mmap = repo.manifest.read(changes[0])
@@ -870,10 +897,9 @@
                 if d:
                     raise util.Abort("deletions found between repo revs")
                 for f in m:
-                    getfile(f, mmap[f])
+                    getfile(f, mmap[f], mmap.flags(f))
                 for f in r:
-                    getfile(f, mmap[f])
-                    util.set_exec(repo.wjoin(f), mmap.execf(f))
+                    getfile(f, mmap[f], mmap.flags(f))
                 for f in m + r:
                     repo.dirstate.normal(f)
                 for f in a:
@@ -886,8 +912,8 @@
                     except: pass
                     repo.dirstate.forget(f)
                 repo.dirstate.setparents(qp, revlog.nullid)
+            del self.applied[start:end]
             self.strip(repo, rev, update=False, backup='strip')
-            del self.applied[start:end]
             if len(self.applied):
                 self.ui.write("Now at: %s\n" % self.applied[-1].name)
             else:
@@ -914,6 +940,8 @@
             self.check_toppatch(repo)
             (top, patchfn) = (self.applied[-1].rev, self.applied[-1].name)
             top = revlog.bin(top)
+            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)
             message, comments, user, date, patchfound = self.readheaders(patchfn)
@@ -925,22 +953,59 @@
                 if line.startswith('diff --git'):
                     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]
+
+            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
+
+            newdate = opts.get('date')
+            if newdate:
+                if setheaderfield(comments, ['# Date '], newdate):
+                    date = newdate
+
+            if msg:
+                comments.append(msg)
+
             patchf.seek(0)
             patchf.truncate()
 
-            msg = opts.get('msg', '').rstrip()
-            if msg:
-                if comments:
-                    # Remove existing message.
-                    ci = 0
-                    subj = None
-                    for mi in xrange(len(message)):
-                        if comments[ci].lower().startswith('subject: '):
-                            subj = comments[ci][9:]
-                        while message[mi] != comments[ci] and message[mi] != subj:
-                            ci += 1
-                        del comments[ci]
-                comments.append(msg)
             if comments:
                 comments = "\n".join(comments) + '\n\n'
                 patchf.write(comments)
@@ -1017,9 +1082,8 @@
                 copies = {}
                 for dst in a:
                     src = repo.dirstate.copied(dst)
-                    if src is None:
-                        continue
-                    copies.setdefault(src, []).append(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
@@ -1049,7 +1113,7 @@
                 for f in m:
                     repo.dirstate.normal(f)
                 for f in mm:
-                    repo.dirstate.normaldirty(f)
+                    repo.dirstate.normallookup(f)
                 for f in forget:
                     repo.dirstate.forget(f)
 
@@ -1061,12 +1125,16 @@
                 else:
                     message = msg
 
+                if not user:
+                    user = changes[1]
+
+                self.applied.pop()
+                self.applied_dirty = 1
                 self.strip(repo, top, update=False,
                            backup='strip')
-                n = repo.commit(filelist, message, changes[1], match=matchfn,
+                n = repo.commit(filelist, message, user, date, match=matchfn,
                                 force=1)
-                self.applied[-1] = statusentry(revlog.hex(n), patchfn)
-                self.applied_dirty = 1
+                self.applied.append(statusentry(revlog.hex(n), patchfn))
                 self.removeundo(repo)
             else:
                 self.printdiff(repo, patchparent, fp=patchf)
@@ -1216,7 +1284,7 @@
             self.ui.warn("saved queue repository parents: %s %s\n" %
                          (hg.short(qpp[0]), hg.short(qpp[1])))
             if qupdate:
-                print "queue directory updating"
+                self.ui.status(_("queue directory updating\n"))
                 r = self.qrepo()
                 if not r:
                     self.ui.warn("Unable to load queue repository\n")
@@ -1355,6 +1423,7 @@
 
                 if not patchname:
                     patchname = normname('%d.diff' % r)
+                self.check_reserved_name(patchname)
                 checkseries(patchname)
                 checkfile(patchname)
                 self.full_series.insert(0, patchname)
@@ -1377,6 +1446,7 @@
                     raise util.Abort(_('-e is incompatible with import from -'))
                 if not patchname:
                     patchname = normname(filename)
+                self.check_reserved_name(patchname)
                 if not os.path.isfile(self.join(patchname)):
                     raise util.Abort(_("patch %s does not exist") % patchname)
             else:
@@ -1391,6 +1461,7 @@
                     raise util.Abort(_("unable to read %s") % patchname)
                 if not patchname:
                     patchname = normname(os.path.basename(filename))
+                self.check_reserved_name(patchname)
                 checkfile(patchname)
                 patchf = self.opener(patchname, "w")
                 patchf.write(text)
@@ -1508,13 +1579,18 @@
     The patch directory must be a nested mercurial repository, as
     would be created by qinit -c.
     '''
+    def patchdir(repo):
+        url = repo.url()
+        if url.endswith('/'):
+            url = url[:-1]
+        return url + '/.hg/patches'
     cmdutil.setremoteconfig(ui, opts)
     if dest is None:
         dest = hg.defaultdest(source)
     sr = hg.repository(ui, ui.expandpath(source))
-    patchdir = opts['patches'] or (sr.url() + '/.hg/patches')
+    patchespath = opts['patches'] or patchdir(sr)
     try:
-        pr = hg.repository(ui, patchdir)
+        pr = hg.repository(ui, patchespath)
     except hg.RepoError:
         raise util.Abort(_('versioned patch repository not found'
                            ' (see qinit -c)'))
@@ -1535,10 +1611,8 @@
                       update=False,
                       stream=opts['uncompressed'])
     ui.note(_('cloning patch repo\n'))
-    spr, dpr = hg.clone(ui, opts['patches'] or (sr.url() + '/.hg/patches'),
-                        dr.url() + '/.hg/patches',
-                        pull=opts['pull'],
-                        update=not opts['noupdate'],
+    spr, dpr = hg.clone(ui, opts['patches'] or patchdir(sr), patchdir(dr),
+                        pull=opts['pull'], update=not opts['noupdate'],
                         stream=opts['uncompressed'])
     if dr.local():
         if qbase:
@@ -1593,6 +1667,13 @@
     return q.qseries(repo, start=l-2, length=1, status='A',
                      summary=opts.get('summary'))
 
+def setupheaderopts(ui, opts):
+    def do(opt,val):
+        if not opts[opt] and opts['current' + opt]:
+            opts[opt] = val
+    do('user', ui.username())
+    do('date', "%d %d" % util.makedate())
+
 def new(ui, repo, patch, *args, **opts):
     """create a new patch
 
@@ -1611,6 +1692,7 @@
     if opts['edit']:
         message = ui.edit(message, ui.username())
     opts['msg'] = message
+    setupheaderopts(ui, opts)
     q.new(repo, patch, *args, **opts)
     q.save_dirty()
     return 0
@@ -1628,11 +1710,15 @@
     q = repo.mq
     message = cmdutil.logmessage(opts)
     if opts['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
         (message, comment, user, date, hasdiff) = q.readheaders(patch)
         message = ui.edit('\n'.join(message), user or ui.username())
+    setupheaderopts(ui, opts)
     ret = q.refresh(repo, pats, msg=message, **opts)
     q.save_dirty()
     return ret
@@ -2081,6 +2167,12 @@
                 return tagscache
 
             mqtags = [(revlog.bin(patch.rev), 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'
+                             % revlog.short(mqtags[-1][0]))
+                return tagscache
+
             mqtags.append((mqtags[-1][0], 'qtip'))
             mqtags.append((mqtags[0][0], 'qbase'))
             mqtags.append((self.changelog.parents(mqtags[0][0])[0], 'qparent'))
@@ -2097,11 +2189,17 @@
             if not q.applied:
                 return super(mqrepo, self)._branchtags()
 
+            cl = self.changelog
+            qbasenode = revlog.bin(q.applied[0].rev)
+            if qbasenode not in cl.nodemap:
+                self.ui.warn('mq status file refers to unknown node %s\n'
+                             % revlog.short(qbasenode))
+                return super(mqrepo, self)._branchtags()
+
             self.branchcache = {} # avoid recursion in changectx
-            cl = self.changelog
             partial, last, lrev = self._readbranchcache()
 
-            qbase = cl.rev(revlog.bin(q.applied[0].rev))
+            qbase = cl.rev(qbasenode)
             start = lrev + 1
             if start < qbase:
                 # update the cache (excluding the patches) and save it
@@ -2123,6 +2221,12 @@
 
 seriesopts = [('s', 'summary', None, _('print first line of patch header'))]
 
+headeropts = [
+    ('U', 'currentuser', None, _('add "From: <current user>" to patch')),
+    ('u', 'user', '', _('add "From: <given user>" to patch')),
+    ('D', 'currentdate', None, _('add "Date: <current date>" to patch')),
+    ('d', 'date', '', _('add "Date: <given date>" to patch'))]
+
 cmdtable = {
     "qapplied": (applied, [] + seriesopts, _('hg qapplied [-s] [PATCH]')),
     "qclone":
@@ -2131,10 +2235,8 @@
           ('U', 'noupdate', None, _('do not update the new working directories')),
           ('', 'uncompressed', None,
            _('use uncompressed transfer (fast over LAN)')),
-          ('e', 'ssh', '', _('specify ssh command to use')),
           ('p', 'patches', '', _('location of source patch repo')),
-          ('', 'remotecmd', '',
-           _('specify hg command to run on the remote side'))],
+         ] + commands.remoteopts,
          _('hg qclone [OPTION]... SOURCE [DEST]')),
     "qcommit|qci":
         (commit,
@@ -2143,9 +2245,8 @@
     "^qdiff":
         (diff,
          [('g', 'git', None, _('use git extended diff format')),
-          ('I', 'include', [], _('include names matching the given patterns')),
-          ('X', 'exclude', [], _('exclude names matching the given patterns')),
-          ('U', 'unified', 3, _('number of lines of context to show'))],
+          ('U', 'unified', 3, _('number of lines of context to show')),
+         ] + commands.walkopts,
          _('hg qdiff [-I] [-X] [-U NUM] [-g] [FILE]...')),
     "qdelete|qremove|qrm":
         (delete,
@@ -2184,9 +2285,8 @@
         (new,
          [('e', 'edit', None, _('edit commit message')),
           ('f', 'force', None, _('import uncommitted changes into patch')),
-          ('I', 'include', [], _('include names matching the given patterns')),
-          ('X', 'exclude', [], _('exclude names matching the given patterns')),
-          ] + commands.commitopts,
+          ('g', 'git', None, _('use git extended diff format')),
+          ] + commands.walkopts + commands.commitopts + headeropts,
          _('hg qnew [-e] [-m TEXT] [-l FILE] [-f] PATCH [FILE]...')),
     "qnext": (next, [] + seriesopts, _('hg qnext [-s]')),
     "qprev": (prev, [] + seriesopts, _('hg qprev [-s]')),
@@ -2209,9 +2309,7 @@
          [('e', 'edit', None, _('edit commit message')),
           ('g', 'git', None, _('use git extended diff format')),
           ('s', 'short', None, _('refresh only files already in the patch')),
-          ('I', 'include', [], _('include names matching the given patterns')),
-          ('X', 'exclude', [], _('exclude names matching the given patterns')),
-          ] + commands.commitopts,
+          ] + commands.walkopts + commands.commitopts + headeropts,
          _('hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] [FILE]...')),
     'qrename|qmv':
         (rename, [], _('hg qrename PATCH1 [PATCH2]')),