mirror of
https://gerrit.googlesource.com/git-repo
synced 2025-05-08 12:33:31 +00:00
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 <gavinmak@google.com> Commit-Queue: Mike Frysinger <vapier@google.com> Tested-by: Mike Frysinger <vapier@google.com>
This commit is contained in:
parent
c448ba9cc7
commit
1acbc14c34
@ -30,8 +30,8 @@ if in \fB\-r\fR mode, do not write the dest\-branch field
|
|||||||
(only of use if the branch names for a sha1 manifest
|
(only of use if the branch names for a sha1 manifest
|
||||||
are sensitive)
|
are sensitive)
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-json\fR
|
\fB\-\-format\fR=\fI\,FORMAT\/\fR
|
||||||
output manifest in JSON format (experimental)
|
output format: xml, json (default: xml)
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-pretty\fR
|
\fB\-\-pretty\fR
|
||||||
format output for humans to read
|
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'
|
|||||||
attribute is set to indicate the remote ref to push changes to via 'repo
|
attribute is set to indicate the remote ref to push changes to via 'repo
|
||||||
upload'.
|
upload'.
|
||||||
.PP
|
.PP
|
||||||
|
Multiple output formats are supported via \fB\-\-format\fR. The default output is XML,
|
||||||
|
and formats are generally "condensed". Use \fB\-\-pretty\fR for more human\-readable
|
||||||
|
variations.
|
||||||
|
.PP
|
||||||
repo Manifest Format
|
repo Manifest Format
|
||||||
.PP
|
.PP
|
||||||
A repo manifest describes the structure of a repo client; that is the
|
A repo manifest describes the structure of a repo client; that is the
|
||||||
|
@ -12,7 +12,9 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import enum
|
||||||
import json
|
import json
|
||||||
|
import optparse
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@ -23,6 +25,16 @@ from repo_logging import RepoLogger
|
|||||||
logger = RepoLogger(__file__)
|
logger = RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
class OutputFormat(enum.Enum):
|
||||||
|
"""Type for the requested output format."""
|
||||||
|
|
||||||
|
# Canonicalized manifest in XML format.
|
||||||
|
XML = enum.auto()
|
||||||
|
|
||||||
|
# Canonicalized manifest in JSON format.
|
||||||
|
JSON = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
class Manifest(PagedCommand):
|
class Manifest(PagedCommand):
|
||||||
COMMON = False
|
COMMON = False
|
||||||
helpSummary = "Manifest inspection utility"
|
helpSummary = "Manifest inspection utility"
|
||||||
@ -42,6 +54,10 @@ revisions set to the current commit hash. These are known as
|
|||||||
In this case, the 'upstream' attribute is set to the ref we were on
|
In this case, the 'upstream' attribute is set to the ref we were on
|
||||||
when the manifest was generated. The 'dest-branch' attribute is set
|
when the manifest was generated. The 'dest-branch' attribute is set
|
||||||
to indicate the remote ref to push changes to via 'repo upload'.
|
to indicate the remote ref to push changes to via 'repo upload'.
|
||||||
|
|
||||||
|
Multiple output formats are supported via --format. The default output
|
||||||
|
is XML, and formats are generally "condensed". Use --pretty for more
|
||||||
|
human-readable variations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -86,11 +102,21 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
|||||||
"(only of use if the branch names for a sha1 manifest are "
|
"(only of use if the branch names for a sha1 manifest are "
|
||||||
"sensitive)",
|
"sensitive)",
|
||||||
)
|
)
|
||||||
|
# Replaced with --format=json. Kept for backwards compatibility.
|
||||||
|
# Can delete in Jun 2026 or later.
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--json",
|
"--json",
|
||||||
default=False,
|
action="store_const",
|
||||||
action="store_true",
|
dest="format",
|
||||||
help="output manifest in JSON format (experimental)",
|
const=OutputFormat.JSON.name.lower(),
|
||||||
|
help=optparse.SUPPRESS_HELP,
|
||||||
|
)
|
||||||
|
formats = tuple(x.lower() for x in OutputFormat.__members__.keys())
|
||||||
|
p.add_option(
|
||||||
|
"--format",
|
||||||
|
default=OutputFormat.XML.name.lower(),
|
||||||
|
choices=formats,
|
||||||
|
help=f"output format: {', '.join(formats)} (default: %default)",
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--pretty",
|
"--pretty",
|
||||||
@ -121,6 +147,8 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
|||||||
if opt.manifest_name:
|
if opt.manifest_name:
|
||||||
self.manifest.Override(opt.manifest_name, False)
|
self.manifest.Override(opt.manifest_name, False)
|
||||||
|
|
||||||
|
output_format = OutputFormat[opt.format.upper()]
|
||||||
|
|
||||||
for manifest in self.ManifestList(opt):
|
for manifest in self.ManifestList(opt):
|
||||||
output_file = opt.output_file
|
output_file = opt.output_file
|
||||||
if output_file == "-":
|
if output_file == "-":
|
||||||
@ -135,8 +163,7 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
|||||||
|
|
||||||
manifest.SetUseLocalManifests(not opt.ignore_local_manifests)
|
manifest.SetUseLocalManifests(not opt.ignore_local_manifests)
|
||||||
|
|
||||||
if opt.json:
|
if output_format == OutputFormat.JSON:
|
||||||
logger.warning("warning: --json is experimental!")
|
|
||||||
doc = manifest.ToDict(
|
doc = manifest.ToDict(
|
||||||
peg_rev=opt.peg_rev,
|
peg_rev=opt.peg_rev,
|
||||||
peg_rev_upstream=opt.peg_rev_upstream,
|
peg_rev_upstream=opt.peg_rev_upstream,
|
||||||
@ -152,7 +179,7 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
|||||||
"separators": (",", ": ") if opt.pretty else (",", ":"),
|
"separators": (",", ": ") if opt.pretty else (",", ":"),
|
||||||
"sort_keys": True,
|
"sort_keys": True,
|
||||||
}
|
}
|
||||||
fd.write(json.dumps(doc, **json_settings))
|
fd.write(json.dumps(doc, **json_settings) + "\n")
|
||||||
else:
|
else:
|
||||||
manifest.Save(
|
manifest.Save(
|
||||||
fd,
|
fd,
|
||||||
|
156
tests/test_subcmds_manifest.py
Normal file
156
tests/test_subcmds_manifest.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
# Copyright (C) 2025 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Unittests for the subcmds/manifest.py module."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import manifest_xml
|
||||||
|
from subcmds import manifest
|
||||||
|
|
||||||
|
|
||||||
|
_EXAMPLE_MANIFEST = """\
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<manifest>
|
||||||
|
<remote name="test-remote" fetch="http://localhost" />
|
||||||
|
<default remote="test-remote" revision="refs/heads/main" />
|
||||||
|
<project name="repohooks" path="src/repohooks"/>
|
||||||
|
<repo-hooks in-project="repohooks" enabled-list="a, b"/>
|
||||||
|
</manifest>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _get_cmd(repodir: Path) -> manifest.Manifest:
|
||||||
|
"""Instantiate a manifest command object to test."""
|
||||||
|
manifests_git = repodir / "manifests.git"
|
||||||
|
manifests_git.mkdir()
|
||||||
|
(manifests_git / "config").write_text(
|
||||||
|
"""
|
||||||
|
[remote "origin"]
|
||||||
|
\turl = http://localhost/manifest
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
client = manifest_xml.RepoClient(repodir=str(repodir))
|
||||||
|
git_event_log = mock.MagicMock(ErrorEvent=mock.Mock(return_value=None))
|
||||||
|
return manifest.Manifest(
|
||||||
|
repodir=client.repodir,
|
||||||
|
client=client,
|
||||||
|
manifest=client.manifest,
|
||||||
|
outer_client=client,
|
||||||
|
outer_manifest=client.manifest,
|
||||||
|
git_event_log=git_event_log,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_output_format_xml_file(tmp_path):
|
||||||
|
"""Test writing XML to a file."""
|
||||||
|
path = tmp_path / "manifest.xml"
|
||||||
|
path.write_text(_EXAMPLE_MANIFEST)
|
||||||
|
outpath = tmp_path / "output.xml"
|
||||||
|
cmd = _get_cmd(tmp_path)
|
||||||
|
opt, args = cmd.OptionParser.parse_args(["--output-file", str(outpath)])
|
||||||
|
cmd.Execute(opt, args)
|
||||||
|
# Normalize the output a bit as we don't exactly care.
|
||||||
|
normalize = lambda data: "\n".join(
|
||||||
|
x.strip() for x in data.splitlines() if x.strip()
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
normalize(outpath.read_text())
|
||||||
|
== """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<manifest>
|
||||||
|
<remote name="test-remote" fetch="http://localhost"/>
|
||||||
|
<default remote="test-remote" revision="refs/heads/main"/>
|
||||||
|
<project name="repohooks" path="src/repohooks"/>
|
||||||
|
<repo-hooks in-project="repohooks" enabled-list="a b"/>
|
||||||
|
</manifest>"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_output_format_xml_stdout(tmp_path, capsys):
|
||||||
|
"""Test writing XML to stdout."""
|
||||||
|
path = tmp_path / "manifest.xml"
|
||||||
|
path.write_text(_EXAMPLE_MANIFEST)
|
||||||
|
cmd = _get_cmd(tmp_path)
|
||||||
|
opt, args = cmd.OptionParser.parse_args(["--format", "xml"])
|
||||||
|
cmd.Execute(opt, args)
|
||||||
|
# Normalize the output a bit as we don't exactly care.
|
||||||
|
normalize = lambda data: "\n".join(
|
||||||
|
x.strip() for x in data.splitlines() if x.strip()
|
||||||
|
)
|
||||||
|
stdout = capsys.readouterr().out
|
||||||
|
assert (
|
||||||
|
normalize(stdout)
|
||||||
|
== """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<manifest>
|
||||||
|
<remote name="test-remote" fetch="http://localhost"/>
|
||||||
|
<default remote="test-remote" revision="refs/heads/main"/>
|
||||||
|
<project name="repohooks" path="src/repohooks"/>
|
||||||
|
<repo-hooks in-project="repohooks" enabled-list="a b"/>
|
||||||
|
</manifest>"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_output_format_json(tmp_path, capsys):
|
||||||
|
"""Test writing JSON."""
|
||||||
|
path = tmp_path / "manifest.xml"
|
||||||
|
path.write_text(_EXAMPLE_MANIFEST)
|
||||||
|
cmd = _get_cmd(tmp_path)
|
||||||
|
opt, args = cmd.OptionParser.parse_args(["--format", "json"])
|
||||||
|
cmd.Execute(opt, args)
|
||||||
|
obj = json.loads(capsys.readouterr().out)
|
||||||
|
assert obj == {
|
||||||
|
"default": {"remote": "test-remote", "revision": "refs/heads/main"},
|
||||||
|
"project": [{"name": "repohooks", "path": "src/repohooks"}],
|
||||||
|
"remote": [{"fetch": "http://localhost", "name": "test-remote"}],
|
||||||
|
"repo-hooks": {"enabled-list": "a b", "in-project": "repohooks"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_output_format_json_pretty(tmp_path, capsys):
|
||||||
|
"""Test writing pretty JSON."""
|
||||||
|
path = tmp_path / "manifest.xml"
|
||||||
|
path.write_text(_EXAMPLE_MANIFEST)
|
||||||
|
cmd = _get_cmd(tmp_path)
|
||||||
|
opt, args = cmd.OptionParser.parse_args(["--format", "json", "--pretty"])
|
||||||
|
cmd.Execute(opt, args)
|
||||||
|
stdout = capsys.readouterr().out
|
||||||
|
assert (
|
||||||
|
stdout
|
||||||
|
== """\
|
||||||
|
{
|
||||||
|
"default": {
|
||||||
|
"remote": "test-remote",
|
||||||
|
"revision": "refs/heads/main"
|
||||||
|
},
|
||||||
|
"project": [
|
||||||
|
{
|
||||||
|
"name": "repohooks",
|
||||||
|
"path": "src/repohooks"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"remote": [
|
||||||
|
{
|
||||||
|
"fetch": "http://localhost",
|
||||||
|
"name": "test-remote"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repo-hooks": {
|
||||||
|
"enabled-list": "a b",
|
||||||
|
"in-project": "repohooks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user