Skip to content

Commit ec0aa78

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 dde5dc2 commit ec0aa78

File tree

8 files changed

+407
-2
lines changed

8 files changed

+407
-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: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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+
store *state.Store,
49+
svc *spice.Service,
50+
) (err error) {
51+
var commit git.Hash
52+
if cmd.Commit == "" {
53+
if !ui.Interactive(view) {
54+
return fmt.Errorf("no commit specified: %w", errNoPrompt)
55+
}
56+
57+
commit, err = cmd.commitPrompt(ctx, log, view, repo, store, svc)
58+
if err != nil {
59+
return fmt.Errorf("prompt for commit: %w", err)
60+
}
61+
} else {
62+
commit, err = repo.PeelToCommit(ctx, cmd.Commit)
63+
if err != nil {
64+
return fmt.Errorf("peel to commit: %w", err)
65+
}
66+
}
67+
68+
log.Debugf("Cherry-picking: %v", commit)
69+
err = repo.CherryPick(ctx, git.CherryPickRequest{
70+
Commits: []git.Hash{commit},
71+
Edit: cmd.Edit,
72+
// If you selected an empty commit,
73+
// you probably want to retain that.
74+
// This still won't allow for no-op cherry-picks.
75+
AllowEmpty: true,
76+
})
77+
if err != nil {
78+
return fmt.Errorf("cherry-pick: %w", err)
79+
}
80+
81+
// TODO: cherry-pick the commit
82+
// TODO: handle --continue/--abort
83+
// TODO: upstack restack
84+
return nil
85+
}
86+
87+
func (cmd *commitPickCmd) commitPrompt(
88+
ctx context.Context,
89+
log *silog.Logger,
90+
view ui.View,
91+
repo *git.Repository,
92+
store *state.Store,
93+
svc *spice.Service,
94+
) (git.Hash, error) {
95+
currentBranch, err := repo.CurrentBranch(ctx)
96+
if err != nil {
97+
// TODO: allow for cherry-pick onto non-branch HEAD.
98+
return "", fmt.Errorf("determine current branch: %w", err)
99+
}
100+
cmd.From = cmp.Or(cmd.From, currentBranch)
101+
102+
upstack, err := svc.ListUpstack(ctx, cmd.From)
103+
if err != nil {
104+
return "", fmt.Errorf("list upstack branches: %w", err)
105+
}
106+
107+
var totalCommits int
108+
branches := make([]widget.CommitPickBranch, 0, len(upstack))
109+
shortToLongHash := make(map[git.Hash]git.Hash)
110+
for _, name := range upstack {
111+
if name == store.Trunk() {
112+
continue
113+
}
114+
115+
// TODO: build commit list for each branch concurrently
116+
b, err := svc.LookupBranch(ctx, name)
117+
if err != nil {
118+
log.Warn("Could not look up branch. Skipping.",
119+
"branch", name, "error", err)
120+
continue
121+
}
122+
123+
// If doing a --from=$other,
124+
// where $other is downstack from current,
125+
// we don't want to list commits for current branch,
126+
// so add an empty entry for it.
127+
if name == currentBranch {
128+
// Don't list the current branch's commits.
129+
branches = append(branches, widget.CommitPickBranch{
130+
Branch: name,
131+
Base: b.Base,
132+
})
133+
continue
134+
}
135+
136+
commits, err := sliceutil.CollectErr(repo.ListCommitsDetails(ctx,
137+
git.CommitRangeFrom(b.Head).
138+
ExcludeFrom(b.BaseHash).
139+
FirstParent()))
140+
if err != nil {
141+
log.Warn("Could not list commits for branch. Skipping.",
142+
"branch", name, "error", err)
143+
continue
144+
}
145+
146+
commitSummaries := make([]widget.CommitSummary, len(commits))
147+
for i, c := range commits {
148+
commitSummaries[i] = widget.CommitSummary{
149+
ShortHash: c.ShortHash,
150+
Subject: c.Subject,
151+
AuthorDate: c.AuthorDate,
152+
}
153+
shortToLongHash[c.ShortHash] = c.Hash
154+
}
155+
156+
branches = append(branches, widget.CommitPickBranch{
157+
Branch: name,
158+
Base: b.Base,
159+
Commits: commitSummaries,
160+
})
161+
totalCommits += len(commitSummaries)
162+
}
163+
164+
if totalCommits == 0 {
165+
log.Warn("Please provide a commit hash to cherry pick from.")
166+
return "", fmt.Errorf("upstack of %v does not have any commits to cherry-pick", cmd.From)
167+
}
168+
169+
msg := fmt.Sprintf("Selected commit will be cherry-picked into %v", currentBranch)
170+
var selected git.Hash
171+
prompt := widget.NewCommitPick().
172+
WithTitle("Pick a commit").
173+
WithDescription(msg).
174+
WithBranches(branches...).
175+
WithValue(&selected)
176+
if err := ui.Run(view, prompt); err != nil {
177+
return "", err
178+
}
179+
180+
if long, ok := shortToLongHash[selected]; ok {
181+
// This will always be true but it doesn't hurt
182+
// to be defensive here.
183+
selected = long
184+
}
185+
return selected, nil
186+
}

doc/includes/cli-reference.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,38 @@ followed by 'gs upstack restack'.
870870
* `--no-edit`: Don't edit the commit message
871871
* `--no-verify`: Bypass pre-commit and commit-msg hooks.
872872

873+
### gs commit pick
874+
875+
```
876+
gs commit (c) pick (p) [<commit>] [flags]
877+
```
878+
879+
Cherry-pick a commit
880+
881+
Apply the changes introduced by a commit to the current branch
882+
and restack the upstack branches.
883+
884+
If a commit is not specified, a prompt will allow picking
885+
from commits of upstack branches of the current branch.
886+
Use the --from option to pick a commit from a different branch
887+
or its upstack.
888+
889+
By default, commit messages for cherry-picked commits will be used verbatim.
890+
Supply --edit to open an editor and change the commit message,
891+
or set the spice.commitPick.edit configuration option to true
892+
to always open an editor for cherry picks.
893+
894+
**Arguments**
895+
896+
* `commit`: Commit to cherry-pick
897+
898+
**Flags**
899+
900+
* `--[no-]edit` ([:material-wrench:{ .middle title="spice.commitPick.edit" }](/cli/config.md#spicecommitpickedit)): Whether to open an editor to edit the commit message.
901+
* `--from=NAME`: Branch whose upstack commits will be considered.
902+
903+
**Configuration**: [spice.commitPick.edit](/cli/config.md#spicecommitpickedit)
904+
873905
### gs commit split
874906

875907
```
@@ -913,7 +945,7 @@ and use --edit to override it.
913945

914946
**Flags**
915947

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

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

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
@@ -112,6 +112,21 @@ Commonly used values are:
112112
- `<name>/`: the committer's name
113113
- `<username>/`: the committer's username
114114

115+
### spice.commitPick.edit
116+
117+
<!-- gs:version unreleased -->
118+
119+
Whether $$gs commit pick$$ should open an editor to modify commit messages
120+
of cherry-picked commits before committing them.
121+
122+
If set to true, opt-out with the `--no-edit` flag.
123+
If set to false, opt-in with the `--edit` flag.
124+
125+
**Accepted values:**
126+
127+
- `true`
128+
- `false` (default)
129+
115130
### spice.forge.github.apiUrl
116131

117132
URL at which the GitHub API is available.

0 commit comments

Comments
 (0)