graphlog: apply file filters --patch/--stat output
authorPatrick Mezard <patrick@mezard.eu>
Sun, 26 Feb 2012 17:12:15 +0100
changeset 16186 af3e67354beb
parent 16185 352053e6cd8e
child 16192 a4413624d014
graphlog: apply file filters --patch/--stat output When passing --patch/--stat, file filters have to be applied to generate the correct diff or stat output: - Without --follow, the static match object can be reused - With --follow, the files displayed at revision X are the ancestors of selected files at parent revision. To do this, we reproduce the ancestry calculations done by --follow, lazily. test-glog.t changes show that --patch output is not satisfying because renames are reported as copies. This can probably be fixed by: - Without --follow: compute files to display, look for renames sources and extend the matcher to include them. - With --follow: detect .path() transitions between parent/child filectx, filter them using the linked changectx .removed() field and extend fcache with them.
hgext/graphlog.py
tests/test-glog.t
--- a/hgext/graphlog.py	Sun Feb 26 17:10:57 2012 +0100
+++ b/hgext/graphlog.py	Sun Feb 26 17:12:15 2012 +0100
@@ -242,8 +242,40 @@
             raise util.Abort(_("-G/--graph option is incompatible with --%s")
                              % op.replace("_", "-"))
 
+def makefilematcher(repo, pats, followfirst):
+    # When displaying a revision with --patch --follow FILE, we have
+    # to know which file of the revision must be diffed. With
+    # --follow, we want the names of the ancestors of FILE in the
+    # revision, stored in "fcache". "fcache" is populated by
+    # reproducing the graph traversal already done by --follow revset
+    # and relating linkrevs to file names (which is not "correct" but
+    # good enough).
+    fcache = {}
+    fcacheready = [False]
+    pctx = repo['.']
+    wctx = repo[None]
+
+    def populate():
+        for fn in pats:
+            for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
+                for c in i:
+                    fcache.setdefault(c.linkrev(), set()).add(c.path())
+
+    def filematcher(rev):
+        if not fcacheready[0]:
+            # Lazy initialization
+            fcacheready[0] = True
+            populate()
+        return scmutil.match(wctx, fcache.get(rev, []), default='path')
+
+    return filematcher
+
 def revset(repo, pats, opts):
