github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/sqle/dprocedures/dolt_rebase.go (about)

     1  // Copyright 2023 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package dprocedures
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  
    21  	"github.com/dolthub/go-mysql-server/sql"
    22  	"github.com/dolthub/go-mysql-server/sql/types"
    23  	goerrors "gopkg.in/src-d/go-errors.v1"
    24  
    25  	"github.com/dolthub/dolt/go/cmd/dolt/cli"
    26  	"github.com/dolthub/dolt/go/libraries/doltcore/cherry_pick"
    27  	"github.com/dolthub/dolt/go/libraries/doltcore/diff"
    28  	"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
    29  	"github.com/dolthub/dolt/go/libraries/doltcore/env/actions"
    30  	"github.com/dolthub/dolt/go/libraries/doltcore/merge"
    31  	"github.com/dolthub/dolt/go/libraries/doltcore/rebase"
    32  	"github.com/dolthub/dolt/go/libraries/doltcore/ref"
    33  	"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
    34  )
    35  
    36  var doltRebaseProcedureSchema = []*sql.Column{
    37  	{
    38  		Name:     "status",
    39  		Type:     types.Int64,
    40  		Nullable: false,
    41  	},
    42  	{
    43  		Name:     "message",
    44  		Type:     types.LongText,
    45  		Nullable: true,
    46  	},
    47  }
    48  
    49  var RebaseActionEnumType = types.MustCreateEnumType([]string{
    50  	rebase.RebaseActionDrop,
    51  	rebase.RebaseActionPick,
    52  	rebase.RebaseActionReword,
    53  	rebase.RebaseActionSquash,
    54  	rebase.RebaseActionFixup}, sql.Collation_Default)
    55  
    56  var DoltRebaseSystemTableSchema = []*sql.Column{
    57  	{
    58  		Name:       "rebase_order",
    59  		Type:       types.MustCreateDecimalType(6, 2),
    60  		Nullable:   false,
    61  		PrimaryKey: true,
    62  	},
    63  	{
    64  		Name:     "action",
    65  		Type:     RebaseActionEnumType,
    66  		Nullable: false,
    67  	},
    68  	{
    69  		Name:     "commit_hash",
    70  		Type:     types.Text,
    71  		Nullable: false,
    72  	},
    73  	{
    74  		Name:     "commit_message",
    75  		Type:     types.Text,
    76  		Nullable: false,
    77  	},
    78  }
    79  
    80  // ErrRebaseUncommittedChanges is used when a rebase is started, but there are uncommitted (and not
    81  // ignored) changes in the working set.
    82  var ErrRebaseUncommittedChanges = fmt.Errorf("cannot start a rebase with uncommitted changes")
    83  
    84  // ErrRebaseConflict is used when a merge conflict is detected while rebasing a commit.
    85  var ErrRebaseConflict = goerrors.NewKind(
    86  	"merge conflict detected while rebasing commit %s. " +
    87  		"the rebase has been automatically aborted")
    88  
    89  // ErrRebaseConflictWithAbortError is used when a merge conflict is detected while rebasing a commit,
    90  // and we are unable to cleanly abort the rebase.
    91  var ErrRebaseConflictWithAbortError = goerrors.NewKind(
    92  	"merge conflict detected while rebasing commit %s. " +
    93  		"attempted to abort rebase operation, but encountered error: %w")
    94  
    95  // SuccessfulRebaseMessage is used when a rebase finishes successfully. The branch that was rebased should be appended
    96  // to the end of the message.
    97  var SuccessfulRebaseMessage = "Successfully rebased and updated refs/heads/"
    98  
    99  var RebaseAbortedMessage = "Interactive rebase aborted"
   100  
   101  func doltRebase(ctx *sql.Context, args ...string) (sql.RowIter, error) {
   102  	res, message, err := doDoltRebase(ctx, args)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	return rowToIter(int64(res), message), nil
   107  }
   108  
   109  func doDoltRebase(ctx *sql.Context, args []string) (int, string, error) {
   110  	if ctx.GetCurrentDatabase() == "" {
   111  		return 1, "", sql.ErrNoDatabaseSelected.New()
   112  	}
   113  
   114  	apr, err := cli.CreateRebaseArgParser().Parse(args)
   115  	if err != nil {
   116  		return 1, "", err
   117  	}
   118  
   119  	switch {
   120  	case apr.Contains(cli.AbortParam):
   121  		err := abortRebase(ctx)
   122  		if err != nil {
   123  			return 1, "", err
   124  		} else {
   125  			return 0, RebaseAbortedMessage, nil
   126  		}
   127  
   128  	case apr.Contains(cli.ContinueFlag):
   129  		rebaseBranch, err := continueRebase(ctx)
   130  		if err != nil {
   131  			return 1, "", err
   132  		} else {
   133  			return 0, SuccessfulRebaseMessage + rebaseBranch, nil
   134  		}
   135  
   136  	default:
   137  		if apr.NArg() == 0 {
   138  			return 1, "", fmt.Errorf("not enough args")
   139  		} else if apr.NArg() > 1 {
   140  			return 1, "", fmt.Errorf("too many args")
   141  		}
   142  		if !apr.Contains(cli.InteractiveFlag) {
   143  			return 1, "", fmt.Errorf("non-interactive rebases not currently supported")
   144  		}
   145  		err = startRebase(ctx, apr.Arg(0))
   146  		if err != nil {
   147  			return 1, "", err
   148  		}
   149  
   150  		currentBranch, err := currentBranch(ctx)
   151  		if err != nil {
   152  			return 1, "", err
   153  		}
   154  
   155  		return 0, fmt.Sprintf("interactive rebase started on branch %s; "+
   156  			"adjust the rebase plan in the dolt_rebase table, then continue rebasing by "+
   157  			"calling dolt_rebase('--continue')", currentBranch), nil
   158  	}
   159  }
   160  
   161  func startRebase(ctx *sql.Context, upstreamPoint string) error {
   162  	if upstreamPoint == "" {
   163  		return fmt.Errorf("no upstream branch specified")
   164  	}
   165  
   166  	err := validateWorkingSetCanStartRebase(ctx)
   167  	if err != nil {
   168  		return err
   169  	}
   170  
   171  	doltSession := dsess.DSessFromSess(ctx.Session)
   172  	headRef, err := doltSession.CWBHeadRef(ctx, ctx.GetCurrentDatabase())
   173  	if err != nil {
   174  		return err
   175  	}
   176  	dbData, ok := doltSession.GetDbData(ctx, ctx.GetCurrentDatabase())
   177  	if !ok {
   178  		return fmt.Errorf("unable to find database %s", ctx.GetCurrentDatabase())
   179  	}
   180  
   181  	rebaseBranch, err := currentBranch(ctx)
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	startCommit, err := dbData.Ddb.ResolveCommitRef(ctx, ref.NewBranchRef(rebaseBranch))
   187  	if err != nil {
   188  		return err
   189  	}
   190  
   191  	commitSpec, err := doltdb.NewCommitSpec(upstreamPoint)
   192  	if err != nil {
   193  		return err
   194  	}
   195  
   196  	optCmt, err := dbData.Ddb.Resolve(ctx, commitSpec, headRef)
   197  	if err != nil {
   198  		return err
   199  	}
   200  	upstreamCommit, ok := optCmt.ToCommit()
   201  	if !ok {
   202  		return doltdb.ErrGhostCommitEncountered
   203  	}
   204  
   205  	// rebaseWorkingBranch is the name of the temporary branch used when performing a rebase. In Git, a rebase
   206  	// happens with a detatched HEAD, but Dolt doesn't support that, we use a temporary branch.
   207  	rebaseWorkingBranch := "dolt_rebase_" + rebaseBranch
   208  	var rsc doltdb.ReplicationStatusController
   209  	err = actions.CreateBranchWithStartPt(ctx, dbData, rebaseWorkingBranch, upstreamPoint, false, &rsc)
   210  	if err != nil {
   211  		return err
   212  	}
   213  	err = commitTransaction(ctx, doltSession, &rsc)
   214  	if err != nil {
   215  		return err
   216  	}
   217  
   218  	// Checkout our new branch
   219  	wsRef, err := ref.WorkingSetRefForHead(ref.NewBranchRef(rebaseWorkingBranch))
   220  	if err != nil {
   221  		return err
   222  	}
   223  	err = doltSession.SwitchWorkingSet(ctx, ctx.GetCurrentDatabase(), wsRef)
   224  	if err != nil {
   225  		return err
   226  	}
   227  
   228  	dbData, ok = doltSession.GetDbData(ctx, ctx.GetCurrentDatabase())
   229  	if !ok {
   230  		return fmt.Errorf("unable to get db datata for database %s", ctx.GetCurrentDatabase())
   231  	}
   232  
   233  	db, err := doltSession.Provider().Database(ctx, ctx.GetCurrentDatabase())
   234  	if err != nil {
   235  		return err
   236  	}
   237  
   238  	workingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase())
   239  	if err != nil {
   240  		return err
   241  	}
   242  
   243  	branchRoots, err := dbData.Ddb.ResolveBranchRoots(ctx, ref.NewBranchRef(rebaseBranch))
   244  	if err != nil {
   245  		return err
   246  	}
   247  
   248  	newWorkingSet, err := workingSet.StartRebase(ctx, upstreamCommit, rebaseBranch, branchRoots.Working)
   249  	if err != nil {
   250  		return err
   251  	}
   252  
   253  	err = doltSession.SetWorkingSet(ctx, ctx.GetCurrentDatabase(), newWorkingSet)
   254  	if err != nil {
   255  		return err
   256  	}
   257  
   258  	// Create the rebase plan and save it in the database
   259  	rebasePlan, err := rebase.CreateDefaultRebasePlan(ctx, startCommit, upstreamCommit)
   260  	if err != nil {
   261  		abortErr := abortRebase(ctx)
   262  		if abortErr != nil {
   263  			return fmt.Errorf("%s: unable to cleanly abort rebase: %s", err.Error(), abortErr.Error())
   264  		}
   265  		return err
   266  	}
   267  	rdb, ok := db.(rebase.RebasePlanDatabase)
   268  	if !ok {
   269  		return fmt.Errorf("expected a dsess.RebasePlanDatabase implementation, but received a %T", db)
   270  	}
   271  	return rdb.SaveRebasePlan(ctx, rebasePlan)
   272  }
   273  
   274  // validateRebaseBranchHasntChanged checks that the branch being rebased hasn't been updated since the rebase started,
   275  // and returns an error if any changes are detected.
   276  func validateRebaseBranchHasntChanged(ctx *sql.Context, branch string, rebaseState *doltdb.RebaseState) error {
   277  	doltSession := dsess.DSessFromSess(ctx.Session)
   278  	doltDb, ok := doltSession.GetDoltDB(ctx, ctx.GetCurrentDatabase())
   279  	if !ok {
   280  		return fmt.Errorf("unable to access DoltDB for database %s", ctx.GetCurrentDatabase())
   281  	}
   282  
   283  	wsRef, err := ref.WorkingSetRefForHead(ref.NewBranchRef(branch))
   284  	if err != nil {
   285  		return err
   286  	}
   287  
   288  	resolvedWorkingSet, err := doltDb.ResolveWorkingSet(ctx, wsRef)
   289  	if err != nil {
   290  		return err
   291  	}
   292  	hash2, err := resolvedWorkingSet.StagedRoot().HashOf()
   293  	if err != nil {
   294  		return err
   295  	}
   296  	hash1, err := rebaseState.PreRebaseWorkingRoot().HashOf()
   297  	if err != nil {
   298  		return err
   299  	}
   300  	if hash1 != hash2 {
   301  		return fmt.Errorf("rebase aborted due to changes in branch %s", branch)
   302  	}
   303  
   304  	return nil
   305  }
   306  
   307  func validateWorkingSetCanStartRebase(ctx *sql.Context) error {
   308  	doltSession := dsess.DSessFromSess(ctx.Session)
   309  
   310  	// Make sure there isn't an active rebase or merge in progress already
   311  	ws, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase())
   312  	if err != nil {
   313  		return err
   314  	}
   315  	if ws.MergeActive() {
   316  		return fmt.Errorf("unable to start rebase while a merge is in progress – abort the current merge before proceeding")
   317  	}
   318  	if ws.RebaseActive() {
   319  		return fmt.Errorf("unable to start rebase while another rebase is in progress – abort the current rebase before proceeding")
   320  	}
   321  
   322  	// Make sure the working set doesn't contain any uncommitted changes
   323  	roots, ok := doltSession.GetRoots(ctx, ctx.GetCurrentDatabase())
   324  	if !ok {
   325  		return fmt.Errorf("unable to get roots for database %s", ctx.GetCurrentDatabase())
   326  	}
   327  	wsOnlyHasIgnoredTables, err := diff.WorkingSetContainsOnlyIgnoredTables(ctx, roots)
   328  	if err != nil {
   329  		return err
   330  	}
   331  	if !wsOnlyHasIgnoredTables {
   332  		return ErrRebaseUncommittedChanges
   333  	}
   334  
   335  	return nil
   336  }
   337  
   338  func abortRebase(ctx *sql.Context) error {
   339  	doltSession := dsess.DSessFromSess(ctx.Session)
   340  
   341  	workingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase())
   342  	if err != nil {
   343  		return err
   344  	}
   345  	if !workingSet.RebaseActive() {
   346  		return fmt.Errorf("no rebase in progress")
   347  	}
   348  
   349  	// Clear the rebase state (even though we're going to delete this branch next)
   350  	rebaseState := workingSet.RebaseState()
   351  	workingSet = workingSet.AbortRebase()
   352  	err = doltSession.SetWorkingSet(ctx, ctx.GetCurrentDatabase(), workingSet)
   353  	if err != nil {
   354  		return err
   355  	}
   356  
   357  	// Delete the working branch
   358  	var rsc doltdb.ReplicationStatusController
   359  	dbData, ok := doltSession.GetDbData(ctx, ctx.GetCurrentDatabase())
   360  	if !ok {
   361  		return fmt.Errorf("unable to get DbData for database %s", ctx.GetCurrentDatabase())
   362  	}
   363  	headRef, err := doltSession.CWBHeadRef(ctx, ctx.GetCurrentDatabase())
   364  	if err != nil {
   365  		return err
   366  	}
   367  	err = actions.DeleteBranch(ctx, dbData, headRef.GetPath(), actions.DeleteOptions{
   368  		Force:                      true,
   369  		AllowDeletingCurrentBranch: true,
   370  	}, doltSession.Provider(), &rsc)
   371  	if err != nil {
   372  		return err
   373  	}
   374  
   375  	// Switch back to the original branch head
   376  	wsRef, err := ref.WorkingSetRefForHead(ref.NewBranchRef(rebaseState.Branch()))
   377  	if err != nil {
   378  		return err
   379  	}
   380  
   381  	return doltSession.SwitchWorkingSet(ctx, ctx.GetCurrentDatabase(), wsRef)
   382  }
   383  
   384  func continueRebase(ctx *sql.Context) (string, error) {
   385  	// TODO: Eventually, when we allow interactive-rebases to be stopped and started (e.g. with the break action,
   386  	//       or for conflict resolution), we'll need to track what step we're at in the rebase plan.
   387  
   388  	// Validate that we are in an interactive rebase
   389  	doltSession := dsess.DSessFromSess(ctx.Session)
   390  	workingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase())
   391  	if err != nil {
   392  		return "", err
   393  	}
   394  	if !workingSet.RebaseActive() {
   395  		return "", fmt.Errorf("no rebase in progress")
   396  	}
   397  
   398  	db, err := doltSession.Provider().Database(ctx, ctx.GetCurrentDatabase())
   399  	if err != nil {
   400  		return "", err
   401  	}
   402  
   403  	rdb, ok := db.(rebase.RebasePlanDatabase)
   404  	if !ok {
   405  		return "", fmt.Errorf("expected a dsess.RebasePlanDatabase implementation, but received a %T", db)
   406  	}
   407  	rebasePlan, err := rdb.LoadRebasePlan(ctx)
   408  	if err != nil {
   409  		return "", err
   410  	}
   411  
   412  	err = rebase.ValidateRebasePlan(ctx, rebasePlan)
   413  	if err != nil {
   414  		return "", err
   415  	}
   416  
   417  	for _, step := range rebasePlan.Steps {
   418  		err = processRebasePlanStep(ctx, &step)
   419  		if err != nil {
   420  			return "", err
   421  		}
   422  	}
   423  
   424  	// Update the branch being rebased to point to the same commit as our temporary working branch
   425  	rebaseBranchWorkingSet, err := doltSession.WorkingSet(ctx, ctx.GetCurrentDatabase())
   426  	if err != nil {
   427  		return "", err
   428  	}
   429  	dbData, ok := doltSession.GetDbData(ctx, ctx.GetCurrentDatabase())
   430  	if !ok {
   431  		return "", fmt.Errorf("unable to get db data for database %s", ctx.GetCurrentDatabase())
   432  	}
   433  
   434  	rebaseBranch := rebaseBranchWorkingSet.RebaseState().Branch()
   435  	rebaseWorkingBranch := "dolt_rebase_" + rebaseBranch
   436  
   437  	// Check that the branch being rebased hasn't been updated since the rebase started
   438  	err = validateRebaseBranchHasntChanged(ctx, rebaseBranch, rebaseBranchWorkingSet.RebaseState())
   439  	if err != nil {
   440  		return "", err
   441  	}
   442  
   443  	// TODO: copyABranch (and the underlying call to doltdb.NewBranchAtCommit) has a race condition
   444  	//       where another session can set the branch head AFTER doltdb.NewBranchAtCommit updates
   445  	//       the branch head, but BEFORE doltdb.NewBranchAtCommit retrieves the working set for the
   446  	//       branch and updates the working root and staged root for the working set. We may be able
   447  	//       to fix this race condition by changing doltdb.NewBranchAtCommit to use
   448  	//       database.CommitWithWorkingSet, since it updates a branch head and working set atomically.
   449  	err = copyABranch(ctx, dbData, rebaseWorkingBranch, rebaseBranch, true, nil)
   450  	if err != nil {
   451  		return "", err
   452  	}
   453  
   454  	// Checkout the branch being rebased
   455  	previousBranchWorkingSetRef, err := ref.WorkingSetRefForHead(ref.NewBranchRef(rebaseBranchWorkingSet.RebaseState().Branch()))
   456  	if err != nil {
   457  		return "", err
   458  	}
   459  	err = doltSession.SwitchWorkingSet(ctx, ctx.GetCurrentDatabase(), previousBranchWorkingSetRef)
   460  	if err != nil {
   461  		return "", err
   462  	}
   463  
   464  	// delete the temporary working branch
   465  	dbData, ok = doltSession.GetDbData(ctx, ctx.GetCurrentDatabase())
   466  	if !ok {
   467  		return "", fmt.Errorf("unable to lookup dbdata")
   468  	}
   469  	return rebaseBranch, actions.DeleteBranch(ctx, dbData, rebaseWorkingBranch, actions.DeleteOptions{
   470  		Force: true,
   471  	}, doltSession.Provider(), nil)
   472  }
   473  
   474  func processRebasePlanStep(ctx *sql.Context, planStep *rebase.RebasePlanStep) error {
   475  	// Make sure we have a transaction opened for the session
   476  	// NOTE: After our first call to cherry-pick, the tx is committed, so a new tx needs to be started
   477  	//       as we process additional rebase actions.
   478  	doltSession := dsess.DSessFromSess(ctx.Session)
   479  	if doltSession.GetTransaction() == nil {
   480  		_, err := doltSession.StartTransaction(ctx, sql.ReadWrite)
   481  		if err != nil {
   482  			return err
   483  		}
   484  	}
   485  
   486  	switch planStep.Action {
   487  	case rebase.RebaseActionDrop:
   488  		return nil
   489  
   490  	case rebase.RebaseActionPick, rebase.RebaseActionReword:
   491  		options := cherry_pick.CherryPickOptions{}
   492  		if planStep.Action == rebase.RebaseActionReword {
   493  			options.CommitMessage = planStep.CommitMsg
   494  		}
   495  		return handleRebaseCherryPick(ctx, planStep.CommitHash, options)
   496  
   497  	case rebase.RebaseActionSquash, rebase.RebaseActionFixup:
   498  		options := cherry_pick.CherryPickOptions{Amend: true}
   499  		if planStep.Action == rebase.RebaseActionSquash {
   500  			commitMessage, err := squashCommitMessage(ctx, planStep.CommitHash)
   501  			if err != nil {
   502  				return err
   503  			}
   504  			options.CommitMessage = commitMessage
   505  		}
   506  		return handleRebaseCherryPick(ctx, planStep.CommitHash, options)
   507  
   508  	default:
   509  		return fmt.Errorf("rebase action '%s' is not supported", planStep.Action)
   510  	}
   511  }
   512  
   513  // handleRebaseCherryPick runs a cherry-pick for the specified |commitHash|, using the specified
   514  // cherry-pick |options| and checks the results for any errors or merge conflicts. The initial
   515  // version of rebase doesn't support conflict resolution, so if any conflicts are detected, the
   516  // rebase is aborted and an error is returned.
   517  func handleRebaseCherryPick(ctx *sql.Context, commitHash string, options cherry_pick.CherryPickOptions) error {
   518  	_, mergeResult, err := cherry_pick.CherryPick(ctx, commitHash, options)
   519  
   520  	var schemaConflict merge.SchemaConflict
   521  	isSchemaConflict := errors.As(err, &schemaConflict)
   522  
   523  	if (mergeResult != nil && mergeResult.HasMergeArtifacts()) || isSchemaConflict {
   524  		// TODO: rebase doesn't currently support conflict resolution, but ideally, when a conflict
   525  		//       is detected, the rebase would be paused and the user would resolve the conflict just
   526  		//       like any other conflict, and then call dolt_rebase --continue to keep going.
   527  		abortErr := abortRebase(ctx)
   528  		if abortErr != nil {
   529  			return ErrRebaseConflictWithAbortError.New(commitHash, abortErr)
   530  		}
   531  		return ErrRebaseConflict.New(commitHash)
   532  	}
   533  	return err
   534  }
   535  
   536  // squashCommitMessage looks up the commit at HEAD and the commit identified by |nextCommitHash| and squashes their two
   537  // commit messages together.
   538  func squashCommitMessage(ctx *sql.Context, nextCommitHash string) (string, error) {
   539  	doltSession := dsess.DSessFromSess(ctx.Session)
   540  	headCommit, err := doltSession.GetHeadCommit(ctx, ctx.GetCurrentDatabase())
   541  	if err != nil {
   542  		return "", err
   543  	}
   544  	headCommitMeta, err := headCommit.GetCommitMeta(ctx)
   545  	if err != nil {
   546  		return "", err
   547  	}
   548  
   549  	ddb, ok := doltSession.GetDoltDB(ctx, ctx.GetCurrentDatabase())
   550  	if !ok {
   551  		return "", fmt.Errorf("unable to get doltdb!")
   552  	}
   553  	spec, err := doltdb.NewCommitSpec(nextCommitHash)
   554  	if err != nil {
   555  		return "", err
   556  	}
   557  	headRef, err := doltSession.CWBHeadRef(ctx, ctx.GetCurrentDatabase())
   558  	if err != nil {
   559  		return "", err
   560  	}
   561  
   562  	optCmt, err := ddb.Resolve(ctx, spec, headRef)
   563  	if err != nil {
   564  		return "", err
   565  	}
   566  	nextCommit, ok := optCmt.ToCommit()
   567  	if !ok {
   568  		return "", doltdb.ErrGhostCommitEncountered
   569  	}
   570  
   571  	nextCommitMeta, err := nextCommit.GetCommitMeta(ctx)
   572  	if err != nil {
   573  		return "", err
   574  	}
   575  	commitMessage := headCommitMeta.Description + "\n\n" + nextCommitMeta.Description
   576  
   577  	return commitMessage, nil
   578  }
   579  
   580  // currentBranch returns the name of the currently checked out branch, or any error if one was encountered.
   581  func currentBranch(ctx *sql.Context) (string, error) {
   582  	doltSession := dsess.DSessFromSess(ctx.Session)
   583  	headRef, err := doltSession.CWBHeadRef(ctx, ctx.GetCurrentDatabase())
   584  	if err != nil {
   585  		return "", err
   586  	}
   587  	return headRef.GetPath(), nil
   588  }