Patchwork [6,of,6,json-style] tests: add tests for json output from hgweb

login
register
mail settings
Submitter Gregory Szorc
Date Dec. 31, 2014, 10:45 p.m.
Message ID <375310e882793f020233.1420065936@gps-mbp.local>
Download mbox | patch
Permalink /patch/7290/
State Changes Requested
Headers show

Comments

Gregory Szorc - Dec. 31, 2014, 10:45 p.m.
# HG changeset patch
# User Gregory Szorc <gregory.szorc@gmail.com>
# Date 1420065389 28800
#      Wed Dec 31 14:36:29 2014 -0800
# Node ID 375310e882793f02023305f1065f616b0b42ab88
# Parent  8a88b921f7b9b467046496ab6aa99d2589dd935c
tests: add tests for json output from hgweb

Now that we've added a json style, let's verify it works via hgweb.

The added test file contains either a basic test or a placeholder for a
test for every existing web command.

Tests have been excluded for cases where JSON output isn't sane. For
example, the "changeset" command invokes the templater multiple times.
This "inner expansion" of values leads to the "outer" template
encountering already-expanded JSON as a string, which it then proceeds
to escape. The output JSON is valid, but the extra escaping is wrong.

Upcoming patches will rework the templater to make this scenario work.
hgweb commands will be fixed to use the new API and tests will be added
to demonstrate they work as expected.
Gregory Szorc - Dec. 31, 2014, 10:57 p.m.
On 12/31/14 2:45 PM, Gregory Szorc wrote:
> # HG changeset patch
> # User Gregory Szorc <gregory.szorc@gmail.com>
> # Date 1420065389 28800
> #      Wed Dec 31 14:36:29 2014 -0800
> # Node ID 375310e882793f02023305f1065f616b0b42ab88
> # Parent  8a88b921f7b9b467046496ab6aa99d2589dd935c
> tests: add tests for json output from hgweb
>
> Now that we've added a json style, let's verify it works via hgweb.
>
> The added test file contains either a basic test or a placeholder for a
> test for every existing web command.
>
> Tests have been excluded for cases where JSON output isn't sane. For
> example, the "changeset" command invokes the templater multiple times.
> This "inner expansion" of values leads to the "outer" template
> encountering already-expanded JSON as a string, which it then proceeds
> to escape. The output JSON is valid, but the extra escaping is wrong.
>
> Upcoming patches will rework the templater to make this scenario work.
> hgweb commands will be fixed to use the new API and tests will be added
> to demonstrate they work as expected.

I wanted to stop here before going on because I wanted to save myself a 
lot of work in case others have fundamental objections to refactoring 
the templater.

Essentially, the magic and minimal json engine I added only works for 
templates that don't contain already-expanded templates. Unfortunately, 
there are a lot of templates that expand "sub templates" inline.

I think that inline expansion of final output formats like JSON and XML 
is quite silly. This is pretty much guaranteeing that there will be 
syntax errors or it will be very painful to avoid syntax errors. e.g. 
JSON not allowing trailing commas or YAML being sensitive to whitespace. 
I think giving the templater a large data structure and recursively 
expanding/serializing that is the way to go.

Unfortunately, getting there is a lot of work. I'd like to teach the 
templater about non-final / deferred expansion. Essentially, instead of 
performing the expansion immediately and passing that string result to 
an "outer" template, we'd instead pass an object that says "use the 
result of this expansion." We would then teach the template engine how 
to expand those special objects when encountered. In the case of JSON, 
it would likely create a new dict and just json stringify the end result.

I'm confident this solution will work. However, there's a lot of nested 
expansion going on. I'd need to add deferred=False (or similar) 
arguments to a lot of functions. It's a lot of work and I imagine a bit 
contentious.

I could just as easily define explicit JSON templates for the json style 
(much like how we do with XML). But, it suffers from the existing 
drawbacks of partial expansion and feels kludgy. I like how the approach 
in this series could lead to a world where templates "just work" for 
machine readable output formats.

Thoughts?
Matt Mackall - Jan. 1, 2015, 11:26 p.m.
On Wed, 2014-12-31 at 14:45 -0800, Gregory Szorc wrote:
> # HG changeset patch
> # User Gregory Szorc <gregory.szorc@gmail.com>
> # Date 1420065389 28800
> #      Wed Dec 31 14:36:29 2014 -0800
> # Node ID 375310e882793f02023305f1065f616b0b42ab88
> # Parent  8a88b921f7b9b467046496ab6aa99d2589dd935c
> tests: add tests for json output from hgweb

