Skip to content
Closed
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
4 changes: 1 addition & 3 deletions .github/workflows/spell-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ jobs:
- name: Fetch PR base
run: |
set -e
git fetch origin "${{ github.base_ref }}" --depth=1 || \
git fetch --unshallow origin "${{ github.base_ref }}" || \
git fetch origin "${{ github.base_ref }}"
git fetch origin "${{ github.base_ref }}"

- name: Compute changed lines
id: diff
Expand Down
10 changes: 9 additions & 1 deletion scripts/ci/changed_lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,20 @@ def run_git_diff(
*,
name_only: bool,
) -> str:
"""Run git diff for the requested pathspecs and return stdout."""
"""Run git diff for the requested pathspecs and return stdout.

``--merge-base`` diffs the requested base against its merge-base with
HEAD, so only the branch's own changes are reported. Diffing against a
live base ref directly would include reversed base drift (lines that
main changed or removed after the branch was created), which made the
spell-check job flag words from main's history on unrelated PRs.
"""
command = [
"git",
"diff",
"--no-ext-diff",
"--diff-filter=ACMRT",
"--merge-base",
]
if name_only:
command.append("--name-only")
Expand Down
88 changes: 88 additions & 0 deletions tests/ci/test_changed_lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
from __future__ import annotations

import importlib.util
import subprocess
import sys
import tempfile
from pathlib import Path
from unittest.mock import patch


SCRIPT_PATH = Path(__file__).resolve().parents[2] / "scripts" / "ci" / "changed_lines.py"
Expand Down Expand Up @@ -53,3 +57,87 @@ def test_extension_pathspecs_are_normalized_for_git_diff() -> None:

assert extensions == [".md", ".txt", ".py"]
assert changed_lines.pathspecs_for_extensions(extensions) == ["*.md", "*.txt", "*.py"]


def _capture_git_command(name_only: bool) -> list[str]:
captured: dict[str, list[str]] = {}

def fake_run(command, **kwargs):
captured["command"] = command
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")

with patch.object(changed_lines.subprocess, "run", side_effect=fake_run):
changed_lines.run_git_diff(Path("."), "origin/main", ["*.md"], name_only=name_only)
return captured["command"]


def test_run_git_diff_diffs_against_merge_base() -> None:
command = _capture_git_command(name_only=False)

assert "--merge-base" in command
assert command.index("--merge-base") < command.index("origin/main")


def test_run_git_diff_name_only_also_diffs_against_merge_base() -> None:
command = _capture_git_command(name_only=True)

assert "--merge-base" in command
assert "--name-only" in command


def test_added_lines_exclude_base_drift() -> None:
with tempfile.TemporaryDirectory() as tmp:
repo = Path(tmp)
git = ["git", "-C", str(repo)]

def run(*args: str, **kwargs: str) -> None:
subprocess.run([*git, *args], check=True, capture_output=True, **kwargs)

run("init", "-q")
run("config", "user.email", "test@example.com")
run("config", "user.name", "Test")
run("config", "commit.gpgsign", "false")
run("checkout", "-q", "-b", "main")

words = repo / "words.txt"
words.write_text("dorny\n", encoding="utf-8")
run("add", "words.txt")
run("commit", "-q", "-m", "main v1")

run("checkout", "-q", "-b", "feature")
words.write_text("dorny\nfancytoken\n", encoding="utf-8")
run("add", "words.txt")
run("commit", "-q", "-m", "feature change")

feature_head = subprocess.run(
[*git, "rev-parse", "HEAD"], check=True, capture_output=True, text=True
).stdout.strip()

run("checkout", "-q", "main")
words.write_text("dorny_renamed\n", encoding="utf-8")
run("add", "words.txt")
run("commit", "-q", "-m", "main v2")

new_main = subprocess.run(
[*git, "rev-parse", "HEAD"], check=True, capture_output=True, text=True
).stdout.strip()

run("update-ref", "refs/remotes/origin/main", new_main)
run("checkout", "-q", feature_head)

output = repo / "added-lines.txt"
argv = [
"changed_lines.py",
"--repo", str(repo),
"--base", "origin/main",
"--extensions", ".txt",
"--mode", "added-lines",
"--output", str(output),
]
with patch.object(sys, "argv", argv):
rc = changed_lines.main()
assert rc == 0

added = output.read_text(encoding="utf-8")
assert "fancytoken" in added
assert "dorny" not in added
Loading