Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 45 additions & 11 deletions pkg/cli/pr_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,24 @@ The target repository defaults to the current repository unless --repo is specif
Examples:
gh aw pr transfer https://github.com/trial/repo/pull/234
gh aw pr transfer https://github.com/PR-OWNER/PR-REPO/pull/234 --repo owner/target-repo
gh aw pr transfer https://github.com/trial/repo/pull/234 --force

The command will:
1. Fetch the PR details (title, body, changes)
2. Apply changes as a single squashed commit
3. Create a new PR in the target repository
4. Copy the original title and description`,
4. Copy the original title and description

If there are conflicts during patch application, use --force to create a PR
with conflict markers that you can resolve manually.`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
prURL := args[0]
targetRepo, _ := cmd.Flags().GetString("repo")
verbose, _ := cmd.Flags().GetBool("verbose")
force, _ := cmd.Flags().GetBool("force")

if err := transferPR(prURL, targetRepo, verbose); err != nil {
if err := transferPR(prURL, targetRepo, verbose, force); err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
os.Exit(1)
}
Expand All @@ -88,6 +93,7 @@ The command will:

cmd.Flags().StringP("repo", "r", "", "Target repository (owner/repo format). Defaults to current repository")
cmd.Flags().BoolP("verbose", "v", false, "Verbose output")
cmd.Flags().BoolP("force", "f", false, "Force transfer even if there are conflicts (creates PR with conflict markers)")

return cmd
}
Expand Down Expand Up @@ -253,8 +259,10 @@ func createPatchFromPR(sourceOwner, sourceRepo string, prInfo *PRInfo, verbose b
}

return patchFile, nil
} // applyPatchToRepo applies a patch to the target repository and returns the branch name
func applyPatchToRepo(patchFile string, prInfo *PRInfo, targetOwner, targetRepo string, verbose bool) (string, error) {
}

// applyPatchToRepo applies a patch to the target repository and returns the branch name
func applyPatchToRepo(patchFile string, prInfo *PRInfo, targetOwner, targetRepo string, verbose, force bool) (string, error) {
// Get current branch to restore later
cmd := exec.Command("git", "branch", "--show-current")
currentBranchOutput, err := cmd.Output()
Expand Down Expand Up @@ -371,10 +379,23 @@ func applyPatchToRepo(patchFile string, prInfo *PRInfo, targetOwner, targetRepo
fmt.Fprintln(os.Stderr, string(rejectOutput))
}

// Try to reset back to original branch and clean up
_ = exec.Command("git", "checkout", currentBranch).Run()
_ = exec.Command("git", "branch", "-D", branchName).Run()
return "", fmt.Errorf("failed to apply patch: %w. You may need to resolve conflicts manually", err)
if !force {
// Try to reset back to original branch and clean up
_ = exec.Command("git", "checkout", currentBranch).Run()
_ = exec.Command("git", "branch", "-D", branchName).Run()
return "", fmt.Errorf("failed to apply patch due to conflicts. Use --force to create a PR with conflict markers that you can resolve manually")
}

// Force mode: apply with --reject to leave conflict markers
if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Applying patch with --reject to create conflict markers..."))
}
rejectCmd := exec.Command("git", "apply", "--reject", patchFile)
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rejectCmd variable is declared twice in the same code path. It's first declared on line 376 (inside the verbose block) and then redeclared on line 393 (in the force mode block). While this works due to Go's scoping rules, it could lead to confusion.

Consider using a single declaration or using different variable names to make the code clearer.

Copilot uses AI. Check for mistakes.
_ = rejectCmd.Run() // Ignore errors since we expect conflicts

if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Patch applied with conflicts - conflict markers have been created"))
}
}
}

Expand All @@ -383,7 +404,12 @@ func applyPatchToRepo(patchFile string, prInfo *PRInfo, targetOwner, targetRepo
}
} // If we didn't use git am, we need to stage and commit manually
if !appliedWithAm {
// Stage all changes
// Check if there are any .rej files (rejected hunks from --reject)
rejFilesCmd := exec.Command("sh", "-c", "find . -name '*.rej' -type f")
rejFilesOutput, _ := rejFilesCmd.Output()
hasConflicts := len(strings.TrimSpace(string(rejFilesOutput))) > 0

Comment on lines +408 to +411
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using sh -c "find . -name '*.rej' -type f" to detect .rej files may have portability issues on Windows systems where sh might not be available. Additionally, running find from the current directory (.) could potentially search the entire repository including unrelated directories.

Consider using Go's filepath.Walk or filepath.Glob for better portability, or at least constraining the search to the git working tree with git ls-files '*.rej'.

Suggested change
rejFilesCmd := exec.Command("sh", "-c", "find . -name '*.rej' -type f")
rejFilesOutput, _ := rejFilesCmd.Output()
hasConflicts := len(strings.TrimSpace(string(rejFilesOutput))) > 0
hasConflicts := false
err := filepath.WalkDir(".", func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && strings.HasSuffix(d.Name(), ".rej") {
hasConflicts = true
// Stop walking as soon as we find one
return filepath.SkipDir
}
return nil
})
if err != nil && verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Error searching for .rej files: %v", err)))
}

Copilot uses AI. Check for mistakes.
// Stage all changes (including .rej files if present)
cmd = exec.Command("git", "add", ".")
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to stage changes: %w", err)
Expand All @@ -394,13 +420,21 @@ func applyPatchToRepo(patchFile string, prInfo *PRInfo, targetOwner, targetRepo
if prInfo.Body != "" {
commitMsg += "\n\n" + prInfo.Body
}
if hasConflicts {
commitMsg += "\n\n⚠️ This PR contains conflicts that need to be resolved."
commitMsg += "\nConflict markers and .rej files have been included in this commit."
}
commitMsg += fmt.Sprintf("\n\nOriginal-PR: %s#%d", prInfo.SourceRepo, prInfo.Number)
commitMsg += fmt.Sprintf("\nOriginal-Author: %s", prInfo.AuthorLogin)

cmd = exec.Command("git", "commit", "-m", commitMsg)
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to commit changes: %w", err)
}

if hasConflicts && verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Committed changes with conflict markers and .rej files"))
}
} else if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Applied patch using git am (includes commit)"))
}
Expand Down Expand Up @@ -543,7 +577,7 @@ func createTransferPR(targetOwner, targetRepo string, prInfo *PRInfo, branchName
}

// transferPR is the main function that orchestrates the PR transfer
func transferPR(prURL, targetRepo string, verbose bool) error {
func transferPR(prURL, targetRepo string, verbose, force bool) error {
if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Starting PR transfer..."))
}
Expand Down Expand Up @@ -724,7 +758,7 @@ func transferPR(prURL, targetRepo string, verbose bool) error {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Applying changes to target repository..."))
}

branchName, err := applyPatchToRepo(patchFile, prInfo, targetOwner, targetRepoName, verbose)
branchName, err := applyPatchToRepo(patchFile, prInfo, targetOwner, targetRepoName, verbose, force)
if err != nil {
return err
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/cli/pr_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,10 @@ func TestNewPRTransferSubcommand(t *testing.T) {
if verboseFlag == nil {
t.Error("Expected --verbose flag to exist")
}

// Check that --force flag exists
forceFlag := cmd.Flags().Lookup("force")
if forceFlag == nil {
t.Error("Expected --force flag to exist")
}
}
Loading