[PATCH] mq: automatically upgrade to git patch when necessary (issue767)

Patrick Mezard pmezard at gmail.com
Tue Dec 29 13:59:00 CST 2009


# HG changeset patch
# User Patrick Mezard <pmezard at gmail.com>
# Date 1262113500 -3600
# Node ID 61f7e9239f3b41d6b8e8e1c2858f2309f954f246
# Parent  c7355a0e1f39968ab39a62e9bf059e5129a61497
mq: automatically upgrade to git patch when necessary (issue767)

diff --git a/hgext/mq.py b/hgext/mq.py
--- a/hgext/mq.py
+++ b/hgext/mq.py
@@ -26,6 +26,18 @@
   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 changes to file modes, copy records or binary files. This
+behaviour can configured with:
+
+  [mq]
+  git = auto/keep/no
+
+If set to 'keep', mq will obey the [diff] section configuration while
+preserving existing git patches upon qrefresh. If set to 'no', mq will
+override the [diff] section and generate regular patches, possibly
+discarding data.
 '''
 
 from mercurial.i18n import _
@@ -226,6 +238,7 @@
         self.active_guards = None
         self.guards_dirty = False
         self._diffopts = None
+        self.gitmode = ui.config('mq', 'git', 'auto')
 
     @util.propertycache
     def applied(self):
@@ -261,7 +274,17 @@
 
     def diffopts(self):
         if self._diffopts is None:
-            self._diffopts = patch.diffopts(self.ui)
+            opts = patch.diffopts(self.ui)
+            if self.gitmode == 'auto':
+                opts.upgrade = True
+            elif self.gitmode == 'keep':
+                pass
+            elif self.gitmode == 'no':
+                opts.git = False
+            else:
+                raise util.Abort(_('mq.git option can be auto/keep/no, got %s')
+                                 % self.gitmode)
+            self._diffopts = opts
         return self._diffopts
 
     def join(self, *p):
@@ -1167,12 +1190,13 @@
                 ph.setdate(newdate)
 
             # if the patch was a git patch, refresh it as a git patch
-            patchf = self.opener(patchfn, 'r')
-            for line in patchf:
-                if line.startswith('diff --git'):
-                    self.diffopts().git = True
-                    break
-            patchf.close()
+            if not self.diffopts().git and self.gitmode == 'keep':
+                patchf = self.opener(patchfn, 'r')
+                for line in patchf:
+                    if line.startswith('diff --git'):
+                        self.diffopts().git = True
+                        break
+                patchf.close()
 
             # only commit new patch when write is complete
             patchf = self.opener(patchfn, 'w', atomictemp=True)
@@ -1253,7 +1277,7 @@
                     patchf.write(chunk)
 
                 try:
-                    if self.diffopts().git:
+                    if self.diffopts().git or self.diffopts().upgrade:
                         copies = {}
                         for dst in a:
                             src = repo.dirstate.copied(dst)
diff --git a/mercurial/mdiff.py b/mercurial/mdiff.py
--- a/mercurial/mdiff.py
+++ b/mercurial/mdiff.py
@@ -27,7 +27,9 @@
     nodates removes dates from diff headers
     ignorews ignores all whitespace changes in the diff
     ignorewsamount ignores changes in the amount of whitespace
-    ignoreblanklines ignores changes whose lines are all blank'''
+    ignoreblanklines ignores changes whose lines are all blank
+    upgrade generates git diffs to avoid data loss
+    '''
 
     defaults = {
         'context': 3,
@@ -38,6 +40,7 @@
         'ignorews': False,
         'ignorewsamount': False,
         'ignoreblanklines': False,
+        'upgrade': False,
         }
 
     __slots__ = defaults.keys()
@@ -55,6 +58,11 @@
             raise util.Abort(_('diff context lines count must be '
                                'an integer, not %r') % self.context)
 
+    def copy(self, **kwargs):
+        opts = dict((k, getattr(self, k)) for k in self.defaults)
+        opts.update(kwargs)
+        return diffopts(**opts)
+
 defaultopts = diffopts()
 
 def wsclean(opts, text, blank=True):
diff --git a/mercurial/patch.py b/mercurial/patch.py
--- a/mercurial/patch.py
+++ b/mercurial/patch.py
@@ -1246,10 +1246,8 @@
     ret.append('\n')
     return ''.join(ret)
 
-def _addmodehdr(header, omode, nmode):
-    if omode != nmode:
-        header.append('old mode %s\n' % omode)
-        header.append('new mode %s\n' % nmode)
+class GitDiffRequired(Exception):
+    pass
 
 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None):
     '''yields diff of changes to files between two nodes, or node and
