Skip to content

Commit

Permalink
feat(stack reorder): Implement internal Reorder API (#141)
Browse files Browse the repository at this point in the history
  • Loading branch information
twavv authored May 31, 2023
1 parent b974c29 commit c53d729
Show file tree
Hide file tree
Showing 9 changed files with 286 additions and 18 deletions.
12 changes: 11 additions & 1 deletion cmd/av/stack_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,17 @@ func stackBranchMove(
return errors.Errorf("cannot rename branch to itself")
}

currentMeta, _ := tx.Branch(oldBranch)
currentMeta, ok := tx.Branch(oldBranch)
if !ok {
defaultBranch, err := repo.DefaultBranch()
if err != nil {
return errors.WrapIf(err, "failed to determine repository default branch")
}
currentMeta.Parent = meta.BranchState{
Name: defaultBranch,
Trunk: true,
}
}
currentMeta.Name = newBranch
tx.DeleteBranch(oldBranch)
tx.SetBranch(currentMeta)
Expand Down
11 changes: 7 additions & 4 deletions internal/meta/jsonfiledb/readtx.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ func (tx *readTx) Repository() (meta.Repository, bool) {
return tx.state.RepositoryState, tx.state.RepositoryState.ID != ""
}

func (tx *readTx) Branch(name string) (branch meta.Branch, ok bool) {
branch, ok = tx.state.BranchState[name]
if !ok {
func (tx *readTx) Branch(name string) (meta.Branch, bool) {
if name == "" {
panic("invariant error: cannot read branch state for empty branch name")
}
branch, ok := tx.state.BranchState[name]
if branch.Name == "" {
branch.Name = name
}
return
return branch, ok
}

func (tx *readTx) AllBranches() map[string]meta.Branch {
Expand Down
52 changes: 49 additions & 3 deletions internal/reorder/cmds.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package reorder

import (
"encoding/json"
"fmt"
"github.com/aviator-co/av/internal/meta"
"io"

"github.com/aviator-co/av/internal/git"
Expand All @@ -12,8 +14,10 @@ import (
type Context struct {
// Repo is the repository the reorder operation is being performed on.
Repo *git.Repo
// DB is the av database of the repository.
DB meta.DB
// State is the current state of the reorder operation.
State State
State *State
// Output is the output stream to write interactive messages to.
// Commands should write to this stream instead of stdout/stderr.
Output io.Writer
Expand All @@ -28,9 +32,51 @@ func (c *Context) Print(a ...any) {
// operation if there is a conflict.
type State struct {
// The current HEAD of the reorder operation.
Head string
Head string `json:"head"`
// The name of the current branch in the reorder operation.
Branch string
Branch string `json:"branch"`
// The sequence of commands to be executed.
// NOTE: we handle marshalling/unmarshalling in the MarshalJSON/UnmarshalJSON methods.
Commands []Cmd `json:"-"`
}

func (s *State) MarshalJSON() ([]byte, error) {
// Create Alias type to avoid copying MarshalJSON method (and avoid infinite recursion).
type Alias State
var cmdStrings []string
for _, cmd := range s.Commands {
cmdStrings = append(cmdStrings, cmd.String())
}
return json.Marshal(&struct {
Commands []string `json:"commands"`
*Alias
}{
Commands: cmdStrings,
Alias: (*Alias)(s),
})
}

func (s *State) UnmarshalJSON(data []byte) error {
// Create Alias type to avoid copying UnmarshalJSON method (and avoid infinite recursion).
type Alias State
var aux struct {
Commands []string `json:"commands"`
Alias
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
var cmds []Cmd
for _, cmdStr := range aux.Commands {
cmd, err := ParseCmd(cmdStr)
if err != nil {
return err
}
cmds = append(cmds, cmd)
}
*s = State(aux.Alias)
s.Commands = cmds
return nil
}

type Cmd interface {
Expand Down
29 changes: 29 additions & 0 deletions internal/reorder/cmds_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package reorder

import (
"encoding/json"
"github.com/stretchr/testify/require"
"testing"
)

func TestState(t *testing.T) {
state := &State{
Branch: "main",
Head: "owouwu",
Commands: []Cmd{
StackBranchCmd{Name: "one", Trunk: "main"},
PickCmd{"abcd"},
StackBranchCmd{Name: "two", Parent: "one"},
PickCmd{"efgh"},
},
}

serialized, err := json.Marshal(state)
require.NoError(t, err, "failed to serialize state")

var deserialized State
err = json.Unmarshal(serialized, &deserialized)
require.NoError(t, err, "failed to deserialize state")

require.Equal(t, *state, deserialized, "deserialized command sequence does not match original")
}
12 changes: 7 additions & 5 deletions internal/reorder/pick.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,17 @@ func (p PickCmd) Execute(ctx *Context) error {
return err
}

ctx.Print(
colors.Success(" - applied commit "),
colors.UserInput(git.ShortSha(p.Commit)),
colors.Success(" without conflict\n"),
)
head, err := ctx.Repo.RevParse(&git.RevParse{Rev: "HEAD"})
if err != nil {
return err
}
ctx.Print(
colors.Success(" - applied commit "),
colors.UserInput(git.ShortSha(p.Commit)),
colors.Success(" without conflict (HEAD is now at "),
colors.UserInput(git.ShortSha(head)),
colors.Success(")\n"),
)
ctx.State.Head = head
return nil
}
Expand Down
5 changes: 4 additions & 1 deletion internal/reorder/pick_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package reorder

import (
"bytes"
"github.com/aviator-co/av/internal/meta/jsonfiledb"
"testing"

"github.com/aviator-co/av/internal/git"
Expand All @@ -16,8 +17,10 @@ func TestPickCmd_String(t *testing.T) {

func TestPickCmd_Execute(t *testing.T) {
repo := gittest.NewTempRepo(t)
db, err := jsonfiledb.OpenRepo(repo)
require.NoError(t, err)
out := &bytes.Buffer{}
ctx := &Context{repo, State{Branch: "main"}, out}
ctx := &Context{repo, db, &State{Branch: "main"}, out}

start, err := repo.RevParse(&git.RevParse{Rev: "HEAD"})
require.NoError(t, err)
Expand Down
33 changes: 30 additions & 3 deletions internal/reorder/reorder.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,34 @@
package reorder

type Opts struct{}
import (
"emperror.dev/errors"
"fmt"
"github.com/aviator-co/av/internal/utils/colors"
"os"
)

func Reorder(opts Opts) error {
panic("not implemented")
// Reorder executes a reorder.
// If the reorder couldn't be completed (due to a conflict), a continuation is returned.
// If the reorder was completed successfully, a nil continuation and nil error is returned.
func Reorder(ctx Context) (*Continuation, error) {
if ctx.Output == nil {
ctx.Output = os.Stderr
}

for _, cmd := range ctx.State.Commands {
err := cmd.Execute(&ctx)
if errors.Is(err, ErrInterruptReorder) {
return &Continuation{State: ctx.State}, nil
} else if err != nil {
return nil, err
}
ctx.State.Commands = ctx.State.Commands[1:]
}

_, _ = fmt.Fprint(ctx.Output, colors.Success("Reorder complete!\n"))
return nil, nil
}

type Continuation struct {
State *State
}
97 changes: 97 additions & 0 deletions internal/reorder/reorder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package reorder_test

import (
"fmt"
"github.com/aviator-co/av/internal/git"
"github.com/aviator-co/av/internal/git/gittest"
"github.com/aviator-co/av/internal/meta/jsonfiledb"
"github.com/aviator-co/av/internal/reorder"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)

func TestReorder(t *testing.T) {
repo := gittest.NewTempRepo(t)
db, err := jsonfiledb.OpenRepo(repo)
require.NoError(t, err)

initial, err := repo.RevParse(&git.RevParse{Rev: "HEAD"})
require.NoError(t, err)

_, err = repo.CheckoutBranch(&git.CheckoutBranch{Name: "one", NewBranch: true})
require.NoError(t, err)
c1a := gittest.CommitFile(t, repo, "file", []byte("hello\n"))
c1b := gittest.CommitFile(t, repo, "file", []byte("hello\nworld\n"))
c2a := gittest.CommitFile(t, repo, "fichier", []byte("bonjour\n"))
c2b := gittest.CommitFile(t, repo, "fichier", []byte("bonjour\nle monde\n"))

continuation, err := reorder.Reorder(reorder.Context{
Repo: repo,
DB: db,
State: &reorder.State{
Branch: "",
Head: "",
Commands: []reorder.Cmd{
reorder.StackBranchCmd{Name: "one", Trunk: fmt.Sprintf("main@%s", initial)},
reorder.PickCmd{Commit: c1a},
reorder.PickCmd{Commit: c1b},
reorder.StackBranchCmd{Name: "two", Parent: "one"},
reorder.PickCmd{Commit: c2a},
reorder.PickCmd{Commit: c2b},
},
},
})
require.NoError(t, err, "expected reorder to complete cleanly")
require.Nil(t, continuation, "expected reorder to complete cleanly")

mainHead, err := repo.RevParse(&git.RevParse{Rev: "main"})
require.NoError(t, err)
assert.Equal(t, initial, mainHead, "expected main to be at initial commit")

oneHead, err := repo.RevParse(&git.RevParse{Rev: "one"})
require.NoError(t, err)
assert.Equal(t, c1b, oneHead, "expected one to be at c1b")

twoHead, err := repo.RevParse(&git.RevParse{Rev: "two"})
require.NoError(t, err)
assert.Equal(t, c2b, twoHead, "expected two to be at c2b")
}

func TestReorderConflict(t *testing.T) {
repo := gittest.NewTempRepo(t)
db, err := jsonfiledb.OpenRepo(repo)
require.NoError(t, err)

initial, err := repo.RevParse(&git.RevParse{Rev: "HEAD"})
require.NoError(t, err)

_, err = repo.CheckoutBranch(&git.CheckoutBranch{Name: "one", NewBranch: true})
require.NoError(t, err)
c1a := gittest.CommitFile(t, repo, "file", []byte("hello\n"))
c1b := gittest.CommitFile(t, repo, "file", []byte("hello\nworld\n"))

_, err = repo.Git("reset", "--hard", initial)
require.NoError(t, err)
c2a := gittest.CommitFile(t, repo, "file", []byte("bonjour\n"))
c2b := gittest.CommitFile(t, repo, "file", []byte("bonjour\nle monde\n"))

continuation, err := reorder.Reorder(reorder.Context{
Repo: repo,
DB: db,
State: &reorder.State{
Branch: "",
Head: "",
Commands: []reorder.Cmd{
reorder.StackBranchCmd{Name: "one", Trunk: fmt.Sprintf("main@%s", initial)},
reorder.PickCmd{Commit: c1a},
reorder.PickCmd{Commit: c1b},
reorder.PickCmd{Commit: c2a},
reorder.PickCmd{Commit: c2b},
},
},
})
require.NoError(t, err, "expected reorder to complete without error even with conflicts")
require.NotNil(t, continuation, "expected continuation to be returned after conflicts")
require.Equal(t, continuation.State.Commands[0], reorder.PickCmd{Commit: c2a})
}
53 changes: 52 additions & 1 deletion internal/reorder/stackbranch.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package reorder

import (
"github.com/aviator-co/av/internal/git"
"github.com/aviator-co/av/internal/meta"
"strings"

"github.com/spf13/pflag"
Expand All @@ -19,11 +21,60 @@ type StackBranchCmd struct {
Parent string
// The name of the trunk branch.
// Mutually exclusive with --parent.
// The branch can be rooted at a given commit by appending "@<commit>" to the
// branch name.
Trunk string
}

func (b StackBranchCmd) Execute(ctx *Context) error {
panic("not implemented")
tx := ctx.DB.WriteTx()
defer tx.Abort()

branch, _ := tx.Branch(b.Name)
var parentState meta.BranchState

// Figure out which commit we need to start this branch at.
var headCommit string

if b.Trunk != "" {
parentState.Name, headCommit, _ = strings.Cut(b.Trunk, "@")
parentState.Trunk = true
} else {
// We assume the parent branch (if not set manually) is the previous branch
// in the reorder operation.
if b.Parent == "" {
b.Parent = ctx.State.Branch
}
if b.Parent == "" {
return ErrInvalidCmd{
"stack-branch",
"--parent=<branch> or --trunk=<branch> must be specified when creating the first branch",
}
}
parentState.Name = b.Parent
var err error

// We always start child branches at the HEAD of their parents.
headCommit, err = ctx.Repo.RevParse(&git.RevParse{Rev: b.Parent})
if err != nil {
return err
}
parentState.Head = headCommit
}
branch.Parent = parentState
tx.SetBranch(branch)

if headCommit == "" {
headCommit = branch.Parent.Name
}
if _, err := ctx.Repo.Git("switch", "--force-create", b.Name); err != nil {
return err
}
if _, err := ctx.Repo.Git("reset", "--hard", headCommit); err != nil {
return err
}

return tx.Commit()
}

func (b StackBranchCmd) String() string {
Expand Down

0 comments on commit c53d729

Please sign in to comment.