Skip to content

Commit 850e75f

Browse files
committed
WIP: new command: gs commit pick
Allows cherry-picking commits into the current branch and restacks the upstack. Two modes of usage: gs commit pick <commit> gs commit pick In the first, not much different from 'git cherry-pick'. In latter form, presents a visualization of commits in upstack branches to allow selecting one. --from=other can be used to view branches and commits from elsewhere. TODO: - [ ] --continue/--abort/--skip flags? - [ ] Doc website update Resolves #372
1 parent 4f40333 commit 850e75f

File tree

8 files changed

+409
-2
lines changed

8 files changed

+409
-2
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Added
2+
body: >-
3+
New 'commit pick' command allows cherry-picking commits
4+
and updating the upstack branches, all with one command.
5+
Run this without any arguments to pick a commit interactively.
6+
time: 2024-12-28T19:33:38.719477-06:00

commit.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ package main
33
type commitCmd struct {
44
Create commitCreateCmd `cmd:"" aliases:"c" help:"Create a new commit"`
55
Amend commitAmendCmd `cmd:"" aliases:"a" help:"Amend the current commit"`
6+
Pick commitPickCmd `cmd:"" aliases:"p" help:"Cherry-pick a commit"`
67
Split commitSplitCmd `cmd:"" aliases:"sp" help:"Split the current commit"`
78
}

