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

feat(branch): add gs branch squash #564

Merged
merged 10 commits into from
Feb 3, 2025
3 changes: 3 additions & 0 deletions .changes/unreleased/Added-20250202-192729.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Added
body: 'branch squash: Squashses all commits in the current branch into a single commit and restacks the upstack branches.'
time: 2025-02-02T19:27:29.806629-08:00
1 change: 1 addition & 0 deletions branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type branchCmd struct {
Delete branchDeleteCmd `cmd:"" aliases:"d,rm" help:"Delete branches"`
Fold branchFoldCmd `cmd:"" aliases:"fo" help:"Merge a branch into its base"`
Split branchSplitCmd `cmd:"" aliases:"sp" help:"Split a branch on commits"`
Squash branchSquashCmd `cmd:"" aliases:"sq" help:"Squash a branch into one commit"`

// Mutation
Edit branchEditCmd `cmd:"" aliases:"e" help:"Edit the commits in a branch"`
Expand Down
2 changes: 1 addition & 1 deletion branch_checkout.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type branchCheckoutCmd struct {
checkoutOptions

Untracked bool `short:"u" config:"branchCheckout.showUntracked" help:"Show untracked branches if one isn't supplied"`
Branch string `arg:"" optional:"" help:"Name of the branch to delete" predictor:"branches"`
Branch string `arg:"" optional:"" help:"Name of the branch to checkout" predictor:"branches"`
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😅 thanks!

}

func (*branchCheckoutCmd) Help() string {
Expand Down
102 changes: 102 additions & 0 deletions branch_squash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"context"
"errors"
"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"
)

type branchSquashCmd struct {
Message string `short:"m" placeholder:"MSG" help:"Use the given message as the commit message."`
}

func (*branchSquashCmd) Help() string {
return text.Dedent(`
Squash all commits in the current branch into a single commit
and restack upstack branches.
`)
}

func (cmd *branchSquashCmd) Run(
ctx context.Context,
log *log.Logger,
view ui.View,
repo *git.Repository,
store *state.Store,
svc *spice.Service,
) error {
branchName, err := repo.CurrentBranch(ctx)
if err != nil {
return fmt.Errorf("get current branch: %w", err)
}

branch, err := svc.LookupBranch(ctx, branchName)
if err != nil {
return fmt.Errorf("lookup branch %q: %w", branchName, err)
}

if err := svc.VerifyRestacked(ctx, branchName); err != nil {
var restackErr *spice.BranchNeedsRestackError
if errors.As(err, &restackErr) {
return fmt.Errorf("branch %v needs to be restacked before it can be squashed", branchName)
}
return fmt.Errorf("verify restacked: %w", err)
}

commitMsg := cmd.Message

if commitMsg == "" {
commitMessages, err := repo.CommitMessageRange(ctx, branch.Head.String(), branch.BaseHash.String())
if err != nil {
return fmt.Errorf("get commit messages: %w", err)
}
commitMsg = commitMessages[len(commitMessages)-1].String()
}

// Checkout the branch in detached mode
if err := (&branchCheckoutCmd{
Branch: branchName,
checkoutOptions: checkoutOptions{
Detach: true,
},
}).Run(ctx, log, view, repo, store, svc); err != nil {
return err
}

// Reset the detached branch to the base commit
if err := repo.Reset(ctx, branch.BaseHash.String(), git.ResetOptions{Mode: git.ResetSoft}); err != nil {
return err
}

// Commit the changes
if err := repo.Commit(ctx, git.CommitRequest{Message: commitMsg}); err != nil {
return err
}

// Replace the HEAD ref of `branchName` with the new commit
headHash, err := repo.Head(ctx)
if err != nil {
return err
}

if err := repo.SetRef(ctx, git.SetRefRequest{
Ref: "refs/heads/" + branchName,
Hash: headHash,
}); err != nil {
return err
}

// Check out the original branch
if err := repo.Checkout(ctx, branchName); err != nil {
return err
}

return (&upstackRestackCmd{}).Run(ctx, log, repo, store, svc)
}
17 changes: 16 additions & 1 deletion doc/includes/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ Use -u/--untracked to show untracked branches in the prompt.

**Arguments**

* `branch`: Name of the branch to delete
* `branch`: Name of the branch to checkout

**Flags**

Expand Down Expand Up @@ -642,6 +642,21 @@ For example:
* `--at=COMMIT:NAME,...`: Commits to split the branch at.
* `--branch=NAME`: Branch to split commits of.

### gs branch squash

```
gs branch (b) squash (sq) [flags]
```

Squash a branch into one commit

Squash all commits in the current branch into a single commit
and restack upstack branches.

**Flags**

* `-m`, `--message=MSG`: Use the given message as the commit message.

### gs branch edit

```
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 @@ -10,6 +10,7 @@
| gs brn | [gs branch rename](/cli/reference.md#gs-branch-rename) |
| gs bs | [gs branch submit](/cli/reference.md#gs-branch-submit) |
| gs bsp | [gs branch split](/cli/reference.md#gs-branch-split) |
| gs bsq | [gs branch squash](/cli/reference.md#gs-branch-squash) |
| gs btr | [gs branch track](/cli/reference.md#gs-branch-track) |
| gs buntr | [gs branch untrack](/cli/reference.md#gs-branch-untrack) |
| gs ca | [gs commit amend](/cli/reference.md#gs-commit-amend) |
Expand Down
47 changes: 47 additions & 0 deletions testdata/script/branch_squash.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Squashing a branch into one commit with 'squash'

as 'Test <[email protected]>'
at '2024-03-30T14:59:32Z'

cd repo
git init
git commit --allow-empty -m 'Initial commit'
gs repo i

# Create a branch with two commits
git add feature1.txt
gs branch create feature-1 -m 'First message in squashed branch'
git add feature2.txt
git commit -m 'Second message in squashed branch'

# Create another branch
git add feature3.txt
gs branch create feature2 -m 'First message in rebased branch'

git graph --branches
cmp stdout $WORK/golden/graph-before.txt

# Go back to branch that will be squashed
gs down
gs branch squash

git graph --branches
cmp stdout $WORK/golden/graph.txt

-- repo/dirty.txt --
Dirty
-- repo/feature1.txt --
Feature 1
-- repo/feature2.txt --
Feature 2
-- repo/feature3.txt --
Feature 3
-- golden/graph-before.txt --
* d805cb2 (HEAD -> feature2) First message in rebased branch
* 0239007 (feature-1) Second message in squashed branch
* 7ebfd80 First message in squashed branch
* 9bad92b (main) Initial commit
-- golden/graph.txt --
* cbeb99e (feature2) First message in rebased branch
* 6ca0ea8 (HEAD -> feature-1) First message in squashed branch
* 9bad92b (main) Initial commit
47 changes: 47 additions & 0 deletions testdata/script/branch_squash_message.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Squashing a branch into one commit with 'squash'

as 'Test <[email protected]>'
at '2024-03-30T14:59:32Z'

cd repo
git init
git commit --allow-empty -m 'Initial commit'
gs repo i

# Create a branch with two commits
git add feature1.txt
gs branch create feature-1 -m 'First message in squashed branch'
git add feature2.txt
git commit -m 'Second message in squashed branch'

# Create another branch
git add feature3.txt
gs branch create feature2 -m 'First message in rebased branch'

git graph --branches
cmp stdout $WORK/golden/graph-before.txt

# Go back to branch that will be squashed
gs down
gs branch squash -m 'squash feature-1 into one commit'

git graph --branches
cmp stdout $WORK/golden/graph.txt

-- repo/dirty.txt --
Dirty
-- repo/feature1.txt --
Feature 1
-- repo/feature2.txt --
Feature 2
-- repo/feature3.txt --
Feature 3
-- golden/graph-before.txt --
* d805cb2 (HEAD -> feature2) First message in rebased branch
* 0239007 (feature-1) Second message in squashed branch
* 7ebfd80 First message in squashed branch
* 9bad92b (main) Initial commit
-- golden/graph.txt --
* 8323247 (feature2) First message in rebased branch
* 3267250 (HEAD -> feature-1) squash feature-1 into one commit
* 9bad92b (main) Initial commit