@@ -1288,24 +1286,47 @@
     modified, added, removed = changes[:3]
 
     if not modified and not added and not removed:
-        return
+        return []
+
+    revs = None
+    if not repo.ui.quiet:
+        hexfunc = repo.ui.debugflag and hex or short
+        revs = [hexfunc(node) for node in [node1, node2] if node]
+
+    copy = {}
+    if opts.git or opts.upgrade:
+        copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
+        copy = copy.copy()
+        for k, v in copy.items():
+            copy[v] = k
+
+    difffn = lambda opts: trydiff(repo, revs, ctx1, ctx2, modified,
+                                  added, removed, copy, getfilectx, opts)
+    if opts.upgrade and not opts.git:
+        try:
+            # Buffer the whole output until we are sure it can be generated
+            return list(difffn(opts.copy(git=False)))
+        except GitDiffRequired:
+            return difffn(opts.copy(git=True))
+    else:
+        return difffn(opts)
+
+def _addmodehdr(header, omode, nmode):
+    if omode != nmode:
+        header.append('old mode %s\n' % omode)
+        header.append('new mode %s\n' % nmode)
+
+def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
+            copy, getfilectx, opts):
 
     date1 = util.datestr(ctx1.date())
     man1 = ctx1.manifest()
 
-    revs = None
-    if not repo.ui.quiet and not opts.git:
-        hexfunc = repo.ui.debugflag and hex or short
-        revs = [hexfunc(node) for node in [node1, node2] if node]
+    gone = set()
+    gitmode = {'l': '120000', 'x': '100755', '': '100644'}
 
     if opts.git:
-        copy, diverge = copies.copies(repo, ctx1, ctx2, repo[nullid])
-        copy = copy.copy()
-        for k, v in copy.items():
-            copy[v] = k
-
-    gone = set()
-    gitmode = {'l': '120000', 'x': '100755', '': '100644'}
+        revs = None
 
     for f in sorted(modified + added + removed):
         to = None
@@ -1317,10 +1338,12 @@
         if f not in removed:
             tn = getfilectx(f, ctx2).data()
         a, b = f, f
-        if opts.git:
+        if opts.git or opts.upgrade:
             if f in added:
                 mode = gitmode[ctx2.flags(f)]
                 if f in copy:
+                    if not opts.git:
+                        raise GitDiffRequired()
                     a = copy[f]
                     omode = gitmode[man1.flags(a)]
                     _addmodehdr(header, omode, mode)
@@ -1333,8 +1356,12 @@
                     header.append('%s to %s\n' % (op, f))
                     to = getfilectx(a, ctx1).data()
                 else:
+                    if not opts.git and ctx2.flags(f):
+                        raise GitDiffRequired()
                     header.append('new file mode %s\n' % mode)
                 if util.binary(tn):
+                    if not opts.git:
+                        raise GitDiffRequired()
                     dodiff = 'binary'
             elif f in removed:
                 # have we already reported a copy above?
@@ -1347,9 +1374,19 @@
                 omode = gitmode[man1.flags(f)]
                 nmode = gitmode[ctx2.flags(f)]
                 _addmodehdr(header, omode, nmode)
-                if util.binary(to) or util.binary(tn):
+                binary = util.binary(to) or util.binary(tn)
+                if binary:
                     dodiff = 'binary'
+                if not opts.git and (man1.flags(f) or ctx2.flags(f) or binary):
+                    raise GitDiffRequired()
             header.insert(0, mdiff.diffline(revs, a, b, opts))
+
+            if not opts.git:
+                # It's simpler to cleanup git changes than branching
+                # all the code
+                dodiff = True
+                header = []
+
         if dodiff:
             if dodiff == 'binary':
                 text = b85diff(to, tn)
diff --git a/tests/test-mq-eol b/tests/test-mq-eol
--- a/tests/test-mq-eol
+++ b/tests/test-mq-eol
@@ -4,6 +4,8 @@
 
 echo "[extensions]" >> $HGRCPATH
 echo "mq=" >> $HGRCPATH
