-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(stack reorder): Implement PickCmd (#126)
Implements the actual `pick` command (along with adding the necessary other things like `Repo.CherryPick`).
- Loading branch information
Showing
10 changed files
with
266 additions
and
11 deletions.
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
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,84 @@ | ||
package git | ||
|
||
import ( | ||
"emperror.dev/errors" | ||
"fmt" | ||
"strings" | ||
) | ||
|
||
type CherryPickResume string | ||
|
||
const ( | ||
CherryPickContinue CherryPickResume = "continue" | ||
CherryPickSkip CherryPickResume = "skip" | ||
CherryPickQuit CherryPickResume = "quit" | ||
CherryPickAbort CherryPickResume = "abort" | ||
) | ||
|
||
type CherryPick struct { | ||
// Commits is a list of commits to apply. | ||
Commits []string | ||
|
||
// NoCommit specifies whether or not to cherry-pick without committing | ||
// (equivalent to the --no-commit flag on `git cherry-pick`). | ||
NoCommit bool | ||
|
||
// FastForward specifies whether or not to fast-forward the current branch | ||
// if possible (equivalent to the --ff flag on `git cherry-pick`). | ||
// If true, and the parent of the commit is the current HEAD, the HEAD | ||
// will be fast forwarded to the commit (instead of re-applied). | ||
FastForward bool | ||
|
||
// Resume specifies how to resume a cherry-pick operation that was | ||
// interrupted by a conflict (equivalent to the --continue, --skip, --quit, | ||
// and --abort flags on `git cherry-pick`). | ||
// Mutually exclusive with all other options. | ||
Resume CherryPickResume | ||
} | ||
|
||
type ErrCherryPickConflict struct { | ||
ConflictingCommit string | ||
Output string | ||
} | ||
|
||
func (e ErrCherryPickConflict) Error() string { | ||
return fmt.Sprintf("cherry-pick conflict: failed to apply %s", ShortSha(e.ConflictingCommit)) | ||
} | ||
|
||
// CherryPick applies the given commits on top of the current HEAD. | ||
// If there are conflicts, ErrCherryPickConflict is returned. | ||
func (r *Repo) CherryPick(opts CherryPick) error { | ||
args := []string{"cherry-pick"} | ||
|
||
if opts.Resume != "" { | ||
args = append(args, fmt.Sprintf("--%s", opts.Resume)) | ||
} else { | ||
if opts.FastForward { | ||
args = append(args, "--ff") | ||
} | ||
if opts.NoCommit { | ||
args = append(args, "--no-commit") | ||
} | ||
args = append(args, opts.Commits...) | ||
} | ||
|
||
run, err := r.Run(&RunOpts{ | ||
Args: args, | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if run.ExitCode != 0 { | ||
cherryPickHead, err := r.readGitFile("CHERRY_PICK_HEAD") | ||
if err != nil { | ||
return errors.WrapIff(err, "expected CHERRY_PICK_HEAD to exist after cherry-pick failure") | ||
} | ||
return ErrCherryPickConflict{ | ||
ConflictingCommit: strings.TrimSpace(cherryPickHead), | ||
Output: string(run.Stderr), | ||
} | ||
} | ||
|
||
return nil | ||
} |
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,42 @@ | ||
package git_test | ||
|
||
import ( | ||
"github.com/aviator-co/av/internal/git" | ||
"github.com/aviator-co/av/internal/git/gittest" | ||
"github.com/aviator-co/av/internal/utils/errutils" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
) | ||
|
||
func TestRepo_CherryPick(t *testing.T) { | ||
repo := gittest.NewTempRepo(t) | ||
|
||
c1 := gittest.CommitFile(t, repo, "file", []byte("first commit\n")) | ||
c2 := gittest.CommitFile(t, repo, "file", []byte("first commit\nsecond commit\n")) | ||
|
||
// Switch back to c1 and test that we can cherry-pick c2 on top of it | ||
if _, err := repo.CheckoutBranch(&git.CheckoutBranch{Name: c1}); err != nil { | ||
t.Fatal(err) | ||
} | ||
require.NoError(t, repo.CherryPick(git.CherryPick{Commits: []string{c2}})) | ||
contents, err := os.ReadFile(filepath.Join(repo.Dir(), "file")) | ||
require.NoError(t, err) | ||
assert.Equal(t, "first commit\nsecond commit\n", string(contents)) | ||
|
||
// Switch back to c1 and check that we can fast-forward to c2 | ||
if _, err := repo.CheckoutBranch(&git.CheckoutBranch{Name: c1}); err != nil { | ||
t.Fatal(err) | ||
} | ||
require.NoError(t, repo.CherryPick(git.CherryPick{Commits: []string{c2}, FastForward: true})) | ||
contents, err = os.ReadFile(filepath.Join(repo.Dir(), "file")) | ||
require.NoError(t, err) | ||
|
||
// We're back to c2, so trying to cherry-pick c1 should fail. | ||
err = repo.CherryPick(git.CherryPick{Commits: []string{c1}}) | ||
conflictErr, ok := errutils.As[git.ErrCherryPickConflict](err) | ||
require.True(t, ok, "expected cherry-pick conflict") | ||
assert.Equal(t, c1, conflictErr.ConflictingCommit) | ||
} |
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,15 @@ | ||
package git | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
) | ||
|
||
// readGitFile reads a file from the .git directory. | ||
func (r *Repo) readGitFile(name string) (string, error) { | ||
data, err := os.ReadFile(filepath.Join(r.GitDir(), name)) | ||
if err != nil { | ||
return "", err | ||
} | ||
return string(data), nil | ||
} |
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 |
---|---|---|
@@ -1,10 +1,42 @@ | ||
package reorder | ||
|
||
import ( | ||
"bytes" | ||
"github.com/aviator-co/av/internal/git" | ||
"github.com/aviator-co/av/internal/git/gittest" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
"testing" | ||
) | ||
|
||
func TestPickCmd_String(t *testing.T) { | ||
assert.Equal(t, "pick mycommit", PickCmd{Commit: "mycommit"}.String()) | ||
} | ||
|
||
func TestPickCmd_Execute(t *testing.T) { | ||
repo := gittest.NewTempRepo(t) | ||
out := &bytes.Buffer{} | ||
ctx := &Context{repo, State{Branch: "main"}, out} | ||
|
||
start, err := repo.RevParse(&git.RevParse{Rev: "HEAD"}) | ||
require.NoError(t, err) | ||
next := gittest.CommitFile(t, repo, "file", []byte("hello\n")) | ||
|
||
t.Run("fast-forward commit", func(t *testing.T) { | ||
_, err = repo.Git("reset", "--hard", start) | ||
require.NoError(t, err) | ||
require.NoError(t, PickCmd{Commit: next}.Execute(ctx), "PickCmd.Execute should succeed with a fast-forward") | ||
require.Equal(t, next, ctx.State.Head, "PickCmd.Execute should update the state's head") | ||
}) | ||
|
||
t.Run("conflicting commit", func(t *testing.T) { | ||
out.Reset() | ||
_, err = repo.Git("reset", "--hard", start) | ||
require.NoError(t, err) | ||
gittest.CommitFile(t, repo, "file", []byte("bonjour\n")) | ||
// Trying to re-apply the commit `next` should be a conflict | ||
err := PickCmd{Commit: next}.Execute(ctx) | ||
require.ErrorIs(t, err, ErrInterruptReorder, "PickCmd.Execute should return ErrInterruptReorder on conflict") | ||
require.Contains(t, out.String(), git.ShortSha(next), "PickCmd.Execute should print the conflicting commit") | ||
}) | ||
} |
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,17 @@ | ||
package errutils | ||
|
||
import "emperror.dev/errors" | ||
|
||
// As is a wrapper around errors.As using generics that returns the concrete | ||
// error type if err is of type T. | ||
func As[T error](err error) (T, bool) { | ||
var concreteErr T | ||
if err == nil { | ||
return concreteErr, false | ||
} | ||
if errors.As(err, &concreteErr) { | ||
return concreteErr, true | ||
} else { | ||
return concreteErr, false | ||
} | ||
} |