Patchwork [6,of,6] cat: add formatter support

login
register
mail settings
Submitter Yuya Nishihara
Date May 30, 2017, 4:07 p.m.
Message ID <a20184dbfeadfa5c8da9.1496160434@mimosa>
Download mbox | patch
Permalink /patch/21073/
State Accepted
Headers show

Comments

Yuya Nishihara - May 30, 2017, 4:07 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1495716824 -32400
#      Thu May 25 21:53:44 2017 +0900
# Node ID a20184dbfeadfa5c8da9a67fecafd972d7e2202d
# Parent  88dd1416922eaf34fd90e2e76a3d941f9a950ba3
cat: add formatter support

This is an example showing how formatter can handle the --output option.
git subrepo isn't supported for now.
Augie Fackler - May 31, 2017, 6:25 p.m.
On Wed, May 31, 2017 at 01:07:14AM +0900, Yuya Nishihara wrote:
> # HG changeset patch
> # User Yuya Nishihara <yuya@tcha.org>
> # Date 1495716824 -32400
> #      Thu May 25 21:53:44 2017 +0900
> # Node ID a20184dbfeadfa5c8da9a67fecafd972d7e2202d
> # Parent  88dd1416922eaf34fd90e2e76a3d941f9a950ba3
> cat: add formatter support

Queued these, thanks.

Should the file contents be armored in json ouptut? IIRC json strings
are explicitly unicode, so if we can't unicode-ify the data we're
someone hosed, aren't we?

(Feel encouraged to do that as a followup - I'd rather we keep moving
on formatter-all-the-things.)

