Skip to content

Commit

Permalink
WIP: new command: gs commit pick
Browse files Browse the repository at this point in the history
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
  • Loading branch information
abhinav committed Dec 29, 2024
1 parent 1d44d01 commit 0585009
Show file tree
Hide file tree
Showing 14 changed files with 1,022 additions and 3 deletions.
6 changes: 6 additions & 0 deletions .changes/unreleased/Added-20241228-193338.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Added
body: >-
New 'commit pick' command allows cherry-picking commits
and updating the upstack branches, all with one command.
Run this without any arguments to pick a commit interactively.
time: 2024-12-28T19:33:38.719477-06:00
1 change: 1 addition & 0 deletions commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ package main
type commitCmd struct {
Create commitCreateCmd `cmd:"" aliases:"c" help:"Create a new commit"`
Amend commitAmendCmd `cmd:"" aliases:"a" help:"Amend the current commit"`
Pick commitPickCmd `cmd:"" aliases:"p" help:"Cherry-pick a commit"`
Split commitSplitCmd `cmd:"" aliases:"sp" help:"Split the current commit"`
}
184 changes: 184 additions & 0 deletions commit_pick.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package main

import (
"cmp"
"context"
"fmt"

"github.com/charmbracelet/log"
"go.abhg.dev/gs/internal/git"
"go.abhg.dev/gs/internal/spice"
"go.abhg.dev/gs/internal/spice/state"
"go.abhg.dev/gs/internal/text"
"go.abhg.dev/gs/internal/ui"
"go.abhg.dev/gs/internal/ui/widget"
)

type commitPickCmd struct {
Commit string `arg:"" optional:"" help:"Commit to cherry-pick"`
// TODO: Support multiple commits similarly to git cherry-pick.

Edit bool `default:"false" negatable:"" config:"commitPick.edit" help:"Whether to open an editor to edit the commit message."`
From string `placeholder:"NAME" predictor:"trackedBranches" help:"Branch whose upstack commits will be considered."`
}

func (*commitPickCmd) Help() string {
return text.Dedent(`
Apply the changes introduced by a commit to the current branch
and restack the upstack branches.
If a commit is not specified, a prompt will allow picking
from commits of upstack branches of the current branch.
Use the --from option to pick a commit from a different branch
or its upstack.
By default, commit messages for cherry-picked commits will be used verbatim.
Supply --edit to open an editor and change the commit message,
or set the spice.commitPick.edit configuration option to true
to always open an editor for cherry picks.
`)
}

func (cmd *commitPickCmd) Run(
ctx context.Context,
log *log.Logger,
view ui.View,
repo *git.Repository,
store *state.Store,
svc *spice.Service,
) (err error) {
var commit git.Hash
if cmd.Commit == "" {
if !ui.Interactive(view) {
return fmt.Errorf("no commit specified: %w", errNoPrompt)
}

commit, err = cmd.commitPrompt(ctx, log, view, repo, store, svc)
if err != nil {
return fmt.Errorf("prompt for commit: %w", err)
}
} else {
commit, err = repo.PeelToCommit(ctx, cmd.Commit)
if err != nil {
return fmt.Errorf("peel to commit: %w", err)
}
}

log.Debugf("Cherry-picking: %v", commit)
err = repo.CherryPick(ctx, git.CherryPickRequest{
Commits: []git.Hash{commit},
Edit: cmd.Edit,
// If you selected an empty commit,
// you probably want to retain that.
// This still won't allow for no-op cherry-picks.
AllowEmpty: true,
})
if err != nil {
return fmt.Errorf("cherry-pick: %w", err)
}

// TODO: cherry-pick the commit
// TODO: handle --continue/--abort
// TODO: upstack restack
return nil
}

