Skip to content

Commit 514fd75

Browse files
committed
tools: add diff-config to compare two config files
diff-config CONFIG1 CONFIG2 prints a unified diff between two resolved configs, ignoring password hashes; TOML by default, --json for JSON representation. Exits 0 when the configs are identical, 1 otherwise. Configs are loaded without value/env overrides so both sides are compared on their own contents. Signed-off-by: Gaëtan Lehmann <gaetan.lehmann@vates.tech>
1 parent 57ce493 commit 514fd75

3 files changed

Lines changed: 69 additions & 0 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ To customize settings for your environment:
211211
- `tools.py dump-config` / `migrate-data-py` print only the values that differ
212212
from the base `config.toml`; add `--all` to include the values that are the
213213
same too.
214+
- `tools.py diff-config CONFIG1 CONFIG2` — compare two config files and print
215+
a unified diff (password hashes are ignored); exit code 0 when identical,
216+
1 when they differ.
214217

215218
The `--config` flag is optional. When not specified, the `XCPNG_CONFIG` env var
216219
is used if set. When neither is set:

lib/config_dump.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
from __future__ import annotations
44

5+
import difflib
6+
import json
7+
58
import tomli_w
69
from pygments import highlight
710
from pygments.formatters import TerminalFormatter
@@ -114,3 +117,29 @@ def render_toml(config: ConfigDict, with_schema: bool = True) -> str:
114117
if with_schema:
115118
data = {"$schema": "./config-schema.json", **data}
116119
return tomli_w.dumps(data, multiline_strings=True)
120+
121+
122+
def config_diff(
123+
config_a: ConfigDict,
124+
config_b: ConfigDict,
125+
name_a: str = "a",
126+
name_b: str = "b",
127+
as_json: bool = False,
128+
) -> str:
129+
"""Return a unified diff between two configs, ignoring password hashes.
130+
131+
Empty string when the configs are identical (modulo $6$... password hashes).
132+
"""
133+
norm_a = _strip_password_hashes(config_a)
134+
norm_b = _strip_password_hashes(config_b)
135+
if as_json:
136+
text_a = json.dumps(norm_a, indent=2, ensure_ascii=False, sort_keys=True)
137+
text_b = json.dumps(norm_b, indent=2, ensure_ascii=False, sort_keys=True)
138+
else:
139+
text_a = render_toml(norm_a, with_schema=False)
140+
text_b = render_toml(norm_b, with_schema=False)
141+
return "".join(difflib.unified_diff(
142+
text_a.splitlines(keepends=True),
143+
text_b.splitlines(keepends=True),
144+
fromfile=name_a, tofile=name_b,
145+
))

lib/tools/cli.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ def _command_migrate(args: argparse.Namespace) -> int:
6565
include_defaults=args.all)
6666

6767

68+
def _command_diff_config(args: argparse.Namespace) -> int:
69+
from lib.config_dump import config_diff
70+
71+
name_a = str(args.config1)
72+
name_b = str(args.config2)
73+
config_a = load_config(override=args.config1, apply_value_overrides=False).model_dump(by_alias=True)
74+
config_b = load_config(override=args.config2, apply_value_overrides=False).model_dump(by_alias=True)
75+
diff = config_diff(config_a, config_b, name_a, name_b, as_json=args.json)
76+
print(diff, end="")
77+
return 1 if diff else 0
78+
79+
6880
def _command_dump_config(args: argparse.Namespace) -> int:
6981
import json
7082

@@ -296,6 +308,31 @@ def _add_config_value_option(target: argparse.ArgumentParser) -> None:
296308
)
297309
subparser_cmd_dump.set_defaults(func=_command_dump_config)
298310

311+
# subparser - command: diff-config
312+
subparser_cmd_diff = subparsers.add_parser(
313+
name="diff-config",
314+
parents=[common_parser],
315+
description="Compare two config files and print their differences",
316+
help="Compare two config files and print their differences",
317+
)
318+
subparser_cmd_diff.add_argument(
319+
"config1",
320+
metavar="CONFIG1",
321+
help="First config file (.toml path or short name)",
322+
)
323+
subparser_cmd_diff.add_argument(
324+
"config2",
325+
metavar="CONFIG2",
326+
help="Second config file (.toml path or short name)",
327+
)
328+
subparser_cmd_diff.add_argument(
329+
"--json",
330+
action="store_true",
331+
default=False,
332+
help="Compare the JSON representations instead of TOML",
333+
)
334+
subparser_cmd_diff.set_defaults(func=_command_diff_config)
335+
299336
args = parser.parse_args()
300337

301338
if args.debug:

0 commit comments

Comments
 (0)