> +  {"author": "test", "branch": [], "child": [], "date": [0.0, 0],
> "desc": "move foo", "extra": {"branch": "default"}, "file": "foo-new",
> "node": "78896eb0e102174ce9278438a95e12543e4367a7", "parent":
> [{"branch": "default", "date": [0.0, 0], "description": "modify foo",
> "file": "foo", "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8",
> "rev": 1, "user": "test"}], "path": "/", "permissions": "", "rename":
> [{"file": "foo", "node": "cebda196bdbe7661cec847739e7c5de89ec6e5a5"}],
> "rev": 3, "text": [{"line": "bar\n", "lineid": "l1", "linenumber": "
> 1", "parity": 0}]} (no-eol)

This should be aiming to agree with the schema produced by hg log -T
json today. Eg:

$ hg log -Tjson -l1
[
 {
  "rev": 26053,
  "node": "3314664606e63e0ae263b71f5210e8153291efe8",
  "branch": "default",
  "phase": "public",
  "user": "Matt Mackall <mpm@selenic.com>",
  "date": [1420152434, 21600],
  "desc": "merge with stable",
  "bookmarks": ["@"],
  "tags": ["tip"],
  "parents": ["28a302e9225d05b35d61da6f70e7187ad3ce8d7c",
"4308087f2fbd90b5beaac53a71d6264684ee0c40"]
 }
]

Same story for other commands.

With your jsonengine approach, this is going to be a real pain to
achieve. It'll mean modifying hgweb and every other style to make the
ancient internal hgweb element names and structures agree with what's
being done on the command line. Or, alternately, having a system to do
some transformations inside jsonengine.

But we've already got a templater for that. My position is still that we
should implement this by adding a mercurial/templates/json with a bunch
of boring templates like we have for raw. Same for xml.
Gregory Szorc - Jan. 6, 2015, 2 a.m.
On 1/1/15 3:26 PM, Matt Mackall wrote:
> On Wed, 2014-12-31 at 14:45 -0800, Gregory Szorc wrote:
>> # HG changeset patch
>> # User Gregory Szorc <gregory.szorc@gmail.com>
>> # Date 1420065389 28800
>> #      Wed Dec 31 14:36:29 2014 -0800
>> # Node ID 375310e882793f02023305f1065f616b0b42ab88
>> # Parent  8a88b921f7b9b467046496ab6aa99d2589dd935c
>> tests: add tests for json output from hgweb
>
>> +  {"author": "test", "branch": [], "child": [], "date": [0.0, 0],
>> "desc": "move foo", "extra": {"branch": "default"}, "file": "foo-new",
>> "node": "78896eb0e102174ce9278438a95e12543e4367a7", "parent":
>> [{"branch": "default", "date": [0.0, 0], "description": "modify foo",
>> "file": "foo", "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8",
>> "rev": 1, "user": "test"}], "path": "/", "permissions": "", "rename":
>> [{"file": "foo", "node": "cebda196bdbe7661cec847739e7c5de89ec6e5a5"}],
>> "rev": 3, "text": [{"line": "bar\n", "lineid": "l1", "linenumber":"
>> 1", "parity": 0}]} (no-eol)
>
> This should be aiming to agree with the schema produced by hg log -T
> json today. Eg:
>
> $ hg log -Tjson -l1
> [
>   {
>    "rev": 26053,
>    "node": "3314664606e63e0ae263b71f5210e8153291efe8",
>    "branch": "default",
>    "phase": "public",
>    "user": "Matt Mackall <mpm@selenic.com>",
>    "date": [1420152434, 21600],
>    "desc": "merge with stable",
>    "bookmarks": ["@"],
>    "tags": ["tip"],
>    "parents": ["28a302e9225d05b35d61da6f70e7187ad3ce8d7c",
> "4308087f2fbd90b5beaac53a71d6264684ee0c40"]
>   }
> ]
>
> Same story for other commands.
>
> With your jsonengine approach, this is going to be a real pain to
> achieve. It'll mean modifying hgweb and every other style to make the
> ancient internal hgweb element names and structures agree with what's
> being done on the command line. Or, alternately, having a system to do
> some transformations inside jsonengine.
>
> But we've already got a templater for that. My position is still that we
> should implement this by adding a mercurial/templates/json with a bunch
> of boring templates like we have for raw. Same for xml.

Can do!

How much leeway do I have with consistency with the CLI? For example, 
the CLI currently renders things like diffstat in the human readable 
form. e.g. "9 +++++----\n tests/test-annotate.t". I think JSON output, 
being destined for machines, should preserve the original metadata 
instead of munge it into an unstructured text field. I'll code patches 
with what I think is more sensible unless I hear otherwise.

Also, I'm finding things like lack of templatekw.showlist usage to be 
quite annoying. e.g. I can't utilize the "start_foo" magic to properly 
encode JSON lists. I'm only now realizing how different the CLI and web 
templating systems are :/ Because of these differences, it looks like I 
may have to pass some variables to the web templater multiple times, 
just encoded slightly differently in order to facilitate proper JSON 
encoding. Or maybe I'll invent some one-off filters defined only for the 
web templater. It's a nice little can of worms in here. Now I understand 
why this wasn't done sooner :)
Matt Mackall - Jan. 6, 2015, 4:47 a.m.
On Mon, 2015-01-05 at 18:00 -0800, Gregory Szorc wrote:
> On 1/1/15 3:26 PM, Matt Mackall wrote:
> > On Wed, 2014-12-31 at 14:45 -0800, Gregory Szorc wrote:
> >> # HG changeset patch
> >> # User Gregory Szorc <gregory.szorc@gmail.com>
> >> # Date 1420065389 28800
> >> #      Wed Dec 31 14:36:29 2014 -0800
> >> # Node ID 375310e882793f02023305f1065f616b0b42ab88
> >> # Parent  8a88b921f7b9b467046496ab6aa99d2589dd935c
> >> tests: add tests for json output from hgweb
> >
> >> +  {"author": "test", "branch": [], "child": [], "date": [0.0, 0],
> >> "desc": "move foo", "extra": {"branch": "default"}, "file": "foo-new",
> >> "node": "78896eb0e102174ce9278438a95e12543e4367a7", "parent":
> >> [{"branch": "default", "date": [0.0, 0], "description": "modify foo",
> >> "file": "foo", "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8",
> >> "rev": 1, "user": "test"}], "path": "/", "permissions": "", "rename":
> >> [{"file": "foo", "node": "cebda196bdbe7661cec847739e7c5de89ec6e5a5"}],
> >> "rev": 3, "text": [{"line": "bar\n", "lineid": "l1", "linenumber":"
> >> 1", "parity": 0}]} (no-eol)
> >
> > This should be aiming to agree with the schema produced by hg log -T
> > json today. Eg:
> >
> > $ hg log -Tjson -l1
> > [
> >   {
> >    "rev": 26053,
> >    "node": "3314664606e63e0ae263b71f5210e8153291efe8",
> >    "branch": "default",
> >    "phase": "public",
> >    "user": "Matt Mackall <mpm@selenic.com>",
> >    "date": [1420152434, 21600],
> >    "desc": "merge with stable",
> >    "bookmarks": ["@"],
> >    "tags": ["tip"],
> >    "parents": ["28a302e9225d05b35d61da6f70e7187ad3ce8d7c",
> > "4308087f2fbd90b5beaac53a71d6264684ee0c40"]
> >   }
> > ]
> >
> > Same story for other commands.
> >
> > With your jsonengine approach, this is going to be a real pain to
> > achieve. It'll mean modifying hgweb and every other style to make the
> > ancient internal hgweb element names and structures agree with what's
> > being done on the command line. Or, alternately, having a system to do
> > some transformations inside jsonengine.
> >
> > But we've already got a templater for that. My position is still that we
> > should implement this by adding a mercurial/templates/json with a bunch
> > of boring templates like we have for raw. Same for xml.
> 
> Can do!
> 
> How much leeway do I have with consistency with the CLI? For example, 
> the CLI currently renders things like diffstat in the human readable 
> form. e.g. "9 +++++----\n tests/test-annotate.t". I think JSON output, 
> being destined for machines, should preserve the original metadata 
> instead of munge it into an unstructured text field.

Yeah, diffstat is dumb. Perhaps add a new field with machine-readable
diffstat. Priorities here would be:

- have matching fields (best)
- have no fields with the same name but different semantics (minimum)

> Also, I'm finding things like lack of templatekw.showlist usage to be 
> quite annoying. e.g. I can't utilize the "start_foo" magic to properly 
> encode JSON lists.

That magic should have never existed and I'd rip it out if I could. Use
join().

Patch

diff --git a/tests/test-hgweb-json.t b/tests/test-hgweb-json.t
new file mode 100644
--- /dev/null
+++ b/tests/test-hgweb-json.t
@@ -0,0 +1,152 @@ 
+#require serve
+
+  $ hg init test
+  $ cd test
+  $ mkdir da
+  $ echo foo > da/foo
+  $ echo foo > foo
+  $ hg -q ci -A -m initial
+  $ echo bar > foo
+  $ hg ci -m 'modify foo'
+  $ echo bar > da/foo
+  $ hg ci -m 'modify da/foo'
+  $ hg bookmark test-bookmark
+  $ hg mv foo foo-new
+  $ hg commit -m 'move foo'
+  $ hg tag -m 'create tag' test-tag
+  $ hg -q up -r 0
+  $ hg -q branch test-branch
+  $ echo branch > foo
+  $ hg commit -m 'create test branch'
+
+  $ hg log -G
+  @  changeset:   5:6ab967a8ab34
+  |  branch:      test-branch
+  |  tag:         tip
+  |  parent:      0:06e557f3edf6
+  |  user:        test
+  |  date:        Thu Jan 01 00:00:00 1970 +0000
+  |  summary:     create test branch
+  |
+  | o  changeset:   4:92d2ccb2a27b
+  | |  bookmark:    test-bookmark
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     create tag
+  | |
+  | o  changeset:   3:78896eb0e102
+  | |  tag:         test-tag
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     move foo
+  | |
+  | o  changeset:   2:8d7c456572ac
+  | |  user:        test
+  | |  date:        Thu Jan 01 00:00:00 1970 +0000
+  | |  summary:     modify da/foo
+  | |
+  | o  changeset:   1:f8bbb9024b10
+  |/   user:        test
+  |    date:        Thu Jan 01 00:00:00 1970 +0000
+  |    summary:     modify foo
+  |
+  o  changeset:   0:06e557f3edf6
+     user:        test
+     date:        Thu Jan 01 00:00:00 1970 +0000
+     summary:     initial
+  
+
+  $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E error.log
+  $ cat hg.pid >> $DAEMON_PIDS
+
+(Try to keep these in roughly the order they are defined in webcommands.py)
+
+(log is handled by filelog/ and changelog/ - ignore it)
+
+(rawfile/ doesn't use templating - nothing to test)
+
+file/ with path returns a file revision
+
+  $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'file/78896eb0e102/foo-new?style=json'
+  200 Script output follows
+  
+  {"author": "test", "branch": [], "child": [], "date": [0.0, 0], "desc": "move foo", "extra": {"branch": "default"}, "file": "foo-new", "node": "78896eb0e102174ce9278438a95e12543e4367a7", "parent": [{"branch": "default", "date": [0.0, 0], "description": "modify foo", "file": "foo", "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8", "rev": 1, "user": "test"}], "path": "/", "permissions": "", "rename": [{"file": "foo", "node": "cebda196bdbe7661cec847739e7c5de89ec6e5a5"}], "rev": 3, "text": [{"line": "bar\n", "lineid": "l1", "linenumber": "     1", "parity": 0}]} (no-eol)
+
+TODO changelog/
+
+TODO shortlog/
+
+TODO changeset/
+
+manifest/ should display manifest listings
+
+  $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'manifest?style=json'
+  200 Script output follows
+  
+  {"archives": [], "bookmarks": [], "branches": [{"name": "test-branch"}], "dentries": [{"basename": "da", "emptydirs": "", "parity": 1, "path": "/da"}], "fentries": [{"basename": "foo", "date": [0.0, 0], "file": "foo", "parity": 0, "permissions": "", "size": 7}], "inbranch": [], "node": "6ab967a8ab3489227a83f80e920faa039a71819f", "path": "/", "rev": 5, "tags": [{"name": "tip"}], "up": "/", "upparity": 0} (no-eol)
+
+tags/ should display tag info
+
+  $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'tags?style=json'
+  200 Script output follows
+  
+  {"entries": [{"date": [0.0, 0], "node": "6ab967a8ab3489227a83f80e920faa039a71819f", "parity": 0, "tag": "tip"}, {"date": [0.0, 0], "node": "78896eb0e102174ce9278438a95e12543e4367a7", "parity": 1, "tag": "test-tag"}], "entriesnotip": [{"date": [0.0, 0], "node": "78896eb0e102174ce9278438a95e12543e4367a7", "parity": 0, "tag": "test-tag"}], "latestentry": [{"date": [0.0, 0], "node": "78896eb0e102174ce9278438a95e12543e4367a7", "parity": 1, "tag": "test-tag"}], "node": "6ab967a8ab3489227a83f80e920faa039a71819f"} (no-eol)
+
+bookmarks/ should display bookmarks info
+
+  $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'bookmarks?style=json'
+  200 Script output follows
+  
+  {"entries": [{"bookmark": "test-bookmark", "date": [0.0, 0], "node": "92d2ccb2a27b6f75b6b0b70c98c19095735dad40", "parity": 0}], "latestentry": [{"bookmark": "test-bookmark", "date": [0.0, 0], "node": "92d2ccb2a27b6f75b6b0b70c98c19095735dad40", "parity": 1}], "node": "6ab967a8ab3489227a83f80e920faa039a71819f"} (no-eol)
+
+branches/ should display branches info
+
+  $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'branches?style=json'
+  200 Script output follows
+  
+  {"entries": [{"branch": "test-branch", "date": [0.0, 0], "node": "6ab967a8ab3489227a83f80e920faa039a71819f", "parity": 0, "status": "open"}, {"branch": "default", "date": [0.0, 0], "node": "92d2ccb2a27b6f75b6b0b70c98c19095735dad40", "parity": 1, "status": "open"}], "latestentry": [{"branch": "test-branch", "date": [0.0, 0], "node": "6ab967a8ab3489227a83f80e920faa039a71819f", "parity": 0, "status": "open"}], "node": "6ab967a8ab3489227a83f80e920faa039a71819f"} (no-eol)
+
+TODO summary/
+
+TODO filediff/
+
+TODO comparison/
+
+annotate/ should display file annotations
+
+  $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'annotate/tip/foo?style=json'
+  200 Script output follows
+  
+  {"annotate": [{"author": "test", "desc": "create test branch", "extra": {"branch": "test-branch"}, "file": "foo", "line": "branch\n", "lineid": "l1", "linenumber": "     1", "node": "6ab967a8ab3489227a83f80e920faa039a71819f", "parity": 0, "rev": 5, "revdate": [0.0, 0], "targetline": 1}], "author": "test", "branch": [{"name": "test-branch"}], "child": [], "date": [0.0, 0], "desc": "create test branch", "extra": {"branch": "test-branch"}, "file": "foo", "node": "6ab967a8ab3489227a83f80e920faa039a71819f", "parent": [{"branch": "default", "date": [0.0, 0], "description": "initial", "file": "foo", "node": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e", "rev": 0, "user": "test"}], "path": "/", "permissions": "", "rename": [], "rev": 5} (no-eol)
+
+filelog/ shows changes for a given file
+
+  $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'filelog/tip/foo?style=json'
+  200 Script output follows
+  
+  {"entries": [{"author": "test", "bookmarks": [], "branch": [{"name": "test-branch"}], "branches": [{"name": "test-branch"}], "child": [], "date": [0.0, 0], "desc": "create test branch", "extra": {"branch": "test-branch"}, "file": "foo", "filerev": 2, "inbranch": [], "node": "6ab967a8ab3489227a83f80e920faa039a71819f", "parent": [{"branch": "default", "date": [0.0, 0], "description": "initial", "file": "foo", "node": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e", "rev": 0, "user": "test"}], "parity": 0, "rename": [], "tags": [{"name": "tip"}]}, {"author": "test", "bookmarks": [], "branch": [], "branches": [], "child": [], "date": [0.0, 0], "desc": "modify foo", "extra": {"branch": "default"}, "file": "foo", "filerev": 1, "inbranch": [], "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8", "parent": [{"branch": "default", "date": [0.0, 0], "description": "initial", "file": "foo", "node": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e", "rev": 0, "user": "test"}], "parity": 1, "rename": [], "tags": []}, {"author": "test", "bookmarks": [], "branch": [], "branches": [], "child": [{"branch": "default", "date": [0.0, 0], "description": "modify foo", "file": "foo", "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8", "rev": 1, "user": "test"}, {"branch": "test-branch", "date": [0.0, 0], "description": "create test branch", "file": "foo", "node": "6ab967a8ab3489227a83f80e920faa039a71819f", "rev": 5, "user": "test"}], "date": [0.0, 0], "desc": "initial", "extra": {"branch": "default"}, "file": "foo", "filerev": 0, "inbranch": [], "node": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e", "parent": [], "parity": 0, "rename": [], "tags": []}], "file": "foo", "latestentry": [{"author": "test", "bookmarks": [], "branch": [{"name": "test-branch"}], "branches": [{"name": "test-branch"}], "child": [], "date": [0.0, 0], "desc": "create test branch", "extra": {"branch": "test-branch"}, "file": "foo", "filerev": 2, "inbranch": [], "node": "6ab967a8ab3489227a83f80e920faa039a71819f", "parent": [], "parity": 0, "rename": [], "tags": [{"name": "tip"}]}], "lessvars": [{"name": "revcount", "separator": "?", "value": "30"}, {"name": "style", "separator": "&", "value": "json"}], "morevars": [{"name": "revcount", "separator": "?", "value": "120"}, {"name": "style", "separator": "&", "value": "json"}], "nav": [{"after": [{"label": "tip", "node": "tip"}], "before": [{"label": "(0)", "node": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e"}]}], "node": "6ab967a8ab3489227a83f80e920faa039a71819f", "revcount": 60} (no-eol)
+
+(archive/ doesn't use templating, so ignore it)
+
+(static/ doesn't use templating, so ignore it)
+
+graph/ should show info necessary to render a graph.
+
+  $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'graph/tip?style=json'
+  200 Script output follows
+  
+  {"bg_height": 39, "canvasheight": 246, "canvaswidth": 78, "changenav": [{"after": [{"label": "tip", "node": "tip"}], "before": [{"label": "(0)", "node": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e"}]}], "cols": 1, "downrev": 0, "jsdata": [["6ab967a8ab34", [0, 1], [[0, 0, 1, -1, ""]], "create test branch", "test", "1970-01-01", ["test-branch", true], ["tip"], []], ["92d2ccb2a27b", [1, 2], [[0, 0, 1, -1, ""], [1, 1, 2, -1, ""]], "create tag", "test", "1970-01-01", ["default", true], [], ["test-bookmark"]], ["78896eb0e102", [1, 2], [[0, 0, 1, -1, ""], [1, 1, 2, -1, ""]], "move foo", "test", "1970-01-01", ["default", false], ["test-tag"], []], ["8d7c456572ac", [1, 2], [[0, 0, 1, -1, ""], [1, 1, 2, -1, ""]], "modify da/foo", "test", "1970-01-01", ["default", false], [], []], ["f8bbb9024b10", [1, 2], [[0, 0, 1, -1, ""], [1, 0, 2, -1, ""]], "modify foo", "test", "1970-01-01", ["default", false], [], []], ["06e557f3edf6", [0, 1], [], "initial", "test", "1970-01-01", ["default", false], [], []]], "lessvars": [{"name": "revcount", "separator": "?", "value": "30"}, {"name": "style", "separator": "&", "value": "json"}], "morevars": [{"name": "revcount", "separator": "?", "value": "120"}, {"name": "style", "separator": "&", "value": "json"}], "node": "6ab967a8ab3489227a83f80e920faa039a71819f", "nodes": [{"age": "1970-01-01", "bookmarks": [], "branches": [{"name": "test-branch"}], "col": 0, "color": 1, "desc": "create test branch", "edges": [{"bcolor": "", "col": 0, "color": 1, "nextcol": 0, "width": -1}], "inbranch": [], "nextrow": 1, "node": "6ab967a8ab34", "row": 0, "tags": [{"name": "tip"}], "user": "test"}, {"age": "1970-01-01", "bookmarks": [{"name": "test-bookmark"}], "branches": [{"name": "default"}], "col": 1, "color": 2, "desc": "create tag", "edges": [{"bcolor": "", "col": 0, "color": 1, "nextcol": 0, "width": -1}, {"bcolor": "", "col": 1, "color": 2, "nextcol": 1, "width": -1}], "inbranch": [], "nextrow": 2, "node": "92d2ccb2a27b", "row": 1, "tags": [], "user": "test"}, {"age": "1970-01-01", "bookmarks": [], "branches": [], "col": 1, "color": 2, "desc": "move foo", "edges": [{"bcolor": "", "col": 0, "color": 1, "nextcol": 0, "width": -1}, {"bcolor": "", "col": 1, "color": 2, "nextcol": 1, "width": -1}], "inbranch": [], "nextrow": 3, "node": "78896eb0e102", "row": 2, "tags": [{"name": "test-tag"}], "user": "test"}, {"age": "1970-01-01", "bookmarks": [], "branches": [], "col": 1, "color": 2, "desc": "modify da/foo", "edges": [{"bcolor": "", "col": 0, "color": 1, "nextcol": 0, "width": -1}, {"bcolor": "", "col": 1, "color": 2, "nextcol": 1, "width": -1}], "inbranch": [], "nextrow": 4, "node": "8d7c456572ac", "row": 3, "tags": [], "user": "test"}, {"age": "1970-01-01", "bookmarks": [], "branches": [], "col": 1, "color": 2, "desc": "modify foo", "edges": [{"bcolor": "", "col": 0, "color": 1, "nextcol": 0, "width": -1}, {"bcolor": "", "col": 1, "color": 2, "nextcol": 0, "width": -1}], "inbranch": [], "nextrow": 5, "node": "f8bbb9024b10", "row": 4, "tags": [], "user": "test"}, {"age": "1970-01-01", "bookmarks": [], "branches": [], "col": 0, "color": 1, "desc": "initial", "edges": [], "inbranch": [], "nextrow": 6, "node": "06e557f3edf6", "row": 5, "tags": [], "user": "test"}], "rev": 5, "revcount": 60, "rows": 6, "truecanvasheight": 234, "uprev": 5} (no-eol)
+
+help/ with no arguments should list help topics
+
+  $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'help?style=json'
+  200 Script output follows
+  
+  {"earlycommands": [{"summary": "add the specified files on the next commit", "topic": "add"}, {"summary": "show changeset information by line for each file", "topic": "annotate"}, {"summary": "make a copy of an existing repository", "topic": "clone"}, {"summary": "commit the specified files or all outstanding changes", "topic": "commit"}, {"summary": "diff repository (or selected files)", "topic": "diff"}, {"summary": "dump the header and diffs for one or more changesets", "topic": "export"}, {"summary": "forget the specified files on the next commit", "topic": "forget"}, {"summary": "create a new repository in the given directory", "topic": "init"}, {"summary": "show revision history of entire repository or files", "topic": "log"}, {"summary": "merge another revision into working directory", "topic": "merge"}, {"summary": "pull changes from the specified source", "topic": "pull"}, {"summary": "push changes to the specified destination", "topic": "push"}, {"summary": "remove the specified files on the next commit", "topic": "remove"}, {"summary": "start stand-alone webserver", "topic": "serve"}, {"summary": "show changed files in the working directory", "topic": "status"}, {"summary": "summarize working directory state", "topic": "summary"}, {"summary": "update working directory (or switch revisions)", "topic": "update"}], "othercommands": [{"summary": "add all new files, delete all missing files", "topic": "addremove"}, {"summary": "create an unversioned archive of a repository revision", "topic": "archive"}, {"summary": "reverse effect of earlier changeset", "topic": "backout"}, {"summary": "subdivision search of changesets", "topic": "bisect"}, {"summary": "create a new bookmark or list existing bookmarks", "topic": "bookmarks"}, {"summary": "set or show the current branch name", "topic": "branch"}, {"summary": "list repository named branches", "topic": "branches"}, {"summary": "create a changegroup file", "topic": "bundle"}, {"summary": "output the current or given revision of files", "topic": "cat"}, {"summary": "show combined config settings from all hgrc files", "topic": "config"}, {"summary": "mark files as copied for the next commit", "topic": "copy"}, {"summary": "list tracked files", "topic": "files"}, {"summary": "copy changes from other branches onto the current branch", "topic": "graft"}, {"summary": "search for a pattern in specified files and revisions", "topic": "grep"}, {"summary": "show branch heads", "topic": "heads"}, {"summary": "show help for a given topic or a help overview", "topic": "help"}, {"summary": "identify the working copy or specified revision", "topic": "identify"}, {"summary": "import an ordered set of patches", "topic": "import"}, {"summary": "show new changesets found in source", "topic": "incoming"}, {"summary": "output the current or given revision of the project manifest", "topic": "manifest"}, {"summary": "show changesets not found in the destination", "topic": "outgoing"}, {"summary": "show aliases for remote repositories", "topic": "paths"}, {"summary": "set or show the current phase name", "topic": "phase"}, {"summary": "roll back an interrupted transaction", "topic": "recover"}, {"summary": "rename files; equivalent of copy + remove", "topic": "rename"}, {"summary": "redo merges or set/view the merge status of files", "topic": "resolve"}, {"summary": "restore files to their checkout state", "topic": "revert"}, {"summary": "print the root (top) of the current working directory", "topic": "root"}, {"summary": "add one or more tags for the current or given revision", "topic": "tag"}, {"summary": "list repository tags", "topic": "tags"}, {"summary": "apply one or more changegroup files", "topic": "unbundle"}, {"summary": "verify the integrity of the repository", "topic": "verify"}, {"summary": "output version and copyright information", "topic": "version"}], "title": "Index", "topics": [{"summary": "Configuration Files", "topic": "config"}, {"summary": "Date Formats", "topic": "dates"}, {"summary": "Diff Formats", "topic": "diffs"}, {"summary": "Environment Variables", "topic": "environment"}, {"summary": "Using Additional Features", "topic": "extensions"}, {"summary": "Specifying File Sets", "topic": "filesets"}, {"summary": "Glossary", "topic": "glossary"}, {"summary": "Syntax for Mercurial Ignore Files", "topic": "hgignore"}, {"summary": "Configuring hgweb", "topic": "hgweb"}, {"summary": "Merge Tools", "topic": "merge-tools"}, {"summary": "Specifying Multiple Revisions", "topic": "multirevs"}, {"summary": "File Name Patterns", "topic": "patterns"}, {"summary": "Working with Phases", "topic": "phases"}, {"summary": "Specifying Single Revisions", "topic": "revisions"}, {"summary": "Specifying Revision Sets", "topic": "revsets"}, {"summary": "Subrepositories", "topic": "subrepos"}, {"summary": "Template Usage", "topic": "templating"}, {"summary": "URL Paths", "topic": "urls"}]} (no-eol)
+
+help/ can return help for a specific topic
+
+  $ $TESTDIR/get-with-headers.py localhost:$HGPORT 'help?node=heads&style=json'
+  200 Script output follows
+  
+  {"doc": "hg heads [-ct] [-r STARTREV] [REV]...\n\nshow branch heads\n\n    With no arguments, show all open branch heads in the repository.\n    Branch heads are changesets that have no descendants on the\n    same branch. They are where development generally takes place and\n    are the usual targets for update and merge operations.\n\n    If one or more REVs are given, only open branch heads on the\n    branches associated with the specified changesets are shown. This\n    means that you can use :hg:`heads .` to see the heads on the\n    currently checked-out branch.\n\n    If -c/--closed is specified, also show branch heads marked closed\n    (see :hg:`commit --close-branch`).\n\n    If STARTREV is specified, only those heads that are descendants of\n    STARTREV will be displayed.\n\n    If -t/--topo is specified, named branch mechanics will be ignored and only\n    topological heads (changesets with no children) will be shown.\n\n    Returns 0 if matching heads are found, 1 if not.\n    \n\noptions:\n\n == =================== =================================================\n -r --rev STARTREV      show only heads which are descendants of STARTREV\n -t --topo              show topological heads only                      \n -a --active            show active branchheads only (DEPRECATED)        \n -c --closed            show normal and closed branch heads              \n    --style STYLE       display using template map file (DEPRECATED)     \n -T --template TEMPLATE display with template                            \n == =================== =================================================\n\nglobal options ([+] can be repeated):\n\n == =================== ==================================================================\n -R --repository REPO   repository root directory or name of overlay bundle file          \n    --cwd DIR           change working directory                                          \n -y --noninteractive    do not prompt, automatically pick the first choice for all prompts\n -q --quiet             suppress output                                                   \n -v --verbose           enable additional output                                          \n    --config CONFIG [+] set/override config option (use 'section.name=value')             \n    --debug             enable debugging output                                           \n    --debugger          start debugger                                                    \n    --encoding ENCODE   set the charset encoding (default: ascii)                         \n    --encodingmode MODE set the charset encoding mode (default: strict)                   \n    --traceback         always print a traceback on exception                             \n    --time              time how long the command takes                                   \n    --profile           print command execution profile                                   \n    --version           output version information and exit                               \n -h --help              display help and exit                                             \n    --hidden            consider hidden changesets                                        \n == =================== ==================================================================\n", "topic": "heads"} (no-eol)