diff --git a/go/cmd/dolt/cli/arg_parser_helpers.go b/go/cmd/dolt/cli/arg_parser_helpers.go index 35fe5d2cec0..599e6b65433 100644 --- a/go/cmd/dolt/cli/arg_parser_helpers.go +++ b/go/cmd/dolt/cli/arg_parser_helpers.go @@ -96,6 +96,17 @@ func CreateMergeArgParser() *argparser.ArgParser { return ap } +func CreateRebaseArgParser() *argparser.ArgParser { + ap := argparser.NewArgParserWithMaxArgs("merge", 1) + ap.TooManyArgsErrorFunc = func(receivedArgs []string) error { + return fmt.Errorf("rebase takes at most one positional argument.") + } + ap.SupportsFlag(AbortParam, "", "Abort an interactive rebase and return the working set to the pre-rebase state") + ap.SupportsFlag(ContinueFlag, "", "Continue an interactive rebase after adjusting the rebase plan") + ap.SupportsFlag(InteractiveFlag, "i", "Start an interactive rebase") + return ap +} + func CreatePushArgParser() *argparser.ArgParser { ap := argparser.NewArgParserWithVariableArgs("push") ap.SupportsString(UserFlag, "", "user", "User name to use when authenticating with the remote. Gets password from the environment variable {{.EmphasisLeft}}DOLT_REMOTE_PASSWORD{{.EmphasisRight}}.") diff --git a/go/cmd/dolt/cli/flags.go b/go/cmd/dolt/cli/flags.go index 92309c1dd8a..574989818dc 100644 --- a/go/cmd/dolt/cli/flags.go +++ b/go/cmd/dolt/cli/flags.go @@ -27,6 +27,7 @@ const ( CheckoutCreateBranch = "b" CreateResetBranch = "B" CommitFlag = "commit" + ContinueFlag = "continue" CopyFlag = "copy" DateParam = "date" DecorateFlag = "decorate" @@ -36,6 +37,7 @@ const ( ForceFlag = "force" HardResetParam = "hard" HostFlag = "host" + InteractiveFlag = "interactive" ListFlag = "list" MergesFlag = "merges" MessageArg = "message" diff --git a/go/gen/fb/serial/workingset.go b/go/gen/fb/serial/workingset.go index bff478fb3d6..63948a2e679 100644 --- a/go/gen/fb/serial/workingset.go +++ b/go/gen/fb/serial/workingset.go @@ -172,7 +172,23 @@ func (rcv *WorkingSet) TryMergeState(obj *MergeState) (*MergeState, error) { return nil, nil } -const WorkingSetNumFields = 7 +func (rcv *WorkingSet) TryRebaseState(obj *RebaseState) (*RebaseState, error) { + o := flatbuffers.UOffsetT(rcv._tab.Offset(18)) + if o != 0 { + x := rcv._tab.Indirect(o + rcv._tab.Pos) + if obj == nil { + obj = new(RebaseState) + } + obj.Init(rcv._tab.Bytes, x) + if RebaseStateNumFields < obj.Table().NumFields() { + return nil, flatbuffers.ErrTableHasUnknownFields + } + return obj, nil + } + return nil, nil +} + +const WorkingSetNumFields = 8 func WorkingSetStart(builder *flatbuffers.Builder) { builder.StartObject(WorkingSetNumFields) @@ -204,6 +220,9 @@ func WorkingSetAddTimestampMillis(builder *flatbuffers.Builder, timestampMillis func WorkingSetAddMergeState(builder *flatbuffers.Builder, mergeState flatbuffers.UOffsetT) { builder.PrependUOffsetTSlot(6, flatbuffers.UOffsetT(mergeState), 0) } +func WorkingSetAddRebaseState(builder *flatbuffers.Builder, rebaseState flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(7, flatbuffers.UOffsetT(rebaseState), 0) +} func WorkingSetEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return builder.EndObject() } @@ -377,3 +396,164 @@ func MergeStateAddIsCherryPick(builder *flatbuffers.Builder, isCherryPick bool) func MergeStateEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return builder.EndObject() } + +type RebaseState struct { + _tab flatbuffers.Table +} + +func InitRebaseStateRoot(o *RebaseState, buf []byte, offset flatbuffers.UOffsetT) error { + n := flatbuffers.GetUOffsetT(buf[offset:]) + return o.Init(buf, n+offset) +} + +func TryGetRootAsRebaseState(buf []byte, offset flatbuffers.UOffsetT) (*RebaseState, error) { + x := &RebaseState{} + return x, InitRebaseStateRoot(x, buf, offset) +} + +func TryGetSizePrefixedRootAsRebaseState(buf []byte, offset flatbuffers.UOffsetT) (*RebaseState, error) { + x := &RebaseState{} + return x, InitRebaseStateRoot(x, buf, offset+flatbuffers.SizeUint32) +} + +func (rcv *RebaseState) Init(buf []byte, i flatbuffers.UOffsetT) error { + rcv._tab.Bytes = buf + rcv._tab.Pos = i + if RebaseStateNumFields < rcv.Table().NumFields() { + return flatbuffers.ErrTableHasUnknownFields + } + return nil +} + +func (rcv *RebaseState) Table() flatbuffers.Table { + return rcv._tab +} + +func (rcv *RebaseState) PreWorkingRootAddr(j int) byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + a := rcv._tab.Vector(o) + return rcv._tab.GetByte(a + flatbuffers.UOffsetT(j*1)) + } + return 0 +} + +func (rcv *RebaseState) PreWorkingRootAddrLength() int { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + return rcv._tab.VectorLen(o) + } + return 0 +} + +func (rcv *RebaseState) PreWorkingRootAddrBytes() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func (rcv *RebaseState) MutatePreWorkingRootAddr(j int, n byte) bool { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + a := rcv._tab.Vector(o) + return rcv._tab.MutateByte(a+flatbuffers.UOffsetT(j*1), n) + } + return false +} + +func (rcv *RebaseState) Branch(j int) byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) + if o != 0 { + a := rcv._tab.Vector(o) + return rcv._tab.GetByte(a + flatbuffers.UOffsetT(j*1)) + } + return 0 +} + +func (rcv *RebaseState) BranchLength() int { + o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) + if o != 0 { + return rcv._tab.VectorLen(o) + } + return 0 +} + +func (rcv *RebaseState) BranchBytes() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func (rcv *RebaseState) MutateBranch(j int, n byte) bool { + o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) + if o != 0 { + a := rcv._tab.Vector(o) + return rcv._tab.MutateByte(a+flatbuffers.UOffsetT(j*1), n) + } + return false +} + +func (rcv *RebaseState) OntoCommitAddr(j int) byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(8)) + if o != 0 { + a := rcv._tab.Vector(o) + return rcv._tab.GetByte(a + flatbuffers.UOffsetT(j*1)) + } + return 0 +} + +func (rcv *RebaseState) OntoCommitAddrLength() int { + o := flatbuffers.UOffsetT(rcv._tab.Offset(8)) + if o != 0 { + return rcv._tab.VectorLen(o) + } + return 0 +} + +func (rcv *RebaseState) OntoCommitAddrBytes() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(8)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func (rcv *RebaseState) MutateOntoCommitAddr(j int, n byte) bool { + o := flatbuffers.UOffsetT(rcv._tab.Offset(8)) + if o != 0 { + a := rcv._tab.Vector(o) + return rcv._tab.MutateByte(a+flatbuffers.UOffsetT(j*1), n) + } + return false +} + +const RebaseStateNumFields = 3 + +func RebaseStateStart(builder *flatbuffers.Builder) { + builder.StartObject(RebaseStateNumFields) +} +func RebaseStateAddPreWorkingRootAddr(builder *flatbuffers.Builder, preWorkingRootAddr flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(preWorkingRootAddr), 0) +} +func RebaseStateStartPreWorkingRootAddrVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT { + return builder.StartVector(1, numElems, 1) +} +func RebaseStateAddBranch(builder *flatbuffers.Builder, branch flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(branch), 0) +} +func RebaseStateStartBranchVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT { + return builder.StartVector(1, numElems, 1) +} +func RebaseStateAddOntoCommitAddr(builder *flatbuffers.Builder, ontoCommitAddr flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(ontoCommitAddr), 0) +} +func RebaseStateStartOntoCommitAddrVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT { + return builder.StartVector(1, numElems, 1) +} +func RebaseStateEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + return builder.EndObject() +} diff --git a/go/libraries/doltcore/cherry_pick/cherry_pick.go b/go/libraries/doltcore/cherry_pick/cherry_pick.go index f288bdcac26..c5fabf1bf09 100644 --- a/go/libraries/doltcore/cherry_pick/cherry_pick.go +++ b/go/libraries/doltcore/cherry_pick/cherry_pick.go @@ -53,7 +53,16 @@ func CherryPick(ctx *sql.Context, commit string, options CherryPickOptions) (str mergeResult, commitMsg, err := cherryPick(ctx, doltSession, roots, dbName, commit) if err != nil { - return "", nil, err + return "", mergeResult, err + } + + // If we're amending the previous commit and a new commit message hasn't been provided, + // grab the previous commit message and reuse it. + if options.Amend && options.CommitMessage == "" { + commitMsg, err = previousCommitMessage(ctx) + if err != nil { + return "", nil, err + } } newWorkingRoot := mergeResult.Root @@ -113,6 +122,20 @@ func CherryPick(ctx *sql.Context, commit string, options CherryPickOptions) (str return h.String(), nil, nil } +func previousCommitMessage(ctx *sql.Context) (string, error) { + doltSession := dsess.DSessFromSess(ctx.Session) + headCommit, err := doltSession.GetHeadCommit(ctx, ctx.GetCurrentDatabase()) + if err != nil { + return "", err + } + headCommitMeta, err := headCommit.GetCommitMeta(ctx) + if err != nil { + return "", err + } + + return headCommitMeta.Description, nil +} + // AbortCherryPick aborts a cherry-pick merge, if one is in progress. If unable to abort for any reason // (e.g. if there is not cherry-pick merge in progress), an error is returned. func AbortCherryPick(ctx *sql.Context, dbName string) error { @@ -222,7 +245,7 @@ func cherryPick(ctx *sql.Context, dSess *dsess.DoltSession, roots doltdb.Roots, } result, err := merge.MergeRoots(ctx, roots.Working, cherryRoot, parentRoot, cherryCommit, parentCommit, dbState.EditOpts(), mo) if err != nil { - return nil, "", err + return result, "", err } workingRootHash, err = result.Root.HashOf() diff --git a/go/libraries/doltcore/doltdb/doltdb.go b/go/libraries/doltcore/doltdb/doltdb.go index ebe156c5aa6..babd9cd020e 100644 --- a/go/libraries/doltcore/doltdb/doltdb.go +++ b/go/libraries/doltcore/doltdb/doltdb.go @@ -1302,18 +1302,12 @@ func (ddb *DoltDB) UpdateWorkingSet( return err } - workingRootRef, stagedRef, mergeState, err := workingSet.writeValues(ctx, ddb) + wsSpec, err := workingSet.writeValues(ctx, ddb, meta) if err != nil { return err } - _, err = ddb.db.withReplicationStatusController(replicationStatus).UpdateWorkingSet(ctx, ds, datas.WorkingSetSpec{ - Meta: meta, - WorkingRoot: workingRootRef, - StagedRoot: stagedRef, - MergeState: mergeState, - }, prevHash) - + _, err = ddb.db.withReplicationStatusController(replicationStatus).UpdateWorkingSet(ctx, ds, *wsSpec, prevHash) return err } @@ -1339,19 +1333,13 @@ func (ddb *DoltDB) CommitWithWorkingSet( return nil, err } - workingRootRef, stagedRef, mergeState, err := workingSet.writeValues(ctx, ddb) + wsSpec, err := workingSet.writeValues(ctx, ddb, meta) if err != nil { return nil, err } commitDataset, _, err := ddb.db.withReplicationStatusController(replicationStatus). - CommitWithWorkingSet(ctx, headDs, wsDs, commit.Roots.Staged.nomsValue(), datas.WorkingSetSpec{ - Meta: meta, - WorkingRoot: workingRootRef, - StagedRoot: stagedRef, - MergeState: mergeState, - }, prevHash, commit.CommitOptions) - + CommitWithWorkingSet(ctx, headDs, wsDs, commit.Roots.Staged.nomsValue(), *wsSpec, prevHash, commit.CommitOptions) if err != nil { return nil, err } diff --git a/go/libraries/doltcore/doltdb/ignore.go b/go/libraries/doltcore/doltdb/ignore.go index 65fc44e38f5..cc1b635c936 100644 --- a/go/libraries/doltcore/doltdb/ignore.go +++ b/go/libraries/doltcore/doltdb/ignore.go @@ -232,6 +232,12 @@ func resolveConflictingPatterns(trueMatches, falseMatches []string, tableName st } func (ip *IgnorePatterns) IsTableNameIgnored(tableName string) (IgnoreResult, error) { + // The dolt_rebase table is automatically ignored by Dolt – it shouldn't ever + // be checked in to a Dolt database. + if strings.ToLower(tableName) == strings.ToLower(RebaseTableName) { + return Ignore, nil + } + trueMatches := []string{} falseMatches := []string{} for _, patternIgnore := range *ip { diff --git a/go/libraries/doltcore/doltdb/system_table.go b/go/libraries/doltcore/doltdb/system_table.go index 69ec0182064..a016dda7a84 100644 --- a/go/libraries/doltcore/doltdb/system_table.go +++ b/go/libraries/doltcore/doltdb/system_table.go @@ -161,6 +161,7 @@ var writeableSystemTables = []string{ SchemasTableName, ProceduresTableName, IgnoreTableName, + RebaseTableName, } var persistedSystemTables = []string{ @@ -311,6 +312,9 @@ const ( TagsTableName = "dolt_tags" IgnoreTableName = "dolt_ignore" + + // RebaseTableName is the rebase system table name. + RebaseTableName = "dolt_rebase" ) const ( diff --git a/go/libraries/doltcore/doltdb/workingset.go b/go/libraries/doltcore/doltdb/workingset.go index 10c7859d4cd..e51beed3304 100755 --- a/go/libraries/doltcore/doltdb/workingset.go +++ b/go/libraries/doltcore/doltdb/workingset.go @@ -19,15 +19,42 @@ import ( "fmt" "time" - "github.com/dolthub/dolt/go/libraries/doltcore/schema" + "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/dolt/go/libraries/doltcore/ref" + "github.com/dolthub/dolt/go/libraries/doltcore/schema" "github.com/dolthub/dolt/go/store/datas" "github.com/dolthub/dolt/go/store/hash" "github.com/dolthub/dolt/go/store/prolly/tree" "github.com/dolthub/dolt/go/store/types" ) +// RebaseState tracks the state of an in-progress rebase action. It records the name of the branch being rebased, the +// commit onto which the new commits will be rebased, and the root value of the previous working set, which is used if +// the rebase is aborted and the working set needs to be restored to its previous state. +type RebaseState struct { + preRebaseWorking *RootValue + ontoCommit *Commit + branch string +} + +// Branch returns the name of the branch being actively rebased. This is the branch that will be updated to point +// at the new commits created by the rebase operation. +func (rs RebaseState) Branch() string { + return rs.branch +} + +// OntoCommit returns the commit onto which new commits are being rebased by the active rebase operation. +func (rs RebaseState) OntoCommit() *Commit { + return rs.ontoCommit +} + +// PreRebaseWorkingRoot stores the RootValue of the working set immediately before the current rebase operation was +// started. This value is used when a rebase is aborted, so that the working set can be restored to its previous state. +func (rs RebaseState) PreRebaseWorkingRoot() *RootValue { + return rs.preRebaseWorking +} + type MergeState struct { // the source commit commit *Commit @@ -166,6 +193,7 @@ type WorkingSet struct { workingRoot *RootValue stagedRoot *RootValue mergeState *MergeState + rebaseState *RebaseState } var _ Rootish = &WorkingSet{} @@ -192,6 +220,11 @@ func (ws WorkingSet) WithMergeState(mergeState *MergeState) *WorkingSet { return &ws } +func (ws WorkingSet) WithRebaseState(rebaseState *RebaseState) *WorkingSet { + ws.rebaseState = rebaseState + return &ws +} + func (ws WorkingSet) WithUnmergableTables(tables []string) *WorkingSet { ws.mergeState.unmergableTables = tables return &ws @@ -212,6 +245,29 @@ func (ws WorkingSet) StartMerge(commit *Commit, commitSpecStr string) *WorkingSe return &ws } +// StartRebase adds rebase tracking metadata to a new working set instance and returns it. Callers must then persist +// the returned working set in a session in order for the new working set to be recorded. |ontoCommit| specifies the +// commit that serves as the base commit for the new commits that will be created by the rebase process, |branch| is +// the branch that is being rebased, and |previousRoot| is root value of the branch being rebased. The HEAD and STAGED +// root values of the branch being rebased must match |previousRoot|; WORKING may be a different root value, but ONLY +// if it contains only ignored tables. +func (ws WorkingSet) StartRebase(ctx *sql.Context, ontoCommit *Commit, branch string, previousRoot *RootValue) (*WorkingSet, error) { + ws.rebaseState = &RebaseState{ + ontoCommit: ontoCommit, + preRebaseWorking: previousRoot, + branch: branch, + } + + ontoRoot, err := ontoCommit.GetRootValue(ctx) + if err != nil { + return nil, err + } + ws.workingRoot = ontoRoot + ws.stagedRoot = ontoRoot + + return &ws, nil +} + // StartCherryPick creates and returns a new working set based off of the current |ws| with the specified |commit| // and |commitSpecStr| referring to the commit being cherry-picked. The returned WorkingSet records that a cherry-pick // operation is in progress (i.e. conflicts being resolved). Note that this function does not update the current @@ -233,11 +289,23 @@ func (ws WorkingSet) AbortMerge() *WorkingSet { return &ws } +func (ws WorkingSet) AbortRebase() *WorkingSet { + ws.workingRoot = ws.rebaseState.preRebaseWorking + ws.stagedRoot = ws.workingRoot + ws.rebaseState = nil + return &ws +} + func (ws WorkingSet) ClearMerge() *WorkingSet { ws.mergeState = nil return &ws } +func (ws WorkingSet) ClearRebase() *WorkingSet { + ws.rebaseState = nil + return &ws +} + func (ws *WorkingSet) WorkingRoot() *RootValue { return ws.workingRoot } @@ -250,10 +318,18 @@ func (ws *WorkingSet) MergeState() *MergeState { return ws.mergeState } +func (ws *WorkingSet) RebaseState() *RebaseState { + return ws.rebaseState +} + func (ws *WorkingSet) MergeActive() bool { return ws.mergeState != nil } +func (ws *WorkingSet) RebaseActive() bool { + return ws.rebaseState != nil +} + // MergeCommitParents returns true if there is an active merge in progress and // the recorded commit being merged into the active branch should be included as // a second parent of the created commit. This is the expected behavior for a @@ -358,6 +434,36 @@ func newWorkingSet(ctx context.Context, name string, vrw types.ValueReadWriter, } } + var rebaseState *RebaseState + if dsws.RebaseState != nil { + preRebaseWorkingAddr := dsws.RebaseState.PreRebaseWorkingAddr() + preRebaseWorkingV, err := vrw.ReadValue(ctx, preRebaseWorkingAddr) + if err != nil { + return nil, err + } + + preRebaseWorkingRoot, err := newRootValue(vrw, ns, preRebaseWorkingV) + if err != nil { + return nil, err + } + + datasOntoCommit, err := dsws.RebaseState.OntoCommit(ctx, vrw) + if err != nil { + return nil, err + } + + ontoCommit, err := NewCommit(ctx, vrw, ns, datasOntoCommit) + if err != nil { + return nil, err + } + + rebaseState = &RebaseState{ + preRebaseWorking: preRebaseWorkingRoot, + ontoCommit: ontoCommit, + branch: dsws.RebaseState.Branch(ctx), + } + } + addr, _ := ds.MaybeHeadAddr() return &WorkingSet{ @@ -367,6 +473,7 @@ func newWorkingSet(ctx context.Context, name string, vrw types.ValueReadWriter, workingRoot: workingRoot, stagedRoot: stagedRoot, mergeState: mergeState, + rebaseState: rebaseState, }, nil } @@ -389,51 +496,74 @@ func (ws *WorkingSet) Ref() ref.WorkingSetRef { return ref.NewWorkingSetRef(ws.Name) } -// writeValues write the values in this working set to the database and returns them -func (ws *WorkingSet) writeValues(ctx context.Context, db *DoltDB) ( - workingRoot types.Ref, - stagedRoot types.Ref, - mergeState *datas.MergeState, - err error, -) { +// writeValues write the values in this working set to the database and returns a datas.WorkingSetSpec with the +// new values in it. +func (ws *WorkingSet) writeValues(ctx context.Context, db *DoltDB, meta *datas.WorkingSetMeta) (spec *datas.WorkingSetSpec, err error) { if ws.stagedRoot == nil || ws.workingRoot == nil { - return types.Ref{}, types.Ref{}, nil, fmt.Errorf("StagedRoot and workingRoot must be set. This is a bug.") + return nil, fmt.Errorf("StagedRoot and workingRoot must be set. This is a bug.") } var r *RootValue - r, workingRoot, err = db.writeRootValue(ctx, ws.workingRoot) + r, workingRoot, err := db.writeRootValue(ctx, ws.workingRoot) if err != nil { - return types.Ref{}, types.Ref{}, nil, err + return nil, err } ws.workingRoot = r - r, stagedRoot, err = db.writeRootValue(ctx, ws.stagedRoot) + r, stagedRoot, err := db.writeRootValue(ctx, ws.stagedRoot) if err != nil { - return types.Ref{}, types.Ref{}, nil, err + return nil, err } ws.stagedRoot = r + var mergeState *datas.MergeState if ws.mergeState != nil { r, preMergeWorking, err := db.writeRootValue(ctx, ws.mergeState.preMergeWorking) if err != nil { - return types.Ref{}, types.Ref{}, nil, err + return nil, err } ws.mergeState.preMergeWorking = r h, err := ws.mergeState.commit.HashOf() if err != nil { - return types.Ref{}, types.Ref{}, nil, err + return nil, err } dCommit, err := datas.LoadCommitAddr(ctx, db.vrw, h) if err != nil { - return types.Ref{}, types.Ref{}, nil, err + return nil, err } mergeState, err = datas.NewMergeState(ctx, db.vrw, preMergeWorking, dCommit, ws.mergeState.commitSpecStr, ws.mergeState.unmergableTables, ws.mergeState.isCherryPick) if err != nil { - return types.Ref{}, types.Ref{}, nil, err + return nil, err + } + } + + var rebaseState *datas.RebaseState + if ws.rebaseState != nil { + r, preRebaseWorking, err := db.writeRootValue(ctx, ws.rebaseState.preRebaseWorking) + if err != nil { + return nil, err + } + ws.rebaseState.preRebaseWorking = r + + h, err := ws.rebaseState.ontoCommit.HashOf() + if err != nil { + return nil, err } + dCommit, err := datas.LoadCommitAddr(ctx, db.vrw, h) + if err != nil { + return nil, err + } + + rebaseState = datas.NewRebaseState(preRebaseWorking.TargetHash(), dCommit.Addr(), ws.rebaseState.branch) } - return workingRoot, stagedRoot, mergeState, nil + return &datas.WorkingSetSpec{ + Meta: meta, + WorkingRoot: workingRoot, + StagedRoot: stagedRoot, + MergeState: mergeState, + RebaseState: rebaseState, + }, nil } diff --git a/go/libraries/doltcore/env/actions/checkout.go b/go/libraries/doltcore/env/actions/checkout.go index 278079907c7..4ecf0a595c5 100644 --- a/go/libraries/doltcore/env/actions/checkout.go +++ b/go/libraries/doltcore/env/actions/checkout.go @@ -211,7 +211,7 @@ func CleanOldWorkingSet( err = doltDb.UpdateWorkingSet( ctx, initialWs.Ref(), - initialWs.WithWorkingRoot(newRoots.Working).WithStagedRoot(newRoots.Staged).ClearMerge(), + initialWs.WithWorkingRoot(newRoots.Working).WithStagedRoot(newRoots.Staged).ClearMerge().ClearRebase(), h, &datas.WorkingSetMeta{ diff --git a/go/libraries/doltcore/env/actions/reset.go b/go/libraries/doltcore/env/actions/reset.go index 86ae55f1570..4c1031fe650 100644 --- a/go/libraries/doltcore/env/actions/reset.go +++ b/go/libraries/doltcore/env/actions/reset.go @@ -173,7 +173,7 @@ func ResetHard( } // TODO - refactor this to ensure the update to the head and working set are transactional. - err = doltDb.UpdateWorkingSet(ctx, ws.Ref(), ws.WithWorkingRoot(roots.Working).WithStagedRoot(roots.Staged).ClearMerge(), h, &datas.WorkingSetMeta{ + err = doltDb.UpdateWorkingSet(ctx, ws.Ref(), ws.WithWorkingRoot(roots.Working).WithStagedRoot(roots.Staged).ClearMerge().ClearRebase(), h, &datas.WorkingSetMeta{ Name: username, Email: email, Timestamp: uint64(time.Now().Unix()), diff --git a/go/libraries/doltcore/rebase/rebase.go b/go/libraries/doltcore/rebase/rebase.go new file mode 100644 index 00000000000..80303548264 --- /dev/null +++ b/go/libraries/doltcore/rebase/rebase.go @@ -0,0 +1,208 @@ +// Copyright 2023 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rebase + +import ( + "fmt" + "io" + + "github.com/dolthub/go-mysql-server/sql" + "github.com/shopspring/decimal" + + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" + "github.com/dolthub/dolt/go/libraries/doltcore/env/actions/commitwalk" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" + "github.com/dolthub/dolt/go/store/hash" +) + +const ( + RebaseActionPick = "pick" + RebaseActionSquash = "squash" + RebaseActionFixup = "fixup" + RebaseActionDrop = "drop" + RebaseActionReword = "reword" +) + +// ErrInvalidRebasePlanSquashFixupWithoutPick is returned when a rebase plan attempts to squash or +// fixup a commit without first picking or rewording a commit. +var ErrInvalidRebasePlanSquashFixupWithoutPick = fmt.Errorf("invalid rebase plan: squash and fixup actions must appear after a pick or reword action") + +// RebasePlanDatabase is a database that can save and load a rebase plan. +type RebasePlanDatabase interface { + // SaveRebasePlan saves the given rebase plan to the database. + SaveRebasePlan(ctx *sql.Context, plan *RebasePlan) error + // LoadRebasePlan loads the rebase plan from the database. + LoadRebasePlan(ctx *sql.Context) (*RebasePlan, error) +} + +// RebasePlan describes the plan for a rebase operation, where commits are reordered, +// or adjusted, and then replayed on top of a base commit to form a new commit history. +type RebasePlan struct { + Steps []RebasePlanStep +} + +// RebasePlanStep describes a single step in a rebase plan, such as dropping a +// commit, squashing a commit into the previous commit, etc. +type RebasePlanStep struct { + RebaseOrder decimal.Decimal + Action string + CommitHash string + CommitMsg string +} + +// CreateDefaultRebasePlan creates and returns the default rebase plan for the commits between +// |startCommit| and |upstreamCommit|, equivalent to the log of startCommit..upstreamCommit. The +// default plan includes each of those commits, in the same order they were originally applied, and +// each step in the plan will have the default, pick, action. If the plan cannot be generated for +// any reason, such as disconnected or invalid commits specified, then an error is returned. +func CreateDefaultRebasePlan(ctx *sql.Context, startCommit, upstreamCommit *doltdb.Commit) (*RebasePlan, error) { + commits, err := findRebaseCommits(ctx, startCommit, upstreamCommit) + if err != nil { + return nil, err + } + + if len(commits) == 0 { + return nil, fmt.Errorf("didn't identify any commits!") + } + + plan := RebasePlan{} + for idx := len(commits) - 1; idx >= 0; idx-- { + commit := commits[idx] + hash, err := commit.HashOf() + if err != nil { + return nil, err + } + meta, err := commit.GetCommitMeta(ctx) + if err != nil { + return nil, err + } + + plan.Steps = append(plan.Steps, RebasePlanStep{ + RebaseOrder: decimal.NewFromFloat32(float32(len(commits) - idx)), + Action: RebaseActionPick, + CommitHash: hash.String(), + CommitMsg: meta.Description, + }) + } + + return &plan, nil +} + +// ValidateRebasePlan returns a validation error for invalid states in a rebase plan, such as +// squash or fixup actions appearing in the plan before a pick or reword action. +func ValidateRebasePlan(ctx *sql.Context, plan *RebasePlan) error { + seenPick := false + seenReword := false + for i, step := range plan.Steps { + // As a sanity check, make sure the rebase order is ascending. This shouldn't EVER happen because the + // results are sorted from the database query, but double check while we're validating the plan. + if i > 0 && plan.Steps[i-1].RebaseOrder.GreaterThanOrEqual(step.RebaseOrder) { + return fmt.Errorf("invalid rebase plan: rebase order must be ascending") + } + + switch step.Action { + case RebaseActionPick: + seenPick = true + + case RebaseActionReword: + seenReword = true + + case RebaseActionFixup, RebaseActionSquash: + if !seenPick && !seenReword { + return ErrInvalidRebasePlanSquashFixupWithoutPick + } + } + + if err := validateCommit(ctx, step.CommitHash); err != nil { + return err + } + } + + return nil +} + +// validateCommit returns an error if the specified |commit| is not able to be resolved. +func validateCommit(ctx *sql.Context, commit string) error { + doltSession := dsess.DSessFromSess(ctx.Session) + + ddb, ok := doltSession.GetDoltDB(ctx, ctx.GetCurrentDatabase()) + if !ok { + return fmt.Errorf("unable to load dolt db") + } + + if !doltdb.IsValidCommitHash(commit) { + return fmt.Errorf("invalid commit hash: %s", commit) + } + + commitSpec, err := doltdb.NewCommitSpec(commit) + if err != nil { + return err + } + _, err = ddb.Resolve(ctx, commitSpec, nil) + if err != nil { + return fmt.Errorf("unable to resolve commit hash %s: %w", commit, err) + } + + return nil +} + +// findRebaseCommits returns the commits that should be included in the default rebase plan when +// rebasing |upstreamBranchCommit| onto the current branch (specified by commit |currentBranchCommit|). +// This is defined as the log of |currentBranchCommit|..|upstreamBranchCommit|, or in other words, the +// commits that are reachable from the current branch HEAD, but are NOT reachable from +// |upstreamBranchCommit|. Additionally, any merge commits in that range are NOT included. +func findRebaseCommits(ctx *sql.Context, currentBranchCommit, upstreamBranchCommit *doltdb.Commit) (commits []*doltdb.Commit, err error) { + doltSession := dsess.DSessFromSess(ctx.Session) + + ddb, ok := doltSession.GetDoltDB(ctx, ctx.GetCurrentDatabase()) + if !ok { + return nil, fmt.Errorf("unable to load dolt db") + } + + currentBranchCommitHash, err := currentBranchCommit.HashOf() + if err != nil { + return + } + + upstreamBranchCommitHash, err := upstreamBranchCommit.HashOf() + if err != nil { + return + } + + // We use the dot-dot revision iterator because it gives us the behavior we want for rebase – it finds all + // commits reachable from |currentBranchCommit| but NOT reachable by |upstreamBranchCommit|. + commitItr, err := commitwalk.GetDotDotRevisionsIterator(ctx, + ddb, []hash.Hash{currentBranchCommitHash}, + ddb, []hash.Hash{upstreamBranchCommitHash}, nil) + if err != nil { + return nil, err + } + + // Drain the iterator into a slice so that we can easily reverse the order of the commits + // so that the oldest commit is first in the generated rebase plan. + for { + _, commit, err := commitItr.Next(ctx) + if err == io.EOF { + return commits, nil + } else if err != nil { + return nil, err + } + + // Don't include merge commits in the rebase plan + if commit.NumParents() == 1 { + commits = append(commits, commit) + } + } +} diff --git a/go/libraries/doltcore/sqle/database.go b/go/libraries/doltcore/sqle/database.go index b38ef661b22..ec77c754cf1 100644 --- a/go/libraries/doltcore/sqle/database.go +++ b/go/libraries/doltcore/sqle/database.go @@ -25,19 +25,24 @@ import ( sqle "github.com/dolthub/go-mysql-server" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/analyzer" + "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/fulltext" "github.com/dolthub/go-mysql-server/sql/plan" "github.com/dolthub/go-mysql-server/sql/planbuilder" + "github.com/dolthub/go-mysql-server/sql/rowexec" "github.com/dolthub/go-mysql-server/sql/types" "github.com/dolthub/vitess/go/vt/sqlparser" + "github.com/shopspring/decimal" "gopkg.in/src-d/go-errors.v1" "github.com/dolthub/dolt/go/libraries/doltcore/branch_control" "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" "github.com/dolthub/dolt/go/libraries/doltcore/env" "github.com/dolthub/dolt/go/libraries/doltcore/env/actions/commitwalk" + "github.com/dolthub/dolt/go/libraries/doltcore/rebase" "github.com/dolthub/dolt/go/libraries/doltcore/ref" "github.com/dolthub/dolt/go/libraries/doltcore/schema" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dprocedures" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dtables" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/globalstate" @@ -82,6 +87,7 @@ var _ sql.ViewDatabase = Database{} var _ sql.EventDatabase = Database{} var _ sql.AliasedDatabase = Database{} var _ fulltext.Database = Database{} +var _ rebase.RebasePlanDatabase = Database{} type ReadOnlyDatabase struct { Database @@ -1643,6 +1649,96 @@ func (db Database) SetCollation(ctx *sql.Context, collation sql.CollationID) err return db.SetRoot(ctx, newRoot) } +// LoadRebasePlan implements the rebase.RebasePlanDatabase interface +func (db Database) LoadRebasePlan(ctx *sql.Context) (*rebase.RebasePlan, error) { + table, ok, err := db.GetTableInsensitive(ctx, doltdb.RebaseTableName) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("unable to find dolt_rebase table") + } + resolvedTable := plan.NewResolvedTable(table, db, nil) + sort := plan.NewSort([]sql.SortField{{ + Column: expression.NewGetField(0, types.MustCreateDecimalType(6, 2), "rebase_order", false), + Order: sql.Ascending, + }}, resolvedTable) + iter, err := rowexec.DefaultBuilder.Build(ctx, sort, nil) + if err != nil { + return nil, err + } + + var rebasePlan rebase.RebasePlan + for { + row, err := iter.Next(ctx) + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + + i, ok := row[1].(uint16) + if !ok { + return nil, fmt.Errorf("invalid enum value in rebase plan: %v (%T)", row[1], row[1]) + } + rebaseAction, ok := dprocedures.RebaseActionEnumType.At(int(i)) + if !ok { + return nil, fmt.Errorf("invalid enum value in rebase plan: %v (%T)", row[1], row[1]) + } + + rebasePlan.Steps = append(rebasePlan.Steps, rebase.RebasePlanStep{ + RebaseOrder: row[0].(decimal.Decimal), + Action: rebaseAction, + CommitHash: row[2].(string), + CommitMsg: row[3].(string), + }) + } + + return &rebasePlan, nil +} + +// SaveRebasePlan implements the rebase.RebasePlanDatabase interface +func (db Database) SaveRebasePlan(ctx *sql.Context, plan *rebase.RebasePlan) error { + pkSchema := sql.NewPrimaryKeySchema(dprocedures.DoltRebaseSystemTableSchema, 2) + // we use createSqlTable, instead of CreateTable to avoid the "dolt_" reserved prefix table name check + err := db.createSqlTable(ctx, doltdb.RebaseTableName, pkSchema, sql.Collation_Default) + if err != nil { + return err + } + + table, ok, err := db.GetTableInsensitive(ctx, doltdb.RebaseTableName) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("unable to find %s table", doltdb.RebaseTableName) + } + + writeableDoltTable, ok := table.(*WritableDoltTable) + if !ok { + return fmt.Errorf("expected a *sqle.WritableDoltTable, but got %T", table) + } + + inserter := writeableDoltTable.Inserter(ctx) + for _, planMember := range plan.Steps { + actionEnumValue := dprocedures.RebaseActionEnumType.IndexOf(strings.ToLower(planMember.Action)) + if actionEnumValue == -1 { + return fmt.Errorf("invalid rebase action: %s", planMember.Action) + } + err = inserter.Insert(ctx, sql.Row{ + planMember.RebaseOrder, + uint16(actionEnumValue), + planMember.CommitHash, + planMember.CommitMsg, + }) + if err != nil { + return err + } + } + + return inserter.Close(ctx) +} + // noopRepoStateWriter is a minimal implementation of RepoStateWriter that does nothing type noopRepoStateWriter struct{} diff --git a/go/libraries/doltcore/sqle/dprocedures/dolt_checkout.go b/go/libraries/doltcore/sqle/dprocedures/dolt_checkout.go index 4eaff0a55b0..7a9d2b87f15 100644 --- a/go/libraries/doltcore/sqle/dprocedures/dolt_checkout.go +++ b/go/libraries/doltcore/sqle/dprocedures/dolt_checkout.go @@ -455,7 +455,6 @@ func checkoutExistingBranch(ctx *sql.Context, dbName string, branchName string, if apr.Contains(cli.MoveFlag) { return doGlobalCheckout(ctx, branchName, apr.Contains(cli.ForceFlag), false) } else { - err = dSess.SwitchWorkingSet(ctx, dbName, wsRef) if err != nil { return err @@ -468,7 +467,6 @@ func checkoutExistingBranch(ctx *sql.Context, dbName string, branchName string, // doGlobalCheckout implements the behavior of the `dolt checkout` command line, moving the working set into // the new branch and persisting the checked-out branch into future sessions func doGlobalCheckout(ctx *sql.Context, branchName string, isForce bool, isNewBranch bool) error { - err := MoveWorkingSetToBranch(ctx, branchName, isForce, isNewBranch) if err != nil && err != doltdb.ErrAlreadyOnBranch { return err diff --git a/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go b/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go new file mode 100644 index 00000000000..9fb8d3f02c6 --- /dev/null +++ b/go/libraries/doltcore/sqle/dprocedures/dolt_rebase.go @@ -0,0 +1,567 @@ +// Copyright 2023 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dprocedures + +import ( + "errors" + "fmt" + + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" + goerrors "gopkg.in/src-d/go-errors.v1" + + "github.com/dolthub/dolt/go/cmd/dolt/cli" + "github.com/dolthub/dolt/go/libraries/doltcore/cherry_pick" + "github.com/dolthub/dolt/go/libraries/doltcore/diff" + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" + "github.com/dolthub/dolt/go/libraries/doltcore/env/actions" + "github.com/dolthub/dolt/go/libraries/doltcore/merge" + "github.com/dolthub/dolt/go/libraries/doltcore/rebase" + "github.com/dolthub/dolt/go/libraries/doltcore/ref" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" +) + +var doltRebaseProcedureSchema = []*sql.Column{ + { + Name: "status", + Type: types.Int64, + Nullable: false, + }, + { + Name: "message", + Type: types.LongText, + Nullable: true, + }, +} + +var RebaseActionEnumType = types.MustCreateEnumType([]string{ + rebase.RebaseActionDrop, + rebase.RebaseActionPick, + rebase.RebaseActionReword, + rebase.RebaseActionSquash, + rebase.RebaseActionFixup}, sql.Collation_Default) + +var DoltRebaseSystemTableSchema = []*sql.Column{ + { + Name: "rebase_order", + Type: types.MustCreateDecimalType(6, 2), + Nullable: false, + }, + { + Name: "action", + Type: RebaseActionEnumType, + Nullable: false, + }, + { + Name: "commit_hash", + Type: types.Text, + Nullable: false, + }, + { + Name: "commit_message", + Type: types.Text, + Nullable: false, + }, +} + +// ErrRebaseUncommittedChanges is used when a rebase is started, but there are uncommitted (and not +// ignored) changes in the working set. +var ErrRebaseUncommittedChanges = fmt.Errorf("cannot start a rebase with uncommitted changes") + +// ErrRebaseConflict is used when a merge conflict is detected while rebasing a commit. +var ErrRebaseConflict = goerrors.NewKind( + "merge conflict detected while rebasing commit %s. " + + "the rebase has been automatically aborted") + +// ErrRebaseConflictWithAbortError is used when a merge conflict is detected while rebasing a commit, +// and we are unable to cleanly abort the rebase. +var ErrRebaseConflictWithAbortError = goerrors.NewKind( + "merge conflict detected while rebasing commit %s. " + + "attempted to abort rebase operation, but encountered error: %w") + +func doltRebase(ctx *sql.Context, args ...string) (sql.RowIter, error) { + res, message, err := doDoltRebase(ctx, args) + if err != nil { + return nil, err + } + return rowToIter(int64(res), message), nil +} + +func doDoltRebase(ctx *sql.Context, args []string) (int, string, error) { + if ctx.GetCurrentDatabase() == "" { + return 1, "", sql.ErrNoDatabaseSelected.New() + } + + apr, err := cli.CreateRebaseArgParser().Parse(args) + if err != nil { + return 1, "", err + } + + switch { + case apr.Contains(cli.AbortParam): + err := abortRebase(ctx) + if err != nil { + return 1, "", err + } else { + return 0, "interactive rebase aborted", nil + } + + case apr.Contains(cli.ContinueFlag): + err := continueRebase(ctx) + if err != nil { + return 1, "", err + } else { + return 0, "interactive rebase completed", nil + } + + default: + if apr.NArg() == 0 { + return 1, "", fmt.Errorf("not enough args") + } else if apr.NArg() > 1 { + return 1, "", fmt.Errorf("too many args") + } + if !apr.Contains(cli.InteractiveFlag) { + return 1, "", fmt.Errorf("non-interactive rebases not currently supported") + } + err = startRebase(ctx, apr.Arg(0)) + if err != nil { + return 1, "", err + } + + currentBranch, err := currentBranch(ctx) + if err != nil { + return 1, "", err + } + + return 0, fmt.Sprintf("interactive rebase started on branch %s; "+ + "adjust the rebase plan in the dolt_rebase table, then continue rebasing by "+ + "calling dolt_rebase('--continue')", currentBranch), nil + } +} + +func startRebase(ctx *sql.Context, upstreamPoint string) error { + if upstreamPoint == "" { + return fmt.Errorf("no upstream branch specified") + } + + err := validateWorkingSetCanStartRebase(ctx) + if err != nil { + return err + } + + doltSession := dsess.DSessFromSess(ctx.Session) + headRef, err := doltSession.CWBHeadRef(ctx, ctx.GetCurrentDatabase()) + if err != nil { + return err + } + dbData, ok := doltSession.GetDbData(ctx, ctx.GetCurrentDatabase()) + if !ok { + return fmt.Errorf("unable to find database %s", ctx.GetCurrentDatabase()) + } + + rebaseBranch, err := currentBranch(ctx) + if err != nil { + return err + } + + startCommit, err := dbData.Ddb.ResolveCommitRef(ctx, ref.NewBranchRef(rebaseBranch)) + if err != nil { + return err + } + + commitSpec, err := doltdb.NewCommitSpec(upstreamPoint) + if err != nil { + return err + } + + upstreamCommit, err := dbData.Ddb.Resolve(ctx, commitSpec, headRef) + if err != nil { + return err + } + + // rebaseWorkingBranch is the name of the temporary branch used when performing a rebase. In Git, a rebase + // happens with a detatched HEAD, but Dolt doesn't support that, we use a temporary branch. + rebaseWorkingBranch := "dolt_rebase_" + rebaseBranch + var rsc doltdb.ReplicationStatusController + err = actions.CreateBranchWithStartPt(ctx, dbData, rebaseWorkingBranch, upstreamPoint, false, &rsc) + if err != nil { + return err + } + err = commitTransaction(ctx, doltSession, &rsc) + if err != nil { + return err + } + + // Checkout our new branch + wsRef, err := ref.WorkingSetRefForHead(ref.NewBranchRef(rebaseWorkingBranch)) + if err != nil { + return err + } + err = doltSession.SwitchWorkingSet(ctx, ctx.GetCurrentDatabase(), wsRef) + if err != nil { + return err + } + + dbData, ok = doltSession.GetDbData(ctx, ctx.GetCurrentDatabase()) + if !ok { + return fmt.Errorf("unable to get db datata for database %s", ctx.GetCurrentDatabase()) + } + + db, err := doltSession.Provider().Database(ctx, ctx.GetCurrentDatabase()) + if err != nil { + return err + } + + workingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase()) + if err != nil { + return err + } + + branchRoots, err := dbData.Ddb.ResolveBranchRoots(ctx, ref.NewBranchRef(rebaseBranch)) + if err != nil { + return err + } + + newWorkingSet, err := workingSet.StartRebase(ctx, upstreamCommit, rebaseBranch, branchRoots.Working) + if err != nil { + return err + } + + err = doltSession.SetWorkingSet(ctx, ctx.GetCurrentDatabase(), newWorkingSet) + if err != nil { + return err + } + + // Create the rebase plan and save it in the database + rebasePlan, err := rebase.CreateDefaultRebasePlan(ctx, startCommit, upstreamCommit) + if err != nil { + return err + } + rdb, ok := db.(rebase.RebasePlanDatabase) + if !ok { + return fmt.Errorf("expected a dsess.RebasePlanDatabase implementation, but received a %T", db) + } + return rdb.SaveRebasePlan(ctx, rebasePlan) +} + +// validateRebaseBranchHasntChanged checks that the branch being rebased hasn't been updated since the rebase started, +// and returns an error if any changes are detected. +func validateRebaseBranchHasntChanged(ctx *sql.Context, branch string, rebaseState *doltdb.RebaseState) error { + doltSession := dsess.DSessFromSess(ctx.Session) + doltDb, ok := doltSession.GetDoltDB(ctx, ctx.GetCurrentDatabase()) + if !ok { + return fmt.Errorf("unable to access DoltDB for database %s", ctx.GetCurrentDatabase()) + } + + wsRef, err := ref.WorkingSetRefForHead(ref.NewBranchRef(branch)) + if err != nil { + return err + } + + resolvedWorkingSet, err := doltDb.ResolveWorkingSet(ctx, wsRef) + if err != nil { + return err + } + hash2, err := resolvedWorkingSet.StagedRoot().HashOf() + if err != nil { + return err + } + hash1, err := rebaseState.PreRebaseWorkingRoot().HashOf() + if err != nil { + return err + } + if hash1 != hash2 { + return fmt.Errorf("rebase aborted due to changes in branch %s", branch) + } + + return nil +} + +func validateWorkingSetCanStartRebase(ctx *sql.Context) error { + doltSession := dsess.DSessFromSess(ctx.Session) + + // Make sure there isn't an active rebase or merge in progress already + ws, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase()) + if err != nil { + return err + } + if ws.MergeActive() { + return fmt.Errorf("unable to start rebase while a merge is in progress – abort the current merge before proceeding") + } + if ws.RebaseActive() { + return fmt.Errorf("unable to start rebase while another rebase is in progress – abort the current rebase before proceeding") + } + + // Make sure the working set doesn't contain any uncommitted changes + roots, ok := doltSession.GetRoots(ctx, ctx.GetCurrentDatabase()) + if !ok { + return fmt.Errorf("unable to get roots for database %s", ctx.GetCurrentDatabase()) + } + wsOnlyHasIgnoredTables, err := diff.WorkingSetContainsOnlyIgnoredTables(ctx, roots) + if err != nil { + return err + } + if !wsOnlyHasIgnoredTables { + return ErrRebaseUncommittedChanges + } + + return nil +} + +func abortRebase(ctx *sql.Context) error { + doltSession := dsess.DSessFromSess(ctx.Session) + + workingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase()) + if err != nil { + return err + } + if !workingSet.RebaseActive() { + return fmt.Errorf("no rebase in progress") + } + + // Clear the rebase state (even though we're going to delete this branch next) + rebaseState := workingSet.RebaseState() + workingSet = workingSet.AbortRebase() + err = doltSession.SetWorkingSet(ctx, ctx.GetCurrentDatabase(), workingSet) + if err != nil { + return err + } + + // Delete the working branch + var rsc doltdb.ReplicationStatusController + dbData, ok := doltSession.GetDbData(ctx, ctx.GetCurrentDatabase()) + if !ok { + return fmt.Errorf("unable to get DbData for database %s", ctx.GetCurrentDatabase()) + } + headRef, err := doltSession.CWBHeadRef(ctx, ctx.GetCurrentDatabase()) + if err != nil { + return err + } + err = actions.DeleteBranch(ctx, dbData, headRef.GetPath(), actions.DeleteOptions{ + Force: true, + AllowDeletingCurrentBranch: true, + }, doltSession.Provider(), &rsc) + if err != nil { + return err + } + + // Switch back to the original branch head + wsRef, err := ref.WorkingSetRefForHead(ref.NewBranchRef(rebaseState.Branch())) + if err != nil { + return err + } + + return doltSession.SwitchWorkingSet(ctx, ctx.GetCurrentDatabase(), wsRef) +} + +func continueRebase(ctx *sql.Context) error { + // TODO: Eventually, when we allow interactive-rebases to be stopped and started (e.g. with the break action, + // or for conflict resolution), we'll need to track what step we're at in the rebase plan. + + // Validate that we are in an interactive rebase + doltSession := dsess.DSessFromSess(ctx.Session) + workingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase()) + if err != nil { + return err + } + if !workingSet.RebaseActive() { + return fmt.Errorf("no rebase in progress") + } + + db, err := doltSession.Provider().Database(ctx, ctx.GetCurrentDatabase()) + if err != nil { + return err + } + + rdb, ok := db.(rebase.RebasePlanDatabase) + if !ok { + return fmt.Errorf("expected a dsess.RebasePlanDatabase implementation, but received a %T", db) + } + rebasePlan, err := rdb.LoadRebasePlan(ctx) + if err != nil { + return err + } + + err = rebase.ValidateRebasePlan(ctx, rebasePlan) + if err != nil { + return err + } + + for _, step := range rebasePlan.Steps { + err = processRebasePlanStep(ctx, &step) + if err != nil { + return err + } + } + + // Update the branch being rebased to point to the same commit as our temporary working branch + rebaseBranchWorkingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase()) + if err != nil { + return err + } + dbData, ok := doltSession.GetDbData(ctx, ctx.GetCurrentDatabase()) + if !ok { + return fmt.Errorf("unable to get db data for database %s", ctx.GetCurrentDatabase()) + } + + rebaseBranch := rebaseBranchWorkingSet.RebaseState().Branch() + rebaseWorkingBranch := "dolt_rebase_" + rebaseBranch + + // Check that the branch being rebased hasn't been updated since the rebase started + err = validateRebaseBranchHasntChanged(ctx, rebaseBranch, rebaseBranchWorkingSet.RebaseState()) + if err != nil { + return err + } + + // TODO: copyABranch (and the underlying call to doltdb.NewBranchAtCommit) has a race condition + // where another session can set the branch head AFTER doltdb.NewBranchAtCommit updates + // the branch head, but BEFORE doltdb.NewBranchAtCommit retrieves the working set for the + // branch and updates the working root and staged root for the working set. We may be able + // to fix this race condition by changing doltdb.NewBranchAtCommit to use + // database.CommitWithWorkingSet, since it updates a branch head and working set atomically. + err = copyABranch(ctx, dbData, rebaseWorkingBranch, rebaseBranch, true, nil) + if err != nil { + return err + } + + // Checkout the branch being rebased + previousBranchWorkingSetRef, err := ref.WorkingSetRefForHead(ref.NewBranchRef(rebaseBranchWorkingSet.RebaseState().Branch())) + if err != nil { + return err + } + err = doltSession.SwitchWorkingSet(ctx, ctx.GetCurrentDatabase(), previousBranchWorkingSetRef) + if err != nil { + return err + } + + // delete the temporary working branch + dbData, ok = doltSession.GetDbData(ctx, ctx.GetCurrentDatabase()) + if !ok { + return fmt.Errorf("unable to lookup dbdata") + } + return actions.DeleteBranch(ctx, dbData, rebaseWorkingBranch, actions.DeleteOptions{ + Force: true, + }, doltSession.Provider(), nil) +} + +func processRebasePlanStep(ctx *sql.Context, planStep *rebase.RebasePlanStep) error { + // Make sure we have a transaction opened for the session + // NOTE: After our first call to cherry-pick, the tx is committed, so a new tx needs to be started + // as we process additional rebase actions. + doltSession := dsess.DSessFromSess(ctx.Session) + if doltSession.GetTransaction() == nil { + _, err := doltSession.StartTransaction(ctx, sql.ReadWrite) + if err != nil { + return err + } + } + + switch planStep.Action { + case rebase.RebaseActionDrop: + return nil + + case rebase.RebaseActionPick, rebase.RebaseActionReword: + options := cherry_pick.CherryPickOptions{} + if planStep.Action == rebase.RebaseActionReword { + options.CommitMessage = planStep.CommitMsg + } + return handleRebaseCherryPick(ctx, planStep.CommitHash, options) + + case rebase.RebaseActionSquash, rebase.RebaseActionFixup: + options := cherry_pick.CherryPickOptions{Amend: true} + if planStep.Action == rebase.RebaseActionSquash { + commitMessage, err := squashCommitMessage(ctx, planStep.CommitHash) + if err != nil { + return err + } + options.CommitMessage = commitMessage + } + return handleRebaseCherryPick(ctx, planStep.CommitHash, options) + + default: + return fmt.Errorf("rebase action '%s' is not supported", planStep.Action) + } +} + +// handleRebaseCherryPick runs a cherry-pick for the specified |commitHash|, using the specified +// cherry-pick |options| and checks the results for any errors or merge conflicts. The initial +// version of rebase doesn't support conflict resolution, so if any conflicts are detected, the +// rebase is aborted and an error is returned. +func handleRebaseCherryPick(ctx *sql.Context, commitHash string, options cherry_pick.CherryPickOptions) error { + _, mergeResult, err := cherry_pick.CherryPick(ctx, commitHash, options) + + var schemaConflict merge.SchemaConflict + isSchemaConflict := errors.As(err, &schemaConflict) + + if (mergeResult != nil && mergeResult.HasMergeArtifacts()) || isSchemaConflict { + // TODO: rebase doesn't currently support conflict resolution, but ideally, when a conflict + // is detected, the rebase would be paused and the user would resolve the conflict just + // like any other conflict, and then call dolt_rebase --continue to keep going. + abortErr := abortRebase(ctx) + if abortErr != nil { + return ErrRebaseConflictWithAbortError.New(commitHash, abortErr) + } + return ErrRebaseConflict.New(commitHash) + } + return err +} + +// squashCommitMessage looks up the commit at HEAD and the commit identified by |nextCommitHash| and squashes their two +// commit messages together. +func squashCommitMessage(ctx *sql.Context, nextCommitHash string) (string, error) { + doltSession := dsess.DSessFromSess(ctx.Session) + headCommit, err := doltSession.GetHeadCommit(ctx, ctx.GetCurrentDatabase()) + if err != nil { + return "", err + } + headCommitMeta, err := headCommit.GetCommitMeta(ctx) + if err != nil { + return "", err + } + + ddb, ok := doltSession.GetDoltDB(ctx, ctx.GetCurrentDatabase()) + if !ok { + return "", fmt.Errorf("unable to get doltdb!") + } + spec, err := doltdb.NewCommitSpec(nextCommitHash) + if err != nil { + return "", err + } + headRef, err := doltSession.CWBHeadRef(ctx, ctx.GetCurrentDatabase()) + if err != nil { + return "", err + } + nextCommit, err := ddb.Resolve(ctx, spec, headRef) + if err != nil { + return "", err + } + nextCommitMeta, err := nextCommit.GetCommitMeta(ctx) + if err != nil { + return "", err + } + commitMessage := headCommitMeta.Description + "\n\n" + nextCommitMeta.Description + + return commitMessage, nil +} + +// currentBranch returns the name of the currently checked out branch, or any error if one was encountered. +func currentBranch(ctx *sql.Context) (string, error) { + doltSession := dsess.DSessFromSess(ctx.Session) + headRef, err := doltSession.CWBHeadRef(ctx, ctx.GetCurrentDatabase()) + if err != nil { + return "", err + } + return headRef.GetPath(), nil +} diff --git a/go/libraries/doltcore/sqle/dprocedures/dolt_reset.go b/go/libraries/doltcore/sqle/dprocedures/dolt_reset.go index 104fe4e165d..eff21ee574e 100644 --- a/go/libraries/doltcore/sqle/dprocedures/dolt_reset.go +++ b/go/libraries/doltcore/sqle/dprocedures/dolt_reset.go @@ -110,7 +110,7 @@ func doDoltReset(ctx *sql.Context, args []string) (int, error) { if err != nil { return 1, err } - err = dSess.SetWorkingSet(ctx, dbName, ws.WithWorkingRoot(roots.Working).WithStagedRoot(roots.Staged).ClearMerge()) + err = dSess.SetWorkingSet(ctx, dbName, ws.WithWorkingRoot(roots.Working).WithStagedRoot(roots.Staged).ClearMerge().ClearRebase()) if err != nil { return 1, err } @@ -132,7 +132,7 @@ func doDoltReset(ctx *sql.Context, args []string) (int, error) { if err != nil { return 1, err } - err = dSess.SetWorkingSet(ctx, dbName, ws.WithStagedRoot(roots.Staged).ClearMerge()) + err = dSess.SetWorkingSet(ctx, dbName, ws.WithStagedRoot(roots.Staged).ClearMerge().ClearRebase()) if err != nil { return 1, err } @@ -170,7 +170,7 @@ func doDoltReset(ctx *sql.Context, args []string) (int, error) { if err != nil { return 1, err } - err = dSess.SetWorkingSet(ctx, dbName, ws.WithStagedRoot(roots.Staged).ClearMerge()) + err = dSess.SetWorkingSet(ctx, dbName, ws.WithStagedRoot(roots.Staged).ClearMerge().ClearRebase()) if err != nil { return 1, err } diff --git a/go/libraries/doltcore/sqle/dprocedures/init.go b/go/libraries/doltcore/sqle/dprocedures/init.go index 26fa618ad76..20d283ddadf 100644 --- a/go/libraries/doltcore/sqle/dprocedures/init.go +++ b/go/libraries/doltcore/sqle/dprocedures/init.go @@ -34,6 +34,7 @@ var DoltProcedures = []sql.ExternalStoredProcedureDetails{ {Name: "dolt_fetch", Schema: int64Schema("status"), Function: doltFetch, AdminOnly: true}, {Name: "dolt_undrop", Schema: int64Schema("status"), Function: doltUndrop, AdminOnly: true}, {Name: "dolt_purge_dropped_databases", Schema: int64Schema("status"), Function: doltPurgeDroppedDatabases, AdminOnly: true}, + {Name: "dolt_rebase", Schema: doltRebaseProcedureSchema, Function: doltRebase}, // dolt_gc is enabled behind a feature flag for now, see dolt_gc.go {Name: "dolt_gc", Schema: int64Schema("status"), Function: doltGC, ReadOnly: true, AdminOnly: true}, diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_engine_test.go b/go/libraries/doltcore/sqle/enginetest/dolt_engine_test.go index 9d4f5fab56c..cddb6fdbfce 100644 --- a/go/libraries/doltcore/sqle/enginetest/dolt_engine_test.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_engine_test.go @@ -1665,6 +1665,30 @@ func TestDoltMerge(t *testing.T) { } } +func TestDoltRebase(t *testing.T) { + for _, script := range DoltRebaseScriptTests { + func() { + h := newDoltHarness(t) + defer h.Close() + h.skipSetupCommit = true + enginetest.TestScript(t, h, script) + }() + } + + testMultiSessionScriptTests(t, DoltRebaseMultiSessionScriptTests) +} + +func TestDoltRebasePrepared(t *testing.T) { + for _, script := range DoltRebaseScriptTests { + func() { + h := newDoltHarness(t) + defer h.Close() + h.skipSetupCommit = true + enginetest.TestScriptPrepared(t, h, script) + }() + } +} + func TestDoltMergePrepared(t *testing.T) { for _, script := range MergeScripts { // harness can't reset effectively when there are new commits / branches created, so use a new harness for diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_queries_rebase.go b/go/libraries/doltcore/sqle/enginetest/dolt_queries_rebase.go new file mode 100644 index 00000000000..dc138c16143 --- /dev/null +++ b/go/libraries/doltcore/sqle/enginetest/dolt_queries_rebase.go @@ -0,0 +1,713 @@ +// Copyright 2022 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package enginetest + +import ( + "github.com/dolthub/go-mysql-server/enginetest/queries" + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/plan" + gmstypes "github.com/dolthub/go-mysql-server/sql/types" + + "github.com/dolthub/dolt/go/libraries/doltcore/rebase" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dprocedures" +) + +var DoltRebaseScriptTests = []queries.ScriptTest{ + { + Name: "dolt_rebase errors: basic errors", + SetUpScript: []string{}, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "call dolt_rebase('--abort');", + ExpectedErrStr: "no rebase in progress", + }, { + Query: "call dolt_rebase('--continue');", + ExpectedErrStr: "no rebase in progress", + }, { + Query: "call dolt_rebase('main');", + ExpectedErrStr: "non-interactive rebases not currently supported", + }, { + Query: "call dolt_rebase('-i');", + ExpectedErrStr: "not enough args", + }, { + Query: "call dolt_rebase('-i', 'main1', 'main2');", + ExpectedErrStr: "rebase takes at most one positional argument.", + }, { + Query: "call dolt_rebase('--abrot');", + ExpectedErrStr: "error: unknown option `abrot'", + }, { + Query: "call dolt_rebase('-i', 'doesnotexist');", + ExpectedErrStr: "branch not found: doesnotexist", + }, + }, + }, + { + Name: "dolt_rebase errors: working set not clean", + SetUpScript: []string{ + "create table t (pk int primary key);", + "call dolt_commit('-Am', 'creating table t');", + "insert into t values (0);", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "call dolt_rebase('-i', 'main');", + ExpectedErrStr: dprocedures.ErrRebaseUncommittedChanges.Error(), + }, + { + Query: "call dolt_add('t');", + Expected: []sql.Row{{0}}, + }, + { + Query: "call dolt_rebase('-i', 'main');", + ExpectedErrStr: dprocedures.ErrRebaseUncommittedChanges.Error(), + }, + }, + }, + { + SkipPrepared: true, + Name: "dolt_rebase errors: no database selected", + SetUpScript: []string{ + "create database temp;", + "use temp;", + "drop database temp;", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "select database();", + Expected: []sql.Row{{nil}}, + }, + { + Query: "call dolt_rebase('-i', 'main');", + ExpectedErrStr: "no database selected", + }, + }, + }, + { + Name: "dolt_rebase errors: active merge, cherry-pick, or rebase", + SetUpScript: []string{ + "create table t (pk int primary key, col1 varchar(100));", + "call dolt_commit('-Am', 'creating table t');", + "call dolt_branch('branch1');", + "insert into t values (0, 'zero');", + "call dolt_commit('-am', 'inserting row 0');", + + "call dolt_checkout('branch1');", + "insert into t values (0, 'nada');", + "call dolt_commit('-am', 'inserting row 0');", + + "set @@autocommit=0;", + }, + Assertions: []queries.ScriptTestAssertion{ + { + // Merging main creates a conflict, so we're in an active + // merge until we resolve. + Query: "call dolt_merge('main');", + Expected: []sql.Row{{"", 0, 1}}, + }, + { + Query: "call dolt_rebase('-i', 'main');", + ExpectedErrStr: "unable to start rebase while a merge is in progress – abort the current merge before proceeding", + }, + }, + }, + { + Name: "dolt_rebase errors: rebase working branch already exists", + SetUpScript: []string{ + "create table t (pk int primary key);", + "call dolt_commit('-Am', 'creating table t');", + "call dolt_branch('branch1');", + "call dolt_branch('dolt_rebase_branch1');", + + "insert into t values (0);", + "call dolt_commit('-am', 'inserting row 0');", + + "call dolt_checkout('branch1');", + "insert into t values (1);", + "call dolt_commit('-am', 'inserting row 1');", + "insert into t values (10);", + "call dolt_commit('-am', 'inserting row 10');", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "call dolt_rebase('-i', 'main');", + ExpectedErrStr: "fatal: A branch named 'dolt_rebase_branch1' already exists.", + }, + }, + }, + { + Name: "dolt_rebase errors: invalid rebase plans", + SetUpScript: []string{ + "create table t (pk int primary key);", + "call dolt_commit('-Am', 'creating table t');", + "call dolt_branch('branch1');", + + "insert into t values (0);", + "call dolt_commit('-am', 'inserting row 0');", + + "call dolt_checkout('branch1');", + "insert into t values (1);", + "call dolt_commit('-am', 'inserting row 1');", + "insert into t values (10);", + "call dolt_commit('-am', 'inserting row 10');", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "call dolt_rebase('-i', 'main');", + Expected: []sql.Row{{0, "interactive rebase started on branch dolt_rebase_branch1; " + + "adjust the rebase plan in the dolt_rebase table, then " + + "continue rebasing by calling dolt_rebase('--continue')"}}, + }, + { + Query: "update dolt_rebase set action='squash';", + Expected: []sql.Row{{gmstypes.OkResult{ + RowsAffected: 2, + InsertID: 0, + Info: plan.UpdateInfo{ + Matched: 2, + Updated: 2, + Warnings: 0, + }, + }}}, + }, + { + Query: "call dolt_rebase('--continue');", + ExpectedErrStr: rebase.ErrInvalidRebasePlanSquashFixupWithoutPick.Error(), + }, + { + Query: "update dolt_rebase set action='drop' where rebase_order=1;", + Expected: []sql.Row{{gmstypes.OkResult{ + RowsAffected: 1, + InsertID: 0, + Info: plan.UpdateInfo{ + Matched: 1, + Updated: 1, + Warnings: 0, + }, + }}}, + }, + { + Query: "call dolt_rebase('--continue');", + ExpectedErrStr: rebase.ErrInvalidRebasePlanSquashFixupWithoutPick.Error(), + }, + { + Query: "update dolt_rebase set action='pick', commit_hash='doesnotexist' where rebase_order=1;", + Expected: []sql.Row{{gmstypes.OkResult{ + RowsAffected: 1, + InsertID: 0, + Info: plan.UpdateInfo{ + Matched: 1, + Updated: 1, + Warnings: 0, + }, + }}}, + }, + { + Query: "call dolt_rebase('--continue');", + ExpectedErrStr: "invalid commit hash: doesnotexist", + }, + { + Query: "update dolt_rebase set commit_hash='0123456789abcdef0123456789abcdef' where rebase_order=1;", + Expected: []sql.Row{{gmstypes.OkResult{ + RowsAffected: 1, + InsertID: 0, + Info: plan.UpdateInfo{ + Matched: 1, + Updated: 1, + Warnings: 0, + }, + }}}, + }, + { + Query: "call dolt_rebase('--continue');", + ExpectedErrStr: "unable to resolve commit hash 0123456789abcdef0123456789abcdef: target commit not found", + }, + }, + }, + { + Name: "dolt_rebase: abort properly cleans up", + SetUpScript: []string{ + "create table t (pk int primary key);", + "call dolt_commit('-Am', 'creating table t');", + "call dolt_branch('branch1');", + + "insert into t values (0);", + "call dolt_commit('-am', 'inserting row 0');", + + "call dolt_checkout('branch1');", + "insert into t values (1);", + "call dolt_commit('-am', 'inserting row 1');", + "insert into t values (10);", + "call dolt_commit('-am', 'inserting row 10');", + "insert into t values (100);", + "call dolt_commit('-am', 'inserting row 100');", + "insert into t values (1000);", + "call dolt_commit('-am', 'inserting row 1000');", + "insert into t values (10000);", + "call dolt_commit('-am', 'inserting row 10000');", + "insert into t values (100000);", + "call dolt_commit('-am', 'inserting row 100000');", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "call dolt_rebase('-i', 'main');", + Expected: []sql.Row{{0, "interactive rebase started on branch dolt_rebase_branch1; " + + "adjust the rebase plan in the dolt_rebase table, then " + + "continue rebasing by calling dolt_rebase('--continue')"}}, + }, + { + Query: "select active_branch();", + Expected: []sql.Row{{"dolt_rebase_branch1"}}, + }, + { + Query: "call dolt_rebase('--abort');", + Expected: []sql.Row{{0, "interactive rebase aborted"}}, + }, + { + Query: "select active_branch();", + Expected: []sql.Row{{"branch1"}}, + }, + { + Query: "select name from dolt_branches", + Expected: []sql.Row{{"main"}, {"branch1"}}, + }, + }, + }, + { + Name: "dolt_rebase: rebase plan using every action", + SetUpScript: []string{ + "create table t (pk int primary key);", + "call dolt_commit('-Am', 'creating table t');", + "call dolt_branch('branch1');", + + "insert into t values (0);", + "call dolt_commit('-am', 'inserting row 0');", + + "call dolt_checkout('branch1');", + "insert into t values (1);", + "call dolt_commit('-am', 'inserting row 1');", + "insert into t values (10);", + "call dolt_commit('-am', 'inserting row 10');", + "insert into t values (100);", + "call dolt_commit('-am', 'inserting row 100');", + "insert into t values (1000);", + "call dolt_commit('-am', 'inserting row 1000');", + "insert into t values (10000);", + "call dolt_commit('-am', 'inserting row 10000');", + "insert into t values (100000);", + "call dolt_commit('-am', 'inserting row 100000');", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "call dolt_rebase('-i', 'main');", + Expected: []sql.Row{{0, "interactive rebase started on branch dolt_rebase_branch1; " + + "adjust the rebase plan in the dolt_rebase table, then " + + "continue rebasing by calling dolt_rebase('--continue')"}}, + }, + { + Query: "select * from dolt_rebase order by rebase_order ASC;", + Expected: []sql.Row{ + {"1", "pick", doltCommit, "inserting row 1"}, + {"2", "pick", doltCommit, "inserting row 10"}, + {"3", "pick", doltCommit, "inserting row 100"}, + {"4", "pick", doltCommit, "inserting row 1000"}, + {"5", "pick", doltCommit, "inserting row 10000"}, + {"6", "pick", doltCommit, "inserting row 100000"}, + }, + }, + { + Query: "update dolt_rebase set rebase_order=6.1 where rebase_order=6;", + Expected: []sql.Row{{gmstypes.OkResult{RowsAffected: uint64(1), Info: plan.UpdateInfo{ + Matched: 1, + Updated: 1, + }}}}, + }, + { + Query: "update dolt_rebase set action='squash' where rebase_order in (2, 3);", + Expected: []sql.Row{{gmstypes.OkResult{RowsAffected: uint64(2), Info: plan.UpdateInfo{ + Matched: 2, + Updated: 2, + }}}}, + }, + { + Query: "update dolt_rebase set action='drop' where rebase_order = 4;", + Expected: []sql.Row{{gmstypes.OkResult{RowsAffected: uint64(1), Info: plan.UpdateInfo{ + Matched: 1, + Updated: 1, + }}}}, + }, + { + Query: "update dolt_rebase set action='reword', commit_message='reworded!' where rebase_order = 5;", + Expected: []sql.Row{{gmstypes.OkResult{RowsAffected: uint64(1), Info: plan.UpdateInfo{ + Matched: 1, + Updated: 1, + }}}}, + }, + { + Query: "update dolt_rebase set action='fixup' where rebase_order = 6.10;", + Expected: []sql.Row{{gmstypes.OkResult{RowsAffected: uint64(1), Info: plan.UpdateInfo{ + Matched: 1, + Updated: 1, + }}}}, + }, + { + Query: "call dolt_rebase('--continue');", + Expected: []sql.Row{{0, "interactive rebase completed"}}, + }, + { + // When rebase completes, rebase status should be cleared + Query: "call dolt_rebase('--continue');", + ExpectedErrStr: "no rebase in progress", + }, + { + // The dolt_rebase table is gone after rebasing completes + Query: "select * from dolt_rebase;", + ExpectedErrStr: "table not found: dolt_rebase", + }, + { + // The working branch for the rebase is deleted after rebasing completes + Query: "select name from dolt_branches", + Expected: []sql.Row{{"main"}, {"branch1"}}, + }, + { + // Assert that the commit history is now composed of different commits + Query: "select message from dolt_log order by date desc;", + Expected: []sql.Row{ + {"reworded!"}, + {"inserting row 1\n\ninserting row 10\n\ninserting row 100"}, + {"inserting row 0"}, + {"creating table t"}, + {"Initialize data repository"}}, + }, + { + Query: "select * from t;", + Expected: []sql.Row{{0}, {1}, {10}, {100}, {10000}, {100000}}, + }, + }, + }, + { + Name: "dolt_rebase: data conflicts", + SetUpScript: []string{ + "create table t (pk int primary key, c1 varchar(100));", + "call dolt_commit('-Am', 'creating table t');", + "call dolt_branch('branch1');", + + "insert into t values (0, 'zero');", + "call dolt_commit('-am', 'inserting row 0');", + + "call dolt_checkout('branch1');", + "insert into t values (1, 'one');", + "call dolt_commit('-am', 'inserting row 1');", + "update t set c1='uno' where pk=1;", + "call dolt_commit('-am', 'updating row 1');", + "update t set c1='ein' where pk=1;", + "call dolt_commit('-am', 'updating row 1');", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "call dolt_rebase('-i', 'main');", + Expected: []sql.Row{{0, "interactive rebase started on branch dolt_rebase_branch1; " + + "adjust the rebase plan in the dolt_rebase table, then " + + "continue rebasing by calling dolt_rebase('--continue')"}}, + }, + { + Query: "select * from dolt_rebase order by rebase_order ASC;", + Expected: []sql.Row{ + {"1", "pick", doltCommit, "inserting row 1"}, + {"2", "pick", doltCommit, "updating row 1"}, + {"3", "pick", doltCommit, "updating row 1"}, + }, + }, + { + Query: "update dolt_rebase set rebase_order=3.5 where rebase_order=1;", + Expected: []sql.Row{{gmstypes.OkResult{RowsAffected: uint64(1), Info: plan.UpdateInfo{ + Matched: 1, + Updated: 1, + }}}}, + }, + { + // Encountering a conflict during a rebase returns an error and aborts the rebase + Query: "call dolt_rebase('--continue');", + ExpectedErr: dprocedures.ErrRebaseConflict, + }, + { + // The rebase state has been cleared after hitting a conflict + Query: "call dolt_rebase('--continue');", + ExpectedErrStr: "no rebase in progress", + }, + { + // We're back to the original branch + Query: "select active_branch();", + Expected: []sql.Row{{"branch1"}}, + }, + { + // The conflicts table should be empty, since the rebase was aborted + Query: "select * from dolt_conflicts;", + Expected: []sql.Row{}, + }, + }, + }, + { + Name: "dolt_rebase: schema conflicts", + SetUpScript: []string{ + "create table t (pk int primary key);", + "call dolt_commit('-Am', 'creating table t');", + "call dolt_branch('branch1');", + + "insert into t values (0);", + "call dolt_commit('-am', 'inserting row 0');", + + "call dolt_checkout('branch1');", + "insert into t values (1);", + "call dolt_commit('-am', 'inserting row 1');", + "alter table t add column c1 varchar(100) NOT NULL;", + "call dolt_commit('-am', 'adding column c1');", + "alter table t modify column c1 varchar(100) comment 'foo';", + "call dolt_commit('-am', 'altering column c1');", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "call dolt_rebase('-i', 'main');", + Expected: []sql.Row{{0, "interactive rebase started on branch dolt_rebase_branch1; " + + "adjust the rebase plan in the dolt_rebase table, then " + + "continue rebasing by calling dolt_rebase('--continue')"}}, + }, + { + Query: "select * from dolt_rebase order by rebase_order ASC;", + Expected: []sql.Row{ + {"1", "pick", doltCommit, "inserting row 1"}, + {"2", "pick", doltCommit, "adding column c1"}, + {"3", "pick", doltCommit, "altering column c1"}, + }, + }, + { + Query: "update dolt_rebase set rebase_order=3.1 where rebase_order=2;", + Expected: []sql.Row{{gmstypes.OkResult{RowsAffected: uint64(1), Info: plan.UpdateInfo{ + Matched: 1, + Updated: 1, + }}}}, + }, + { + // Encountering a conflict during a rebase returns an error and aborts the rebase + Query: "call dolt_rebase('--continue');", + ExpectedErr: dprocedures.ErrRebaseConflict, + }, + { + // The rebase state has been cleared after hitting a conflict + Query: "call dolt_rebase('--continue');", + ExpectedErrStr: "no rebase in progress", + }, + { + // We're back to the original branch + Query: "select active_branch();", + Expected: []sql.Row{{"branch1"}}, + }, + { + // The schema conflicts table should be empty, since the rebase was aborted + Query: "select * from dolt_schema_conflicts;", + Expected: []sql.Row{}, + }, + }, + }, + { + // Tests that the rebase plan can be changed in non-standard ways, such as adding new commits to the plan + // and completely removing commits from the plan. These changes are also valid with Git. + Name: "dolt_rebase: non-standard plan changes", + SetUpScript: []string{ + "create table t (pk int primary key);", + "call dolt_commit('-Am', 'creating table t');", + "call dolt_branch('branch1');", + "call dolt_branch('branch2');", + + "insert into t values (0);", + "call dolt_commit('-am', 'inserting row 0');", + + "call dolt_checkout('branch2');", + "insert into t values (999);", + "call dolt_commit('-am', 'inserting row 999');", + + "call dolt_checkout('branch1');", + "insert into t values (1);", + "call dolt_commit('-am', 'inserting row 1');", + "insert into t values (2);", + "call dolt_commit('-am', 'inserting row 2');", + "insert into t values (3);", + "call dolt_commit('-am', 'inserting row 3');", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "select active_branch();", + Expected: []sql.Row{{"branch1"}}, + }, + { + Query: "select * from t;", + Expected: []sql.Row{{1}, {2}, {3}}, + }, + { + Query: "call dolt_rebase('-i', 'main');", + Expected: []sql.Row{{0, "interactive rebase started on branch dolt_rebase_branch1; " + + "adjust the rebase plan in the dolt_rebase table, then " + + "continue rebasing by calling dolt_rebase('--continue')"}}, + }, + { + Query: "select * from dolt_rebase order by rebase_order;", + Expected: []sql.Row{ + {"1", "pick", doltCommit, "inserting row 1"}, + {"2", "pick", doltCommit, "inserting row 2"}, + {"3", "pick", doltCommit, "inserting row 3"}, + }, + }, + { + Query: "delete from dolt_rebase where rebase_order > 1;", + Expected: []sql.Row{{gmstypes.NewOkResult(2)}}, + }, + { + Query: "insert into dolt_rebase values (2.12, 'pick', hashof('branch2'), 'inserting row 0');", + Expected: []sql.Row{{gmstypes.NewOkResult(1)}}, + }, + { + Query: "call dolt_rebase('--continue');", + Expected: []sql.Row{{0, "interactive rebase completed"}}, + }, + { + Query: "select message from dolt_log;", + Expected: []sql.Row{ + {"inserting row 999"}, + {"inserting row 1"}, + {"inserting row 0"}, + {"creating table t"}, + {"Initialize data repository"}, + }, + }, + { + Query: "select * from t;", + Expected: []sql.Row{{0}, {1}, {999}}, + }, + }, + }, + { + // Merge commits are skipped during a rebase + Name: "dolt_rebase: merge commits", + SetUpScript: []string{ + "create table t (pk int primary key);", + "call dolt_commit('-Am', 'creating table t');", + "call dolt_branch('branch1');", + + "insert into t values (0);", + "call dolt_commit('-am', 'inserting row 0');", + + "call dolt_checkout('branch1');", + "insert into t values (1);", + "call dolt_commit('-am', 'inserting row 1');", + "insert into t values (2);", + "call dolt_commit('-am', 'inserting row 2');", + "call dolt_merge('main');", + "insert into t values (3);", + "call dolt_commit('-am', 'inserting row 3');", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "select message from dolt_log;", + Expected: []sql.Row{ + {"inserting row 3"}, + {"Merge branch 'main' into branch1"}, + {"inserting row 2"}, + {"inserting row 0"}, + {"inserting row 1"}, + {"creating table t"}, + {"Initialize data repository"}, + }, + }, + { + Query: "call dolt_rebase('-i', 'main');", + Expected: []sql.Row{{0, "interactive rebase started on branch dolt_rebase_branch1; " + + "adjust the rebase plan in the dolt_rebase table, then " + + "continue rebasing by calling dolt_rebase('--continue')"}}, + }, + { + Query: "select * from dolt_rebase order by rebase_order;", + Expected: []sql.Row{ + {"1", "pick", doltCommit, "inserting row 1"}, + {"2", "pick", doltCommit, "inserting row 2"}, + {"3", "pick", doltCommit, "inserting row 3"}, + }, + }, + { + Query: "call dolt_rebase('--continue');", + Expected: []sql.Row{{0, "interactive rebase completed"}}, + }, + { + Query: "select message from dolt_log;", + Expected: []sql.Row{ + {"inserting row 3"}, + {"inserting row 2"}, + {"inserting row 1"}, + {"inserting row 0"}, + {"creating table t"}, + {"Initialize data repository"}, + }, + }, + }, + }, +} + +var DoltRebaseMultiSessionScriptTests = []queries.ScriptTest{ + { + // When the branch HEAD is changed while a rebase is in progress, the rebase should fail + Name: "dolt_rebase errors: branch HEAD changed during rebase", + SetUpScript: []string{ + "create table t (pk int primary key);", + "call dolt_commit('-Am', 'creating table t');", + "call dolt_checkout('-b', 'branch1');", + "insert into t values (1);", + "call dolt_commit('-am', 'inserting row 1');", + "insert into t values (2);", + "call dolt_commit('-am', 'inserting row 2');", + "insert into t values (3);", + "call dolt_commit('-am', 'inserting row 3');", + }, + Assertions: []queries.ScriptTestAssertion{ + { + Query: "/* client a */ select active_branch();", + Expected: []sql.Row{{"branch1"}}, + }, + { + Query: "/* client b */ call dolt_checkout('branch1');", + Expected: []sql.Row{{0, "Switched to branch 'branch1'"}}, + }, + { + Query: "/* client b */ select active_branch();", + Expected: []sql.Row{{"branch1"}}, + }, + { + Query: "/* client a */ call dolt_rebase('-i', 'main');", + Expected: []sql.Row{{0, "interactive rebase started on branch dolt_rebase_branch1; " + + "adjust the rebase plan in the dolt_rebase table, then " + + "continue rebasing by calling dolt_rebase('--continue')"}}, + }, + { + Query: "/* client b */ insert into t values (1000);", + Expected: []sql.Row{}, + }, + { + Query: "/* client b */ call dolt_commit('-am', 'inserting row 1000');", + SkipResultsCheck: true, + }, + { + Query: "/* client a */ call dolt_rebase('--continue');", + ExpectedErrStr: "Error 1105 (HY000): rebase aborted due to changes in branch branch1", + }, + }, + }, +} diff --git a/go/libraries/doltcore/sqle/enginetest/dolt_server_test.go b/go/libraries/doltcore/sqle/enginetest/dolt_server_test.go index 4e2887811ae..0a7981c33fb 100755 --- a/go/libraries/doltcore/sqle/enginetest/dolt_server_test.go +++ b/go/libraries/doltcore/sqle/enginetest/dolt_server_test.go @@ -527,8 +527,10 @@ func testMultiSessionScriptTests(t *testing.T, tests []queries.ScriptTest) { } else if assertion.Expected != nil { require.NoError(t, err) assertResultsEqual(t, assertion.Expected, rows) + } else if assertion.SkipResultsCheck { + // no-op } else { - require.Fail(t, "unsupported ScriptTestAssertion property: %v", assertion) + t.Fatalf("unsupported ScriptTestAssertion property: %v", assertion) } if rows != nil { require.NoError(t, rows.Close()) diff --git a/go/serial/workingset.fbs b/go/serial/workingset.fbs index e9e53e0a14c..1d45a7c22f3 100644 --- a/go/serial/workingset.fbs +++ b/go/serial/workingset.fbs @@ -24,6 +24,7 @@ table WorkingSet { timestamp_millis:uint64; merge_state:MergeState; + rebase_state:RebaseState; } table MergeState { @@ -42,6 +43,17 @@ table MergeState { is_cherry_pick:bool; } +table RebaseState { + // The address of the the working root value before the rebase started. + pre_working_root_addr:[ubyte] (required); + + // The branch being rebased. + branch:[ubyte] (required); + + // The commit that we are rebasing onto. + onto_commit_addr:[ubyte] (required); +} + // KEEP THIS IN SYNC WITH fileidentifiers.go file_identifier "WRST"; diff --git a/go/store/datas/database_common.go b/go/store/datas/database_common.go index 02b70011ff0..d86577c0a1e 100644 --- a/go/store/datas/database_common.go +++ b/go/store/datas/database_common.go @@ -392,7 +392,7 @@ func (db *database) doSetHead(ctx context.Context, ds Dataset, addr hash.Hash, w } // TODO - construct new meta instance rather than using the default - updateWS := workingset_flatbuffer(cmtRtHsh, &cmtRtHsh, nil, nil) + updateWS := workingset_flatbuffer(cmtRtHsh, &cmtRtHsh, nil, nil, nil) ref, err := db.WriteValue(ctx, types.SerialMessage(updateWS)) if err != nil { return prolly.AddressMap{}, err @@ -540,7 +540,7 @@ func (db *database) doFastForward(ctx context.Context, ds Dataset, newHeadAddr h } // TODO - construct new meta instance rather than using the default - updateWS := workingset_flatbuffer(cmtRtHsh, &cmtRtHsh, nil, nil) + updateWS := workingset_flatbuffer(cmtRtHsh, &cmtRtHsh, nil, nil, nil) ref, err := db.WriteValue(ctx, types.SerialMessage(updateWS)) if err != nil { return prolly.AddressMap{}, err @@ -766,12 +766,12 @@ func (db *database) UpdateStashList(ctx context.Context, ds Dataset, stashListAd }) } -func (db *database) UpdateWorkingSet(ctx context.Context, ds Dataset, workingSet WorkingSetSpec, prevHash hash.Hash) (Dataset, error) { +func (db *database) UpdateWorkingSet(ctx context.Context, ds Dataset, workingSetSpec WorkingSetSpec, prevHash hash.Hash) (Dataset, error) { return db.doHeadUpdate( ctx, ds, func(ds Dataset) error { - addr, ref, err := newWorkingSet(ctx, db, workingSet.Meta, workingSet.WorkingRoot, workingSet.StagedRoot, workingSet.MergeState) + addr, ref, err := newWorkingSet(ctx, db, workingSetSpec) if err != nil { return err } @@ -841,7 +841,7 @@ func (db *database) CommitWithWorkingSet( val types.Value, workingSetSpec WorkingSetSpec, prevWsHash hash.Hash, opts CommitOptions, ) (Dataset, Dataset, error) { - wsAddr, wsValRef, err := newWorkingSet(ctx, db, workingSetSpec.Meta, workingSetSpec.WorkingRoot, workingSetSpec.StagedRoot, workingSetSpec.MergeState) + wsAddr, wsValRef, err := newWorkingSet(ctx, db, workingSetSpec) if err != nil { return Dataset{}, Dataset{}, err } diff --git a/go/store/datas/dataset.go b/go/store/datas/dataset.go index 18dd9e69886..e4fa181f319 100644 --- a/go/store/datas/dataset.go +++ b/go/store/datas/dataset.go @@ -158,6 +158,32 @@ type WorkingSetHead struct { WorkingAddr hash.Hash StagedAddr *hash.Hash MergeState *MergeState + RebaseState *RebaseState +} + +type RebaseState struct { + preRebaseWorkingAddr *hash.Hash + ontoCommitAddr *hash.Hash + branch string +} + +func (rs *RebaseState) PreRebaseWorkingAddr() hash.Hash { + if rs.preRebaseWorkingAddr != nil { + return *rs.preRebaseWorkingAddr + } else { + return hash.Hash{} + } +} + +func (rs *RebaseState) Branch(_ context.Context) string { + return rs.branch +} + +func (rs *RebaseState) OntoCommit(ctx context.Context, vr types.ValueReader) (*Commit, error) { + if rs.ontoCommitAddr != nil { + return LoadCommitAddr(ctx, vr, *rs.ontoCommitAddr) + } + return nil, nil } type MergeState struct { @@ -398,6 +424,18 @@ func (h serialWorkingSetHead) HeadWorkingSet() (*WorkingSetHead, error) { } ret.MergeState.isCherryPick = mergeState.IsCherryPick() } + + rebaseState, err := h.msg.TryRebaseState(nil) + if err != nil { + return nil, err + } + if rebaseState != nil { + ret.RebaseState = NewRebaseState( + hash.New(rebaseState.PreWorkingRootAddrBytes()), + hash.New(rebaseState.OntoCommitAddrBytes()), + string(rebaseState.BranchBytes())) + } + return &ret, nil } diff --git a/go/store/datas/workingset.go b/go/store/datas/workingset.go index a13bc430911..61c869905f1 100755 --- a/go/store/datas/workingset.go +++ b/go/store/datas/workingset.go @@ -29,16 +29,20 @@ const ( workingSetMetaField = "meta" workingRootRefField = "workingRootRef" stagedRootRefField = "stagedRootRef" - mergeStateField = "mergeState" ) const ( mergeStateName = "MergeState" + mergeStateField = "mergeState" mergeStateCommitSpecField = "commitSpec" mergeStateCommitField = "commit" mergeStateWorkingPreMergeField = "workingPreMerge" ) +const ( + rebaseStateField = "rebaseState" +) + const ( workingSetMetaName = "WorkingSetMeta" workingSetMetaNameField = "name" @@ -82,9 +86,10 @@ type WorkingSetSpec struct { WorkingRoot types.Ref StagedRoot types.Ref MergeState *MergeState + RebaseState *RebaseState } -// NewWorkingSet creates a new working set object. +// newWorkingSet creates a new working set object. // A working set is a value that has been persisted but is not necessarily referenced by a Commit. As the name implies, // it's storage for data changes that have not yet been incorporated into the commit graph but need durable storage. // @@ -101,10 +106,16 @@ type WorkingSetSpec struct { // // ``` // where M is a struct type and R is a ref type. -func newWorkingSet(ctx context.Context, db *database, meta *WorkingSetMeta, workingRef, stagedRef types.Ref, mergeState *MergeState) (hash.Hash, types.Ref, error) { +func newWorkingSet(ctx context.Context, db *database, workingSetSpec WorkingSetSpec) (hash.Hash, types.Ref, error) { + meta := workingSetSpec.Meta + workingRef := workingSetSpec.WorkingRoot + stagedRef := workingSetSpec.StagedRoot + mergeState := workingSetSpec.MergeState + rebaseState := workingSetSpec.RebaseState + if db.Format().UsesFlatbuffers() { stagedAddr := stagedRef.TargetHash() - data := workingset_flatbuffer(workingRef.TargetHash(), &stagedAddr, mergeState, meta) + data := workingset_flatbuffer(workingRef.TargetHash(), &stagedAddr, mergeState, rebaseState, meta) r, err := db.WriteValue(ctx, types.SerialMessage(data)) if err != nil { @@ -151,10 +162,11 @@ func newWorkingSet(ctx context.Context, db *database, meta *WorkingSetMeta, work return ref.TargetHash(), ref, nil } -func workingset_flatbuffer(working hash.Hash, staged *hash.Hash, mergeState *MergeState, meta *WorkingSetMeta) serial.Message { +// workingset_flatbuffer creates a flatbuffer message for working set metadata. +func workingset_flatbuffer(working hash.Hash, staged *hash.Hash, mergeState *MergeState, rebaseState *RebaseState, meta *WorkingSetMeta) serial.Message { builder := flatbuffers.NewBuilder(1024) workingoff := builder.CreateByteVector(working[:]) - var stagedOff, mergeStateOff flatbuffers.UOffsetT + var stagedOff, mergeStateOff, rebaseStateOffset flatbuffers.UOffsetT if staged != nil { stagedOff = builder.CreateByteVector((*staged)[:]) } @@ -172,6 +184,17 @@ func workingset_flatbuffer(working hash.Hash, staged *hash.Hash, mergeState *Mer mergeStateOff = serial.MergeStateEnd(builder) } + if rebaseState != nil { + preRebaseRootAddrOffset := builder.CreateByteVector((*rebaseState.preRebaseWorkingAddr)[:]) + ontoAddrOffset := builder.CreateByteVector((*rebaseState.ontoCommitAddr)[:]) + branchOffset := builder.CreateString(rebaseState.branch) + serial.RebaseStateStart(builder) + serial.RebaseStateAddPreWorkingRootAddr(builder, preRebaseRootAddrOffset) + serial.RebaseStateAddBranch(builder, branchOffset) + serial.RebaseStateAddOntoCommitAddr(builder, ontoAddrOffset) + rebaseStateOffset = serial.RebaseStateEnd(builder) + } + var nameOff, emailOff, descOff flatbuffers.UOffsetT if meta != nil { nameOff = builder.CreateString(meta.Name) @@ -187,6 +210,10 @@ func workingset_flatbuffer(working hash.Hash, staged *hash.Hash, mergeState *Mer if mergeStateOff != 0 { serial.WorkingSetAddMergeState(builder, mergeStateOff) } + if rebaseStateOffset != 0 { + serial.WorkingSetAddRebaseState(builder, rebaseStateOffset) + } + if meta != nil { serial.WorkingSetAddName(builder, nameOff) serial.WorkingSetAddEmail(builder, emailOff) @@ -233,6 +260,14 @@ func NewMergeState( } } +func NewRebaseState(preRebaseWorkingRoot hash.Hash, commitAddr hash.Hash, branch string) *RebaseState { + return &RebaseState{ + preRebaseWorkingAddr: &preRebaseWorkingRoot, + ontoCommitAddr: &commitAddr, + branch: branch, + } +} + func IsWorkingSet(v types.Value) (bool, error) { if s, ok := v.(types.Struct); ok { // We're being more lenient here than in other checks, to make it more likely we can release changes to the