@@ -236,29 +236,9 @@ func createPatchFromPR(sourceOwner, sourceRepo string, prInfo *PRInfo, verbose b
236236 return "" , errors .New ("PR diff is empty" )
237237 }
238238
239- // Create proper mailbox format patch that git am expects
240- var patchBuilder strings.Builder
241-
242- // Required mailbox format headers for git am
243- fmt .Fprintf (& patchBuilder , "From %s Mon Sep 17 00:00:00 2001\n " , prInfo .HeadSHA )
244- fmt .Fprintf (& patchBuilder , "From: %s <%s@users.noreply.github.com>\n " , prInfo .AuthorLogin , prInfo .AuthorLogin )
245- fmt .Fprintf (& patchBuilder , "Date: %s\n " , time .Now ().Format (time .RFC1123 ))
246- fmt .Fprintf (& patchBuilder , "Subject: [PATCH] %s\n " , prInfo .Title )
247- patchBuilder .WriteString ("\n " )
248-
249- if prInfo .Body != "" {
250- fmt .Fprintf (& patchBuilder , "%s\n " , prInfo .Body )
251- patchBuilder .WriteString ("\n " )
252- }
253-
254- fmt .Fprintf (& patchBuilder , "Original-PR: %s#%d\n " , prInfo .SourceRepo , prInfo .Number )
255- fmt .Fprintf (& patchBuilder , "Original-Author: %s\n " , prInfo .AuthorLogin )
256- patchBuilder .WriteString ("---\n " )
257-
258- // Add the actual diff content
259- patchBuilder .Write (diffContent )
260-
261- if err := os .WriteFile (patchFile , []byte (patchBuilder .String ()), constants .FilePermPublic ); err != nil {
239+ // Keep the patch as a raw diff. PR metadata is added to the commit separately
240+ // so untrusted body text cannot be interpreted as mailbox or patch content.
241+ if err := os .WriteFile (patchFile , diffContent , constants .FilePermPublic ); err != nil {
262242 return "" , fmt .Errorf ("could not write patch file; ensure required prerequisites are configured, then retry: %w" , err )
263243 }
264244
@@ -267,18 +247,34 @@ func createPatchFromPR(sourceOwner, sourceRepo string, prInfo *PRInfo, verbose b
267247 }
268248
269249 return patchFile , nil
270- } // applyPatchToRepo applies a patch to the target repository and returns the branch name
271- func applyPatchToRepo ( patchFile string , prInfo * PRInfo , targetOwner , targetRepo string , verbose bool ) ( string , error ) {
272- // Get current branch to restore later
273- currentBranch , err := getCurrentBranch ()
250+ }
251+
252+ func ensureCleanGitWorktree () error {
253+ output , err := exec . Command ( "git" , "status" , "--porcelain" ). Output ()
274254 if err != nil {
275- return "" , fmt .Errorf ("could not get current branch; ensure required prerequisites are configured, then retry: %w" , err )
255+ return fmt .Errorf ("could not inspect git status; ensure this is a valid git repository, then retry: %w" , err )
256+ }
257+ if strings .TrimSpace (string (output )) != "" {
258+ return errors .New ("target repository has uncommitted changes; commit, stash, or remove them before transferring a PR" )
276259 }
260+ return nil
261+ }
277262
263+ func resetGitWorktreeToHEAD () error {
264+ if err := exec .Command ("git" , "reset" , "--hard" , "HEAD" ).Run (); err != nil {
265+ return fmt .Errorf ("could not reset transfer branch to HEAD: %w" , err )
266+ }
267+ if err := exec .Command ("git" , "clean" , "-fd" ).Run (); err != nil {
268+ return fmt .Errorf ("could not remove untracked files from failed patch apply: %w" , err )
269+ }
270+ return nil
271+ }
272+
273+ func checkoutUpdatedDefaultBranch (targetOwner , targetRepo string , verbose bool ) error {
278274 // Get the default branch of the target repository
279275 defaultBranchOutput , err := workflow .RunGH ("Fetching default branch..." , "api" , fmt .Sprintf ("/repos/%s/%s" , targetOwner , targetRepo ), "--jq" , ".default_branch" )
280276 if err != nil {
281- return "" , fmt .Errorf ("could not get default branch; ensure required prerequisites are configured, then retry: %w" , err )
277+ return fmt .Errorf ("could not get default branch; ensure required prerequisites are configured, then retry: %w" , err )
282278 }
283279 defaultBranch := strings .TrimSpace (string (defaultBranchOutput ))
284280
@@ -289,133 +285,129 @@ func applyPatchToRepo(patchFile string, prInfo *PRInfo, targetOwner, targetRepo
289285
290286 cmd := exec .Command ("git" , "checkout" , defaultBranch )
291287 if err := cmd .Run (); err != nil {
292- return "" , fmt .Errorf ("could not checkout default branch %s; ensure the branch exists locally and has no conflicting changes, then retry: %w" , defaultBranch , err )
288+ return fmt .Errorf ("could not checkout default branch %s; ensure the branch exists locally and has no conflicting changes, then retry: %w" , defaultBranch , err )
293289 }
294290
295291 cmd = exec .Command ("git" , "pull" , "origin" , defaultBranch )
296292 if err := cmd .Run (); err != nil {
297- return "" , fmt .Errorf ("could not pull latest %s from origin; ensure network access and remote permissions are available, then retry: %w" , defaultBranch , err )
293+ return fmt .Errorf ("could not pull latest %s from origin; ensure network access and remote permissions are available, then retry: %w" , defaultBranch , err )
298294 }
295+ return nil
296+ }
299297
300- // Create a new branch for the transfer based on the updated default branch
301- branchName := fmt .Sprintf ("transfer-pr-%d-%d" , prInfo .Number , time .Now ().Unix ())
298+ func applyPatchToIndexWithFallback (patchFile , currentBranch , branchName string , verbose bool ) error {
302299 if verbose {
303- fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Creating branch: " + branchName ))
300+ fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Applying patch with git apply..." ))
304301 }
305302
306- if err := createAndSwitchBranch (branchName , verbose ); err != nil {
307- return "" , fmt .Errorf ("could not create new branch; ensure required prerequisites are configured, then retry: %w" , err )
303+ cmd := exec .Command ("git" , "apply" , "--3way" , "--index" , patchFile )
304+ if err := cmd .Run (); err != nil {
305+ return applyPatchToIndexFallback (patchFile , currentBranch , branchName , verbose )
308306 }
307+ return nil
308+ }
309309
310- // Apply the patch
310+ func applyPatchToIndexFallback ( patchFile , currentBranch , branchName string , verbose bool ) error {
311311 if verbose {
312- fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Applying patch..." ))
313-
314- // Show some info about the patch file
315- patchContent , err := os .ReadFile (patchFile )
316- if err == nil {
317- lines := strings .Split (string (patchContent ), "\n " )
318- fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("Patch file has %d lines" , len (lines ))))
319- if len (lines ) > 0 {
320- fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("First line: " + lines [0 ]))
321- }
322- }
312+ fmt .Fprintln (os .Stderr , console .FormatWarningMessage ("3-way merge failed, trying with whitespace options..." ))
323313 }
324-
325- // Check if patch looks like a mailbox format (starts with "From ")
326- patchContent , err := os .ReadFile (patchFile )
327- if err != nil {
328- return "" , fmt .Errorf ("could not read patch file; ensure required prerequisites are configured, then retry: %w" , err )
314+ if resetErr := resetGitWorktreeToHEAD (); resetErr != nil {
315+ _ = exec .Command ("git" , "checkout" , currentBranch ).Run ()
316+ _ = exec .Command ("git" , "branch" , "-D" , branchName ).Run ()
317+ return resetErr
329318 }
330319
331- var appliedWithAm bool
332- isMailboxFormat := strings .HasPrefix (string (patchContent ), "From " )
333-
334- if isMailboxFormat {
335- // Try git am for mailbox format patches
336- if verbose {
337- fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Applying mailbox format patch with git am..." ))
338- }
339-
340- cmd = exec .Command ("git" , "am" , patchFile )
341- if err := cmd .Run (); err == nil {
342- appliedWithAm = true
343- if verbose {
344- fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Successfully applied patch with git am" ))
345- }
346- } else {
347- if verbose {
348- fmt .Fprintln (os .Stderr , console .FormatWarningMessage ("git am failed, trying git apply..." ))
349- }
350- // Reset any partial am state
351- _ = exec .Command ("git" , "am" , "--abort" ).Run ()
320+ cmd := exec .Command ("git" , "apply" , "--index" , "--ignore-space-change" , "--ignore-whitespace" , patchFile )
321+ if err := cmd .Run (); err != nil {
322+ reportPatchRejectDetails (patchFile , verbose )
323+ if resetErr := resetGitWorktreeToHEAD (); resetErr != nil {
324+ return resetErr
352325 }
326+ _ = exec .Command ("git" , "checkout" , currentBranch ).Run ()
327+ _ = exec .Command ("git" , "branch" , "-D" , branchName ).Run ()
328+ return fmt .Errorf ("could not apply patch; resolve conflicts manually, then rerun transfer-pr. underlying error: %w" , err )
353329 }
330+ return nil
331+ }
354332
355- if ! appliedWithAm {
356- // Try git apply for standard diff format or as fallback
357- if verbose {
358- fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Applying patch with git apply..." ))
359- }
333+ func reportPatchRejectDetails (patchFile string , verbose bool ) {
334+ if ! verbose {
335+ return
336+ }
337+ fmt .Fprintln (os .Stderr , console .FormatWarningMessage ("Standard apply failed, trying with --reject to see what failed..." ))
338+ if resetErr := resetGitWorktreeToHEAD (); resetErr != nil {
339+ fmt .Fprintln (os .Stderr , console .FormatWarningMessage (fmt .Sprintf ("Failed to reset before generating reject details: %v" , resetErr )))
340+ }
341+ rejectCmd := exec .Command ("git" , "apply" , "--reject" , patchFile )
342+ rejectOutput , _ := rejectCmd .CombinedOutput ()
343+ fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Patch rejection details:" ))
344+ fmt .Fprintln (os .Stderr , string (rejectOutput ))
345+ }
360346
361- cmd = exec .Command ("git" , "apply" , "--3way" , patchFile )
362- if err := cmd .Run (); err != nil {
363- if verbose {
364- fmt .Fprintln (os .Stderr , console .FormatWarningMessage ("3-way merge failed, trying with whitespace options..." ))
365- }
347+ func logPatchSummary (patchFile string , verbose bool ) {
348+ if ! verbose {
349+ return
350+ }
351+ fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Applying patch..." ))
352+ patchContent , err := os .ReadFile (patchFile )
353+ if err != nil {
354+ return
355+ }
356+ lines := strings .Split (string (patchContent ), "\n " )
357+ fmt .Fprintln (os .Stderr , console .FormatInfoMessage (fmt .Sprintf ("Patch file has %d lines" , len (lines ))))
358+ if len (lines ) > 0 {
359+ fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("First line: " + lines [0 ]))
360+ }
361+ }
366362
367- // Try with --ignore-space-change and --ignore-whitespace
368- cmd = exec .Command ("git" , "apply" , "--ignore-space-change" , "--ignore-whitespace" , patchFile )
369- if err := cmd .Run (); err != nil {
370- if verbose {
371- fmt .Fprintln (os .Stderr , console .FormatWarningMessage ("Standard apply failed, trying with --reject to see what failed..." ))
363+ // applyPatchToRepo applies a patch to the target repository and returns the branch name
364+ func applyPatchToRepo (patchFile string , prInfo * PRInfo , targetOwner , targetRepo string , verbose bool ) (string , error ) {
365+ currentBranch , err := getCurrentBranch ()
366+ if err != nil {
367+ return "" , fmt .Errorf ("could not get current branch; ensure required prerequisites are configured, then retry: %w" , err )
368+ }
369+ if err := ensureCleanGitWorktree (); err != nil {
370+ return "" , err
371+ }
372+ if err := checkoutUpdatedDefaultBranch (targetOwner , targetRepo , verbose ); err != nil {
373+ return "" , err
374+ }
372375
373- // Try with --reject to see which parts fail
374- rejectCmd := exec .Command ("git" , "apply" , "--reject" , patchFile )
375- rejectOutput , _ := rejectCmd .CombinedOutput ()
376- fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Patch rejection details:" ))
377- fmt .Fprintln (os .Stderr , string (rejectOutput ))
378- }
376+ branchName := fmt .Sprintf ("transfer-pr-%d-%d" , prInfo .Number , time .Now ().Unix ())
377+ if verbose {
378+ fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Creating branch: " + branchName ))
379+ }
380+ if err := createAndSwitchBranch (branchName , verbose ); err != nil {
381+ return "" , fmt .Errorf ("could not create new branch; ensure required prerequisites are configured, then retry: %w" , err )
382+ }
379383
380- // Try to reset back to original branch and clean up
381- _ = exec .Command ("git" , "checkout" , currentBranch ).Run ()
382- _ = exec .Command ("git" , "branch" , "-D" , branchName ).Run ()
383- return "" , fmt .Errorf ("could not apply patch; resolve conflicts manually, then rerun transfer-pr. underlying error: %w" , err )
384- }
385- }
384+ logPatchSummary (patchFile , verbose )
385+ if err := applyPatchToIndexWithFallback (patchFile , currentBranch , branchName , verbose ); err != nil {
386+ return "" , err
387+ }
386388
387- if verbose {
388- fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Successfully applied patch with git apply" ))
389- }
390- } // If we didn't use git am, we need to stage and commit manually
391- if ! appliedWithAm {
392- // Stage all changes
393- cmd = exec .Command ("git" , "add" , "." )
394- if err := cmd .Run (); err != nil {
395- return "" , fmt .Errorf ("could not stage changes; ensure required prerequisites are configured, then retry: %w" , err )
396- }
389+ if verbose {
390+ fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Successfully applied patch with git apply" ))
391+ }
397392
398- // Create commit with meaningful message
399- commitMsg := fmt .Sprintf ("Transfer PR #%d from %s\n \n %s" , prInfo .Number , prInfo .SourceRepo , prInfo .Title )
400- if prInfo .Body != "" {
401- commitMsg += "\n \n " + prInfo .Body
402- }
403- commitMsg += fmt .Sprintf ("\n \n Original-PR: %s#%d" , prInfo .SourceRepo , prInfo .Number )
404- commitMsg += "\n Original-Author: " + prInfo .AuthorLogin
393+ // Create the commit separately from patch application.
394+ commitMsg := fmt .Sprintf ("Transfer PR #%d from %s\n \n %s" , prInfo .Number , prInfo .SourceRepo , prInfo .Title )
395+ if prInfo .Body != "" {
396+ commitMsg += "\n \n " + prInfo .Body
397+ }
398+ commitMsg += fmt .Sprintf ("\n \n Original-PR: %s#%d" , prInfo .SourceRepo , prInfo .Number )
399+ commitMsg += "\n Original-Author: " + prInfo .AuthorLogin
405400
406- cmd = exec .Command ("git" , "commit" , "-m" , commitMsg )
407- if err := cmd .Run (); err != nil {
408- return "" , fmt .Errorf ("could not commit changes; ensure required prerequisites are configured, then retry: %w" , err )
409- }
410- } else if verbose {
411- fmt .Fprintln (os .Stderr , console .FormatInfoMessage ("Applied patch using git am (includes commit)" ))
401+ cmd := exec .Command ("git" , "commit" , "-m" , commitMsg )
402+ if err := cmd .Run (); err != nil {
403+ return "" , fmt .Errorf ("could not commit changes; ensure required prerequisites are configured, then retry: %w" , err )
412404 }
413405
414406 return branchName , nil
415407}
416408
417409// createTransferPR creates a new PR in the target repository
418- func createTransferPR (targetOwner , targetRepo string , prInfo * PRInfo , branchName string , verbose bool ) error {
410+ func createTransferPR (targetOwner , targetRepo string , prInfo * PRInfo , branchName string , verbose bool ) error { //nolint:largefunc
419411 // Check if user has write access to target repository
420412 hasWriteAccess , err := checkRepositoryAccess (targetOwner , targetRepo )
421413 if err != nil && verbose {
@@ -544,7 +536,7 @@ func createTransferPR(targetOwner, targetRepo string, prInfo *PRInfo, branchName
544536}
545537
546538// transferPR is the main function that orchestrates the PR transfer
547- func transferPR (prURL , targetRepo string , verbose bool ) error {
539+ func transferPR (prURL , targetRepo string , verbose bool ) error { //nolint:largefunc
548540 prLog .Printf ("Starting PR transfer: url=%s, targetRepo=%s" , prURL , targetRepo )
549541
550542 if verbose {
0 commit comments