Skip to content

Commit 2983c40

Browse files
authored
commit amend: prompt to create a branch on trunk (#709)
If 'commit amend' is called on the trunk branch, prompt the user to create a branch instead as that's rarely what users will actually want to do. Refs #700
1 parent 21e4d50 commit 2983c40

File tree

4 files changed

+161
-7
lines changed

4 files changed

+161
-7
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Changed
2+
body: >-
3+
commit amend:
4+
To prevent mistakes, when 'gs commit amend' is called from the trunk branch,
5+
confirm user intent, providing an option to create a new branch instead.
6+
time: 2025-06-22T15:24:32.431866-07:00

commit_amend.go

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ import (
1010
"go.abhg.dev/gs/internal/spice"
1111
"go.abhg.dev/gs/internal/spice/state"
1212
"go.abhg.dev/gs/internal/text"
13+
"go.abhg.dev/gs/internal/ui"
1314
)
1415

1516
type commitAmendCmd struct {
17+
branchCreateConfig // TODO: find a way to avoid this
18+
1619
All bool `short:"a" help:"Stage all changes before committing."`
1720
AllowEmpty bool `help:"Create a commit even if it contains no changes."`
1821
Message string `short:"m" placeholder:"MSG" help:"Use the given message as the commit message."`
@@ -37,6 +40,8 @@ func (*commitAmendCmd) Help() string {
3740
func (cmd *commitAmendCmd) Run(
3841
ctx context.Context,
3942
log *silog.Logger,
43+
view ui.View,
44+
repo *git.Repository,
4045
wt *git.Worktree,
4146
store *state.Store,
4247
svc *spice.Service,
@@ -46,6 +51,74 @@ func (cmd *commitAmendCmd) Run(
4651
log.Warn("flag '-n' is deprecated; use '--no-edit' instead")
4752
}
4853

54+
var detachedHead bool
55+
currentBranch, err := wt.CurrentBranch(ctx)
56+
if err != nil {
57+
if !errors.Is(err, git.ErrDetachedHead) {
58+
return fmt.Errorf("get current branch: %w", err)
59+
}
60+
detachedHead = true
61+
currentBranch = ""
62+
}
63+
64+
if currentBranch == store.Trunk() {
65+
if !ui.Interactive(view) {
66+
log.Warnf("You are about to amend a commit on the trunk branch (%v).", store.Trunk())
67+
} else {
68+
var (
69+
amendOnTrunk bool
70+
branchName string
71+
)
72+
fields := []ui.Field{
73+
ui.NewList[bool]().
74+
WithTitle("Do you want to amend a commit on trunk?").
75+
WithDescription(fmt.Sprintf("You are about to amend a commit on the trunk branch (%v). "+
76+
"This is usually not what you want to do.", store.Trunk())).
77+
WithItems(
78+
ui.ListItem[bool]{
79+
Title: "Yes",
80+
Description: func(bool) string {
81+
return "Amend the commit on trunk"
82+
},
83+
Value: true,
84+
},
85+
ui.ListItem[bool]{
86+
Title: "No",
87+
Description: func(bool) string {
88+
return "Create a branch and commit there instead"
89+
},
90+
Value: false,
91+
},
92+
).
93+
WithValue(&amendOnTrunk),
94+
ui.Defer(func() ui.Field {
95+
if amendOnTrunk {
96+
return nil
97+
}
98+
99+
return ui.NewInput().
100+
WithTitle("Branch name").
101+
WithDescription("What do you want to call the new branch?").
102+
WithValue(&branchName)
103+
}),
104+
}
105+
if err := ui.Run(view, fields...); err != nil {
106+
return fmt.Errorf("run prompt: %w", err)
107+
}
108+
if !amendOnTrunk {
109+
// TODO: shared commitOptions struct?
110+
return (&branchCreateCmd{
111+
branchCreateConfig: cmd.branchCreateConfig,
112+
Name: branchName,
113+
All: cmd.All,
114+
NoVerify: cmd.NoVerify,
115+
Message: cmd.Message,
116+
Commit: true,
117+
}).Run(ctx, log, repo, wt, store, svc)
118+
}
119+
}
120+
}
121+
49122
if err := wt.Commit(ctx, git.CommitRequest{
50123
Message: cmd.Message,
51124
AllowEmpty: cmd.AllowEmpty,
@@ -62,13 +135,9 @@ func (cmd *commitAmendCmd) Run(
62135
return nil
63136
}
64137

65-
currentBranch, err := wt.CurrentBranch(ctx)
66-
if err != nil {
67-
if errors.Is(err, git.ErrDetachedHead) {
68-
log.Debug("HEAD is detached, skipping restack")
69-
return nil
70-
}
71-
return fmt.Errorf("get current branch: %w", err)
138+
if detachedHead {
139+
log.Debug("HEAD is detached, skipping restack")
140+
return nil
72141
}
73142

74143
return (&upstackRestackCmd{

doc/includes/cli-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,8 @@ followed by 'gs upstack restack'.
874874
* `--no-edit`: Don't edit the commit message
875875
* `--no-verify`: Bypass pre-commit and commit-msg hooks.
876876

877+
**Configuration**: [spice.branchCreate.prefix](/cli/config.md#spicebranchcreateprefix)
878+
877879
### gs commit split
878880

879881
```
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# commit amend on trunk branch should offer to create a branch instead.
2+
3+
as 'Test <[email protected]>'
4+
at '2025-06-22T21:28:29Z'
5+
6+
cd repo
7+
git init
8+
git commit --allow-empty -m 'Initial commit'
9+
gs repo init
10+
11+
# Make a commit on trunk to amend
12+
git add foo.txt
13+
git commit -m 'Add foo.txt'
14+
15+
# Non-interactive mode warns but proceeds
16+
mv foo-1.txt foo.txt
17+
git add foo.txt
18+
gs commit amend -m 'Amended without prompt' --no-prompt
19+
cmp stderr $WORK/golden/no-prompt.txt
20+
git diff --quiet # ensure no diff
21+
git log --oneline -1 --pretty=format:'%s'
22+
stdout 'Amended without prompt'
23+
24+
# Interactive mode should prompt for confirmation.
25+
env ROBOT_INPUT=$WORK/golden/robot.txt ROBOT_OUTPUT=$WORK/robot.actual
26+
27+
# First prompt, we'll answer yes and proceed.
28+
mv foo-2.txt foo.txt
29+
git add foo.txt
30+
gs commit amend -m 'Amended with prompt'
31+
git diff --quiet # ensure no diff
32+
git log --oneline -1 --pretty=format:'%s'
33+
stdout 'Amended with prompt'
34+
35+
# Second prompt, we'll answer no and get a new branch instead.
36+
# This should work even if the branchPrefix config is set.
37+
git config spice.branchCreate.prefix 'feature/'
38+
mv foo-3.txt foo.txt
39+
git add foo.txt
40+
gs commit amend -m 'Committed on new branch'
41+
42+
cmp $WORK/robot.actual $WORK/golden/robot.txt
43+
44+
git branch --show-current
45+
stdout 'feature/my-new-branch'
46+
47+
-- repo/foo.txt --
48+
0
49+
-- repo/foo-1.txt --
50+
1
51+
-- repo/foo-2.txt --
52+
2
53+
-- repo/foo-3.txt --
54+
3
55+
-- golden/no-prompt.txt --
56+
WRN You are about to amend a commit on the trunk branch (main).
57+
-- golden/robot.txt --
58+
===
59+
> Do you want to amend a commit on trunk?:
60+
> ▶ Yes
61+
> Amend the commit on trunk
62+
> No
63+
> Create a branch and commit there instead
64+
> You are about to amend a commit on the trunk branch (main). This is usually not what you want to do.
65+
"Yes"
66+
===
67+
> Do you want to amend a commit on trunk?:
68+
> ▶ Yes
69+
> Amend the commit on trunk
70+
> No
71+
> Create a branch and commit there instead
72+
> You are about to amend a commit on the trunk branch (main). This is usually not what you want to do.
73+
"No"
74+
===
75+
> Branch name:
76+
> What do you want to call the new branch?
77+
"my-new-branch"

0 commit comments

Comments
 (0)