feat: add colorized option for highstate output - #68982
Conversation
10dc1af to
a45beb9
Compare
a45beb9 to
8dea3a0
Compare
9f479f5 to
b5f906d
Compare
|
Thanks for putting this together. Having properly colorized unified diffs in the highstate output is a massive quality-of-life improvement for troubleshooting long state files. However, looking closely at the implementation in salt/output/highstate.py, there are a few architectural gaps and potential regressions that we should address before this gets merged:
colors = salt.utils.color.get_colors(__opts__.get("color"), __opts__.get("color_theme"))Because the highstate outputter runs these utility functions iteratively across every single state return block, pulling the entire terminal ANSI mapping from scratch adds unnecessary performance overhead (especially on large runs with hundreds of states or multiple minions). The Fix: The main entry point
# Does not go through the Salt loader, so nested_indent is guaranteed to apply correctly
regardless of Salt version.Bypassing the standard nested outputter breaks standard user configuration setups (like
Suggested Refactor Strategy Here is how you can refactor it cleanly:
def _render_diff(diff_str, indent, colors):
"""
Render a unified diff string with per-line ANSI colorization.
"""
prefix = " " * indent
if __opts__.get("color") is False:
return "\n".join(prefix + line for line in diff_str.splitlines())
GREEN = str(colors["GREEN"])
ENDC = str(colors["ENDC"])
RED = str(colors["RED"])
CYAN = str(colors["CYAN"])
WHITE = str(colors["LIGHT_GRAY"])
LIGHT_RED = str(colors["LIGHT_RED"])
result = []
for line in diff_str.splitlines():
if line.startswith("---"):
color = LIGHT_RED
elif line.startswith("+++"):
color = GREEN
elif line.startswith("@@"):
color = CYAN
elif line.startswith("+"):
color = GREEN
elif line.startswith("-"):
color = RED
else:
color = WHITE
result.append(f"{prefix}{color}{line}{ENDC}")
return "\n".join(result)
By running the standard nested outputter first, we capture the baseline format. We then parse out the exact block that belongs to the string-based diff using a regex pattern that respects whatever indentation depth was generated: def _nested_changes_colorized(changes, colors):
"""
Print the changes data with diff colorization by intercepting the standard
nested outputter payload.
"""
# 1. Get the baseline structure from the actual nested outputter
nested_output = salt.output.out_format(changes, "nested", __opts__, nested_indent=14)
# If there's no diff string to format, return it standard
if not isinstance(changes, dict) or not isinstance(changes.get("diff"), str):
return "\n" + nested_output
# 2. Extract the raw diff block to figure out its runtime indentation dynamically
# This matches 'diff:' and captures everything up until the next unindented sibling key
diff_pattern = re.compile(r"^(\s*)diff:\n([\s\S]*?)(?=(?:^\s*[\w\d_.-]+:|\Z))", re.MULTILINE)
match = diff_pattern.search(nested_output)
if match:
key_indent_str, raw_diff_content = match.groups()
key_indent = len(key_indent_str)
val_indent = key_indent + 4
# Colorize just the diff payload line-by-line
colorized_diff = _render_diff(changes["diff"], val_indent, colors)
# Reconstruct the block with the colorized diff string
CYAN = str(colors["CYAN"])
ENDC = str(colors["ENDC"])
colorized_block = f"{key_indent_str}{CYAN}diff:{ENDC}\n{colorized_diff}\n"
# Swap the plain text block out for the colorized block safely
nested_output = diff_pattern.sub(colorized_block, nested_output).rstrip()
return "\n" + nested_output
if colorize:
return True, _nested_changes_colorized(changes, colors)This keeps the engine highly performant, preserves standard loader behaviors for the structural output configuration, and eliminates mutable dictionary state manipulations. Let me know what you think! |
|
@twangboy I changed a few things around and now it works even with Can you review and let me know what you think of this version? |
Colorize unified diffs in the highstate outputter: added lines are green, removed lines are red, hunk headers are cyan, and context lines are gray. `newfile:` and other change keys are rendered with the same indentation as the current functionality provides.
- Black reformat the lines.extend() generator call in _render_changes_dict - Replace U+2192 arrows in _render_diff docstring with ASCII -> so the cp1252 docstring-encoding hook passes (these characters break salt-run -d and salt -d on Windows where stdout uses the locale-default encoding)
Replace the per-sentinel regex compile/scan loop in _nested_changes_colorized with a single combined pattern that matches every __COLORDIFF_N__ sentinel in one sweep, looking up each raw diff by its captured id. Avoids repeated full-string scans (and potential layout clobbering) when a state such as file.recurse produces multiple diffs, per PR review feedback.
- Hoist the sentinel template and match pattern to module-level constants (_COLORDIFF_SENTINEL / _COLORDIFF_RE) instead of building the f-string in _assign_sentinel and recompiling the regex on every _nested_changes_colorized call, keeping the placeholder format defined in one place so producer and consumer can't drift apart. - Neutralize embedded terminal escape sequences in the (untrusted) diff content in _render_diff before applying the colorization escape codes, matching the nested outputter's strip_colors behavior; the colorized path had been bypassing that protection. - Render the +++ to-file header in bold green (LIGHT_GREEN) to mirror the bold red --- from-file header, and update the docstring, changelog, and CLI docs to match. - Drop the _color handling from the orchestration branch of _format_changes, which is never reached (orchestration=True is not passed anywhere). - Add a test for escape-sequence neutralization and asserts the file-header colors in the shared helper. - test_highstate.py used re.sub in _strip_ansi without importing re. - _format_host built its color mapping straight from __opts__["color"], so a value of None produced empty color codes and silently disabled diff colorization. Treat None as "auto -> on" to fix the check in _render_diff.
|
Shoot, I did another run-through with a few more tweaks, can you take one more look please? My apologies for the force-push, I squashed some disparate commits into the most recent one and hadn't realized you already approved. |
What does this PR do?
Adds optional colorization for diff chunks in the highstate output module.
disclaimer: I used AI to assist me in creating and testing this code, but it works appropriately as far as I can tell. The original version I had wasn't nearly as clean and smooth as this ended up.
I had some trouble getting all the pre-commit hooks to play nice on my Fedora laptop but I'm fairly sure that a workflow run will succeed. I'll obviously make changes if that's not the case.
What issues does this PR fix or reference?
I didn't submit an issue for this; I did however mention it a while back in Discord.
This is me attempting to upstream the polished final result.
Previous Behavior
All diff output in highstate output module was green regardless of additions/deletions
New Behavior
If specified, the full_color, changes_color and such output modes will show a colorized diff in the state return output (such as for
file.managed). The results are colorized like so:Merge requirements satisfied?
[NOTICE] Bug fixes or features added to Salt require tests.
Commits signed with GPG?
No, but signed with ssh key (so it looks like GitHub itself re-signed it with gpg key ID
B5690EEEBB952194)