Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

223 lines
7.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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/cockroachdb/apd/v3"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/types"
"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"
RebaseActionEdit = "edit"
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 *apd.Decimal
Action string
CommitHash string
CommitMsg string
}
// RebaseOrderAsFloat returns the RebaseOrder as a float32. Float32 provides enough scale and precision to hold
// rebase order values, since they are limited to two decimal points of precision and six total digits.
func (rps *RebasePlanStep) RebaseOrderAsFloat() float32 {
f64, _ := rps.RebaseOrder.Float64()
return float32(f64)
}
// 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: types.DecimalFromFloat32(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.Cmp(step.RebaseOrder) >= 0 {
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[*sql.Context](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 {
_, optCmt, _, _, err := commitItr.Next(ctx)
if err == io.EOF {
return commits, nil
} else if err != nil {
return nil, err
}
commit, ok := optCmt.ToCommit()
if !ok {
return nil, doltdb.ErrGhostCommitEncountered // Not sure if we can get this far. commit walk is going to be a bear.
}
// Don't include merge commits in the rebase plan
if commit.NumParents() == 1 {
commits = append(commits, commit)
}
}
}