Skip to content

Commit 0585009

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 1d44d01 commit 0585009

File tree

14 files changed

+1022
-3
lines changed

14 files changed

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

doc/includes/cli-reference.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ gs (git-spice) is a command line tool for stacking Git branches.
1212
* `-C`, `--dir=DIR`: Change to DIR before doing anything
1313
* `--[no-]prompt`: Whether to prompt for missing information
1414

15-
**Configuration**: [spice.forge.github.url](/cli/config.md#spiceforgegithuburl), [spice.forge.github.apiUrl](/cli/config.md#spiceforgegithubapiurl), [spice.forge.gitlab.url](/cli/config.md#spiceforgegitlaburl), [spice.forge.gitlab.oauth.clientID](/cli/config.md#spiceforgegitlaboauthclientid)
15+
**Configuration**: [spice.forge.gitlab.url](/cli/config.md#spiceforgegitlaburl), [spice.forge.gitlab.oauth.clientID](/cli/config.md#spiceforgegitlaboauthclientid), [spice.forge.github.url](/cli/config.md#spiceforgegithuburl), [spice.forge.github.apiUrl](/cli/config.md#spiceforgegithubapiurl)
1616

1717
## Shell
1818

@@ -820,6 +820,38 @@ followed by 'gs upstack restack'.
820820
* `--no-edit`: Don't edit the commit message
821821
* `--no-verify`: Bypass pre-commit and commit-msg hooks.
822822

823+
### gs commit pick
824+
825+
```
826+
gs commit (c) pick (p) [<commit>] [flags]
827+
```
828+
829+
Cherry-pick a commit
830+
831+
Apply the changes introduced by a commit to the current branch
832+
and restack the upstack branches.
833+
834+
If a commit is not specified, a prompt will allow picking
835+
from commits of upstack branches of the current branch.
836+
Use the --from option to pick a commit from a different branch
837+
or its upstack.
838+
839+
By default, commit messages for cherry-picked commits will be used verbatim.
840+
Supply --edit to open an editor and change the commit message,
841+
or set the spice.commitPick.edit configuration option to true
842+
to always open an editor for cherry picks.
843+
844+
**Arguments**
845+
846+
* `commit`: Commit to cherry-pick
847+
848+
**Flags**
849+
850+
* `--[no-]edit` ([:material-wrench:{ .middle title="spice.commitPick.edit" }](/cli/config.md#spicecommitpickedit)): Whether to open an editor to edit the commit message.
851+
* `--from=NAME`: Branch whose upstack commits will be considered.
852+
853+
**Configuration**: [spice.commitPick.edit](/cli/config.md#spicecommitpickedit)
854+
823855
### gs commit split
824856

825857
```
@@ -863,7 +895,7 @@ and use --edit to override it.
863895

864896
**Flags**
865897

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

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

doc/includes/cli-shorthands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
| gs buntr | [gs branch untrack](/cli/reference.md#gs-branch-untrack) |
1515
| gs ca | [gs commit amend](/cli/reference.md#gs-commit-amend) |
1616
| gs cc | [gs commit create](/cli/reference.md#gs-commit-create) |
17+
| gs cp | [gs commit pick](/cli/reference.md#gs-commit-pick) |
1718
| gs csp | [gs commit split](/cli/reference.md#gs-commit-split) |
1819
| gs dse | [gs downstack edit](/cli/reference.md#gs-downstack-edit) |
1920
| 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
@@ -54,6 +54,21 @@ and use the `--commit` flag to commit changes when needed.
5454
- `true` (default)
5555
- `false`
5656

57+
### spice.commitPick.edit
58+
59+
<!-- gs:version unreleased -->
60+
61+
Whether $$gs commit pick$$ should open an editor to modify commit messages
62+
of cherry-picked commits before committing them.
63+
64+
If set to true, opt-out with the `--no-edit` flag.
65+
If set to false, opt-in with the `--edit` flag.
66+
67+
**Accepted values:**
68+
69+
- `true`
70+
- `false` (default)
71+
5772
### spice.forge.github.apiUrl
5873

5974
URL at which the GitHub API is available.

0 commit comments

Comments
 (0)