diff --git a/copier_update/github.py b/copier_update/github.py index 261bf58..e95be0e 100644 --- a/copier_update/github.py +++ b/copier_update/github.py @@ -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", diff --git a/copier_update/tests/test_github.py b/copier_update/tests/test_github.py index e214a38..bd4a33d 100644 --- a/copier_update/tests/test_github.py +++ b/copier_update/tests/test_github.py @@ -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"] diff --git a/copier_update/tests/test_updater.py b/copier_update/tests/test_updater.py index 2f0b475..fbabe7c 100644 --- a/copier_update/tests/test_updater.py +++ b/copier_update/tests/test_updater.py @@ -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): @@ -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="") @@ -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) @@ -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 diff --git a/copier_update/updater.py b/copier_update/updater.py index 1321710..467610d 100644 --- a/copier_update/updater.py +++ b/copier_update/updater.py @@ -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: @@ -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 @@ -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) @@ -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: