From f38b1e3c01d60c5033058d0f6946f03398e5ae76 Mon Sep 17 00:00:00 2001 From: m3taversal Date: Wed, 15 Apr 2026 16:57:28 +0100 Subject: [PATCH] fix: handle already-merged PRs + retry worktree config.lock Two fixes for the 18-PR merge blockage: 1. When cherry-pick returns "already merged" (all commits empty because content is already on main), close the PR directly instead of trying to push the stale branch SHA to main. The branch ref points at old commits that aren't descendants of current main, so the push would always fail as non-fast-forward. 2. Retry worktree add once with jittered delay when config.lock contention occurs from parallel domain merges. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/merge.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/merge.py b/lib/merge.py index fd05a51..0f28670 100644 --- a/lib/merge.py +++ b/lib/merge.py @@ -318,6 +318,10 @@ async def _cherry_pick_onto_main(branch: str) -> tuple[bool, str]: # Delete stale local branch if it exists from a previous failed attempt await _git("branch", "-D", clean_branch) rc, out = await _git("worktree", "add", "-b", clean_branch, worktree_path, "origin/main") + if rc != 0 and "could not lock config" in out: + await asyncio.sleep(random.uniform(0.5, 2.0)) + await _git("branch", "-D", clean_branch) + rc, out = await _git("worktree", "add", "-b", clean_branch, worktree_path, "origin/main") if rc != 0: return False, f"worktree add failed: {out}" @@ -1456,6 +1460,25 @@ async def _merge_domain_queue(conn, domain: str) -> tuple[int, int]: failed += 1 continue + # Content already on main — close PR, skip push, clean up branch. + # Cherry-pick returns "already merged" when all commits are empty. + # The branch ref still points at old commits (not a descendant of main), + # so pushing branch_sha:main would fail as non-fast-forward. + if "already" in pick_msg.lower(): + conn.execute( + "UPDATE prs SET status = 'merged', merged_at = datetime('now'), last_error = NULL WHERE number = ?", + (pr_num,), + ) + db.audit(conn, "merge", "merged", json.dumps({"pr": pr_num, "branch": branch, "note": "content already on main"})) + leo_token = get_agent_token("leo") + await forgejo_api("POST", repo_path(f"issues/{pr_num}/comments"), + {"body": f"Content already on main — closing.\nBranch: `{branch}`"}) + await forgejo_api("PATCH", repo_path(f"pulls/{pr_num}"), {"state": "closed"}, token=leo_token) + await _delete_remote_branch(branch) + logger.info("PR #%d already merged (content on main), closed", pr_num) + succeeded += 1 + continue + # Local ff-push: cherry-picked branch is a descendant of origin/main. # Regular push = fast-forward. Non-ff rejected by default (same safety). # --force-with-lease removed: Forgejo categorically blocks it on protected branches.