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.