func (cmd *commitPickCmd) commitPrompt(
ctx context.Context,
log *log.Logger,
view ui.View,
repo *git.Repository,
store *state.Store,
svc *spice.Service,
) (git.Hash, error) {
currentBranch, err := repo.CurrentBranch(ctx)
if err != nil {
// TODO: allow for cherry-pick onto non-branch HEAD.
return "", fmt.Errorf("determine current branch: %w", err)
}
cmd.From = cmp.Or(cmd.From, currentBranch)

upstack, err := svc.ListUpstack(ctx, cmd.From)
if err != nil {
return "", fmt.Errorf("list upstack branches: %w", err)
}

var totalCommits int
branches := make([]widget.CommitPickBranch, 0, len(upstack))
shortToLongHash := make(map[git.Hash]git.Hash)
for _, name := range upstack {
if name == store.Trunk() {
continue
}

// TODO: build commit list for each branch concurrently
b, err := svc.LookupBranch(ctx, name)
if err != nil {
log.Warn("Could not look up branch. Skipping.",
"branch", name, "error", err)
continue
}

// If doing a --from=$other,
// where $other is downstack from current,
// we don't want to list commits for current branch,
// so add an empty entry for it.
if name == currentBranch {
// Don't list the current branch's commits.
branches = append(branches, widget.CommitPickBranch{
Branch: name,
Base: b.Base,
})
continue
}

commits, err := repo.ListCommitsDetails(ctx,
git.CommitRangeFrom(b.Head).
ExcludeFrom(b.BaseHash).
FirstParent())
if err != nil {
log.Warn("Could not list commits for branch. Skipping.",
"branch", name, "error", err)
}

commitSummaries := make([]widget.CommitSummary, len(commits))
for i, c := range commits {
commitSummaries[i] = widget.CommitSummary{
ShortHash: c.ShortHash,
Subject: c.Subject,
AuthorDate: c.AuthorDate,
}
shortToLongHash[c.ShortHash] = c.Hash
}

branches = append(branches, widget.CommitPickBranch{
Branch: name,
Base: b.Base,
Commits: commitSummaries,
})
totalCommits += len(commitSummaries)
}

if totalCommits == 0 {
log.Warn("Please provide a commit hash to cherry pick from.")
return "", fmt.Errorf("upstack of %v does not have any commits to cherry-pick", cmd.From)
}

msg := fmt.Sprintf("Selected commit will be cherry-picked into %v", currentBranch)
var selected git.Hash
prompt := widget.NewCommitPick().
WithTitle("Pick a commit").
WithDescription(msg).
WithBranches(branches...).
WithValue(&selected)
if err := ui.Run(view, prompt); err != nil {
return "", err
}

if long, ok := shortToLongHash[selected]; ok {
// This will always be true but it doesn't hurt
// to be defensive here.
selected = long
}
return selected, nil
}
36 changes: 34 additions & 2 deletions doc/includes/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ gs (git-spice) is a command line tool for stacking Git branches.
* `-C`, `--dir=DIR`: Change to DIR before doing anything
* `--[no-]prompt`: Whether to prompt for missing information

**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)
**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)

## Shell

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

### gs commit pick

```
gs commit (c) pick (p) [<commit>] [flags]
```

Cherry-pick a commit

Apply the changes introduced by a commit to the current branch
and restack the upstack branches.

If a commit is not specified, a prompt will allow picking
from commits of upstack branches of the current branch.
Use the --from option to pick a commit from a different branch
or its upstack.

By default, commit messages for cherry-picked commits will be used verbatim.
Supply --edit to open an editor and change the commit message,
or set the spice.commitPick.edit configuration option to true
to always open an editor for cherry picks.

**Arguments**

* `commit`: Commit to cherry-pick

**Flags**

* `--[no-]edit` ([:material-wrench:{ .middle title="spice.commitPick.edit" }](/cli/config.md#spicecommitpickedit)): Whether to open an editor to edit the commit message.
* `--from=NAME`: Branch whose upstack commits will be considered.

**Configuration**: [spice.commitPick.edit](/cli/config.md#spicecommitpickedit)

### gs commit split

```
Expand Down Expand Up @@ -863,7 +895,7 @@ and use --edit to override it.

**Flags**

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

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

Expand Down
1 change: 1 addition & 0 deletions doc/includes/cli-shorthands.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
| gs buntr | [gs branch untrack](/cli/reference.md#gs-branch-untrack) |
| gs ca | [gs commit amend](/cli/reference.md#gs-commit-amend) |
| gs cc | [gs commit create](/cli/reference.md#gs-commit-create) |
| gs cp | [gs commit pick](/cli/reference.md#gs-commit-pick) |
| gs csp | [gs commit split](/cli/reference.md#gs-commit-split) |
| gs dse | [gs downstack edit](/cli/reference.md#gs-downstack-edit) |
| gs dss | [gs downstack submit](/cli/reference.md#gs-downstack-submit) |
Expand Down
15 changes: 15 additions & 0 deletions doc/src/cli/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ and use the `--commit` flag to commit changes when needed.
- `true` (default)
- `false`

### spice.commitPick.edit

<!-- gs:version unreleased -->

Whether $$gs commit pick$$ should open an editor to modify commit messages
of cherry-picked commits before committing them.

If set to true, opt-out with the `--no-edit` flag.
If set to false, opt-in with the `--edit` flag.

**Accepted values:**

- `true`
- `false` (default)

### spice.forge.github.apiUrl

URL at which the GitHub API is available.
Expand Down
Loading

0 comments on commit 0585009

Please sign in to comment.