Skip to content

Commit 826595e

Browse files
yvesllpdgendt
authored andcommitted
project: compare: add -f/--format for machine-readable output
Mirror the format-string interface of "west list" on "west compare": when --format is given, print one line per dirty project using the format string, and skip the banner/status output. Reuses the format_project() helper extracted in the previous commit, so all "west list --format" keys ({name}, {url}, {path}, {abspath}, {posixpath}, {revision}, {sha}, {groups}, etc.) work identically. Signed-off-by: YvesLL <toyears@hotmail.com>
1 parent 51070ab commit 826595e

2 files changed

Lines changed: 161 additions & 6 deletions

File tree

src/west/app/project.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -863,13 +863,38 @@ def __init__(self):
863863
The command also prints output for the manifest repository if it
864864
has nonempty status.
865865
866-
The output is meant to be human-readable, and may change. It is
867-
not a stable interface to write scripts against. This command
868-
requires git 2.22 or later.'''),
866+
The default output is meant to be human-readable, and may change.
867+
It is not a stable interface to write scripts against. If you need
868+
machine-readable output (e.g. to feed back into "west update" or
869+
"west forall"), pass --format; see FORMAT STRINGS below.
870+
871+
This command requires git 2.22 or later.'''),
869872
)
870873

871874
def do_add_parser(self, parser_adder):
872-
parser = self._parser(parser_adder, epilog=ACTIVE_CLONED_PROJECTS_HELP)
875+
parser = self._parser(
876+
parser_adder,
877+
epilog=f'''\
878+
{ACTIVE_CLONED_PROJECTS_HELP}
879+
880+
FORMAT STRINGS
881+
--------------
882+
883+
When --format is given, "west compare" prints one line per project
884+
with changes, rendered using the given Python format string. The
885+
supported keys are the same ones documented in "west list --help".
886+
Unlike the default output, --format output is a stable interface
887+
suitable for scripting, for example:
888+
889+
POSIX:
890+
891+
west compare --format '{{name}}' | while read -r name; do west update "$name"; done
892+
893+
PowerShell:
894+
895+
west compare --format '{{name}}' | ForEach-Object {{ west update $_ }}
896+
''',
897+
)
873898
parser.add_argument(
874899
'projects',
875900
metavar='PROJECT',
@@ -900,6 +925,13 @@ def do_add_parser(self, parser_adder):
900925
or any compare.ignore-branches configuration
901926
option''',
902927
)
928+
parser.add_argument(
929+
'-f',
930+
'--format',
931+
help='''if given, print one line per project with changes
932+
using this format string (stable, machine-readable
933+
output); see FORMAT STRINGS below''',
934+
)
903935
return parser
904936

905937
def do_run(self, args, ignored):
@@ -915,14 +947,21 @@ def do_run(self, args, ignored):
915947

916948
failed = []
917949
printed_output = False
950+
951+
def compare_project(project, fmt):
952+
if fmt is None:
953+
self.compare(project)
954+
else:
955+
self._format_project(project, fmt)
956+
918957
for project in self._cloned_projects(args, only_active=not args.all):
919958
if isinstance(project, ManifestProject):
920959
# West doesn't track the relationship between the manifest
921960
# repository and any remote, but users are still interested
922961
# in printing output for comparisons that makes sense.
923962
if self._has_nonempty_status(project):
924963
try:
925-
self.compare(project)
964+
compare_project(project, args.format)
926965
printed_output = True
927966
except subprocess.CalledProcessError:
928967
failed.append(project)
@@ -960,7 +999,7 @@ def has_checked_out_branch(project):
960999
):
9611000
continue
9621001

963-
self.compare(project)
1002+
compare_project(project, args.format)
9641003
printed_output = True
9651004
except subprocess.CalledProcessError:
9661005
failed.append(project)

tests/test_project.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,122 @@ def test_compare(config_tmpdir, west_init_tmpdir):
628628
assert 'mybranch' in cmd('compare --no-ignore-branches')
629629

630630

631+
def test_compare_format_manifest(config_tmpdir, west_init_tmpdir):
632+
# ManifestProject special-casing: empty output, single-line
633+
# machine-readable output without any banner, and the path/sha/url
634+
# keys that are distinct from regular projects -- same conventions
635+
# as 'west list'.
636+
637+
# No dirty projects -> no output at all.
638+
assert cmd('compare --format {name}') == ''
639+
640+
# Dirty the manifest repo.
641+
foo = west_init_tmpdir / 'zephyr' / 'foo'
642+
with open(foo, 'w'):
643+
pass
644+
645+
# The whole point of --format is stable machine-readable output:
646+
# no banner ('=== ...'), exactly one line per dirty project, and
647+
# the ManifestProject is named 'manifest' just like 'west list'.
648+
out = cmd('compare --format {name}')
649+
assert '===' not in out
650+
assert out.splitlines() == ['manifest']
651+
652+
# Path-style keys for the ManifestProject come from manifest.repo_*,
653+
# not project.*. {sha}/{url} are 'N/A' and {revision} is 'HEAD'.
654+
# {groups} is empty. These all match 'west list' behavior.
655+
assert cmd('compare -f {path}').strip() == 'zephyr'
656+
assert cmd('compare -f {abspath}').strip() == str(west_init_tmpdir / 'zephyr')
657+
assert cmd('compare -f {posixpath}').strip() == (Path(west_init_tmpdir / 'zephyr').as_posix())
658+
assert cmd('compare -f {sha}').strip() == 'N/A'
659+
assert cmd('compare -f {url}').strip() == 'N/A'
660+
assert cmd('compare -f {revision}').strip() == 'HEAD'
661+
assert cmd(['compare', '-f', '[{groups}]']).strip() == '[]'
662+
663+
# --exit-code still fires when --format produced any output.
664+
with pytest.raises(SystemExit):
665+
cmd('compare --exit-code --format {name}')
666+
667+
# Cleanup -> empty output again.
668+
os.unlink(foo)
669+
assert cmd('compare --format {name}') == ''
670+
671+
672+
def test_compare_format_projects(config_tmpdir, west_init_tmpdir):
673+
# Regular (non-manifest) projects: every format key, multi-project
674+
# ordering, and interactions with --all / named projects / the
675+
# manifest.group-filter configuration option.
676+
677+
cmd('update')
678+
kconfiglib = west_init_tmpdir / 'subdir' / 'Kconfiglib'
679+
bar = kconfiglib / 'bar'
680+
with open(bar, 'w'):
681+
pass
682+
683+
# {sha} must resolve to a real 40-char hex SHA for a cloned project
684+
# (not 'N/A'): this verifies DelayFormat actually gets invoked when
685+
# the format string references {sha}.
686+
line = cmd(['compare', '-f', '{name}|{sha}']).strip()
687+
name, sha = line.split('|')
688+
assert name == 'Kconfiglib'
689+
assert re.match(r'^[0-9a-f]{40}$', sha)
690+
691+
# All remaining keys.
692+
out = cmd([
693+
'compare',
694+
'-f',
695+
'{name}|{url}|{path}|{abspath}|{posixpath}|{revision}|{groups}',
696+
]).strip()
697+
fields = out.split('|')
698+
assert fields[0] == 'Kconfiglib'
699+
assert fields[1].endswith('/Kconfiglib') # url from test fixture's url-base
700+
# {path} preserves the manifest YAML value verbatim (always forward
701+
# slashes), regardless of OS -- same behavior as 'west list -f {path}'.
702+
assert fields[2] == 'subdir/Kconfiglib'
703+
assert fields[3] == str(kconfiglib)
704+
assert fields[4] == Path(kconfiglib).as_posix()
705+
assert fields[5] == 'zephyr'
706+
assert fields[6] == 'Kconfiglib-group' # set in test fixture's manifest
707+
708+
# Multiple dirty projects produce one line per project, in manifest
709+
# order (Kconfiglib before tagged_repo in this fixture's manifest).
710+
tagged = west_init_tmpdir / 'tagged_repo' / 'baz'
711+
with open(tagged, 'w'):
712+
pass
713+
assert cmd('compare -f {name}').splitlines() == ['Kconfiglib', 'tagged_repo']
714+
os.unlink(tagged)
715+
716+
# An inactive project is skipped by default but appears with --all
717+
# or when named explicitly (same active-project semantics as plain
718+
# 'west compare').
719+
cmd('config manifest.group-filter -- -Kconfiglib-group')
720+
assert cmd('compare -f {name}') == ''
721+
assert cmd('compare --all -f {name}').strip() == 'Kconfiglib'
722+
assert cmd('compare -f {name} Kconfiglib').strip() == 'Kconfiglib'
723+
cmd('config -d manifest.group-filter')
724+
725+
os.unlink(bar)
726+
727+
728+
def test_compare_format_errors(config_tmpdir, west_init_tmpdir):
729+
# Malformed format strings must fail cleanly via self.die() ->
730+
# SystemExit, not with an uncaught KeyError/IndexError traceback.
731+
# Dirty the manifest repo so the format path is actually reached.
732+
foo = west_init_tmpdir / 'zephyr' / 'foo'
733+
with open(foo, 'w'):
734+
pass
735+
736+
# Unknown named key.
737+
with pytest.raises(SystemExit):
738+
cmd('compare -f {bogus}')
739+
740+
# Positional argument (fmt.format() has no positional args).
741+
with pytest.raises(SystemExit):
742+
cmd('compare -f {0}')
743+
744+
os.unlink(foo)
745+
746+
631747
def test_diff(west_init_tmpdir):
632748
# FIXME: Check output
633749

0 commit comments

Comments
 (0)