Skip to content
Merged
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
2 changes: 1 addition & 1 deletion copier_update/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def create_pull_request(
) -> str:
body = "Automated update from the repository's Copier template."
if invalid:
body += "\n\n> [!WARNING]\n> Copier generated changes that fail `git diff --check`. Review them before merging."
body += "\n\n> [!WARNING]\n> Copier left unresolved conflict markers. Resolve them before merging."
response = self._request(
"POST",
f"/repos/{repository.full_name}/pulls",
Expand Down
2 changes: 1 addition & 1 deletion copier_update/tests/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,4 @@ def test_marks_invalid_update_pull_request():
url = client.create_pull_request(repository, "token", "copier-update", "Update from Copier", invalid=True)

assert url == "https://github.com/owner/repository/pull/1"
assert "fail `git diff --check`" in session.requests[0][2]["json"]["body"]
assert "Copier left unresolved conflict markers" in session.requests[0][2]["json"]["body"]
57 changes: 54 additions & 3 deletions copier_update/tests/test_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pytest

from copier_update.github import GitHubAppClient, Installation, Repository
from copier_update.updater import Updater
from copier_update.updater import InvalidUpdateError, Updater


class FakeClient(GitHubAppClient):
Expand Down Expand Up @@ -73,7 +73,7 @@ def _run(self, command, *, cwd=None, token=None, capture_output=False):
self.commands.append((command, token))
if command[:2] == ["git", "clone"]:
Path(command[-1]).mkdir()
if command == ["git", "diff", "--check"] and self.invalid_diff:
if command[-3:] == ["diff", "--cached", "--check"] and self.invalid_diff:
raise subprocess.CalledProcessError(2, command, output=self.invalid_diff)
stdout = self.status if command[:3] == ["git", "status", "--porcelain"] else ""
return subprocess.CompletedProcess(command, 0, stdout=stdout, stderr="")
Expand Down Expand Up @@ -239,7 +239,7 @@ def test_repository_update_opens_pull_request_when_copier_leaves_conflicts():
invalid_diff="README.md:1: leftover conflict marker\n",
)

with pytest.raises(RuntimeError, match="Opened .* with changes that fail git diff --check"):
with pytest.raises(RuntimeError, match="Opened .* with unresolved conflict markers"):
updater.update_repository(repository, ".copier-answers.yaml", "repository-token")

assert any(command[:2] == ["git", "push"] for command, _ in updater.commands)
Expand All @@ -251,3 +251,54 @@ def test_repository_update_opens_pull_request_when_copier_leaves_conflicts():
True,
)
]


@pytest.mark.parametrize(
"contents,conflicted",
[
("Updated text\n", False),
("Updated text \n\n", False),
(" \tUpdated text\n", False),
("<<<<<<< before\nOld text\n=======\nNew text\n>>>>>>> after\n", True),
],
ids=["clean", "trailing-whitespace", "space-before-tab", "conflict-markers"],
)
@pytest.mark.parametrize("new_file", [False, True], ids=["modified-file", "new-file"])
def test_repository_update_detects_only_conflicts_with_real_git(tmp_path, monkeypatch, contents, conflicted, new_file):
source = tmp_path / "source"
source.mkdir()
subprocess.run(["git", "init", "--initial-branch=main", str(source)], check=True, capture_output=True)
(source / "README.md").write_text("Original text\n")
subprocess.run(["git", "add", "."], cwd=source, check=True)
subprocess.run(
["git", "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "Initial commit"],
cwd=source,
check=True,
capture_output=True,
)
repository = Repository(id=20, full_name="owner/repository", default_branch="main")
client = FakeClient([repository])
updater = Updater(client)
run = updater._run

def run_locally(command, *, cwd=None, token=None, capture_output=False):
command = list(command)
if command[:2] == ["git", "clone"]:
command[-2] = str(source)
elif command[0] == "copier":
assert cwd is not None
(cwd / ("new.md" if new_file else "README.md")).write_text(contents)
return subprocess.CompletedProcess(command, 0)
elif command[:2] == ["git", "push"]:
return subprocess.CompletedProcess(command, 0)
return run(command, cwd=cwd, capture_output=capture_output)

monkeypatch.setattr(updater, "_run", run_locally)

if conflicted:
with pytest.raises(InvalidUpdateError):
updater.update_repository(repository, ".copier-answers.yaml", "token")
else:
assert updater.update_repository(repository, ".copier-answers.yaml", "token")

assert client.pull_requests[0][-1] is conflicted
21 changes: 13 additions & 8 deletions copier_update/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class UpdateSummary:


class InvalidUpdateError(RuntimeError):
"""Raised after opening an update pull request containing invalid changes."""
"""Raised after opening an update pull request containing conflict markers."""


class Updater:
Expand Down Expand Up @@ -141,12 +141,6 @@ def update_repository(self, repository: Repository, answers_file: str, token: st
token=token,
)
self._run(["copier", "update", "-A", "-f", "-a", answers_file], cwd=repository_path, token=token)
invalid_changes = ""
try:
self._run(["git", "diff", "--check"], cwd=repository_path, capture_output=True)
except subprocess.CalledProcessError as error:
invalid_changes = (error.stdout or error.stderr or "").strip()

if not self._has_meaningful_changes(repository_path):
LOGGER.info("No update available for %s", repository.full_name)
return False
Expand All @@ -156,6 +150,17 @@ def update_repository(self, repository: Repository, answers_file: str, token: st
self._run(["git", "config", "user.email", "python-templates-copier-update[bot]@users.noreply.github.com"], cwd=repository_path)
self._run(["git", "checkout", "-b", branch], cwd=repository_path)
self._run(["git", "add", "--all"], cwd=repository_path)
invalid_changes = ""
try:
self._run(
["git", "-c", "core.whitespace=-trailing-space,-space-before-tab", "diff", "--cached", "--check"],
cwd=repository_path,
capture_output=True,
)
except subprocess.CalledProcessError as error:
if error.returncode != 2:
raise
invalid_changes = (error.stdout or error.stderr or "").strip()
self._run(["git", "commit", "-s", "-m", title], cwd=repository_path)
self._run(["git", "push", "origin", branch], cwd=repository_path, token=token)

Expand All @@ -168,7 +173,7 @@ def update_repository(self, repository: Repository, answers_file: str, token: st
)
LOGGER.info("Opened %s", pull_request_url)
if invalid_changes:
raise InvalidUpdateError(f"Opened {pull_request_url} with changes that fail git diff --check:\n{invalid_changes}")
raise InvalidUpdateError(f"Opened {pull_request_url} with unresolved conflict markers:\n{invalid_changes}")
return True

def _has_meaningful_changes(self, repository_path: Path) -> bool:
Expand Down