commit_pick.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package main
2+
3+
import (
4+
"cmp"
5+
"context"
6+
"fmt"
7+
8+
"go.abhg.dev/gs/internal/git"
9+
"go.abhg.dev/gs/internal/silog"
10+
"go.abhg.dev/gs/internal/sliceutil"
11+
"go.abhg.dev/gs/internal/spice"
12+
"go.abhg.dev/gs/internal/spice/state"
13+
"go.abhg.dev/gs/internal/text"
14+
"go.abhg.dev/gs/internal/ui"
15+
"go.abhg.dev/gs/internal/ui/widget"
16+
)
17+
18+
type commitPickCmd struct {
19+
Commit string `arg:"" optional:"" help:"Commit to cherry-pick"`
20+
// TODO: Support multiple commits similarly to git cherry-pick.
21+
22+
Edit bool `default:"false" negatable:"" config:"commitPick.edit" help:"Whether to open an editor to edit the commit message."`
23+
From string `placeholder:"NAME" predictor:"trackedBranches" help:"Branch whose upstack commits will be considered."`
24+
}
25+
26+
func (*commitPickCmd) Help() string {
27+
return text.Dedent(`
28+
Apply the changes introduced by a commit to the current branch
29+
and restack the upstack branches.
30+
31+
If a commit is not specified, a prompt will allow picking
32+
from commits of upstack branches of the current branch.
33+
Use the --from option to pick a commit from a different branch
34+
or its upstack.
35+
36+
By default, commit messages for cherry-picked commits will be used verbatim.
37+
Supply --edit to open an editor and change the commit message,
38+
or set the spice.commitPick.edit configuration option to true
39+
to always open an editor for cherry picks.
40+
`)
41+
}
42+
43+
func (cmd *commitPickCmd) Run(
44+
ctx context.Context,
45+
log *silog.Logger,
46+
view ui.View,
47+
repo *git.Repository,
48+
wt *git.Worktree,
49+
store *state.Store,
50+
svc *spice.Service,
51+
) (err error) {
52+
var commit git.Hash
53+
if cmd.Commit == "" {
54+
if !ui.Interactive(view) {
55+
return fmt.Errorf("no commit specified: %w", errNoPrompt)
56+
}
57+
58+
commit, err = cmd.commitPrompt(ctx, log, view, repo, wt, store, svc)
59+
if err != nil {
60+
return fmt.Errorf("prompt for commit: %w", err)
61+
}
62+
} else {
63+
commit, err = repo.PeelToCommit(ctx, cmd.Commit)
64+
if err != nil {
65+
return fmt.Errorf("peel to commit: %w", err)
66+
}
67+
}
68+
69+
log.Debugf("Cherry-picking: %v", commit)
70+
err = repo.CherryPick(ctx, git.CherryPickRequest{
71+
Commits: []git.Hash{commit},
72+
Edit: cmd.Edit,
73+
// If you selected an empty commit,
74+
// you probably want to retain that.
75+
// This still won't allow for no-op cherry-picks.
76+
AllowEmpty: true,
77+
})
78+
if err != nil {
79+
return fmt.Errorf("cherry-pick: %w", err)
80+
}
81+
82+
// TODO: cherry-pick the commit
83+
// TODO: handle --continue/--abort
84+
// TODO: upstack restack
85+
return nil
86+
}
87+
88+
func (cmd *commitPickCmd) commitPrompt(
89+
ctx context.Context,
90+
log *silog.Logger,
91+
view ui.View,
92+
repo *git.Repository,
93+
wt *git.Worktree,
94+
store *state.Store,
95+
svc *spice.Service,
96+
) (git.Hash, error) {
97+
currentBranch, err := wt.CurrentBranch(ctx)
98+
if err != nil {
99+
// TODO: allow for cherry-pick onto non-branch HEAD.
100+
return "", fmt.Errorf("determine current branch: %w", err)
101+
}
102+
cmd.From = cmp.Or(cmd.From, currentBranch)
103+
104+
upstack, err := svc.ListUpstack(ctx, cmd.From)
105+
if err != nil {
106+
return "", fmt.Errorf("list upstack branches: %w", err)
107+
}
108+
109+
var totalCommits int
110+
branches := make([]widget.CommitPickBranch, 0, len(upstack))
111+
shortToLongHash := make(map[git.Hash]git.Hash)
112+
for _, name := range upstack {
113+
if name == store.Trunk() {
114+
continue
115+
}
116+
117+
// TODO: build commit list for each branch concurrently
118+
b, err := svc.LookupBranch(ctx, name)
119+
if err != nil {
120+
log.Warn("Could not look up branch. Skipping.",
121+
"branch", name, "error", err)
122+
continue
123+
}
124+
125+
// If doing a --from=$other,
126+
// where $other is downstack from current,
127+
// we don't want to list commits for current branch,
128+
// so add an empty entry for it.
129+
if name == currentBranch {
130+
// Don't list the current branch's commits.
131+
branches = append(branches, widget.CommitPickBranch{
132+
Branch: name,
133+
Base: b.Base,
134+
})
135+
continue
136+
}
137+
138+
commits, err := sliceutil.CollectErr(repo.ListCommitsDetails(ctx,
139+
git.CommitRangeFrom(b.Head).
140+
ExcludeFrom(b.BaseHash).
141+
FirstParent()))
142+
if err != nil {
143+
log.Warn("Could not list commits for branch. Skipping.",
144+
"branch", name, "error", err)
145+
continue
146+
}
147+
148+
commitSummaries := make([]widget.CommitSummary, len(commits))
149+
for i, c := range commits {
150+
commitSummaries[i] = widget.CommitSummary{
151+
ShortHash: c.ShortHash,
152+
Subject: c.Subject,
153+
AuthorDate: c.AuthorDate,
154+
}
155+
shortToLongHash[c.ShortHash] = c.Hash
156+
}
157+
158+
branches = append(branches, widget.CommitPickBranch{
159+
Branch: name,
160+
Base: b.Base,
161+
Commits: commitSummaries,
162+
})
163+
totalCommits += len(commitSummaries)
164+
}
165+
166+
if totalCommits == 0 {
167+
log.Warn("Please provide a commit hash to cherry pick from.")
168+
return "", fmt.Errorf("upstack of %v does not have any commits to cherry-pick", cmd.From)
169+
}
170+
171+
msg := fmt.Sprintf("Selected commit will be cherry-picked into %v", currentBranch)
172+
var selected git.Hash
173+
prompt := widget.NewCommitPick().
174+
WithTitle("Pick a commit").
175+
WithDescription(msg).
176+
WithBranches(branches...).
177+
WithValue(&selected)
178+
if err := ui.Run(view, prompt); err != nil {
179+
return "", err
180+
}
181+
182+
if long, ok := shortToLongHash[selected]; ok {
183+
// This will always be true but it doesn't hurt
184+
// to be defensive here.
185+
selected = long
186+
}
187+
return selected, nil
188+
}