+echo "[diff]" >> $HGRCPATH
+echo "nodates=1" >> $HGRCPATH
 
 cat > makepatch.py <<EOF
 f = file('eol.diff', 'wb')
diff --git a/tests/test-mq-eol.out b/tests/test-mq-eol.out
--- a/tests/test-mq-eol.out
+++ b/tests/test-mq-eol.out
@@ -23,7 +23,7 @@
 now at: eol.diff
 test message<LF>
 <LF>
-diff --git a/a b/a<LF>
+diff -r 0d0bf99a8b7a a<LF>
 --- a/a<LF>
 +++ b/a<LF>
 @@ -1,5 +1,5 @@<LF>
diff --git a/tests/test-mq-git b/tests/test-mq-git
new file mode 100755
--- /dev/null
+++ b/tests/test-mq-git
@@ -0,0 +1,106 @@
+#!/bin/sh
+
+echo "[extensions]" >> $HGRCPATH
+echo "mq=" >> $HGRCPATH
+echo "[diff]" >> $HGRCPATH
+echo "nodates=1" >> $HGRCPATH
+
+echo % test mq.upgrade
+hg init repo-upgrade
+cd repo-upgrade
+
+echo % regular patch creation
+echo a > a
+hg add a
+hg qnew -d '0 0' -f pa
+cat .hg/patches/pa
+echo % git patch after qrefresh and execute bit
+chmod +x a
+hg qrefresh -d '0 0' 
+cat .hg/patches/pa
+
+echo % regular patch for file removal
+hg rm a
+hg qnew -d '0 0' -f rma
+cat .hg/patches/rma
+
+echo % git patch creation for execute bit
+echo b > b
+chmod +x b
+hg add b
+hg qnew -d '0 0' -f pb
+cat .hg/patches/pb
+echo % regular patch after execute bit removal
+chmod -x b
+hg qrefresh -d '0 0' 
+cat .hg/patches/pb
+
+echo % git patch creation for copy
+hg cp b b2
+hg qnew -d '0 0' -f copyb
+cat .hg/patches/copyb
+
+echo % regular patch creation for text change
+echo b >> b
+hg qnew -d '0 0' -f changeb
+cat .hg/patches/changeb
+
+echo % git patch after qrefresh on binary file
+python -c "file('b', 'wb').write('\0')"
+hg qrefresh -d '0 0' 
+cat .hg/patches/changeb
+
+echo % regular patch after changing binary into text file
+echo bbb > b
+hg qrefresh -d '0 0' 
+cat .hg/patches/changeb
+
+echo % regular patch with file filtering
+echo regular > regular
+echo exec > exec
+chmod +x exec
+python -c "file('binary', 'wb').write('\0')"
+hg cp b copy
+hg add regular exec binary
+hg qnew -d '0 0' -f regular regular
+cat .hg/patches/regular
+hg qrefresh -d '0 0' 
+
+echo % git patch when using --git
+echo a >> regular
+hg qnew -d '0 0' --git -f git
+cat .hg/patches/git
+echo % regular patch after qrefresh without --git
+hg qrefresh -d '0 0' 
+cat .hg/patches/git
+cd ..
+
+hg init repo-keep
+cd repo-keep
+echo % test mq.git=keep
+echo '[mq]' > .hg/hgrc
+echo 'git = keep' >> .hg/hgrc
+echo a > a
+hg add a
+hg qnew -d '0 0' -f --git git
+cat .hg/patches/git
+echo a >> a
+hg qrefresh -d '0 0'
+cat .hg/patches/git
+cd ..
+
+hg init repo-no
+cd repo-no
+echo % test mq.git=no
+echo '[mq]' > .hg/hgrc
+echo 'git = no' >> .hg/hgrc
+echo a > a
+chmod +x a
+hg add a
+hg qnew -d '0 0' -f regular
+cat .hg/patches/regular
+echo a >> a
+chmod +x a
+hg qrefresh -d '0 0'
+cat .hg/patches/regular
+cd ..
\ No newline at end of file
diff --git a/tests/test-mq-git.out b/tests/test-mq-git.out
new file mode 100644
--- /dev/null
+++ b/tests/test-mq-git.out
@@ -0,0 +1,152 @@
+% test mq.upgrade
+% regular patch creation
+# HG changeset patch
+# Date 0 0
+
+diff -r 000000000000 -r 5d9da5fe342b a
+--- /dev/null
++++ b/a
+@@ -0,0 +1,1 @@
++a
+% git patch after qrefresh and execute bit
+# HG changeset patch
+# Date 0 0
+
+diff --git a/a b/a
+new file mode 100755
+--- /dev/null
++++ b/a
+@@ -0,0 +1,1 @@
++a
+% regular patch for file removal
+# HG changeset patch
+# Date 0 0
+
+diff -r cf31b8738214 -r 94897e9819cb a
+--- a/a
++++ /dev/null
+@@ -1,1 +0,0 @@
+-a
+% git patch creation for execute bit
+# HG changeset patch
+# Date 0 0
+
+diff --git a/b b/b
+new file mode 100755
+--- /dev/null
++++ b/b
+@@ -0,0 +1,1 @@
++b
+% regular patch after execute bit removal
+# HG changeset patch
+# Date 0 0
+
+diff -r 94897e9819cb b
+--- /dev/null
++++ b/b
+@@ -0,0 +1,1 @@
++b
+% git patch creation for copy
+# HG changeset patch
+# Date 0 0
+
+diff --git a/b b/b2
+copy from b
+copy to b2
+% regular patch creation for text change
+# HG changeset patch
+# Date 0 0
+
+diff -r 7334c7fc8cbe -r bd5fafe02efe b
+--- a/b
++++ b/b
+@@ -1,1 +1,2 @@
+ b
++b
+% git patch after qrefresh on binary file
+# HG changeset patch
+# Date 0 0
+
+diff --git a/b b/b
+index 61780798228d17af2d34fce4cfbdf35556832472..f76dd238ade08917e6712764a16a22005a50573d
+GIT binary patch
+literal 1
+Ic${MZ000310RR91
+
+% regular patch after changing binary into text file
+# HG changeset patch
+# Date 0 0
+
+diff -r 7334c7fc8cbe b
+--- a/b
++++ b/b
+@@ -1,1 +1,1 @@
+-b
++bbb
+% regular patch with file filtering
+# HG changeset patch
+# Date 0 0
+
+diff -r 921b0108acc0 -r d7603636026d regular
+--- /dev/null
++++ b/regular
+@@ -0,0 +1,1 @@
++regular
+% git patch when using --git
+# HG changeset patch
+# Date 0 0
+
+diff --git a/regular b/regular
+--- a/regular
++++ b/regular
+@@ -1,1 +1,2 @@
+ regular
++a
+% regular patch after qrefresh without --git
+# HG changeset patch
+# Date 0 0
+
+diff -r f56b7240b02e regular
+--- a/regular
++++ b/regular
+@@ -1,1 +1,2 @@
+ regular
++a
+% test mq.git=keep
+# HG changeset patch
+# Date 0 0
+
+diff --git a/a b/a
+new file mode 100644
+--- /dev/null
++++ b/a
+@@ -0,0 +1,1 @@
++a
+# HG changeset patch
+# Date 0 0
+
+diff --git a/a b/a
+new file mode 100644
+--- /dev/null
++++ b/a
+@@ -0,0 +1,2 @@
++a
++a
+% test mq.git=no
+# HG changeset patch
+# Date 0 0
+
+diff -r 000000000000 -r d27466151582 a
+--- /dev/null
++++ b/a
+@@ -0,0 +1,1 @@
++a
+# HG changeset patch
+# Date 0 0
+
+diff -r 000000000000 a
+--- /dev/null
++++ b/a
+@@ -0,0 +1,2 @@
++a
++a
diff --git a/tests/test-mq.out b/tests/test-mq.out
--- a/tests/test-mq.out
+++ b/tests/test-mq.out
@@ -21,6 +21,17 @@
   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 changes to file modes, copy records or binary files. This behaviour can
+configured with:
+
+  [mq] git = auto/keep/no
+
+If set to 'keep', mq will obey the [diff] section configuration while
+preserving existing git patches upon qrefresh. If set to 'no', mq will
+override the [diff] section and generate regular patches, possibly discarding
+data.
+
 list of commands:
 
  qapplied     print the patches already applied


More information about the Mercurial-devel mailing list