Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: new command: gs commit pick #536

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading