Skip to content
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
34 changes: 30 additions & 4 deletions .github/scripts/backport.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,24 +74,38 @@ def validate_candidate(
candidate: str | None,
*,
require_exact_patch: bool,
resolution_report: Path | None = None,
) -> dict[str, object]:
"""Validate a committed candidate or the currently staged conflict resolution."""
"""Validate a committed candidate or the currently staged conflict resolution.

A conflict-resolution report may declare paths kept at the release version and extra paths edited to carry the
source intent into release's layout; both are listed for review in the draft backport PR.
"""
source_paths = changed_paths(source_parent, source)
kept_paths, extra_paths = _resolution_exceptions(resolution_report)
if candidate is None:
_validate_staged_worktree()
candidate_paths = changed_paths(target, cached=True)
else:
candidate_paths = changed_paths(target, candidate)

unexpected_paths = candidate_paths - source_paths
unexpected_paths = candidate_paths - source_paths - extra_paths
if unexpected_paths:
raise BackportValidationError(
"the candidate changes files outside the original PR: " + _format_paths(unexpected_paths)
)

missing_paths = source_paths - candidate_paths
changed_kept_paths = {path for path in kept_paths if _tree_entry(target, path) != _candidate_entry(candidate, path)}
if changed_kept_paths:
raise BackportValidationError(
"the candidate changes paths declared as kept at the release version: " + _format_paths(changed_kept_paths)
)
declared_paths = kept_paths | extra_paths
nonmatching_missing_paths = {
path for path in missing_paths if _tree_entry(source, path) != _candidate_entry(candidate, path)
path
for path in missing_paths - declared_paths
if _tree_entry(source, path) != _candidate_entry(candidate, path)
Comment on lines 98 to +108

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 New checks lack regression tests

The new kept-path and extra-path exceptions have no automated tests. The reported offline checks cover successful historical resolutions, but not the checks that must still reject a bad candidate. Future changes could weaken these checks or reject valid backports unnoticed.

Add tests using temporary Git repositories to prove that:

  • Kept paths preserve both content and mode.
  • Undeclared extra paths fail.
  • A declared extra path can restore a cleanly merged file.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

}
if nonmatching_missing_paths:
raise BackportValidationError(
Expand All @@ -110,7 +124,9 @@ def validate_candidate(
return {
"candidate_paths": sorted(candidate_paths),
"exact_patch": require_exact_patch,
"missing_paths_already_present": sorted(missing_paths),
"extra_paths": sorted(extra_paths),
"kept_paths": sorted(kept_paths),
"missing_paths_already_present": sorted(missing_paths - declared_paths),
"source_paths": sorted(source_paths),
}

Expand All @@ -133,6 +149,14 @@ def _expected_pr_paths(path: Path) -> set[str]:
return paths


def _resolution_exceptions(path: Path | None) -> tuple[set[str], set[str]]:
"""Read the kept and extra paths declared by the conflict resolver."""
if path is None:
return set(), set()
report = json.loads(path.read_text(encoding="utf-8"))
return set(report["kept_paths"]), set(report["extra_paths"])


def _validate_staged_worktree():
"""Require a fully resolved, fully staged conflict-resolution worktree."""
unmerged = _run(["git", "diff", "--name-only", "--diff-filter=U", "-z"], text=False).stdout
Expand Down Expand Up @@ -242,6 +266,7 @@ def _create_parser() -> argparse.ArgumentParser:
candidate.add_argument("--target", required=True)
candidate.add_argument("--candidate")
candidate.add_argument("--exact_patch", action="store_true")
candidate.add_argument("--resolution_report", type=Path, help="Paths report written by the conflict resolver.")
return parser


Expand All @@ -264,6 +289,7 @@ def main() -> int:
args.target,
args.candidate,
require_exact_patch=args.exact_patch,
resolution_report=args.resolution_report,
)
print(json.dumps(result, sort_keys=True))
except (BackportValidationError, subprocess.CalledProcessError, ValueError) as error:
Expand Down
Loading
Loading