Skip to content

Commit 1acbc14

Browse files
vapierLUCI
authored and
LUCI
committed
manifest: generalize --json as --format=<format>
This will make it easier to add more formats without exploding the common --xxx space and checking a large set of boolean flags. Also fill out the test coverage while we're here. Bug: b/412725063 Change-Id: I754013dc6cb3445f8a0979cefec599d55dafdcff Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/471941 Reviewed-by: Gavin Mak <[email protected]> Commit-Queue: Mike Frysinger <[email protected]> Tested-by: Mike Frysinger <[email protected]>
1 parent c448ba9 commit 1acbc14

File tree

3 files changed

+195
-8
lines changed

3 files changed

+195
-8
lines changed

man/repo-manifest.1

+6-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ if in \fB\-r\fR mode, do not write the dest\-branch field
3030
(only of use if the branch names for a sha1 manifest
3131
are sensitive)
3232
.TP
33-
\fB\-\-json\fR
34-
output manifest in JSON format (experimental)
33+
\fB\-\-format\fR=\fI\,FORMAT\/\fR
34+
output format: xml, json (default: xml)
3535
.TP
3636
\fB\-\-pretty\fR
3737
format output for humans to read
@@ -78,6 +78,10 @@ set to the ref we were on when the manifest was generated. The 'dest\-branch'
7878
attribute is set to indicate the remote ref to push changes to via 'repo
7979
upload'.
8080
.PP
81+
Multiple output formats are supported via \fB\-\-format\fR. The default output is XML,
82+
and formats are generally "condensed". Use \fB\-\-pretty\fR for more human\-readable
83+
variations.
84+
.PP
8185
repo Manifest Format
8286
.PP
8387
A repo manifest describes the structure of a repo client; that is the

subcmds/manifest.py

+33-6
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import enum
1516
import json
17+
import optparse
1618
import os
1719
import sys
1820

@@ -23,6 +25,16 @@
2325
logger = RepoLogger(__file__)
2426

2527

