From 746dee530f53b02b7b09f176ff3ba99c571a4c60 Mon Sep 17 00:00:00 2001
From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com>
Date: Wed, 21 Oct 2026 10:10:41 +0200
Subject: [PATCH] fix: compare a fork clone with the project main
A fork's clone has origin alone. Pushed to the fork's main, the commit
left origin/main...HEAD empty and --changed reported nothing with rc 0.
When no remote names the project, its main is fetched and compared, and
a fetch that fails is an error.
---
scripts/validate_pr.py | 60 ++++++++++++++++++++++++++++++----
tests/test_refresh_failures.py | 45 +++++++++++++++++++++++++
2 files changed, 99 insertions(+), 6 deletions(-)
diff --git a/scripts/validate_pr.py b/scripts/validate_pr.py
index b1a922b3..393c2e6a 100644
--- a/scripts/validate_pr.py
+++ b/scripts/validate_pr.py
@@ -20,6 +20,7 @@ from __future__ import annotations
import argparse
import json
import os
+import re
import subprocess
import sys
@@ -413,20 +414,67 @@ def _git_lines(*args: str) -> list[str] | None:
return [line for line in result.stdout.splitlines() if line]
+PROJECT_URLS = (
+ "https://github.com/Abdess/retrobios",
+ "https://github.com/Abdess/retroarch_system",
+)
+
+
+def _repo_key(url: str) -> str:
+ """owner/name of a clone URL, whatever its scheme."""
+ path = url.strip().rstrip("/").removesuffix(".git")
+ return "/".join(re.split(r"[/:]", path)[-2:]).casefold()
+
+
+def _project_main_line() -> str | None:
+ """The project's main line, fetched when no remote of this clone names it.
+
+ A fork's clone has origin alone. Once the commit is pushed to the fork's
+ main, origin/main is HEAD and the diff against it was empty, so --changed
+ found nothing and exited 0. None leaves the upstream/ and origin/ lines
+ in charge: a remote of that name names the project, or the clone has no
+ remote at all and only its uncommitted files can be read.
+ """
+ remotes = _git_lines("remote") or []
+ if not remotes:
+ return None
+ project = {_repo_key(url) for url in PROJECT_URLS}
+ for remote in remotes:
+ urls = _git_lines("remote", "get-url", "--all", remote) or []
+ if any(_repo_key(url) in project for url in urls):
+ return None if remote in ("upstream", "origin") else f"{remote}/main"
+ fetched = subprocess.run(
+ ["git", "fetch", "--quiet", "--no-tags", PROJECT_URLS[0], "main"],
+ capture_output=True, text=True, check=False,
+ )
+ if fetched.returncode != 0:
+ raise RuntimeError(
+ f"no remote of this clone is {PROJECT_URLS[0]}, and fetching its "
+ f"main failed: {fetched.stderr.strip()}"
+ )
+ return "FETCH_HEAD"
+
+
def get_changed_files() -> list[str]:
"""BIOS files this checkout changes against the project's main line.
- Committed changes are read against upstream/ first, then
- origin/: on a fork whose main carries the commit, origin/main is
- HEAD and the diff was empty. Uncommitted and untracked files count too,
+ Committed changes are read against the project's main line: the remote
+ that names the project (upstream/ first, then origin/), or
+ the project's main fetched when no remote does. Uncommitted and
+ untracked files count too,
since CONTRIBUTING has the file copied into bios/ before any commit.
A git that fails outright is an error, never "nothing changed".
"""
changed: list[str] = []
compared = False
- for remote in ("upstream", "origin"):
- for base in ("main", "master", "v2"):
- committed = _git_lines("diff", "--name-only", f"{remote}/{base}...HEAD")
+ project_base = _project_main_line()
+ remotes = [[project_base]] if project_base else [
+ [f"{remote}/{base}" for base in ("main", "master", "v2")]
+ for remote in ("upstream", "origin")
+ ]
+ for candidates in remotes:
+ for line in candidates:
+ committed = _git_lines("diff", "--name-only", f"{line}...HEAD")
if committed is None:
continue
compared = True
diff --git a/tests/test_refresh_failures.py b/tests/test_refresh_failures.py
index ddcdd6b3..524d354b 100644
--- a/tests/test_refresh_failures.py
+++ b/tests/test_refresh_failures.py
@@ -87,6 +87,51 @@ class ChangesNotYetCommittedAreRead(unittest.TestCase):
self.assertIn("bios/new.bin", proc.stdout)
+class AForkPushedToItsOwnMain(unittest.TestCase):
+ """A fork's clone has origin alone. Once the commit was pushed to the
+ fork's main, origin/main was HEAD, the diff was empty, and --changed
+ said "No changed BIOS files detected" with rc 0."""
+
+ GIT = ["git", "-c", "user.email=a@b", "-c", "user.name=a",
+ "-c", "init.defaultBranch=main"]
+
+ def _git(self, *args: str, cwd: str | None = None) -> None:
+ subprocess.run([*self.GIT, *args], cwd=cwd, check=True, capture_output=True)
+
+ def _changed(self, clone: str, project: str) -> subprocess.CompletedProcess:
+ return subprocess.run(
+ [sys.executable, "-c", (
+ f"import sys; sys.path.insert(0, {str(REPO_ROOT / 'scripts')!r}); "
+ "import validate_pr; "
+ f"validate_pr.PROJECT_URLS = ({project!r},); "
+ "print(validate_pr.get_changed_files())"
+ )],
+ cwd=clone, capture_output=True, text=True, timeout=60, check=False,
+ )
+
+ def test_the_project_main_is_fetched_and_compared(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ project, fork, clone = (str(Path(tmp, n)) for n in ("project", "fork", "clone"))
+ self._git("init", "-q", project)
+ Path(project, "README").write_text("x")
+ self._git("add", "README", cwd=project)
+ self._git("commit", "-qm", "init", cwd=project)
+ self._git("clone", "-q", "--bare", project, fork)
+ self._git("clone", "-q", fork, clone)
+ Path(clone, "bios").mkdir()
+ Path(clone, "bios", "new.bin").write_bytes(b"x")
+ self._git("add", "bios/new.bin", cwd=clone)
+ self._git("commit", "-qm", "add", cwd=clone)
+ self._git("push", "-q", "origin", "main", cwd=clone)
+ proc = self._changed(clone, project)
+ self.assertEqual(proc.returncode, 0, proc.stderr)
+ self.assertIn("bios/new.bin", proc.stdout)
+ # A project that cannot be reached is an error, never "nothing".
+ missing = self._changed(clone, str(Path(tmp, "absent")))
+ self.assertNotEqual(missing.returncode, 0)
+ self.assertIn("fetching its main failed", missing.stderr)
+
+
class RefreshFailuresAreFailures(unittest.TestCase):
def test_a_failed_refresh_is_returned_and_exits_nonzero(self):
report = {"entries": [{"status": "UPDATED", "key": "dolphin-sys"},