Patchwork [5,of,5] grep: add formatter support

login
register
mail settings
Submitter Yuya Nishihara
Date Aug. 25, 2016, 3:24 p.m.
Message ID <ee8d8324b0cfd94c619f.1472138698@mimosa>
Download mbox | patch
Permalink /patch/16416/
State Accepted
Headers show

Comments

Yuya Nishihara - Aug. 25, 2016, 3:24 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1471500836 -32400
#      Thu Aug 18 15:13:56 2016 +0900
# Node ID ee8d8324b0cfd94c619f81b5351b727b9f640fb7
# Parent  68f772b9eee689b7d65647a96e1224e755dd26ed
grep: add formatter support

Several fields are renamed to be consistent with the annotate command, which
doesn't mean the last call for the name unification [1]. Actually, I'd rather
rename line_number to linenumber, linenum, lineno or line, but I want to
port the grep command to formatter first.

 [1]: https://www.mercurial-scm.org/wiki/GenericTemplatingPlan#Dictionary

I don't have any better name for the list of matched/unmatched texts, so
they are just called as "texts".
Augie Fackler - Aug. 29, 2016, 1:35 p.m.
On Fri, Aug 26, 2016 at 12:24:58AM +0900, Yuya Nishihara wrote:
> # HG changeset patch
> # User Yuya Nishihara <yuya@tcha.org>
> # Date 1471500836 -32400
> #      Thu Aug 18 15:13:56 2016 +0900
> # Node ID ee8d8324b0cfd94c619f81b5351b727b9f640fb7
> # Parent  68f772b9eee689b7d65647a96e1224e755dd26ed
> grep: add formatter support

Queued these, thanks