28+
class OutputFormat(enum.Enum):
29+
"""Type for the requested output format."""
30+
31+
# Canonicalized manifest in XML format.
32+
XML = enum.auto()
33+
34+
# Canonicalized manifest in JSON format.
35+
JSON = enum.auto()
36+
37+
2638
class Manifest(PagedCommand):
2739
COMMON = False
2840
helpSummary = "Manifest inspection utility"
@@ -42,6 +54,10 @@ class Manifest(PagedCommand):
4254
In this case, the 'upstream' attribute is set to the ref we were on
4355
when the manifest was generated. The 'dest-branch' attribute is set
4456
to indicate the remote ref to push changes to via 'repo upload'.
57+
58+
Multiple output formats are supported via --format. The default output
59+
is XML, and formats are generally "condensed". Use --pretty for more
60+
human-readable variations.
4561
"""
4662

4763
@property
@@ -86,11 +102,21 @@ def _Options(self, p):
86102
"(only of use if the branch names for a sha1 manifest are "
87103
"sensitive)",
88104
)
105+
# Replaced with --format=json. Kept for backwards compatibility.
106+
# Can delete in Jun 2026 or later.
89107
p.add_option(
90108
"--json",
91-
default=False,
92-
action="store_true",
93-
help="output manifest in JSON format (experimental)",
109+
action="store_const",
110+
dest="format",
111+
const=OutputFormat.JSON.name.lower(),
112+
help=optparse.SUPPRESS_HELP,
113+
)
114+
formats = tuple(x.lower() for x in OutputFormat.__members__.keys())
115+
p.add_option(
116+
"--format",
117+
default=OutputFormat.XML.name.lower(),
118+
choices=formats,
119+
help=f"output format: {', '.join(formats)} (default: %default)",
94120
)
95121
p.add_option(
96122
"--pretty",
@@ -121,6 +147,8 @@ def _Output(self, opt):
121147
if opt.manifest_name:
122148
self.manifest.Override(opt.manifest_name, False)
123149

150+
output_format = OutputFormat[opt.format.upper()]
151+
124152
for manifest in self.ManifestList(opt):
125153
output_file = opt.output_file
126154
if output_file == "-":
@@ -135,8 +163,7 @@ def _Output(self, opt):
135163

136164
manifest.SetUseLocalManifests(not opt.ignore_local_manifests)
137165

138-
if opt.json:
139-
logger.warning("warning: --json is experimental!")
166+
if output_format == OutputFormat.JSON:
140167
doc = manifest.ToDict(
141168
peg_rev=opt.peg_rev,
142169
peg_rev_upstream=opt.peg_rev_upstream,
@@ -152,7 +179,7 @@ def _Output(self, opt):
152179
"separators": (",", ": ") if opt.pretty else (",", ":"),
153180
"sort_keys": True,
154181
}
155-
fd.write(json.dumps(doc, **json_settings))
182+
fd.write(json.dumps(doc, **json_settings) + "\n")
156183
else:
157184
manifest.Save(
158185
fd,

tests/test_subcmds_manifest.py

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# Copyright (C) 2025 The Android Open Source Project
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Unittests for the subcmds/manifest.py module."""
16+
17+
import json
18+
from pathlib import Path
19+
from unittest import mock
20+
21+
import manifest_xml
22+
from subcmds import manifest
23+
24+
25+
_EXAMPLE_MANIFEST = """\
26+
<?xml version="1.0" encoding="UTF-8"?>
27+
<manifest>
28+
<remote name="test-remote" fetch="http://localhost" />
29+
<default remote="test-remote" revision="refs/heads/main" />
30+
<project name="repohooks" path="src/repohooks"/>
31+
<repo-hooks in-project="repohooks" enabled-list="a, b"/>
32+
</manifest>
33+
"""
34+
35+
36+
def _get_cmd(repodir: Path) -> manifest.Manifest:
37+
"""Instantiate a manifest command object to test."""
38+
manifests_git = repodir / "manifests.git"
39+
manifests_git.mkdir()
40+
(manifests_git / "config").write_text(
41+
"""
42+
[remote "origin"]
43+
\turl = http://localhost/manifest
44+
"""
45+
)
46+
client = manifest_xml.RepoClient(repodir=str(repodir))
47+
git_event_log = mock.MagicMock(ErrorEvent=mock.Mock(return_value=None))
48+
return manifest.Manifest(
49+
repodir=client.repodir,
50+
client=client,
51+
manifest=client.manifest,
52+
outer_client=client,
53+
outer_manifest=client.manifest,
54+
git_event_log=git_event_log,
55+
)
56+
57+
58+
def test_output_format_xml_file(tmp_path):
59+
"""Test writing XML to a file."""
60+
path = tmp_path / "manifest.xml"
61+
path.write_text(_EXAMPLE_MANIFEST)
62+
outpath = tmp_path / "output.xml"
63+
cmd = _get_cmd(tmp_path)
64+
opt, args = cmd.OptionParser.parse_args(["--output-file", str(outpath)])
65+
cmd.Execute(opt, args)
66+
# Normalize the output a bit as we don't exactly care.
67+
normalize = lambda data: "\n".join(
68+
x.strip() for x in data.splitlines() if x.strip()
69+
)
70+
assert (
71+
normalize(outpath.read_text())
72+
== """<?xml version="1.0" encoding="UTF-8"?>
73+
<manifest>
74+
<remote name="test-remote" fetch="http://localhost"/>
75+
<default remote="test-remote" revision="refs/heads/main"/>
76+
<project name="repohooks" path="src/repohooks"/>
77+
<repo-hooks in-project="repohooks" enabled-list="a b"/>
78+
</manifest>"""
79+
)
80+
81+
82+
def test_output_format_xml_stdout(tmp_path, capsys):
83+
"""Test writing XML to stdout."""
84+
path = tmp_path / "manifest.xml"
85+
path.write_text(_EXAMPLE_MANIFEST)
86+
cmd = _get_cmd(tmp_path)
87+
opt, args = cmd.OptionParser.parse_args(["--format", "xml"])
88+
cmd.Execute(opt, args)
89+
# Normalize the output a bit as we don't exactly care.
90+
normalize = lambda data: "\n".join(
91+
x.strip() for x in data.splitlines() if x.strip()
92+
)
93+
stdout = capsys.readouterr().out
94+
assert (
95+
normalize(stdout)
96+
== """<?xml version="1.0" encoding="UTF-8"?>
97+
<manifest>
98+
<remote name="test-remote" fetch="http://localhost"/>
99+
<default remote="test-remote" revision="refs/heads/main"/>
100+
<project name="repohooks" path="src/repohooks"/>
101+
<repo-hooks in-project="repohooks" enabled-list="a b"/>
102+
</manifest>"""
103+
)
104+
105+
106+
def test_output_format_json(tmp_path, capsys):
107+
"""Test writing JSON."""
108+
path = tmp_path / "manifest.xml"
109+
path.write_text(_EXAMPLE_MANIFEST)
110+
cmd = _get_cmd(tmp_path)
111+
opt, args = cmd.OptionParser.parse_args(["--format", "json"])
112+
cmd.Execute(opt, args)
113+
obj = json.loads(capsys.readouterr().out)
114+
assert obj == {
115+
"default": {"remote": "test-remote", "revision": "refs/heads/main"},
116+
"project": [{"name": "repohooks", "path": "src/repohooks"}],
117+
"remote": [{"fetch": "http://localhost", "name": "test-remote"}],
118+
"repo-hooks": {"enabled-list": "a b", "in-project": "repohooks"},
119+
}
120+
121+
122+
def test_output_format_json_pretty(tmp_path, capsys):
123+
"""Test writing pretty JSON."""
124+
path = tmp_path / "manifest.xml"
125+
path.write_text(_EXAMPLE_MANIFEST)
126+
cmd = _get_cmd(tmp_path)
127+
opt, args = cmd.OptionParser.parse_args(["--format", "json", "--pretty"])
128+
cmd.Execute(opt, args)
129+
stdout = capsys.readouterr().out
130+
assert (
131+
stdout
132+
== """\
133+
{
134+
"default": {
135+
"remote": "test-remote",
136+
"revision": "refs/heads/main"
137+
},
138+
"project": [
139+
{
140+
"name": "repohooks",
141+
"path": "src/repohooks"
142+
}
143+
],
144+
"remote": [
145+
{
146+
"fetch": "http://localhost",
147+
"name": "test-remote"
148+
}
149+
],
150+
"repo-hooks": {
151+
"enabled-list": "a b",
152+
"in-project": "repohooks"
153+
}
154+
}
155+
"""
156+
)

0 commit comments

Comments
 (0)