@@ -229,6 +229,45 @@ def test_publish_succeeds_with_dirty_target_tree(self) -> None:
229229 capture_output = True , text = True ).stdout
230230 self .assertIn ("fix/DIRTY-my-fix" , refs )
231231
232+ def test_republish_force_updates_existing_pr_branch (self ) -> None :
233+ # iterate-do (#108): re-publishing a rebuilt bundle commits a FRESH branch off the
234+ # current base and pushes it to the EXISTING PR branch — not a fast-forward of the
235+ # prior attempt. A plain push is rejected (the re-Done bundle never publishes);
236+ # publish must force-with-lease so the branch is updated to the rebuilt commit.
237+ import subprocess as sp
238+ repo = self .tmp / "checkout"
239+ origin = self .tmp / "origin.git"
240+ sp .run (["git" , "init" , "-q" , "--bare" , str (origin )], check = True )
241+ sp .run (["git" , "clone" , "-q" , str (origin ), str (repo )], check = True )
242+ run = lambda * a : sp .run (["git" , "-C" , str (repo ), * a ], check = True , capture_output = True )
243+ run ("config" , "user.email" , "t@example.com" )
244+ run ("config" , "user.name" , "T" )
245+ run ("config" , "commit.gpgsign" , "false" )
246+ (repo / "file.txt" ).write_text ("base\n " , encoding = "utf-8" )
247+ run ("add" , "-A" ); run ("commit" , "-q" , "-m" , "base" )
248+ run ("branch" , "-M" , "main" ); run ("push" , "-q" , "-u" , "origin" , "main" )
249+
250+ self .cfg .base_remote = "origin"
251+ self .cfg .repo_checkouts = {"example-org/example-repo" : str (repo )}
252+ d = _bundle (self .cfg , "REDO" , brief_body = _FIX_BRIEF , accepted = True )
253+
254+ def publish_fix (line : str ) -> str :
255+ # A distinct fix off the same base → a sibling (non-ff) commit on re-publish.
256+ (d / "patch.diff" ).write_text (
257+ "diff --git a/file.txt b/file.txt\n --- a/file.txt\n +++ b/file.txt\n "
258+ f"@@ -1 +1,2 @@\n base\n +{ line } \n " , encoding = "utf-8" )
259+ buf = io .StringIO ()
260+ with redirect_stdout (buf ), redirect_stderr (buf ):
261+ rc = publish .publish (self .cfg , "REDO" , open_pr = False , by = "T" , today = "2026-06-05" )
262+ self .assertEqual (rc , 0 , buf .getvalue ())
263+ return sp .run (["git" , "-C" , str (repo ), "ls-remote" , "origin" , "fix/REDO-my-fix" ],
264+ capture_output = True , text = True ).stdout .split ()[0 ]
265+
266+ tip1 = publish_fix ("first fix" )
267+ tip2 = publish_fix ("second fix" ) # was rejected (non-fast-forward) before #108
268+ self .assertTrue (tip1 and tip2 )
269+ self .assertNotEqual (tip1 , tip2 ) # the PR branch was force-updated to the rebuild
270+
232271 def test_pr_head_is_fork_owner_qualified (self ) -> None :
233272 """Regression (#23b): a fork-based PR's --head must be OWNER:BRANCH, else gh
234273 resolves the branch against the base repo and fails 'Head ref must be a
0 commit comments