Skip to content

Commit cf2c160

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 cd459fb commit cf2c160

3 files changed

Lines changed: 68 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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
import difflib
6+
import json
57
import re
68

79
from typing import Any
@@ -220,3 +222,29 @@ def write_section(
220222
for key in root_keys:
221223
result.append(f"{key} = {format_toml_value(config[key])}")
222224
return "\n".join(result + lines) + "\n"
225+
226+
227+
def config_diff(
228+
config_a: dict[str, Any],
229+
config_b: dict[str, Any],
230+
name_a: str = "a",
231+
name_b: str = "b",
232+
as_json: bool = False,
233+
) -> str:
234+
"""Return a unified diff between two configs, ignoring password hashes.
235+
236+
Empty string when the configs are identical (modulo $6$... password hashes).
237+
"""
238+
norm_a = _strip_password_hashes(config_a)
239+
norm_b = _strip_password_hashes(config_b)
240+
if as_json:
241+
text_a = json.dumps(norm_a, indent=2, ensure_ascii=False, sort_keys=True)
242+
text_b = json.dumps(norm_b, indent=2, ensure_ascii=False, sort_keys=True)
243+
else:
244+
text_a = render_toml(norm_a, with_schema=False)
245+
text_b = render_toml(norm_b, with_schema=False)
246+
return "".join(difflib.unified_diff(
247+
text_a.splitlines(keepends=True),
248+
text_b.splitlines(keepends=True),
249+
fromfile=name_a, tofile=name_b,
250+
))

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()
74+
config_b = load_config(override=args.config2, apply_value_overrides=False).model_dump()
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

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

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

302339
if args.debug:

0 commit comments

Comments
 (0)