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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ gh stack create <name>

Start tracking an existing branch by setting its parent.

By default, adopts the current branch. The parent must be either the trunk or another tracked branch.
By default, adopts the current branch. The parent must be either the trunk or another tracked branch. If the branch is already tracked, `adopt` updates its parent to the specified branch (or does nothing if the parent is unchanged).

#### adopt Usage

Expand Down
27 changes: 22 additions & 5 deletions cmd/adopt.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,15 @@ func runAdopt(cmd *cobra.Command, args []string) error {
return fmt.Errorf("branch %q does not exist", branchName)
}

// Check if already tracked
if _, getParentErr := cfg.GetParent(branchName); getParentErr == nil {
return fmt.Errorf("branch %q is already tracked", branchName)
// A branch cannot be its own parent
if parent == branchName {
return fmt.Errorf("cannot adopt: branch %q cannot be its own parent", branchName)
}

// Check if already tracked, capture old parent if so
oldParent, alreadyTracked := "", false
if p, getParentErr := cfg.GetParent(branchName); getParentErr == nil {
oldParent, alreadyTracked = p, true
}

// Validate parent is trunk or tracked
Expand Down Expand Up @@ -94,6 +100,14 @@ func runAdopt(cmd *cobra.Command, args []string) error {
}
}

s := style.New()

// No-op: already tracked with the same parent
if alreadyTracked && oldParent == parent {
fmt.Printf("%s Branch %s is already tracked with parent %s\n", s.WarningIcon(), s.Branch(branchName), s.Branch(parent))
return nil
}
Comment on lines +105 to +109

// Set parent
if err := cfg.SetParent(branchName, parent); err != nil {
return err
Expand All @@ -105,7 +119,10 @@ func runAdopt(cmd *cobra.Command, args []string) error {
_ = cfg.SetForkPoint(branchName, forkPoint) //nolint:errcheck // best effort
}

s := style.New()
fmt.Printf("%s Adopted branch %s with parent %s\n", s.SuccessIcon(), s.Branch(branchName), s.Branch(parent))
if alreadyTracked {
fmt.Printf("%s Updated branch %s parent from %s to %s\n", s.SuccessIcon(), s.Branch(branchName), s.Branch(oldParent), s.Branch(parent))
} else {
fmt.Printf("%s Adopted branch %s with parent %s\n", s.SuccessIcon(), s.Branch(branchName), s.Branch(parent))
}
return nil
}
23 changes: 16 additions & 7 deletions cmd/adopt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestAdoptBranch(t *testing.T) {
}
}

func TestAdoptRejectsAlreadyTracked(t *testing.T) {
func TestAdoptReparentsAlreadyTracked(t *testing.T) {
dir := setupTestRepo(t)

cfg, _ := config.Load(dir)
Expand All @@ -46,14 +46,23 @@ func TestAdoptRejectsAlreadyTracked(t *testing.T) {
trunk, _ := g.CurrentBranch()
cfg.SetTrunk(trunk)

// Create and track a branch
g.CreateBranch("tracked-feature")
cfg.SetParent("tracked-feature", trunk)
// Create two branches; track feature-a under trunk
g.CreateBranch("feature-a")
cfg.SetParent("feature-a", trunk)
g.CreateBranch("feature-b")
cfg.SetParent("feature-b", trunk)

// Simulate what runAdopt does when reparenting feature-b under feature-a
if err := cfg.SetParent("feature-b", "feature-a"); err != nil {
t.Fatalf("SetParent failed: %v", err)
}

// Trying to get parent should succeed (it's tracked)
_, err := cfg.GetParent("tracked-feature")
parent, err := cfg.GetParent("feature-b")
if err != nil {
t.Error("expected branch to be tracked")
t.Fatalf("GetParent failed: %v", err)
}
if parent != "feature-a" {
t.Errorf("expected parent %q after reparent, got %q", "feature-a", parent)
}
}

Expand Down
78 changes: 78 additions & 0 deletions e2e/adopt_orphan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,84 @@ package e2e_test

import "testing"

func TestAdoptReparentsTrackedBranch(t *testing.T) {
env := NewTestEnv(t)
env.MustRun("init")

// Create feat-a and feat-b both off trunk (main)
env.MustRun("create", "feat-a")
env.CreateCommit("feat-a work")

env.Git("checkout", "main")
env.MustRun("create", "feat-b")
env.CreateCommit("feat-b work")

// feat-b is currently tracked with parent main; reparent onto feat-a
result := env.MustRun("adopt", "--branch", "feat-b", "feat-a")

env.AssertStackParent("feat-b", "feat-a")
if !result.ContainsStdout("feat-a") {
t.Errorf("expected stdout to mention feat-a, got: %s", result.Stdout)
}
if !result.ContainsStdout("main") {
t.Errorf("expected stdout to mention old parent main, got: %s", result.Stdout)
}
}

func TestAdoptNoOpWhenParentUnchangedE2E(t *testing.T) {
env := NewTestEnv(t)
env.MustRun("init")

env.Git("checkout", "-b", "existing-branch")
env.CreateCommit("some work")
env.MustRun("adopt", "main")

// Adopt again with same parent: should succeed and print a warning
result := env.Run("adopt", "main")
if !result.Success() {
t.Errorf("expected no-op adopt to succeed, got exit %d: %s", result.ExitCode, result.Stderr)
}
if !result.ContainsStdout("already tracked") {
t.Errorf("expected 'already tracked' in stdout, got: %s", result.Stdout)
}
}

func TestAdoptSelfParentRejected(t *testing.T) {
env := NewTestEnv(t)
env.MustRun("init")

env.MustRun("create", "feat-a")
env.CreateCommit("a work")

result := env.Run("adopt", "--branch", "feat-a", "feat-a")
if result.Success() {
t.Error("expected self-parent adopt to fail, but it succeeded")
}
if !result.ContainsStderr("own parent") {
t.Errorf("expected 'own parent' in stderr, got: %s", result.Stderr)
}
}

func TestAdoptReparentCycleDetected(t *testing.T) {
env := NewTestEnv(t)
env.MustRun("init")

// Build trunk → feat-a → feat-b
env.MustRun("create", "feat-a")
env.CreateCommit("a work")
env.MustRun("create", "feat-b")
env.CreateCommit("b work")

// Attempting to reparent feat-a under feat-b would create a cycle
result := env.Run("adopt", "--branch", "feat-a", "feat-b")
if result.Success() {
t.Error("expected cycle detection to fail, but adopt succeeded")
}
if !result.ContainsStderr("cycle") {
t.Errorf("expected 'cycle' in stderr, got: %s", result.Stderr)
}
}

func TestAdoptExistingBranch(t *testing.T) {
env := NewTestEnv(t)
env.MustRun("init")
Expand Down
Loading