Skip to content

Show a diff when a file will be or gets modified #1385

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: 3.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyinfra/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class ConfigDefaults:
IGNORE_ERRORS: bool = False
# Shell to use to execute commands
SHELL: str = "sh"
DIFF: bool = False


config_defaults = {key: value for key, value in ConfigDefaults.__dict__.items() if key.isupper()}
Expand Down
15 changes: 15 additions & 0 deletions pyinfra/facts/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,3 +657,18 @@ def process(self, output):
if output and (output[0] == f"{MISSING}{self.path}"):
return None
return output


class FileContents(FactBase):
"""
Returns the contents of a file as a list of lines. Works with both sha1sum and sha1. Returns
``None`` if the file doest not exist.
"""

@override
def command(self, path):
return make_formatted_string_command("cat {0}", QuoteString(path))

@override
def process(self, output):
return output
20 changes: 20 additions & 0 deletions pyinfra/operations/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pathlib import Path
from typing import IO, Any, Union

import click
from jinja2 import TemplateRuntimeError, TemplateSyntaxError, UndefinedError

from pyinfra import host, logger, state
Expand Down Expand Up @@ -46,6 +47,7 @@
Block,
Directory,
File,
FileContents,
FindFiles,
FindInFile,
Flags,
Expand All @@ -62,6 +64,7 @@
MetadataTimeField,
adjust_regex,
ensure_mode_int,
generate_color_diff,
get_timestamp,
sed_delete,
sed_replace,
Expand Down Expand Up @@ -1030,6 +1033,23 @@ def put(
# File exists, check sum and check user/group/mode/atime/mtime if supplied
else:
if not _file_equal(local_sum_path, dest):
if state.config.DIFF:
# Generate diff when contents change
current_contents = host.get_fact(FileContents, path=dest)
if current_contents:
current_lines = [line + "\n" for line in current_contents]
else:
current_lines = []

logger.info(f"\n Will modify {click.style(dest, bold=True)}")

with get_file_io(src, "r") as f:
desired_lines = f.readlines()

for line in generate_color_diff(current_lines, desired_lines):
logger.info(f" {line}")
logger.info("")

yield FileUploadCommand(
local_file,
dest,
Expand Down
2 changes: 1 addition & 1 deletion pyinfra/operations/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def repo(
if branch and host.get_fact(GitBranch, repo=dest) != branch:
git_commands.append("fetch") # fetch to ensure we have the branch locally
git_commands.append("checkout {0}".format(branch))
if branch and branch in host.get_fact(GitTag, repo=dest):
if branch and branch in (host.get_fact(GitTag, repo=dest) or []):
git_commands.append("checkout {0}".format(branch))
is_tag = True
if pull and not is_tag:
Expand Down
36 changes: 35 additions & 1 deletion pyinfra/operations/util/files.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from __future__ import annotations

import difflib
import re
from datetime import datetime, timezone
from enum import Enum
from typing import Callable
from typing import Callable, Generator

import click

from pyinfra.api import QuoteString, StringCommand

Expand Down Expand Up @@ -207,3 +210,34 @@ def adjust_regex(line: str, escape_regex_characters: bool) -> str:
match_line = "{0}.*$".format(match_line)

return match_line


def generate_color_diff(
current_lines: list[str], desired_lines: list[str]
) -> Generator[str, None, None]:
def _format_range_unified(start: int, stop: int) -> str:
beginning = start + 1 # lines start numbering with one
length = stop - start
if length == 1:
return "{}".format(beginning)
if not length:
beginning -= 1 # empty ranges begin at line just before the range
return "{},{}".format(beginning, length)

for group in difflib.SequenceMatcher(None, current_lines, desired_lines).get_grouped_opcodes(2):
first, last = group[0], group[-1]
file1_range = _format_range_unified(first[1], last[2])
file2_range = _format_range_unified(first[3], last[4])
yield "@@ -{} +{} @@".format(file1_range, file2_range)

for tag, i1, i2, j1, j2 in group:
if tag == "equal":
for line in current_lines[i1:i2]:
yield " " + line.rstrip()
continue
if tag in {"replace", "delete"}:
for line in current_lines[i1:i2]:
yield click.style("- " + line.rstrip(), "red")
if tag in {"replace", "insert"}:
for line in desired_lines[j1:j2]:
yield click.style("+ " + line.rstrip(), "green")
12 changes: 12 additions & 0 deletions pyinfra_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ def _print_support(ctx, param, value):
default=False,
help="Don't execute operations on the target hosts.",
)
@click.option(
"--diff",
is_flag=True,
default=False,
help="Show the differences when changing text files and templates.",
)
@click.option(
"-y",
"--yes",
Expand Down Expand Up @@ -267,6 +273,7 @@ def _main(
group_data,
config_filename: str,
dry: bool,
diff: bool,
yes: bool,
limit: Iterable,
no_wait: bool,
Expand Down Expand Up @@ -310,6 +317,7 @@ def _main(
shell_executable,
fail_percent,
yes,
diff,
)
override_data = _set_override_data(
data,
Expand Down Expand Up @@ -549,6 +557,7 @@ def _set_config(
shell_executable,
fail_percent,
yes,
diff,
):
logger.info("--> Loading config...")

Expand Down Expand Up @@ -583,6 +592,9 @@ def _set_config(
if fail_percent is not None:
config.FAIL_PERCENT = fail_percent

if diff:
config.DIFF = True

return config


Expand Down
6 changes: 6 additions & 0 deletions tests/facts/files.FileContents/file.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"arg": "myfile",
"command": "cat myfile",
"output": ["line1", "line2"],
"fact": ["line1", "line2"]
}
6 changes: 6 additions & 0 deletions tests/facts/files.FileContents/no_file.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"arg": ["test"],
"command": "cat test",
"output": null,
"fact": null
}
3 changes: 3 additions & 0 deletions tests/operations/files.put/different_remote.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
},
"files.Sha1File": {
"path=/home/somefile.txt": "nowt"
},
"files.FileContents": {
"path=/home/somefile.txt": []
}
},
"commands": [
Expand Down
7 changes: 5 additions & 2 deletions tests/operations/files.put/fallback_md5.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
},
"files.Md5File": {
"path=/home/somefile.txt": "nowt"
}
},
"files.FileContents": {
"path=/home/somefile.txt": null
},
},
"commands": [
["upload", "/somefile.txt", "/home/somefile.txt"]
]
}
}
7 changes: 5 additions & 2 deletions tests/operations/files.put/fallback_sha256.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@
},
"files.Sha256File": {
"path=/home/somefile.txt": "nowt"
}
},
"files.FileContents": {
"path=/home/somefile.txt": null
},
},
"commands": [
["upload", "/somefile.txt", "/home/somefile.txt"]
]
}
}
3 changes: 3 additions & 0 deletions tests/operations/server.user/keys_delete.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
"files.Sha256File": {
"path=homedir/.ssh/authorized_keys": null
},
"files.FileContents": {
"path=homedir/.ssh/authorized_keys": null
},
"server.Groups": {}
},
"commands": [
Expand Down
1 change: 1 addition & 0 deletions tests/test_cli/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,5 +188,6 @@ def test_deploy_operation_direct(self):
debug_all=False,
debug_operations=False,
config_filename="config.py",
diff=True,
)
assert e.args == (0,)
10 changes: 10 additions & 0 deletions tests/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,16 @@ def noop(self, description):
def get_temp_filename(*args, **kwargs):
return "_tempfile_"

def get_file(
self,
remote_filename,
filename_or_io,
remote_temp_filename=None,
print_output=False,
*arguments,
):
return True

@staticmethod
def _get_fact_key(fact_cls):
return "{0}.{1}".format(fact_cls.__module__.split(".")[-1], fact_cls.__name__)
Expand Down