mirror of
https://github.com/github/gh-stack.git
synced 2026-09-14 20:26:28 +08:00
9421bd66a3
* Avoid replaying amended parent commits Preserve a branch's last valid base when its parent is rewritten, and only use verified ancestor commits as rebase boundaries. Recover previously corrupted metadata from the parent reflog when possible, otherwise stop safely instead of replaying superseded parent commits. * Record adopted branch merge bases Store the actual common ancestor when adding an existing branch so cascade rebases replay only that branch's unique commits while retaining the amended-parent safety guard. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30bcbf2a-5ef1-4bdb-a0fc-618294ae8ded --------- Copilot-Session: 30bcbf2a-5ef1-4bdb-a0fc-618294ae8ded
592 lines
19 KiB
Go
592 lines
19 KiB
Go
package cmd
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/github/gh-stack/internal/config"
|
|
"github.com/github/gh-stack/internal/git"
|
|
"github.com/github/gh-stack/internal/stack"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// saveStack is a helper to pre-create a stack file for add tests.
|
|
func saveStack(t *testing.T, gitDir string, s stack.Stack) {
|
|
t.Helper()
|
|
sf := &stack.StackFile{
|
|
SchemaVersion: 1,
|
|
Stacks: []stack.Stack{s},
|
|
}
|
|
require.NoError(t, stack.Save(gitDir, sf), "saving seed stack")
|
|
}
|
|
|
|
func TestAdd_CreatesNewBranch(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
saveStack(t, gitDir, stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}},
|
|
})
|
|
|
|
var createdBranch, checkedOut string
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
CreateBranchFn: func(name, base string) error {
|
|
createdBranch = name
|
|
return nil
|
|
},
|
|
CheckoutBranchFn: func(name string) error {
|
|
checkedOut = name
|
|
return nil
|
|
},
|
|
})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
runAdd(cfg, &addOptions{}, []string{"newbranch"})
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
require.NotContains(t, output, "\u2717", "unexpected error")
|
|
assert.Equal(t, "newbranch", createdBranch, "CreateBranch")
|
|
assert.Equal(t, "newbranch", checkedOut, "CheckoutBranch")
|
|
|
|
sf, err := stack.Load(gitDir)
|
|
require.NoError(t, err, "loading stack")
|
|
names := sf.Stacks[0].BranchNames()
|
|
assert.Equal(t, "newbranch", names[len(names)-1], "top branch")
|
|
}
|
|
|
|
func TestAdd_OnlyAllowedOnTopOfStack(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
saveStack(t, gitDir, stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{
|
|
{Branch: "b1"},
|
|
{Branch: "b2"},
|
|
{Branch: "b3"},
|
|
},
|
|
})
|
|
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
runAdd(cfg, &addOptions{}, []string{"newbranch"})
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
assert.Contains(t, output, "top of the stack")
|
|
assert.Contains(t, output, "gh stack modify")
|
|
}
|
|
|
|
func TestAdd_MutuallyExclusiveFlags(t *testing.T) {
|
|
restore := git.SetOps(&git.MockOps{})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
runAdd(cfg, &addOptions{stageAll: true, stageTracked: true, message: "msg"}, []string{"branch"})
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
assert.Contains(t, output, "mutually exclusive")
|
|
}
|
|
|
|
func TestAdd_StagingWithoutMessageUsesEditor(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
saveStack(t, gitDir, stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}},
|
|
})
|
|
|
|
interactiveCalled := false
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
RevParseMultiFn: func(refs []string) ([]string, error) {
|
|
return []string{"aaa", "bbb"}, nil
|
|
},
|
|
RevParseFn: func(ref string) (string, error) { return "abc", nil },
|
|
CreateBranchFn: func(name, base string) error { return nil },
|
|
CheckoutBranchFn: func(name string) error { return nil },
|
|
StageAllFn: func() error { return nil },
|
|
HasStagedChangesFn: func() bool { return true },
|
|
CommitInteractiveFn: func() (string, error) {
|
|
interactiveCalled = true
|
|
return "def1234567890", nil
|
|
},
|
|
})
|
|
defer restore()
|
|
|
|
cfg, _, _ := config.NewTestConfig()
|
|
runAdd(cfg, &addOptions{stageAll: true}, []string{"new-branch"})
|
|
|
|
assert.True(t, interactiveCalled, "expected CommitInteractive to be called when -m is omitted")
|
|
}
|
|
|
|
func TestAdd_EmptyBranchCommitsInPlace(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
saveStack(t, gitDir, stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}},
|
|
})
|
|
|
|
createBranchCalled := false
|
|
commitCalled := false
|
|
stageAllCalled := false
|
|
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
RevParseMultiFn: func(refs []string) ([]string, error) {
|
|
// Return same SHA for parent and current branch — branch has no unique commits
|
|
return []string{"aaa111", "aaa111"}, nil
|
|
},
|
|
StageAllFn: func() error {
|
|
stageAllCalled = true
|
|
return nil
|
|
},
|
|
HasStagedChangesFn: func() bool { return true },
|
|
CommitFn: func(msg string) (string, error) {
|
|
commitCalled = true
|
|
return "abc1234567890", nil
|
|
},
|
|
CreateBranchFn: func(name, base string) error {
|
|
createBranchCalled = true
|
|
return nil
|
|
},
|
|
})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
runAdd(cfg, &addOptions{stageAll: true, message: "Auth middleware"}, nil)
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
require.NotContains(t, output, "\u2717", "unexpected error")
|
|
assert.True(t, stageAllCalled, "expected StageAll to be called")
|
|
assert.True(t, commitCalled, "expected Commit to be called")
|
|
assert.False(t, createBranchCalled, "CreateBranch should NOT be called for empty branch commit-in-place")
|
|
}
|
|
|
|
func TestAdd_BranchWithCommitsCreatesNew(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
saveStack(t, gitDir, stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}},
|
|
})
|
|
|
|
createCalled := false
|
|
checkoutCalled := false
|
|
commitCalled := false
|
|
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
RevParseMultiFn: func(refs []string) ([]string, error) {
|
|
// Parent and current branch point to different commits (branch has commits)
|
|
return []string{"aaa", "bbb"}, nil
|
|
},
|
|
CreateBranchFn: func(name, base string) error {
|
|
createCalled = true
|
|
return nil
|
|
},
|
|
CheckoutBranchFn: func(name string) error {
|
|
checkoutCalled = true
|
|
return nil
|
|
},
|
|
HasStagedChangesFn: func() bool { return true },
|
|
CommitFn: func(msg string) (string, error) {
|
|
commitCalled = true
|
|
return "def1234567890", nil
|
|
},
|
|
})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
runAdd(cfg, &addOptions{stageAll: true, message: "API routes"}, nil)
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
require.NotContains(t, output, "\u2717", "unexpected error")
|
|
assert.True(t, createCalled, "expected CreateBranch to be called")
|
|
assert.True(t, checkoutCalled, "expected CheckoutBranch to be called")
|
|
assert.True(t, commitCalled, "expected Commit to be called on the new branch")
|
|
}
|
|
|
|
func TestAdd_ExplicitNameUsedVerbatim(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
saveStack(t, gitDir, stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}},
|
|
})
|
|
|
|
var createdBranch string
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
CreateBranchFn: func(name, base string) error {
|
|
createdBranch = name
|
|
return nil
|
|
},
|
|
})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
runAdd(cfg, &addOptions{}, []string{"mybranch"})
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
require.NotContains(t, output, "\u2717", "unexpected error")
|
|
assert.Equal(t, "mybranch", createdBranch)
|
|
}
|
|
|
|
func TestAdd_MessageAutoGeneratesDateSlug(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
saveStack(t, gitDir, stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}},
|
|
})
|
|
|
|
var createdBranch string
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
RevParseMultiFn: func(refs []string) ([]string, error) {
|
|
return []string{"aaa", "bbb"}, nil
|
|
},
|
|
CreateBranchFn: func(name, base string) error {
|
|
createdBranch = name
|
|
return nil
|
|
},
|
|
HasStagedChangesFn: func() bool { return true },
|
|
CommitFn: func(msg string) (string, error) {
|
|
return "def1234567890", nil
|
|
},
|
|
})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
runAdd(cfg, &addOptions{stageAll: true, message: "next feature"}, nil)
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
require.NotContains(t, output, "\u2717", "unexpected error")
|
|
today := time.Now().Format("01-02")
|
|
assert.Equal(t, today+"-next_feature", createdBranch)
|
|
}
|
|
|
|
func TestAdd_FullyMergedStackBlocked(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
saveStack(t, gitDir, stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{
|
|
{Branch: "b1", PullRequest: &stack.PullRequestRef{Number: 1, Merged: true}},
|
|
{Branch: "b2", PullRequest: &stack.PullRequestRef{Number: 2, Merged: true}},
|
|
},
|
|
})
|
|
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b2", nil },
|
|
})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
runAdd(cfg, &addOptions{}, []string{"newbranch"})
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
assert.Contains(t, output, "All branches in this stack have been merged")
|
|
}
|
|
|
|
func TestAdd_NothingToCommit(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
saveStack(t, gitDir, stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}},
|
|
})
|
|
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
RevParseMultiFn: func(refs []string) ([]string, error) {
|
|
return []string{"aaa", "aaa"}, nil // same SHA = empty branch
|
|
},
|
|
StageAllFn: func() error { return nil },
|
|
HasStagedChangesFn: func() bool { return false },
|
|
})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
runAdd(cfg, &addOptions{stageAll: true, message: "msg"}, nil)
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
assert.Contains(t, output, "no changes to commit")
|
|
}
|
|
|
|
func TestAdd_PromptForBranchName(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
saveStack(t, gitDir, stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}},
|
|
})
|
|
|
|
var createdBranch string
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
CreateBranchFn: func(name, base string) error {
|
|
createdBranch = name
|
|
return nil
|
|
},
|
|
CheckoutBranchFn: func(name string) error { return nil },
|
|
RevParseFn: func(ref string) (string, error) { return "abc", nil },
|
|
})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
|
|
var gotPrompt string
|
|
cfg.InputFn = func(prompt string) (string, error) {
|
|
gotPrompt = prompt
|
|
return "my-branch", nil
|
|
}
|
|
|
|
err := runAdd(cfg, &addOptions{}, nil)
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
require.NoError(t, err)
|
|
require.NotContains(t, output, "\u2717", "unexpected error")
|
|
assert.Contains(t, gotPrompt, ":", "prompt should end with a colon")
|
|
assert.Equal(t, "my-branch", createdBranch, "input should be used as-is")
|
|
}
|
|
|
|
func TestAdd_PromptInputUsedVerbatim(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
saveStack(t, gitDir, stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}},
|
|
})
|
|
|
|
var createdBranch string
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
CreateBranchFn: func(name, base string) error {
|
|
createdBranch = name
|
|
return nil
|
|
},
|
|
CheckoutBranchFn: func(name string) error { return nil },
|
|
RevParseFn: func(ref string) (string, error) { return "abc", nil },
|
|
})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
|
|
cfg.InputFn = func(prompt string) (string, error) {
|
|
return "custom/other-name", nil
|
|
}
|
|
|
|
err := runAdd(cfg, &addOptions{}, nil)
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
require.NoError(t, err)
|
|
require.NotContains(t, output, "\u2717", "unexpected error")
|
|
assert.Equal(t, "custom/other-name", createdBranch, "typed input should be used verbatim")
|
|
}
|
|
|
|
func TestAdd_FromTrunk(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
saveStack(t, gitDir, stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}},
|
|
})
|
|
|
|
var createdBranch string
|
|
var checkedOut string
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "main", nil },
|
|
CreateBranchFn: func(name, base string) error {
|
|
createdBranch = name
|
|
return nil
|
|
},
|
|
CheckoutBranchFn: func(name string) error {
|
|
checkedOut = name
|
|
return nil
|
|
},
|
|
RevParseFn: func(ref string) (string, error) { return "sha-" + ref, nil },
|
|
})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
err := runAdd(cfg, &addOptions{}, []string{"newbranch"})
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
// When on trunk, idx < 0 so the middle-of-stack check passes.
|
|
// Add should succeed and create the new branch.
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "newbranch", createdBranch)
|
|
assert.Equal(t, "newbranch", checkedOut)
|
|
assert.NotContains(t, output, "\u2717")
|
|
|
|
sf, err := stack.Load(gitDir)
|
|
require.NoError(t, err)
|
|
names := sf.Stacks[0].BranchNames()
|
|
assert.Equal(t, "newbranch", names[len(names)-1], "new branch should be appended to stack")
|
|
}
|
|
|
|
func TestAdd_AdoptsExistingBranch(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
saveStack(t, gitDir, stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}},
|
|
})
|
|
|
|
createBranchCalled := false
|
|
var checkedOut string
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
BranchExistsFn: func(name string) bool { return name == "existing-branch" },
|
|
MergeBaseFn: func(parent, branch string) (string, error) {
|
|
assert.Equal(t, "b1", parent)
|
|
assert.Equal(t, "existing-branch", branch)
|
|
return "common-base", nil
|
|
},
|
|
CreateBranchFn: func(name, base string) error {
|
|
createBranchCalled = true
|
|
return nil
|
|
},
|
|
CheckoutBranchFn: func(name string) error {
|
|
checkedOut = name
|
|
return nil
|
|
},
|
|
RevParseFn: func(ref string) (string, error) { return "abc", nil },
|
|
})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
err := runAdd(cfg, &addOptions{}, []string{"existing-branch"})
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
require.NoError(t, err)
|
|
require.NotContains(t, output, "\u2717", "unexpected error")
|
|
assert.False(t, createBranchCalled, "CreateBranch should NOT be called for existing branch")
|
|
assert.Equal(t, "existing-branch", checkedOut, "should checkout the existing branch")
|
|
assert.Contains(t, output, "Adopted")
|
|
|
|
sf, err := stack.Load(gitDir)
|
|
require.NoError(t, err)
|
|
names := sf.Stacks[0].BranchNames()
|
|
assert.Equal(t, "existing-branch", names[len(names)-1], "adopted branch appended to stack")
|
|
assert.Equal(t, "common-base", sf.Stacks[0].Branches[len(sf.Stacks[0].Branches)-1].Base,
|
|
"adopted branch should record the actual common ancestor")
|
|
}
|
|
|
|
func TestAdd_RejectsExistingBranchInStack(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
// Two stacks: the current one and another that owns "taken-branch"
|
|
sf := &stack.StackFile{
|
|
SchemaVersion: 1,
|
|
Stacks: []stack.Stack{
|
|
{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}},
|
|
},
|
|
{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "taken-branch"}},
|
|
},
|
|
},
|
|
}
|
|
require.NoError(t, stack.Save(gitDir, sf), "saving seed stacks")
|
|
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
BranchExistsFn: func(name string) bool { return name == "taken-branch" },
|
|
})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
err := runAdd(cfg, &addOptions{}, []string{"taken-branch"})
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
assert.ErrorIs(t, err, ErrInvalidArgs)
|
|
assert.Contains(t, output, "already exists in the stack")
|
|
}
|
|
|
|
func TestAdd_AdoptsExistingBranchWithCommit(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
saveStack(t, gitDir, stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}},
|
|
})
|
|
|
|
createBranchCalled := false
|
|
commitCalled := false
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
BranchExistsFn: func(name string) bool { return name == "existing-branch" },
|
|
MergeBaseFn: func(string, string) (string, error) { return "common-base", nil },
|
|
RevParseMultiFn: func(refs []string) ([]string, error) {
|
|
return []string{"aaa", "bbb"}, nil // different SHAs = branch has commits
|
|
},
|
|
CreateBranchFn: func(name, base string) error {
|
|
createBranchCalled = true
|
|
return nil
|
|
},
|
|
CheckoutBranchFn: func(name string) error { return nil },
|
|
RevParseFn: func(ref string) (string, error) { return "abc", nil },
|
|
StageAllFn: func() error { return nil },
|
|
HasStagedChangesFn: func() bool { return true },
|
|
CommitFn: func(msg string) (string, error) {
|
|
commitCalled = true
|
|
return "def1234567890", nil
|
|
},
|
|
})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
err := runAdd(cfg, &addOptions{stageAll: true, message: "new commit"}, []string{"existing-branch"})
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
require.NoError(t, err)
|
|
require.NotContains(t, output, "\u2717", "unexpected error")
|
|
assert.False(t, createBranchCalled, "CreateBranch should NOT be called")
|
|
assert.True(t, commitCalled, "Commit should be called on the adopted branch")
|
|
assert.Contains(t, output, "Adopted")
|
|
}
|
|
|
|
func TestAdd_AdoptExistingBranchWithoutCommonBaseFails(t *testing.T) {
|
|
gitDir := t.TempDir()
|
|
saveStack(t, gitDir, stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}},
|
|
})
|
|
|
|
checkedOut := false
|
|
restore := git.SetOps(&git.MockOps{
|
|
GitDirFn: func() (string, error) { return gitDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
BranchExistsFn: func(name string) bool { return name == "unrelated" },
|
|
MergeBaseFn: func(string, string) (string, error) { return "", assert.AnError },
|
|
CheckoutBranchFn: func(string) error {
|
|
checkedOut = true
|
|
return nil
|
|
},
|
|
})
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
err := runAdd(cfg, &addOptions{}, []string{"unrelated"})
|
|
output := collectOutput(cfg, outR, errR)
|
|
|
|
assert.ErrorIs(t, err, ErrSilent)
|
|
assert.False(t, checkedOut)
|
|
assert.Contains(t, output, "failed to determine the common base")
|
|
|
|
sf, loadErr := stack.Load(gitDir)
|
|
require.NoError(t, loadErr)
|
|
assert.Equal(t, []string{"b1"}, sf.Stacks[0].BranchNames())
|
|
}
|