>
> Several fields are renamed to be consistent with the annotate command, which
> doesn't mean the last call for the name unification [1]. Actually, I'd rather
> rename line_number to linenumber, linenum, lineno or line, but I want to
> port the grep command to formatter first.
>
>  [1]: https://www.mercurial-scm.org/wiki/GenericTemplatingPlan#Dictionary
>
> I don't have any better name for the list of matched/unmatched texts, so
> they are just called as "texts".
>
> diff --git a/mercurial/commands.py b/mercurial/commands.py
> --- a/mercurial/commands.py
> +++ b/mercurial/commands.py
> @@ -4283,7 +4283,7 @@ def _dograft(ui, repo, *revs, **opts):
>       _('only search files changed within revision range'), _('REV')),
>      ('u', 'user', None, _('list the author (long with -v)')),
>      ('d', 'date', None, _('list the date (short with -q)')),
> -    ] + walkopts,
> +    ] + formatteropts + walkopts,
>      _('[OPTION]... PATTERN [FILE]...'),
>      inferrepo=True)
>  def grep(ui, repo, pattern, *pats, **opts):
> @@ -4380,59 +4380,75 @@ def grep(ui, repo, pattern, *pats, **opt
>                  for i in xrange(blo, bhi):
>                      yield ('+', b[i])
>
> -    def display(fn, ctx, pstates, states):
> +    def display(fm, fn, ctx, pstates, states):
>          rev = ctx.rev()
> +        if fm:
> +            formatuser = str
> +        else:
> +            formatuser = ui.shortuser
>          if ui.quiet:
> -            datefunc = util.shortdate
> +            datefmt = '%Y-%m-%d'
>          else:
> -            datefunc = util.datestr
> +            datefmt = '%a %b %d %H:%M:%S %Y %1%2'
>          found = False
>          @util.cachefunc
>          def binary():
>              flog = getfile(fn)
>              return util.binary(flog.read(ctx.filenode(fn)))
>
> +        fieldnamemap = {'filename': 'file', 'linenumber': 'line_number'}
>          if opts.get('all'):
>              iter = difflinestates(pstates, states)
>          else:
>              iter = [('', l) for l in states]
>          for change, l in iter:
> +            fm.startitem()
> +            fm.data(node=fm.hexfunc(ctx.node()))
>              cols = [
>                  ('filename', fn, True),
> -                ('rev', str(rev), True),
> -                ('linenumber', str(l.linenum), opts.get('line_number')),
> +                ('rev', rev, True),
> +                ('linenumber', l.linenum, opts.get('line_number')),
>              ]
>              if opts.get('all'):
>                  cols.append(('change', change, True))
>              cols.extend([
> -                ('user', ui.shortuser(ctx.user()), opts.get('user')),
> -                ('date', datefunc(ctx.date()), opts.get('date')),
> +                ('user', formatuser(ctx.user()), opts.get('user')),
> +                ('date', fm.formatdate(ctx.date(), datefmt), opts.get('date')),
>              ])
>              lastcol = next(name for name, data, cond in reversed(cols) if cond)
>              for name, data, cond in cols:
> -                if cond:
> -                    ui.write(data, label='grep.%s' % name)
> +                field = fieldnamemap.get(name, name)
> +                fm.condwrite(cond, field, '%s', data, label='grep.%s' % name)
>                  if cond and name != lastcol:
> -                    ui.write(sep, label='grep.sep')
> +                    fm.plain(sep, label='grep.sep')
>              if not opts.get('files_with_matches'):
> -                ui.write(sep, label='grep.sep')
> +                fm.plain(sep, label='grep.sep')
>                  if not opts.get('text') and binary():
> -                    ui.write(_(" Binary file matches"))
> +                    fm.plain(_(" Binary file matches"))
>                  else:
> -                    displaymatches(l)
> -            ui.write(eol)
> +                    displaymatches(fm.nested('texts'), l)
> +            fm.plain(eol)
>              found = True
>              if opts.get('files_with_matches'):
>                  break
>          return found
>
> -    def displaymatches(l):
> +    def displaymatches(fm, l):
>          p = 0
>          for s, e in l.findpos():
> -            ui.write(l.line[p:s])
> -            ui.write(l.line[s:e], label='grep.match')
> +            if p < s:
> +                fm.startitem()
> +                fm.write('text', '%s', l.line[p:s])
> +                fm.data(matched=False)
> +            fm.startitem()
> +            fm.write('text', '%s', l.line[s:e], label='grep.match')
> +            fm.data(matched=True)
>              p = e
> -        ui.write(l.line[p:])
> +        if p < len(l.line):
> +            fm.startitem()
> +            fm.write('text', '%s', l.line[p:])
> +            fm.data(matched=False)
> +        fm.end()
>
>      skip = {}
>      revfiles = {}
> @@ -4475,6 +4491,7 @@ def grep(ui, repo, pattern, *pats, **opt
>                  except error.LookupError:
>                      pass
>
> +    fm = ui.formatter('grep', opts)
>      for ctx in cmdutil.walkchangerevs(repo, matchfn, opts, prep):
>          rev = ctx.rev()
>          parent = ctx.p1().rev()
> @@ -4487,7 +4504,7 @@ def grep(ui, repo, pattern, *pats, **opt
>                  continue
>              pstates = matches.get(parent, {}).get(copy or fn, [])
>              if pstates or states:
> -                r = display(fn, ctx, pstates, states)
> +                r = display(fm, fn, ctx, pstates, states)
>                  found = found or r
>                  if r and not opts.get('all'):
>                      skip[fn] = True
> @@ -4495,6 +4512,7 @@ def grep(ui, repo, pattern, *pats, **opt
>                          skip[copy] = True
>          del matches[rev]
>          del revfiles[rev]
> +    fm.end()
>
>      return not found
>
> diff --git a/tests/test-completion.t b/tests/test-completion.t
> --- a/tests/test-completion.t
> +++ b/tests/test-completion.t
> @@ -278,7 +278,7 @@ Show all commands + options
>    debugwireargs: three, four, five, ssh, remotecmd, insecure
>    files: rev, print0, include, exclude, template, subrepos
>    graft: rev, continue, edit, log, force, currentdate, currentuser, date, user, tool, dry-run
> -  grep: print0, all, text, follow, ignore-case, files-with-matches, line-number, rev, user, date, include, exclude
> +  grep: print0, all, text, follow, ignore-case, files-with-matches, line-number, rev, user, date, template, include, exclude
>    heads: rev, topo, active, closed, style, template
>    help: extension, command, keyword, system
>    identify: rev, num, id, branch, tags, bookmarks, ssh, remotecmd, insecure
> diff --git a/tests/test-grep.t b/tests/test-grep.t
> --- a/tests/test-grep.t
> +++ b/tests/test-grep.t
> @@ -40,6 +40,61 @@ simple with color
>    \x1b[0;35mport\x1b[0m\x1b[0;36m:\x1b[0m\x1b[0;32m4\x1b[0m\x1b[0;36m:\x1b[0mva\x1b[0;31;1mport\x1b[0might (esc)
>    \x1b[0;35mport\x1b[0m\x1b[0;36m:\x1b[0m\x1b[0;32m4\x1b[0m\x1b[0;36m:\x1b[0mim\x1b[0;31;1mport\x1b[0m/ex\x1b[0;31;1mport\x1b[0m (esc)
>
> +simple templated
> +
> +  $ hg grep port \
> +  > -T '{file}:{rev}:{node|short}:{texts % "{if(matched, text|upper, text)}"}\n'
> +  port:4:914fa752cdea:exPORT
> +  port:4:914fa752cdea:vaPORTight
> +  port:4:914fa752cdea:imPORT/exPORT
> +
> +simple JSON (no "change" field)
> +
> +  $ hg grep -Tjson port
> +  [
> +   {
> +    "date": [4.0, 0],
> +    "file": "port",
> +    "line_number": 1,
> +    "node": "914fa752cdea87777ac1a8d5c858b0c736218f6c",
> +    "rev": 4,
> +    "texts": [{"matched": false, "text": "ex"}, {"matched": true, "text": "port"}],
> +    "user": "spam"
> +   },
> +   {
> +    "date": [4.0, 0],
> +    "file": "port",
> +    "line_number": 2,
> +    "node": "914fa752cdea87777ac1a8d5c858b0c736218f6c",
> +    "rev": 4,
> +    "texts": [{"matched": false, "text": "va"}, {"matched": true, "text": "port"}, {"matched": false, "text": "ight"}],
> +    "user": "spam"
> +   },
> +   {
> +    "date": [4.0, 0],
> +    "file": "port",
> +    "line_number": 3,
> +    "node": "914fa752cdea87777ac1a8d5c858b0c736218f6c",
> +    "rev": 4,
> +    "texts": [{"matched": false, "text": "im"}, {"matched": true, "text": "port"}, {"matched": false, "text": "/ex"}, {"matched": true, "text": "port"}],
> +    "user": "spam"
> +   }
> +  ]
> +
> +simple JSON without matching lines
> +
> +  $ hg grep -Tjson -l port
> +  [
> +   {
> +    "date": [4.0, 0],
> +    "file": "port",
> +    "line_number": 1,
> +    "node": "914fa752cdea87777ac1a8d5c858b0c736218f6c",
> +    "rev": 4,
> +    "user": "spam"
> +   }
> +  ]
> +
>  all
>
>    $ hg grep --traceback --all -nu port port
> @@ -53,6 +108,102 @@ all
>    port:1:2:+:eggs:export
>    port:0:1:+:spam:import
>
> +all JSON
> +
> +  $ hg grep --all -Tjson port port
> +  [
> +   {
> +    "change": "-",
> +    "date": [4.0, 0],
> +    "file": "port",
> +    "line_number": 4,
> +    "node": "914fa752cdea87777ac1a8d5c858b0c736218f6c",
> +    "rev": 4,
> +    "texts": [{"matched": false, "text": "im"}, {"matched": true, "text": "port"}, {"matched": false, "text": "/ex"}, {"matched": true, "text": "port"}],
> +    "user": "spam"
> +   },
> +   {
> +    "change": "+",
> +    "date": [3.0, 0],
> +    "file": "port",
> +    "line_number": 4,
> +    "node": "95040cfd017d658c536071c6290230a613c4c2a6",
> +    "rev": 3,
> +    "texts": [{"matched": false, "text": "im"}, {"matched": true, "text": "port"}, {"matched": false, "text": "/ex"}, {"matched": true, "text": "port"}],
> +    "user": "eggs"
> +   },
> +   {
> +    "change": "-",
> +    "date": [2.0, 0],
> +    "file": "port",
> +    "line_number": 1,
> +    "node": "3b325e3481a1f07435d81dfdbfa434d9a0245b47",
> +    "rev": 2,
> +    "texts": [{"matched": false, "text": "im"}, {"matched": true, "text": "port"}],
> +    "user": "spam"
> +   },
> +   {
> +    "change": "-",
> +    "date": [2.0, 0],
> +    "file": "port",
> +    "line_number": 2,
> +    "node": "3b325e3481a1f07435d81dfdbfa434d9a0245b47",
> +    "rev": 2,
> +    "texts": [{"matched": false, "text": "ex"}, {"matched": true, "text": "port"}],
> +    "user": "spam"
> +   },
> +   {
> +    "change": "+",
> +    "date": [2.0, 0],
> +    "file": "port",
> +    "line_number": 1,
> +    "node": "3b325e3481a1f07435d81dfdbfa434d9a0245b47",
> +    "rev": 2,
> +    "texts": [{"matched": false, "text": "ex"}, {"matched": true, "text": "port"}],
> +    "user": "spam"
> +   },
> +   {
> +    "change": "+",
> +    "date": [2.0, 0],
> +    "file": "port",
> +    "line_number": 2,
> +    "node": "3b325e3481a1f07435d81dfdbfa434d9a0245b47",
> +    "rev": 2,
> +    "texts": [{"matched": false, "text": "va"}, {"matched": true, "text": "port"}, {"matched": false, "text": "ight"}],
> +    "user": "spam"
> +   },
> +   {
> +    "change": "+",
> +    "date": [2.0, 0],
> +    "file": "port",
> +    "line_number": 3,
> +    "node": "3b325e3481a1f07435d81dfdbfa434d9a0245b47",
> +    "rev": 2,
> +    "texts": [{"matched": false, "text": "im"}, {"matched": true, "text": "port"}, {"matched": false, "text": "/ex"}, {"matched": true, "text": "port"}],
> +    "user": "spam"
> +   },
> +   {
> +    "change": "+",
> +    "date": [1.0, 0],
> +    "file": "port",
> +    "line_number": 2,
> +    "node": "8b20f75c158513ff5ac80bd0e5219bfb6f0eb587",
> +    "rev": 1,
> +    "texts": [{"matched": false, "text": "ex"}, {"matched": true, "text": "port"}],
> +    "user": "eggs"
> +   },
> +   {
> +    "change": "+",
> +    "date": [0.0, 0],
> +    "file": "port",
> +    "line_number": 1,
> +    "node": "f31323c9217050ba245ee8b537c713ec2e8ab226",
> +    "rev": 0,
> +    "texts": [{"matched": false, "text": "im"}, {"matched": true, "text": "port"}],
> +    "user": "spam"
> +   }
> +  ]
> +
>  other
>
>    $ hg grep -l port port
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel

Patch

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -4283,7 +4283,7 @@  def _dograft(ui, repo, *revs, **opts):
      _('only search files changed within revision range'), _('REV')),
     ('u', 'user', None, _('list the author (long with -v)')),
     ('d', 'date', None, _('list the date (short with -q)')),
-    ] + walkopts,
+    ] + formatteropts + walkopts,
     _('[OPTION]... PATTERN [FILE]...'),
     inferrepo=True)
 def grep(ui, repo, pattern, *pats, **opts):
@@ -4380,59 +4380,75 @@  def grep(ui, repo, pattern, *pats, **opt
                 for i in xrange(blo, bhi):
                     yield ('+', b[i])
 
-    def display(fn, ctx, pstates, states):
+    def display(fm, fn, ctx, pstates, states):
         rev = ctx.rev()
+        if fm:
+            formatuser = str
+        else:
+            formatuser = ui.shortuser
         if ui.quiet:
-            datefunc = util.shortdate
+            datefmt = '%Y-%m-%d'
         else:
-            datefunc = util.datestr
+            datefmt = '%a %b %d %H:%M:%S %Y %1%2'
         found = False
         @util.cachefunc
         def binary():
             flog = getfile(fn)
             return util.binary(flog.read(ctx.filenode(fn)))
 
+        fieldnamemap = {'filename': 'file', 'linenumber': 'line_number'}
         if opts.get('all'):
             iter = difflinestates(pstates, states)
         else:
             iter = [('', l) for l in states]
         for change, l in iter:
+            fm.startitem()
+            fm.data(node=fm.hexfunc(ctx.node()))
             cols = [
                 ('filename', fn, True),
-                ('rev', str(rev), True),
-                ('linenumber', str(l.linenum), opts.get('line_number')),
+                ('rev', rev, True),
+                ('linenumber', l.linenum, opts.get('line_number')),
             ]
             if opts.get('all'):
                 cols.append(('change', change, True))
             cols.extend([
-                ('user', ui.shortuser(ctx.user()), opts.get('user')),
-                ('date', datefunc(ctx.date()), opts.get('date')),
+                ('user', formatuser(ctx.user()), opts.get('user')),
+                ('date', fm.formatdate(ctx.date(), datefmt), opts.get('date')),
             ])
             lastcol = next(name for name, data, cond in reversed(cols) if cond)
             for name, data, cond in cols:
-                if cond:
-                    ui.write(data, label='grep.%s' % name)
+                field = fieldnamemap.get(name, name)
+                fm.condwrite(cond, field, '%s', data, label='grep.%s' % name)
                 if cond and name != lastcol:
-                    ui.write(sep, label='grep.sep')
+                    fm.plain(sep, label='grep.sep')
             if not opts.get('files_with_matches'):
-                ui.write(sep, label='grep.sep')
+                fm.plain(sep, label='grep.sep')
                 if not opts.get('text') and binary():
-                    ui.write(_(" Binary file matches"))
+                    fm.plain(_(" Binary file matches"))
                 else:
-                    displaymatches(l)
-            ui.write(eol)
+                    displaymatches(fm.nested('texts'), l)
+            fm.plain(eol)
             found = True
             if opts.get('files_with_matches'):
                 break
         return found
 
-    def displaymatches(l):
+    def displaymatches(fm, l):
         p = 0
         for s, e in l.findpos():
-            ui.write(l.line[p:s])
-            ui.write(l.line[s:e], label='grep.match')
+            if p < s:
+                fm.startitem()
+                fm.write('text', '%s', l.line[p:s])
+                fm.data(matched=False)
+            fm.startitem()
+            fm.write('text', '%s', l.line[s:e], label='grep.match')
+            fm.data(matched=True)
             p = e
-        ui.write(l.line[p:])
+        if p < len(l.line):
+            fm.startitem()
+            fm.write('text', '%s', l.line[p:])
+            fm.data(matched=False)
+        fm.end()
 
     skip = {}
     revfiles = {}
@@ -4475,6 +4491,7 @@  def grep(ui, repo, pattern, *pats, **opt
                 except error.LookupError:
                     pass
 
+    fm = ui.formatter('grep', opts)
     for ctx in cmdutil.walkchangerevs(repo, matchfn, opts, prep):
         rev = ctx.rev()
         parent = ctx.p1().rev()
@@ -4487,7 +4504,7 @@  def grep(ui, repo, pattern, *pats, **opt
                 continue
             pstates = matches.get(parent, {}).get(copy or fn, [])
             if pstates or states:
-                r = display(fn, ctx, pstates, states)
+                r = display(fm, fn, ctx, pstates, states)
                 found = found or r
                 if r and not opts.get('all'):
                     skip[fn] = True
@@ -4495,6 +4512,7 @@  def grep(ui, repo, pattern, *pats, **opt
                         skip[copy] = True
         del matches[rev]
         del revfiles[rev]
+    fm.end()
 
     return not found
 
diff --git a/tests/test-completion.t b/tests/test-completion.t
--- a/tests/test-completion.t
+++ b/tests/test-completion.t
@@ -278,7 +278,7 @@  Show all commands + options
   debugwireargs: three, four, five, ssh, remotecmd, insecure
   files: rev, print0, include, exclude, template, subrepos
   graft: rev, continue, edit, log, force, currentdate, currentuser, date, user, tool, dry-run
-  grep: print0, all, text, follow, ignore-case, files-with-matches, line-number, rev, user, date, include, exclude
+  grep: print0, all, text, follow, ignore-case, files-with-matches, line-number, rev, user, date, template, include, exclude
   heads: rev, topo, active, closed, style, template
   help: extension, command, keyword, system
   identify: rev, num, id, branch, tags, bookmarks, ssh, remotecmd, insecure
diff --git a/tests/test-grep.t b/tests/test-grep.t
--- a/tests/test-grep.t
+++ b/tests/test-grep.t
@@ -40,6 +40,61 @@  simple with color
   \x1b[0;35mport\x1b[0m\x1b[0;36m:\x1b[0m\x1b[0;32m4\x1b[0m\x1b[0;36m:\x1b[0mva\x1b[0;31;1mport\x1b[0might (esc)
   \x1b[0;35mport\x1b[0m\x1b[0;36m:\x1b[0m\x1b[0;32m4\x1b[0m\x1b[0;36m:\x1b[0mim\x1b[0;31;1mport\x1b[0m/ex\x1b[0;31;1mport\x1b[0m (esc)
 
+simple templated
+
+  $ hg grep port \
+  > -T '{file}:{rev}:{node|short}:{texts % "{if(matched, text|upper, text)}"}\n'
+  port:4:914fa752cdea:exPORT
+  port:4:914fa752cdea:vaPORTight
+  port:4:914fa752cdea:imPORT/exPORT
+
+simple JSON (no "change" field)
+
+  $ hg grep -Tjson port
+  [
+   {
+    "date": [4.0, 0],
+    "file": "port",
+    "line_number": 1,
+    "node": "914fa752cdea87777ac1a8d5c858b0c736218f6c",
+    "rev": 4,
+    "texts": [{"matched": false, "text": "ex"}, {"matched": true, "text": "port"}],
+    "user": "spam"
+   },
+   {
+    "date": [4.0, 0],
+    "file": "port",
+    "line_number": 2,
+    "node": "914fa752cdea87777ac1a8d5c858b0c736218f6c",
+    "rev": 4,
+    "texts": [{"matched": false, "text": "va"}, {"matched": true, "text": "port"}, {"matched": false, "text": "ight"}],
+    "user": "spam"
+   },
+   {
+    "date": [4.0, 0],
+    "file": "port",
+    "line_number": 3,
+    "node": "914fa752cdea87777ac1a8d5c858b0c736218f6c",
+    "rev": 4,
+    "texts": [{"matched": false, "text": "im"}, {"matched": true, "text": "port"}, {"matched": false, "text": "/ex"}, {"matched": true, "text": "port"}],
+    "user": "spam"
+   }
+  ]
+
+simple JSON without matching lines
+
+  $ hg grep -Tjson -l port
+  [
+   {
+    "date": [4.0, 0],
+    "file": "port",
+    "line_number": 1,
+    "node": "914fa752cdea87777ac1a8d5c858b0c736218f6c",
+    "rev": 4,
+    "user": "spam"
+   }
+  ]
+
 all
 
   $ hg grep --traceback --all -nu port port
@@ -53,6 +108,102 @@  all
   port:1:2:+:eggs:export
   port:0:1:+:spam:import
 
+all JSON
+
+  $ hg grep --all -Tjson port port
+  [
+   {
+    "change": "-",
+    "date": [4.0, 0],
+    "file": "port",
+    "line_number": 4,
+    "node": "914fa752cdea87777ac1a8d5c858b0c736218f6c",
+    "rev": 4,
+    "texts": [{"matched": false, "text": "im"}, {"matched": true, "text": "port"}, {"matched": false, "text": "/ex"}, {"matched": true, "text": "port"}],
+    "user": "spam"
+   },
+   {
+    "change": "+",
+    "date": [3.0, 0],
+    "file": "port",
+    "line_number": 4,
+    "node": "95040cfd017d658c536071c6290230a613c4c2a6",
+    "rev": 3,
+    "texts": [{"matched": false, "text": "im"}, {"matched": true, "text": "port"}, {"matched": false, "text": "/ex"}, {"matched": true, "text": "port"}],
+    "user": "eggs"
+   },
+   {
+    "change": "-",
+    "date": [2.0, 0],
+    "file": "port",
+    "line_number": 1,
+    "node": "3b325e3481a1f07435d81dfdbfa434d9a0245b47",
+    "rev": 2,
+    "texts": [{"matched": false, "text": "im"}, {"matched": true, "text": "port"}],
+    "user": "spam"
+   },
+   {
+    "change": "-",
+    "date": [2.0, 0],
+    "file": "port",
+    "line_number": 2,
+    "node": "3b325e3481a1f07435d81dfdbfa434d9a0245b47",
+    "rev": 2,
+    "texts": [{"matched": false, "text": "ex"}, {"matched": true, "text": "port"}],
+    "user": "spam"
+   },
+   {
+    "change": "+",
+    "date": [2.0, 0],
+    "file": "port",
+    "line_number": 1,
+    "node": "3b325e3481a1f07435d81dfdbfa434d9a0245b47",
+    "rev": 2,
+    "texts": [{"matched": false, "text": "ex"}, {"matched": true, "text": "port"}],
+    "user": "spam"
+   },
+   {
+    "change": "+",
+    "date": [2.0, 0],
+    "file": "port",
+    "line_number": 2,
+    "node": "3b325e3481a1f07435d81dfdbfa434d9a0245b47",
+    "rev": 2,
+    "texts": [{"matched": false, "text": "va"}, {"matched": true, "text": "port"}, {"matched": false, "text": "ight"}],
+    "user": "spam"
+   },
+   {
+    "change": "+",
+    "date": [2.0, 0],
+    "file": "port",
+    "line_number": 3,
+    "node": "3b325e3481a1f07435d81dfdbfa434d9a0245b47",
+    "rev": 2,
+    "texts": [{"matched": false, "text": "im"}, {"matched": true, "text": "port"}, {"matched": false, "text": "/ex"}, {"matched": true, "text": "port"}],
+    "user": "spam"
+   },
+   {
+    "change": "+",
+    "date": [1.0, 0],
+    "file": "port",
+    "line_number": 2,
+    "node": "8b20f75c158513ff5ac80bd0e5219bfb6f0eb587",
+    "rev": 1,
+    "texts": [{"matched": false, "text": "ex"}, {"matched": true, "text": "port"}],
+    "user": "eggs"
+   },
+   {
+    "change": "+",
+    "date": [0.0, 0],
+    "file": "port",
+    "line_number": 1,
+    "node": "f31323c9217050ba245ee8b537c713ec2e8ab226",
+    "rev": 0,
+    "texts": [{"matched": false, "text": "im"}, {"matched": true, "text": "port"}],
+    "user": "spam"
+   }
+  ]
+
 other
 
   $ hg grep -l port port