-    """Return revset str built of revisions, log options and file patterns.
+    """Return (expr, filematcher) where expr is a revset string built
+    of revisions, log options and file patterns. If --stat or --patch
+    are not passed filematcher is None. Otherwise it a a callable
+    taking a revision number and returning a match objects filtering
+    the files to be detailed when displaying the revision.
     """
     opt2revset = {
         'follow':           ('follow()', None),
@@ -329,6 +361,13 @@
         else:
             opts['_patslog'] = list(pats)
 
+    filematcher = None
+    if opts.get('patch') or opts.get('stat'):
+        if follow:
+            filematcher = makefilematcher(repo, pats, followfirst)
+        else:
+            filematcher = lambda rev: match
+
     revset = []
     for op, val in opts.iteritems():
         if not val:
@@ -349,9 +388,10 @@
         revset = '(' + ' and '.join(revset) + ')'
     else:
         revset = 'all()'
-    return revset
+    return revset, filematcher
 
-def generate(ui, dag, displayer, showparents, edgefn, getrenamed=None):
+def generate(ui, dag, displayer, showparents, edgefn, getrenamed=None,
+             filematcher=None):
     seen, state = [], asciistate()
     for rev, type, ctx, parents in dag:
         char = ctx.node() in showparents and '@' or 'o'
@@ -362,7 +402,10 @@
                 rename = getrenamed(fn, ctx.rev())
                 if rename:
                     copies.append((fn, rename[0]))
-        displayer.show(ctx, copies=copies)
+        revmatchfn = None
+        if filematcher is not None:
+            revmatchfn = filematcher(ctx.rev())
+        displayer.show(ctx, copies=copies, matchfn=revmatchfn)
         lines = displayer.hunk.pop(rev).split('\n')[:-1]
         displayer.flush(rev)
         edges = edgefn(type, char, lines, seen, rev, parents)
@@ -389,7 +432,8 @@
 
     check_unsupported_flags(pats, opts)
 
-    revs = sorted(scmutil.revrange(repo, [revset(repo, pats, opts)]), reverse=1)
+    expr, filematcher = revset(repo, pats, opts)
+    revs = sorted(scmutil.revrange(repo, [expr]), reverse=1)
     limit = cmdutil.loglimit(opts)
     if limit is not None:
         revs = revs[:limit]
@@ -403,7 +447,8 @@
         getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
     displayer = show_changeset(ui, repo, opts, buffered=True)
     showparents = [ctx.node() for ctx in repo[None].parents()]
-    generate(ui, revdag, displayer, showparents, asciiedges, getrenamed)
+    generate(ui, revdag, displayer, showparents, asciiedges, getrenamed,
+             filematcher)
 
 def graphrevs(repo, nodes, opts):
     limit = cmdutil.loglimit(opts)
--- a/tests/test-glog.t	Sun Feb 26 17:10:57 2012 +0100
+++ b/tests/test-glog.t	Sun Feb 26 17:12:15 2012 +0100
@@ -90,7 +90,7 @@
   > def uisetup(ui):
   >     def printrevset(orig, ui, repo, *pats, **opts):
   >         if opts.get('print_revset'):
-  >             expr = graphlog.revset(repo, pats, opts)
+  >             expr = graphlog.revset(repo, pats, opts)[0]
   >             tree = revset.parse(expr)[0]
   >             ui.write(tree, "\n")
   >             return 0
@@ -1655,3 +1655,99 @@
   abort: can only follow copies/renames for explicit filenames
   abort: can only follow copies/renames for explicit filenames
   abort: can only follow copies/renames for explicit filenames
+
+Test --patch and --stat with --follow and --follow-first
+
+  $ hg up -q 3
+  $ hg log -G --git --patch b
+  o  changeset:   1:216d4c92cf98
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     copy a b
+  |
+  |  diff --git a/a b/b
+  |  copy from a
+  |  copy to b
+  |
+
+  $ hg log -G --git --stat b
+  o  changeset:   1:216d4c92cf98
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     copy a b
+  |
+  |   a |  0
+  |   1 files changed, 0 insertions(+), 0 deletions(-)
+  |
+
+  $ hg log -G --git --patch --follow b
+  o  changeset:   1:216d4c92cf98
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     copy a b
+  |
+  |  diff --git a/a b/b
+  |  copy from a
+  |  copy to b
+  |
+  o  changeset:   0:f8035bb17114
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     add a
+  
+     diff --git a/a b/a
+     new file mode 100644
+     --- /dev/null
+     +++ b/a
+     @@ -0,0 +1,1 @@
+     +a
+  
+
+  $ hg log -G --git --stat --follow b
+  o  changeset:   1:216d4c92cf98
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     copy a b
+  |
+  |   a |  0
+  |   1 files changed, 0 insertions(+), 0 deletions(-)
+  |
+  o  changeset:   0:f8035bb17114
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     add a
+  
+      a |  1 +
+      1 files changed, 1 insertions(+), 0 deletions(-)
+  
+
+  $ hg up -q 6
+  $ hg log -G --git --patch --follow-first e
+  @    changeset:   6:fc281d8ff18d
+  |\   tag:         tip
+  | |  parent:      5:99b31f1c2782
+  | |  parent:      4:17d952250a9d
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     merge 5 and 4
+  | |
+  | |  diff --git a/e b/e
+  | |  --- a/e
+  | |  +++ b/e
+  | |  @@ -1,1 +1,1 @@
+  | |  -ee
+  | |  +merge
+  | |
+  o |  changeset:   5:99b31f1c2782
+  | |  parent:      3:5918b8d165d1
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     add another e
+  | |
+  | |  diff --git a/e b/e
+  | |  new file mode 100644
+  | |  --- /dev/null
+  | |  +++ b/e
+  | |  @@ -0,0 +1,1 @@
+  | |  +ee
+  | |