[PATCH] mq: automatically upgrade to git patch when necessary (issue767)
Matt Mackall
mpm at selenic.com
Tue Dec 29 20:18:29 UTC 2009
On Tue, 2009-12-29 at 20:59 +0100, Patrick Mezard wrote:
> # 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)
Hiding in here is most of another oft-requested feature: reporting when
normal diff or export commands generate lossy patches. Given that we
probably don't want such patches to fail in the middle, I think
exceptions are the wrong reporting mechanism to use here.
I'd rather see the loss-detection bits on their own first and some
thoughts as to how to implement the lossy patch reporting, followed by
the mq-specific bits.
>
> 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
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel at selenic.com
> http://selenic.com/mailman/listinfo/mercurial-devel
--
http://selenic.com : development and support for Mercurial and Linux
More information about the Mercurial-devel
mailing list