Skip to content
Merged
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
152 changes: 142 additions & 10 deletions cmd/parent.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,54 @@ import (
"os"

"github.com/javoire/stackinator/internal/git"
"github.com/javoire/stackinator/internal/github"
"github.com/javoire/stackinator/internal/ui"
"github.com/spf13/cobra"
)

var parentCmd = &cobra.Command{
Use: "parent",
Short: "Show the parent of the current branch",
Long: `Display the parent branch of the current branch in the stack.
Use: "parent [new-parent]",
Short: "Show or change the parent of the current branch",
Long: `Show or change the parent branch of the current branch in the stack.

If the current branch has no parent set, it will show that the branch
is not part of a stack.`,
Without arguments, displays the current parent branch.

With a branch argument, changes the parent to the specified branch. This updates
the stack parent relationship in git config and, if a PR exists for the current
branch, automatically updates the PR base to match the new parent.`,
Example: ` # Show parent of current branch
stack parent`,
stack parent

# Change current branch to be based on a different parent
stack parent feature-auth

# Preview what would happen
stack parent main --dry-run`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
gitClient := git.NewGitClient()

if err := runParent(gitClient); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
if len(args) == 0 {
// Show parent
if err := runShowParent(gitClient); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
} else {
// Reparent
newParent := args[0]
repo := github.ParseRepoFromURL(gitClient.GetRemoteURL("origin"))
githubClient := github.NewGitHubClient(repo)

if err := runReparent(gitClient, githubClient, newParent); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
},
}

func runParent(gitClient git.GitClient) error {
func runShowParent(gitClient git.GitClient) error {
// Get current branch
currentBranch, err := gitClient.GetCurrentBranch()
if err != nil {
Expand All @@ -47,3 +71,111 @@ func runParent(gitClient git.GitClient) error {
return nil
}

func runReparent(gitClient git.GitClient, githubClient github.GitHubClient, newParent string) error {
// Get current branch
currentBranch, err := gitClient.GetCurrentBranch()
if err != nil {
return fmt.Errorf("failed to get current branch: %w", err)
}

// Get current parent (may be empty if not in a stack)
currentParent := gitClient.GetConfig(fmt.Sprintf("branch.%s.stackparent", currentBranch))

// Check if new parent is the same as current parent
if currentParent != "" && newParent == currentParent {
fmt.Printf("Branch %s is already parented to %s\n", ui.Branch(currentBranch), ui.Branch(newParent))
return nil
}

// Verify new parent branch exists
if !gitClient.BranchExists(newParent) {
return fmt.Errorf("new parent branch %s does not exist", newParent)
}

// Check if this would create a cycle
if newParent == currentBranch {
return fmt.Errorf("cannot set branch as its own parent")
}

// Check if new parent is a descendant of current branch (would create cycle)
if isDescendant(gitClient, currentBranch, newParent) {
return fmt.Errorf("cannot reparent to %s: it is a descendant of %s (would create a cycle)", newParent, currentBranch)
}

// Print appropriate message based on whether we're adding to stack or reparenting
if currentParent == "" {
fmt.Printf("Adding %s to stack with parent %s\n", ui.Branch(currentBranch), ui.Branch(newParent))
} else {
fmt.Printf("Reparenting %s: %s -> %s\n", ui.Branch(currentBranch), ui.Branch(currentParent), ui.Branch(newParent))
}

// Update git config
configKey := fmt.Sprintf("branch.%s.stackparent", currentBranch)
if err := gitClient.SetConfig(configKey, newParent); err != nil {
return fmt.Errorf("failed to update parent config: %w", err)
}

// Check if there's a PR for this branch
pr, err := githubClient.GetPRForBranch(currentBranch)
if err != nil {
// Error fetching PR info, but config was updated successfully
fmt.Println(ui.Success(fmt.Sprintf("Updated parent to %s", ui.Branch(newParent))))
fmt.Printf("Warning: failed to check for PR: %v\n", err)
return nil
}

if pr != nil {
// PR exists, update its base
fmt.Printf("Updating PR #%d base: %s -> %s\n", pr.Number, ui.Branch(pr.Base), ui.Branch(newParent))

if err := githubClient.UpdatePRBase(pr.Number, newParent); err != nil {
// Config was updated but PR base update failed
fmt.Println(ui.Success(fmt.Sprintf("Updated parent to %s", ui.Branch(newParent))))
return fmt.Errorf("failed to update PR base: %w", err)
}

if !dryRun {
fmt.Println(ui.Success(fmt.Sprintf("Updated parent to %s", ui.Branch(newParent))))
fmt.Println(ui.Success(fmt.Sprintf("Updated PR #%d base to %s", pr.Number, ui.Branch(newParent))))
}
} else {
// No PR exists
if !dryRun {
fmt.Println(ui.Success(fmt.Sprintf("Updated parent to %s", ui.Branch(newParent))))
fmt.Println(" (no PR found for this branch)")
}
}

return nil
}

// isDescendant checks if possibleDescendant is a descendant of ancestor in the stack
func isDescendant(gitClient git.GitClient, ancestor, possibleDescendant string) bool {
// Walk up from possibleDescendant to see if we reach ancestor
current := possibleDescendant
visited := make(map[string]bool)

for current != "" {
// Prevent infinite loops
if visited[current] {
return false
}
visited[current] = true

// Get parent of current
parent := gitClient.GetConfig(fmt.Sprintf("branch.%s.stackparent", current))
if parent == "" {
// Reached the top of the stack without finding ancestor
return false
}

if parent == ancestor {
// Found ancestor in the chain
return true
}

current = parent
}

return false
}
155 changes: 0 additions & 155 deletions cmd/reparent.go

This file was deleted.

1 change: 0 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ func init() {
rootCmd.AddCommand(pruneCmd)
rootCmd.AddCommand(parentCmd)
rootCmd.AddCommand(renameCmd)
rootCmd.AddCommand(reparentCmd)
rootCmd.AddCommand(worktreeCmd)
rootCmd.AddCommand(configCmd)
rootCmd.AddCommand(upCmd)
Expand Down
27 changes: 11 additions & 16 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,21 @@ Flags:

- `--force`, `-f` - Use `--force` instead of `--force-with-lease` for push (bypasses safety checks)

## `stack parent`
## `stack parent [new-parent]`

Display the parent branch of the current branch in the stack.
Show or change the parent branch of the current branch in the stack.

Without arguments, displays the current parent. With a branch argument, changes the parent to the specified branch and updates the PR base if a PR exists.

```bash
# Show parent of current branch
stack parent

# Change current branch to be based on a different parent
stack parent feature-auth

# Preview what would happen
stack parent main --dry-run
```

## `stack prune`
Expand Down Expand Up @@ -100,20 +109,6 @@ stack rename feature-improved-name
stack rename feature-improved-name --dry-run
```

## `stack reparent <new-parent>`

Change the parent branch of the current branch in the stack.

Updates the stack parent relationship and, if a PR exists, automatically updates the PR base to match the new parent.

```bash
# Change current branch to be based on a different parent
stack reparent feature-auth

# Preview what would happen
stack reparent main --dry-run
```

## `stack worktree <branch-name> [base-branch]`

Create a git worktree in the configured worktrees directory for the specified branch.
Expand Down