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

Matt Mackall mpm at selenic.com
Tue Dec 29 14:18:29 CST 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