>
> This is an example showing how formatter can handle the --output option.
> git subrepo isn't supported for now.
>
> diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
> --- a/mercurial/cmdutil.py
> +++ b/mercurial/cmdutil.py
> @@ -2632,21 +2632,21 @@ def remove(ui, repo, m, prefix, after, f
>
>      return ret
>
> -def cat(ui, repo, ctx, matcher, fntemplate, prefix, **opts):
> +def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
>      err = 1
>
>      def write(path):
> +        filename = None
>          if fntemplate:
>              filename = makefilename(repo, fntemplate, ctx.node(),
>                                      pathname=os.path.join(prefix, path))
> -            fp = open(filename, 'wb')
> -        else:
> -            fp = _unclosablefile(ui.fout)
> -        with fp:
> +        with formatter.maybereopen(basefm, filename, opts) as fm:
>              data = ctx[path].data()
>              if opts.get('decode'):
>                  data = repo.wwritedata(path, data)
> -            fp.write(data)
> +            fm.startitem()
> +            fm.write('data', '%s', data)
> +            fm.data(abspath=path, path=matcher.rel(path))
>
>      # Automation often uses hg cat on single files, so special case it
>      # for performance to avoid the cost of parsing the manifest.
> @@ -2670,7 +2670,7 @@ def cat(ui, repo, ctx, matcher, fntempla
>          try:
>              submatch = matchmod.subdirmatcher(subpath, matcher)
>
> -            if not sub.cat(submatch, fntemplate,
> +            if not sub.cat(submatch, basefm, fntemplate,
>                             os.path.join(prefix, sub._path), **opts):
>                  err = 0
>          except error.RepoLookupError:
> diff --git a/mercurial/commands.py b/mercurial/commands.py
> --- a/mercurial/commands.py
> +++ b/mercurial/commands.py
> @@ -35,6 +35,7 @@ from . import (
>      error,
>      exchange,
>      extensions,
> +    formatter,
>      graphmod,
>      hbisect,
>      help,
> @@ -1338,7 +1339,7 @@ def bundle(ui, repo, fname, dest=None, *
>       _('print output to file with formatted name'), _('FORMAT')),
>      ('r', 'rev', '', _('print the given revision'), _('REV')),
>      ('', 'decode', None, _('apply any matching decode filter')),
> -    ] + walkopts,
> +    ] + walkopts + formatteropts,
>      _('[OPTION]... FILE...'),
>      inferrepo=True)
>  def cat(ui, repo, file1, *pats, **opts):
> @@ -1368,9 +1369,13 @@ def cat(ui, repo, file1, *pats, **opts):
>      if cmdutil.isstdiofilename(fntemplate):
>          fntemplate = ''
>
> -    if not fntemplate:
> +    if fntemplate:
> +        fm = formatter.nullformatter(ui, 'cat')
> +    else:
>          ui.pager('cat')
> -    return cmdutil.cat(ui, repo, ctx, m, fntemplate, '', **opts)
> +        fm = ui.formatter('cat', opts)
> +    with fm:
> +        return cmdutil.cat(ui, repo, ctx, m, fm, fntemplate, '', **opts)
>
>  @command('^clone',
>      [('U', 'noupdate', None, _('the clone will include an empty working '
> diff --git a/mercurial/subrepo.py b/mercurial/subrepo.py
> --- a/mercurial/subrepo.py
> +++ b/mercurial/subrepo.py
> @@ -538,7 +538,7 @@ class abstractsubrepo(object):
>          self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
>          return 1
>
> -    def cat(self, match, fntemplate, prefix, **opts):
> +    def cat(self, match, fm, fntemplate, prefix, **opts):
>          return 1
>
>      def status(self, rev2, **opts):
> @@ -767,11 +767,11 @@ class hgsubrepo(abstractsubrepo):
>                                   dry_run, similarity)
>
>      @annotatesubrepoerror
> -    def cat(self, match, fntemplate, prefix, **opts):
> +    def cat(self, match, fm, fntemplate, prefix, **opts):
>          rev = self._state[1]
>          ctx = self._repo[rev]
> -        return cmdutil.cat(self.ui, self._repo, ctx, match, fntemplate, prefix,
> -                           **opts)
> +        return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
> +                           prefix, **opts)
>
>      @annotatesubrepoerror
>      def status(self, rev2, **opts):
> @@ -1833,7 +1833,7 @@ class gitsubrepo(abstractsubrepo):
>
>
>      @annotatesubrepoerror
> -    def cat(self, match, fntemplate, prefix, **opts):
> +    def cat(self, match, fm, fntemplate, prefix, **opts):
>          rev = self._state[1]
>          if match.anypats():
>              return 1 #No support for include/exclude yet
> @@ -1841,6 +1841,7 @@ class gitsubrepo(abstractsubrepo):
>          if not match.files():
>              return 1
>
> +        # TODO: add support for non-plain formatter (see cmdutil.cat())
>          for f in match.files():
>              output = self._gitcommand(["show", "%s:%s" % (rev, f)])
>              fp = cmdutil.makefileobj(self._subparent, fntemplate,
> diff --git a/tests/test-cat.t b/tests/test-cat.t
> --- a/tests/test-cat.t
> +++ b/tests/test-cat.t
> @@ -63,6 +63,46 @@ Test fileset
>    tmp/h_45116003780e
>    tmp/r_2
>
> +Test template output
> +
> +  $ hg --cwd tmp cat ../b ../c -T '== {path} ({abspath}) ==\n{data}'
> +  == ../b (b) == (glob)
> +  1
> +  == ../c (c) == (glob)
> +  3
> +
> +  $ hg cat b c -Tjson --output -
> +  [
> +   {
> +    "abspath": "b",
> +    "data": "1\n",
> +    "path": "b"
> +   },
> +   {
> +    "abspath": "c",
> +    "data": "3\n",
> +    "path": "c"
> +   }
> +  ]
> +
> +  $ hg cat b c -Tjson --output 'tmp/%p.json'
> +  $ cat tmp/b.json
> +  [
> +   {
> +    "abspath": "b",
> +    "data": "1\n",
> +    "path": "b"
> +   }
> +  ]
> +  $ cat tmp/c.json
> +  [
> +   {
> +    "abspath": "c",
> +    "data": "3\n",
> +    "path": "c"
> +   }
> +  ]
> +
>  Test working directory
>
>    $ echo b-wdir > b
> diff --git a/tests/test-completion.t b/tests/test-completion.t
> --- a/tests/test-completion.t
> +++ b/tests/test-completion.t
> @@ -241,7 +241,7 @@ Show all commands + options
>    branch: force, clean
>    branches: active, closed, template
>    bundle: force, rev, branch, base, all, type, ssh, remotecmd, insecure
> -  cat: output, rev, decode, include, exclude
> +  cat: output, rev, decode, include, exclude, template
>    config: untrusted, edit, local, global, template
>    copy: after, force, include, exclude, dry-run
>    debugancestor:
> diff --git a/tests/test-hook.t b/tests/test-hook.t
> --- a/tests/test-hook.t
> +++ b/tests/test-hook.t
> @@ -99,9 +99,9 @@ test generic hooks
>    abort: pre-identify hook exited with status 1
>    [255]
>    $ hg cat b
> -  pre-cat hook: HG_ARGS=cat b HG_HOOKNAME=pre-cat HG_HOOKTYPE=pre-cat HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': ''} HG_PATS=['b']
> +  pre-cat hook: HG_ARGS=cat b HG_HOOKNAME=pre-cat HG_HOOKTYPE=pre-cat HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': '', 'template': ''} HG_PATS=['b']
>    b
> -  post-cat hook: HG_ARGS=cat b HG_HOOKNAME=post-cat HG_HOOKTYPE=post-cat HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': ''} HG_PATS=['b'] HG_RESULT=0
> +  post-cat hook: HG_ARGS=cat b HG_HOOKNAME=post-cat HG_HOOKTYPE=post-cat HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': '', 'template': ''} HG_PATS=['b'] HG_RESULT=0
>
>    $ cd ../b
>    $ hg pull ../a
> diff --git a/tests/test-subrepo.t b/tests/test-subrepo.t
> --- a/tests/test-subrepo.t
> +++ b/tests/test-subrepo.t
> @@ -1020,6 +1020,14 @@ Prepare a repo with subrepo
>    $ hg cat sub/repo/foo
>    test
>    test
> +  $ hg cat sub/repo/foo -Tjson
> +  [
> +   {
> +    "abspath": "foo",
> +    "data": "test\ntest\n",
> +    "path": "sub/repo/foo" (glob)
> +   }
> +  ]
>    $ mkdir -p tmp/sub/repo
>    $ hg cat -r 0 --output tmp/%p_p sub/repo/foo
>    $ cat tmp/sub/repo/foo_p
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Yuya Nishihara - June 1, 2017, 2:16 p.m.
On Wed, 31 May 2017 14:25:47 -0400, Augie Fackler wrote:
> On Wed, May 31, 2017 at 01:07:14AM +0900, Yuya Nishihara wrote:
> > # HG changeset patch
> > # User Yuya Nishihara <yuya@tcha.org>
> > # Date 1495716824 -32400
> > #      Thu May 25 21:53:44 2017 +0900
> > # Node ID a20184dbfeadfa5c8da9a67fecafd972d7e2202d
> > # Parent  88dd1416922eaf34fd90e2e76a3d941f9a950ba3
> > cat: add formatter support
> 
> Queued these, thanks.
> 
> Should the file contents be armored in json ouptut? IIRC json strings
> are explicitly unicode, so if we can't unicode-ify the data we're
> someone hosed, aren't we?

It's handled by encoding.toutf8b(). The file name is also arbitrary bytes
on Unix.
Augie Fackler - June 1, 2017, 2:35 p.m.
On Thu, Jun 1, 2017 at 10:16 AM, Yuya Nishihara <yuya@tcha.org> wrote:
> On Wed, 31 May 2017 14:25:47 -0400, Augie Fackler wrote:
>> On Wed, May 31, 2017 at 01:07:14AM +0900, Yuya Nishihara wrote:
>> > # HG changeset patch
>> > # User Yuya Nishihara <yuya@tcha.org>
>> > # Date 1495716824 -32400
>> > #      Thu May 25 21:53:44 2017 +0900
>> > # Node ID a20184dbfeadfa5c8da9a67fecafd972d7e2202d
>> > # Parent  88dd1416922eaf34fd90e2e76a3d941f9a950ba3
>> > cat: add formatter support
>>
>> Queued these, thanks.
>>
>> Should the file contents be armored in json ouptut? IIRC json strings
>> are explicitly unicode, so if we can't unicode-ify the data we're
>> someone hosed, aren't we?
>
> It's handled by encoding.toutf8b(). The file name is also arbitrary bytes
> on Unix.

Excellent, thanks.

Patch

diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -2632,21 +2632,21 @@  def remove(ui, repo, m, prefix, after, f
 
     return ret
 
-def cat(ui, repo, ctx, matcher, fntemplate, prefix, **opts):
+def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
     err = 1
 
     def write(path):
+        filename = None
         if fntemplate:
             filename = makefilename(repo, fntemplate, ctx.node(),
                                     pathname=os.path.join(prefix, path))
-            fp = open(filename, 'wb')
-        else:
-            fp = _unclosablefile(ui.fout)
-        with fp:
+        with formatter.maybereopen(basefm, filename, opts) as fm:
             data = ctx[path].data()
             if opts.get('decode'):
                 data = repo.wwritedata(path, data)
-            fp.write(data)
+            fm.startitem()
+            fm.write('data', '%s', data)
+            fm.data(abspath=path, path=matcher.rel(path))
 
     # Automation often uses hg cat on single files, so special case it
     # for performance to avoid the cost of parsing the manifest.
@@ -2670,7 +2670,7 @@  def cat(ui, repo, ctx, matcher, fntempla
         try:
             submatch = matchmod.subdirmatcher(subpath, matcher)
 
-            if not sub.cat(submatch, fntemplate,
+            if not sub.cat(submatch, basefm, fntemplate,
                            os.path.join(prefix, sub._path), **opts):
                 err = 0
         except error.RepoLookupError:
diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -35,6 +35,7 @@  from . import (
     error,
     exchange,
     extensions,
+    formatter,
     graphmod,
     hbisect,
     help,
@@ -1338,7 +1339,7 @@  def bundle(ui, repo, fname, dest=None, *
      _('print output to file with formatted name'), _('FORMAT')),
     ('r', 'rev', '', _('print the given revision'), _('REV')),
     ('', 'decode', None, _('apply any matching decode filter')),
-    ] + walkopts,
+    ] + walkopts + formatteropts,
     _('[OPTION]... FILE...'),
     inferrepo=True)
 def cat(ui, repo, file1, *pats, **opts):
@@ -1368,9 +1369,13 @@  def cat(ui, repo, file1, *pats, **opts):
     if cmdutil.isstdiofilename(fntemplate):
         fntemplate = ''
 
-    if not fntemplate:
+    if fntemplate:
+        fm = formatter.nullformatter(ui, 'cat')
+    else:
         ui.pager('cat')
-    return cmdutil.cat(ui, repo, ctx, m, fntemplate, '', **opts)
+        fm = ui.formatter('cat', opts)
+    with fm:
+        return cmdutil.cat(ui, repo, ctx, m, fm, fntemplate, '', **opts)
 
 @command('^clone',
     [('U', 'noupdate', None, _('the clone will include an empty working '
diff --git a/mercurial/subrepo.py b/mercurial/subrepo.py
--- a/mercurial/subrepo.py
+++ b/mercurial/subrepo.py
@@ -538,7 +538,7 @@  class abstractsubrepo(object):
         self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
         return 1
 
-    def cat(self, match, fntemplate, prefix, **opts):
+    def cat(self, match, fm, fntemplate, prefix, **opts):
         return 1
 
     def status(self, rev2, **opts):
@@ -767,11 +767,11 @@  class hgsubrepo(abstractsubrepo):
                                  dry_run, similarity)
 
     @annotatesubrepoerror
-    def cat(self, match, fntemplate, prefix, **opts):
+    def cat(self, match, fm, fntemplate, prefix, **opts):
         rev = self._state[1]
         ctx = self._repo[rev]
-        return cmdutil.cat(self.ui, self._repo, ctx, match, fntemplate, prefix,
-                           **opts)
+        return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
+                           prefix, **opts)
 
     @annotatesubrepoerror
     def status(self, rev2, **opts):
@@ -1833,7 +1833,7 @@  class gitsubrepo(abstractsubrepo):
 
 
     @annotatesubrepoerror
-    def cat(self, match, fntemplate, prefix, **opts):
+    def cat(self, match, fm, fntemplate, prefix, **opts):
         rev = self._state[1]
         if match.anypats():
             return 1 #No support for include/exclude yet
@@ -1841,6 +1841,7 @@  class gitsubrepo(abstractsubrepo):
         if not match.files():
             return 1
 
+        # TODO: add support for non-plain formatter (see cmdutil.cat())
         for f in match.files():
             output = self._gitcommand(["show", "%s:%s" % (rev, f)])
             fp = cmdutil.makefileobj(self._subparent, fntemplate,
diff --git a/tests/test-cat.t b/tests/test-cat.t
--- a/tests/test-cat.t
+++ b/tests/test-cat.t
@@ -63,6 +63,46 @@  Test fileset
   tmp/h_45116003780e
   tmp/r_2
 
+Test template output
+
+  $ hg --cwd tmp cat ../b ../c -T '== {path} ({abspath}) ==\n{data}'
+  == ../b (b) == (glob)
+  1
+  == ../c (c) == (glob)
+  3
+
+  $ hg cat b c -Tjson --output -
+  [
+   {
+    "abspath": "b",
+    "data": "1\n",
+    "path": "b"
+   },
+   {
+    "abspath": "c",
+    "data": "3\n",
+    "path": "c"
+   }
+  ]
+
+  $ hg cat b c -Tjson --output 'tmp/%p.json'
+  $ cat tmp/b.json
+  [
+   {
+    "abspath": "b",
+    "data": "1\n",
+    "path": "b"
+   }
+  ]
+  $ cat tmp/c.json
+  [
+   {
+    "abspath": "c",
+    "data": "3\n",
+    "path": "c"
+   }
+  ]
+
 Test working directory
 
   $ echo b-wdir > b
diff --git a/tests/test-completion.t b/tests/test-completion.t
--- a/tests/test-completion.t
+++ b/tests/test-completion.t
@@ -241,7 +241,7 @@  Show all commands + options
   branch: force, clean
   branches: active, closed, template
   bundle: force, rev, branch, base, all, type, ssh, remotecmd, insecure
-  cat: output, rev, decode, include, exclude
+  cat: output, rev, decode, include, exclude, template
   config: untrusted, edit, local, global, template
   copy: after, force, include, exclude, dry-run
   debugancestor: 
diff --git a/tests/test-hook.t b/tests/test-hook.t
--- a/tests/test-hook.t
+++ b/tests/test-hook.t
@@ -99,9 +99,9 @@  test generic hooks
   abort: pre-identify hook exited with status 1
   [255]
   $ hg cat b
-  pre-cat hook: HG_ARGS=cat b HG_HOOKNAME=pre-cat HG_HOOKTYPE=pre-cat HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': ''} HG_PATS=['b']
+  pre-cat hook: HG_ARGS=cat b HG_HOOKNAME=pre-cat HG_HOOKTYPE=pre-cat HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': '', 'template': ''} HG_PATS=['b']
   b
-  post-cat hook: HG_ARGS=cat b HG_HOOKNAME=post-cat HG_HOOKTYPE=post-cat HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': ''} HG_PATS=['b'] HG_RESULT=0
+  post-cat hook: HG_ARGS=cat b HG_HOOKNAME=post-cat HG_HOOKTYPE=post-cat HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': '', 'template': ''} HG_PATS=['b'] HG_RESULT=0
 
   $ cd ../b
   $ hg pull ../a
diff --git a/tests/test-subrepo.t b/tests/test-subrepo.t
--- a/tests/test-subrepo.t
+++ b/tests/test-subrepo.t
@@ -1020,6 +1020,14 @@  Prepare a repo with subrepo
   $ hg cat sub/repo/foo
   test
   test
+  $ hg cat sub/repo/foo -Tjson
+  [
+   {
+    "abspath": "foo",
+    "data": "test\ntest\n",
+    "path": "sub/repo/foo" (glob)
+   }
+  ]
   $ mkdir -p tmp/sub/repo
   $ hg cat -r 0 --output tmp/%p_p sub/repo/foo
   $ cat tmp/sub/repo/foo_p