Skip to content

Commit ed1f1df

Browse files
committed
Show a diff when a file will be or gets modified
1 parent ab96666 commit ed1f1df

File tree

3 files changed

+62
-2
lines changed

3 files changed

+62
-2
lines changed

pyinfra/operations/files.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@
1010
import traceback
1111
from datetime import timedelta
1212
from fnmatch import fnmatch
13-
from io import StringIO
13+
from io import BytesIO, StringIO
1414
from pathlib import Path
1515
from typing import IO, Any, Union
1616

17+
import click
1718
from jinja2 import TemplateRuntimeError, TemplateSyntaxError, UndefinedError
1819

1920
from pyinfra import host, logger, state
@@ -59,6 +60,7 @@
5960
from .util.files import (
6061
adjust_regex,
6162
ensure_mode_int,
63+
generate_color_diff,
6264
get_timestamp,
6365
sed_delete,
6466
sed_replace,
@@ -915,6 +917,20 @@ def put(
915917

916918
# Check sha1sum, upload if needed
917919
if local_sum != remote_sum:
920+
current_contents = BytesIO()
921+
922+
# Generate diff when contents change
923+
host.get_file(dest, current_contents)
924+
current_lines = current_contents.getvalue().decode("utf-8").splitlines(keepends=True)
925+
logger.info(f"\n Will modify {click.style(dest, bold=True)}")
926+
927+
with get_file_io(src, "r") as f:
928+
desired_lines = f.readlines()
929+
930+
for line in generate_color_diff(current_lines, desired_lines):
931+
logger.info(f" {line}")
932+
logger.info("")
933+
918934
yield FileUploadCommand(
919935
local_file,
920936
dest,

pyinfra/operations/util/files.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from __future__ import annotations
22

3+
import difflib
34
import re
45
from datetime import datetime
5-
from typing import Callable
6+
from typing import Callable, Generator
7+
8+
import click
69

710
from pyinfra.api import QuoteString, StringCommand
811

@@ -173,3 +176,34 @@ def adjust_regex(line: str, escape_regex_characters: bool) -> str:
173176
match_line = "{0}.*$".format(match_line)
174177

175178
return match_line
179+
180+
181+
def generate_color_diff(
182+
current_lines: list[str], desired_lines: list[str]
183+
) -> Generator[str, None, None]:
184+
def _format_range_unified(start: int, stop: int) -> str:
185+
beginning = start + 1 # lines start numbering with one
186+
length = stop - start
187+
if length == 1:
188+
return "{}".format(beginning)
189+
if not length:
190+
beginning -= 1 # empty ranges begin at line just before the range
191+
return "{},{}".format(beginning, length)
192+
193+
for group in difflib.SequenceMatcher(None, current_lines, desired_lines).get_grouped_opcodes(2):
194+
first, last = group[0], group[-1]
195+
file1_range = _format_range_unified(first[1], last[2])
196+
file2_range = _format_range_unified(first[3], last[4])
197+
yield "@@ -{} +{} @@".format(file1_range, file2_range)
198+
199+
for tag, i1, i2, j1, j2 in group:
200+
if tag == "equal":
201+
for line in current_lines[i1:i2]:
202+
yield " " + line.rstrip()
203+
continue
204+
if tag in {"replace", "delete"}:
205+
for line in current_lines[i1:i2]:
206+
yield click.style("- " + line.rstrip(), "red")
207+
if tag in {"replace", "insert"}:
208+
for line in desired_lines[j1:j2]:
209+
yield click.style("+ " + line.rstrip(), "green")

tests/util.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,16 @@ def noop(self, description):
187187
def get_temp_filename(*args, **kwargs):
188188
return "_tempfile_"
189189

190+
def get_file(
191+
self,
192+
remote_filename,
193+
filename_or_io,
194+
remote_temp_filename=None,
195+
print_output=False,
196+
*arguments,
197+
):
198+
return True
199+
190200
@staticmethod
191201
def _get_fact_key(fact_cls):
192202
return "{0}.{1}".format(fact_cls.__module__.split(".")[-1], fact_cls.__name__)

0 commit comments

Comments
 (0)