From e8e2c9008b75e430b53b182b1a5a612b4c0c185a Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Mon, 15 Jun 2026 10:09:06 -0400 Subject: [PATCH] fix(digest): rebaser must fetch feature branches with a full refspec A --single-branch clone + bare `git fetch origin ` updates FETCH_HEAD but not refs/remotes/origin/, so the subsequent `git checkout --detach origin/` failed and collapsed the whole ddmrp rebase pass into one error row. Fetch with refs/heads/:refs/remotes/origin/; force the per-iteration checkout. --- .github/scripts/fork_sync_digest.py | 8 +++-- .../tests/test_rebase_feature_branches.py | 32 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/.github/scripts/fork_sync_digest.py b/.github/scripts/fork_sync_digest.py index 12fce86..97442b9 100644 --- a/.github/scripts/fork_sync_digest.py +++ b/.github/scripts/fork_sync_digest.py @@ -327,7 +327,10 @@ def git(*args, **kw): ["git", *args], cwd=repo_dir, capture_output=True, **kw ) - fetched = git("fetch", "origin", branch) + # Explicit refspec: the clone is --single-branch, so a bare + # `fetch origin ` only updates FETCH_HEAD, not + # refs/remotes/origin/ — leaving the checkout below with no ref. + fetched = git("fetch", "origin", f"refs/heads/{branch}:refs/remotes/origin/{branch}") if fetched.returncode != 0: return row(500, "error", None, f"fetch failed: {fetched.stderr.decode()[:200]}") @@ -336,7 +339,8 @@ def git(*args, **kw): if behind == 0: return row(200, "already-current", 0, "up to date with base") - git("checkout", "--detach", f"origin/{branch}", check=True) + # -f: discard the prior iteration's rebased worktree state cleanly. + git("checkout", "--detach", "-f", f"origin/{branch}", check=True) git("clean", "-fdq") mergebase = git("merge-base", base_ref, f"origin/{branch}").stdout.decode().strip() # Content-preserving rebase: hooks off (nothing new to lint — the branch diff --git a/.github/scripts/tests/test_rebase_feature_branches.py b/.github/scripts/tests/test_rebase_feature_branches.py index 2175e52..c7d710c 100644 --- a/.github/scripts/tests/test_rebase_feature_branches.py +++ b/.github/scripts/tests/test_rebase_feature_branches.py @@ -51,3 +51,35 @@ def boom(*a, **k): # noqa: ANN002, ANN003 monkeypatch.setattr(fsd.subprocess, "run", boom) assert fsd.rebase_feature_branches("ledoent/ddmrp", ["19.0-mig-*"], "19.0") == [] + + +class _Fake: + def __init__(self, rc=0, out=b"", err=b""): + self.returncode, self.stdout, self.stderr = rc, out, err + + +def test_rebase_one_fetches_with_full_refspec(monkeypatch): + # Regression: a --single-branch clone + bare `fetch origin ` + # populates FETCH_HEAD but NOT origin/, so the checkout below + # explodes. The fetch must use refs/heads/:refs/remotes/origin/. + from pathlib import Path + + calls = [] + + def fake_run(cmd, cwd=None, capture_output=False, env=None, check=False, **k): + calls.append(cmd) + args = [c for c in cmd if c != "git"] + if "rev-list" in args: + return _Fake(0, out=b"2\n") + if "merge-base" in args: + return _Fake(0, out=b"abc123\n") + return _Fake(0) + + monkeypatch.setattr(fsd.subprocess, "run", fake_run) + res = fsd._rebase_one(Path("/tmp/x"), "ledoent/ddmrp", "19.0-mig-foo", "19.0", "origin/19.0") + assert res["rebase_status"] == "rebased" + fetch_cmds = [c for c in calls if "fetch" in c] + assert fetch_cmds and any( + "refs/heads/19.0-mig-foo:refs/remotes/origin/19.0-mig-foo" in c + for c in fetch_cmds + ), f"fetch must use a full refspec, got: {fetch_cmds}"