-
Notifications
You must be signed in to change notification settings - Fork 21
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
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
cc3710f
feat(branch): add `gs branch squash`
jonboiser 7b3a819
git/commit: learn about -t/--template
abhinav b317a28
squash: Use -t/--template to generate initial commit message
abhinav d16f2e1
squash: detach and restore with defer
abhinav 8e1fdf1
doc: explain why we're doing a soft reset
abhinav a31fa26
update-ref: ensure that the branch hasn't moved
abhinav a118e3c
test: update test cases
abhinav 34338db
add --no-verify flag
abhinav 1a2a8cd
doc: adjust the help text
abhinav 337f359
test: verify branch is restored after aborting commit
abhinav File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"strings" | ||
|
||
"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 { | ||
NoVerify bool `help:"Bypass pre-commit and commit-msg hooks."` | ||
|
||
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. | ||
|
||
An editor will open to edit the commit message of the squashed commit. | ||
Use the -m/--message flag to specify a commit message without editing. | ||
`) | ||
} | ||
|
||
func (cmd *branchSquashCmd) Run( | ||
ctx context.Context, | ||
log *log.Logger, | ||
view ui.View, | ||
repo *git.Repository, | ||
store *state.Store, | ||
svc *spice.Service, | ||
) (err error) { | ||
branchName, err := repo.CurrentBranch(ctx) | ||
if err != nil { | ||
return fmt.Errorf("get current branch: %w", err) | ||
} | ||
|
||
if branchName == store.Trunk() { | ||
return fmt.Errorf("cannot squash the trunk branch") | ||
} | ||
|
||
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) | ||
} | ||
|
||
// If no message was specified, | ||
// combine the commit messages of all commits in the branch | ||
// to form the initial commit message for the squashed commit. | ||
var commitTemplate string | ||
if cmd.Message == "" { | ||
commitMessages, err := repo.CommitMessageRange(ctx, branch.Head.String(), branch.BaseHash.String()) | ||
if err != nil { | ||
return fmt.Errorf("get commit messages: %w", err) | ||
} | ||
|
||
var sb strings.Builder | ||
sb.WriteString("The original commit messages were:\n\n") | ||
for i, msg := range commitMessages { | ||
if i > 0 { | ||
sb.WriteString("\n") | ||
} | ||
|
||
fmt.Fprintf(&sb, "%v\n", msg) | ||
} | ||
|
||
commitTemplate = sb.String() | ||
} | ||
|
||
// Detach the HEAD so that we don't mess with the current branch | ||
// until the operation is confirmed successful. | ||
if err := repo.DetachHead(ctx, branchName); err != nil { | ||
return fmt.Errorf("detach HEAD: %w", err) | ||
} | ||
var reattachedHead bool | ||
defer func() { | ||
// Reattach the HEAD to the original branch | ||
// if we return early before the operation is complete. | ||
if !reattachedHead { | ||
if cerr := repo.Checkout(ctx, branchName); cerr != nil { | ||
log.Error("could not check out original branch", | ||
"branch", branchName, | ||
"error", cerr) | ||
err = errors.Join(err, cerr) | ||
} | ||
} | ||
}() | ||
|
||
// Perform a soft reset to the base commit. | ||
// The working tree and index will remain unchanged, | ||
// so the contents of the head commit will be staged. | ||
if err := repo.Reset(ctx, branch.BaseHash.String(), git.ResetOptions{ | ||
Mode: git.ResetSoft, | ||
}); err != nil { | ||
return fmt.Errorf("reset to base commit: %w", err) | ||
} | ||
|
||
if err := repo.Commit(ctx, git.CommitRequest{ | ||
Message: cmd.Message, | ||
Template: commitTemplate, | ||
NoVerify: cmd.NoVerify, | ||
}); err != nil { | ||
return fmt.Errorf("commit squashed changes: %w", err) | ||
} | ||
|
||
headHash, err := repo.Head(ctx) | ||
if err != nil { | ||
return fmt.Errorf("get HEAD hash: %w", err) | ||
} | ||
|
||
if err := repo.SetRef(ctx, git.SetRefRequest{ | ||
Ref: "refs/heads/" + branchName, | ||
Hash: headHash, | ||
// Ensure that another tree didn't update the branch | ||
// while we weren't looking. | ||
OldHash: branch.Head, | ||
}); err != nil { | ||
return fmt.Errorf("update branch ref: %w", err) | ||
} | ||
|
||
if cerr := repo.Checkout(ctx, branchName); cerr != nil { | ||
return fmt.Errorf("checkout branch: %w", cerr) | ||
} | ||
reattachedHead = true | ||
|
||
return (&upstackRestackCmd{}).Run(ctx, log, repo, store, svc) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
# Squashing a branch into one commit with 'squash' | ||
|
||
as 'Test <[email protected]>' | ||
at '2025-02-02T20:00:01Z' | ||
|
||
cd repo | ||
git init | ||
git commit --allow-empty -m 'Initial commit' | ||
gs repo init | ||
|
||
# Create a branch with two commits | ||
git add feature1.txt | ||
gs branch create feature1 -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 feature3 -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 | ||
|
||
mkdir $WORK/output | ||
env EDITOR=mockedit | ||
env MOCKEDIT_RECORD=$WORK/output/initial-msg.txt | ||
env MOCKEDIT_GIVE=$WORK/input/squashed-msg.txt | ||
gs branch squash | ||
|
||
git graph --branches | ||
cmp stdout $WORK/golden/graph.txt | ||
cmp $WORK/output/initial-msg.txt $WORK/golden/initial-msg.txt | ||
|
||
-- repo/dirty.txt -- | ||
Dirty | ||
-- repo/feature1.txt -- | ||
Feature 1 | ||
-- repo/feature2.txt -- | ||
Feature 2 | ||
-- repo/feature3.txt -- | ||
Feature 3 | ||
-- golden/graph-before.txt -- | ||
* a956270 (HEAD -> feature3) First message in rebased branch | ||
* adfd1b6 (feature1) Second message in squashed branch | ||
* 3143d6f First message in squashed branch | ||
* 2b5f0cb (main) Initial commit | ||
-- golden/graph.txt -- | ||
* c012e99 (feature3) First message in rebased branch | ||
* 67f0d51 (HEAD -> feature1) Squashed commit message | ||
* 2b5f0cb (main) Initial commit | ||
-- input/squashed-msg.txt -- | ||
Squashed commit message | ||
|
||
This contains features 1 and 2. | ||
-- golden/initial-msg.txt -- | ||
The original commit messages were: | ||
|
||
Second message in squashed branch | ||
|
||
First message in squashed branch | ||
|
||
# Please enter the commit message for your changes. Lines starting | ||
# with '#' will be ignored, and an empty message aborts the commit. | ||
# | ||
# HEAD detached from refs/heads/feature1 | ||
# Changes to be committed: | ||
# new file: feature1.txt | ||
# new file: feature2.txt | ||
# | ||
# Untracked files: | ||
# dirty.txt | ||
# |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# Common errors with 'branch squash'. | ||
|
||
as 'Test <[email protected]>' | ||
at '2025-02-02T20:18:19Z' | ||
|
||
cd repo | ||
git init | ||
git commit --allow-empty -m 'Initial commit' | ||
gs repo init | ||
|
||
! gs branch squash | ||
stderr 'cannot squash the trunk branch' | ||
|
||
# feat1 -> feat2, with feat1 diverging | ||
git add feat1.txt | ||
gs bc -m feat1 | ||
git add feat2.txt | ||
gs bc -m feat2 | ||
git add feat2-pt2.txt | ||
gs cc -m 'feat2 part 2' | ||
gs down | ||
git add feat1-pt2.txt | ||
git commit -m 'feat1 part 2' | ||
gs up | ||
|
||
# branch is not restacked | ||
! gs branch squash | ||
stderr 'branch feat2 needs to be restacked' | ||
gs stack restack # fix it | ||
|
||
# abort the commit | ||
git diff --quiet # verify no changes | ||
git graph --branches | ||
cmp stdout $WORK/golden/abort-before.txt | ||
env EDITOR=mockedit MOCKEDIT_GIVE=$WORK/input/empty.txt | ||
! gs branch squash | ||
stderr 'empty commit message' | ||
# must still be in the branch | ||
git graph --branches | ||
cmp stdout $WORK/golden/abort-before.txt | ||
|
||
-- repo/feat1.txt -- | ||
-- repo/feat1-pt2.txt -- | ||
-- repo/feat2.txt -- | ||
-- repo/feat2-pt2.txt -- | ||
-- input/empty.txt -- | ||
-- golden/abort-before.txt -- | ||
* 01fbf9b (HEAD -> feat2) feat2 part 2 | ||
* 4b08080 feat2 | ||
* 833ec52 (feat1) feat1 part 2 | ||
* 48855c7 feat1 | ||
* da2d8d1 (main) Initial commit |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😅 thanks!