doc/includes/cli-reference.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,38 @@ followed by 'gs upstack restack'.
955955

956956
**Configuration**: [spice.branchCreate.prefix](/cli/config.md#spicebranchcreateprefix)
957957

958+
### gs commit pick
959+
960+
```
961+
gs commit (c) pick (p) [<commit>] [flags]
962+
```
963+
964+
Cherry-pick a commit
965+
966+
Apply the changes introduced by a commit to the current branch
967+
and restack the upstack branches.
968+
969+
If a commit is not specified, a prompt will allow picking
970+
from commits of upstack branches of the current branch.
971+
Use the --from option to pick a commit from a different branch
972+
or its upstack.
973+
974+
By default, commit messages for cherry-picked commits will be used verbatim.
975+
Supply --edit to open an editor and change the commit message,
976+
or set the spice.commitPick.edit configuration option to true
977+
to always open an editor for cherry picks.
978+
979+
**Arguments**
980+
981+
* `commit`: Commit to cherry-pick
982+
983+
**Flags**
984+
985+
* `--[no-]edit` ([:material-wrench:{ .middle title="spice.commitPick.edit" }](/cli/config.md#spicecommitpickedit)): Whether to open an editor to edit the commit message.
986+
* `--from=NAME`: Branch whose upstack commits will be considered.
987+
988+
**Configuration**: [spice.commitPick.edit](/cli/config.md#spicecommitpickedit)
989+
958990
### gs commit split
959991

960992
```
@@ -998,7 +1030,7 @@ and use --edit to override it.
9981030

9991031
**Flags**
10001032

1001-
* `--[no-]edit` ([:material-wrench:{ .middle title="spice.rebaseContinue.edit" }](/cli/config.md#spicerebasecontinueedit)): Whehter to open an editor to edit the commit message.
1033+
* `--[no-]edit` ([:material-wrench:{ .middle title="spice.rebaseContinue.edit" }](/cli/config.md#spicerebasecontinueedit)): Whether to open an editor to edit the commit message.
10021034

10031035
**Configuration**: [spice.rebaseContinue.edit](/cli/config.md#spicerebasecontinueedit)
10041036

doc/includes/cli-shorthands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
| gs buntr | [gs branch untrack](/cli/reference.md#gs-branch-untrack) |
1616
| gs ca | [gs commit amend](/cli/reference.md#gs-commit-amend) |
1717
| gs cc | [gs commit create](/cli/reference.md#gs-commit-create) |
18+
| gs cp | [gs commit pick](/cli/reference.md#gs-commit-pick) |
1819
| gs csp | [gs commit split](/cli/reference.md#gs-commit-split) |
1920
| gs dse | [gs downstack edit](/cli/reference.md#gs-downstack-edit) |
2021
| gs dss | [gs downstack submit](/cli/reference.md#gs-downstack-submit) |

doc/src/cli/config.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,21 @@ should print a message when switching branches.
126126
- `true` (default)
127127
- `false`
128128

129+
### spice.commitPick.edit
130+
131+
<!-- gs:version unreleased -->
132+
133+
Whether $$gs commit pick$$ should open an editor to modify commit messages
134+
of cherry-picked commits before committing them.
135+
136+
If set to true, opt-out with the `--no-edit` flag.
137+
If set to false, opt-in with the `--edit` flag.
138+
139+
**Accepted values:**
140+
141+
- `true`
142+
- `false` (default)
143+
129144
### spice.forge.github.apiUrl
130145

131146
URL at which the GitHub API is available.

0 commit comments

Comments
 (0)