github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/env/actions/checkout.go (about)

     1  // Copyright 2021 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 actions
    16  
    17  import (
    18  	"context"
    19  	"time"
    20  
    21  	"github.com/dolthub/dolt/go/store/datas"
    22  
    23  	"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
    24  	"github.com/dolthub/dolt/go/libraries/doltcore/env"
    25  	"github.com/dolthub/dolt/go/libraries/doltcore/ref"
    26  	"github.com/dolthub/dolt/go/libraries/utils/set"
    27  	"github.com/dolthub/dolt/go/store/hash"
    28  )
    29  
    30  func CheckoutAllTables(ctx context.Context, roots doltdb.Roots) (doltdb.Roots, error) {
    31  	tbls, err := doltdb.UnionTableNames(ctx, roots.Working, roots.Staged, roots.Head)
    32  	if err != nil {
    33  		return doltdb.Roots{}, err
    34  	}
    35  
    36  	return MoveTablesFromHeadToWorking(ctx, roots, tbls)
    37  }
    38  
    39  // CheckoutTables takes in a set of tables and docs and checks them out to another branch.
    40  func CheckoutTables(ctx context.Context, roots doltdb.Roots, tables []string) (doltdb.Roots, error) {
    41  	return MoveTablesFromHeadToWorking(ctx, roots, tables)
    42  }
    43  
    44  // MoveTablesFromHeadToWorking replaces the tables named from the given head to the given working root, overwriting any
    45  // working changes, and returns the new resulting roots
    46  func MoveTablesFromHeadToWorking(ctx context.Context, roots doltdb.Roots, tbls []string) (doltdb.Roots, error) {
    47  	var unknownTbls []string
    48  	for _, tblName := range tbls {
    49  		tbl, ok, err := roots.Staged.GetTable(ctx, doltdb.TableName{Name: tblName})
    50  		if err != nil {
    51  			return doltdb.Roots{}, err
    52  		}
    53  		fkc, err := roots.Staged.GetForeignKeyCollection(ctx)
    54  		if err != nil {
    55  			return doltdb.Roots{}, err
    56  		}
    57  
    58  		if !ok {
    59  			tbl, ok, err = roots.Head.GetTable(ctx, doltdb.TableName{Name: tblName})
    60  			if err != nil {
    61  				return doltdb.Roots{}, err
    62  			}
    63  
    64  			fkc, err = roots.Head.GetForeignKeyCollection(ctx)
    65  			if err != nil {
    66  				return doltdb.Roots{}, err
    67  			}
    68  
    69  			if !ok {
    70  				unknownTbls = append(unknownTbls, tblName)
    71  				continue
    72  			}
    73  		}
    74  
    75  		roots.Working, err = roots.Working.PutTable(ctx, doltdb.TableName{Name: tblName}, tbl)
    76  		if err != nil {
    77  			return doltdb.Roots{}, err
    78  		}
    79  
    80  		roots.Working, err = roots.Working.PutForeignKeyCollection(ctx, fkc)
    81  		if err != nil {
    82  			return doltdb.Roots{}, err
    83  		}
    84  	}
    85  
    86  	if len(unknownTbls) > 0 {
    87  		// Return table not exist error before RemoveTables, which fails silently if the table is not on the root.
    88  		err := validateTablesExist(ctx, roots.Working, unknownTbls)
    89  		if err != nil {
    90  			return doltdb.Roots{}, err
    91  		}
    92  
    93  		roots.Working, err = roots.Working.RemoveTables(ctx, false, false, unknownTbls...)
    94  
    95  		if err != nil {
    96  			return doltdb.Roots{}, err
    97  		}
    98  	}
    99  
   100  	return roots, nil
   101  }
   102  
   103  // RootsForBranch returns the roots needed for a branch checkout. |roots.Head| should be the pre-checkout head. The
   104  // returned roots struct has |Head| set to |branchRoot|.
   105  func RootsForBranch(ctx context.Context, roots doltdb.Roots, branchRoot doltdb.RootValue, force bool) (doltdb.Roots, error) {
   106  	conflicts := set.NewStrSet([]string{})
   107  	if roots.Head == nil {
   108  		roots.Working = branchRoot
   109  		roots.Staged = branchRoot
   110  		roots.Head = branchRoot
   111  		return roots, nil
   112  	}
   113  
   114  	wrkTblHashes, err := moveModifiedTables(ctx, roots.Head, branchRoot, roots.Working, conflicts, force)
   115  	if err != nil {
   116  		return doltdb.Roots{}, err
   117  	}
   118  
   119  	stgTblHashes, err := moveModifiedTables(ctx, roots.Head, branchRoot, roots.Staged, conflicts, force)
   120  	if err != nil {
   121  		return doltdb.Roots{}, err
   122  	}
   123  
   124  	if conflicts.Size() > 0 {
   125  		return doltdb.Roots{}, ErrCheckoutWouldOverwrite{conflicts.AsSlice()}
   126  	}
   127  
   128  	workingForeignKeys, err := moveForeignKeys(ctx, roots.Head, branchRoot, roots.Working, force)
   129  	if err != nil {
   130  		return doltdb.Roots{}, err
   131  	}
   132  
   133  	stagedForeignKeys, err := moveForeignKeys(ctx, roots.Head, branchRoot, roots.Staged, force)
   134  	if err != nil {
   135  		return doltdb.Roots{}, err
   136  	}
   137  
   138  	roots.Working, err = writeTableHashes(ctx, branchRoot, wrkTblHashes)
   139  	if err != nil {
   140  		return doltdb.Roots{}, err
   141  	}
   142  
   143  	roots.Staged, err = writeTableHashes(ctx, branchRoot, stgTblHashes)
   144  	if err != nil {
   145  		return doltdb.Roots{}, err
   146  	}
   147  
   148  	roots.Working, err = roots.Working.PutForeignKeyCollection(ctx, workingForeignKeys)
   149  	if err != nil {
   150  		return doltdb.Roots{}, err
   151  	}
   152  
   153  	roots.Staged, err = roots.Staged.PutForeignKeyCollection(ctx, stagedForeignKeys)
   154  	if err != nil {
   155  		return doltdb.Roots{}, err
   156  	}
   157  
   158  	roots.Head = branchRoot
   159  	return roots, nil
   160  }
   161  
   162  // CleanOldWorkingSet resets the source branch's working set to the branch head, leaving the source branch unchanged
   163  func CleanOldWorkingSet(
   164  	ctx context.Context,
   165  	dbData env.DbData,
   166  	doltDb *doltdb.DoltDB,
   167  	username, email string,
   168  	initialRoots doltdb.Roots,
   169  	initialHeadRef ref.DoltRef,
   170  	initialWs *doltdb.WorkingSet,
   171  ) error {
   172  	// reset the source branch's working set to the branch head, leaving the source branch unchanged
   173  	err := ResetHard(ctx, dbData, doltDb, username, email, "", initialRoots, initialHeadRef, initialWs)
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	// Annoyingly, after the ResetHard above we need to get all the roots again, because the working set has changed
   179  	cm, err := doltDb.ResolveCommitRef(ctx, initialHeadRef)
   180  	if err != nil {
   181  		return err
   182  	}
   183  
   184  	headRoot, err := cm.ResolveRootValue(ctx)
   185  	if err != nil {
   186  		return err
   187  	}
   188  
   189  	workingSet, err := doltDb.ResolveWorkingSet(ctx, initialWs.Ref())
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	resetRoots := doltdb.Roots{
   195  		Head:    headRoot,
   196  		Working: workingSet.WorkingRoot(),
   197  		Staged:  workingSet.StagedRoot(),
   198  	}
   199  
   200  	// we also have to do a clean, because we the ResetHard won't touch any new tables (tables only in the working set)
   201  	newRoots, err := CleanUntracked(ctx, resetRoots, []string{}, false, true)
   202  	if err != nil {
   203  		return err
   204  	}
   205  
   206  	h, err := workingSet.HashOf()
   207  	if err != nil {
   208  		return err
   209  	}
   210  
   211  	err = doltDb.UpdateWorkingSet(
   212  		ctx,
   213  		initialWs.Ref(),
   214  		initialWs.WithWorkingRoot(newRoots.Working).WithStagedRoot(newRoots.Staged).ClearMerge().ClearRebase(),
   215  		h,
   216  
   217  		&datas.WorkingSetMeta{
   218  			Name:        username,
   219  			Email:       email,
   220  			Timestamp:   uint64(time.Now().Unix()),
   221  			Description: "reset hard",
   222  		},
   223  		nil,
   224  	)
   225  	if err != nil {
   226  		return err
   227  	}
   228  	return nil
   229  }
   230  
   231  // BranchHeadRoot returns the root value at the branch head with the name given
   232  func BranchHeadRoot(ctx context.Context, db *doltdb.DoltDB, brName string) (doltdb.RootValue, error) {
   233  	cs, err := doltdb.NewCommitSpec(brName)
   234  	if err != nil {
   235  		return nil, doltdb.RootValueUnreadable{RootType: doltdb.HeadRoot, Cause: err}
   236  	}
   237  
   238  	optCmt, err := db.Resolve(ctx, cs, nil)
   239  	if err != nil {
   240  		return nil, doltdb.RootValueUnreadable{RootType: doltdb.HeadRoot, Cause: err}
   241  	}
   242  
   243  	cm, ok := optCmt.ToCommit()
   244  	if !ok {
   245  		return nil, doltdb.ErrGhostCommitEncountered
   246  	}
   247  
   248  	branchRoot, err := cm.GetRootValue(ctx)
   249  	if err != nil {
   250  		return nil, err
   251  	}
   252  	return branchRoot, nil
   253  }
   254  
   255  // moveModifiedTables handles working set changes during a branch change.
   256  // When moving between branches, changes in the working set should travel with you.
   257  // Working set changes cannot be moved if the table differs between the old and new head,
   258  // in this case, we throw a conflict and error (as per Git).
   259  func moveModifiedTables(ctx context.Context, oldRoot, newRoot, changedRoot doltdb.RootValue, conflicts *set.StrSet, force bool) (map[string]hash.Hash, error) {
   260  	resultMap := make(map[string]hash.Hash)
   261  	tblNames, err := newRoot.GetTableNames(ctx, doltdb.DefaultSchemaName)
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  
   266  	for _, tblName := range tblNames {
   267  		oldHash, _, err := oldRoot.GetTableHash(ctx, tblName)
   268  		if err != nil {
   269  			return nil, err
   270  		}
   271  
   272  		newHash, _, err := newRoot.GetTableHash(ctx, tblName)
   273  		if err != nil {
   274  			return nil, err
   275  		}
   276  
   277  		changedHash, _, err := changedRoot.GetTableHash(ctx, tblName)
   278  		if err != nil {
   279  			return nil, err
   280  		}
   281  
   282  		if oldHash == changedHash {
   283  			resultMap[tblName] = newHash
   284  		} else if oldHash == newHash {
   285  			resultMap[tblName] = changedHash
   286  		} else if force {
   287  			resultMap[tblName] = newHash
   288  		} else {
   289  			conflicts.Add(tblName)
   290  		}
   291  	}
   292  
   293  	tblNames, err = changedRoot.GetTableNames(ctx, doltdb.DefaultSchemaName)
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  
   298  	for _, tblName := range tblNames {
   299  		if _, exists := resultMap[tblName]; !exists {
   300  			oldHash, _, err := oldRoot.GetTableHash(ctx, tblName)
   301  			if err != nil {
   302  				return nil, err
   303  			}
   304  
   305  			changedHash, _, err := changedRoot.GetTableHash(ctx, tblName)
   306  			if err != nil {
   307  				return nil, err
   308  			}
   309  
   310  			if oldHash == emptyHash {
   311  				resultMap[tblName] = changedHash
   312  			} else if force {
   313  				resultMap[tblName] = oldHash
   314  			} else if oldHash != changedHash {
   315  				conflicts.Add(tblName)
   316  			}
   317  		}
   318  	}
   319  
   320  	return resultMap, nil
   321  }
   322  
   323  // moveForeignKeys returns the foreign key collection that should be used for the new working set.
   324  func moveForeignKeys(ctx context.Context, oldRoot, newRoot, changedRoot doltdb.RootValue, force bool) (*doltdb.ForeignKeyCollection, error) {
   325  	oldFks, err := oldRoot.GetForeignKeyCollection(ctx)
   326  	if err != nil {
   327  		return nil, err
   328  	}
   329  
   330  	newFks, err := newRoot.GetForeignKeyCollection(ctx)
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  
   335  	changedFks, err := changedRoot.GetForeignKeyCollection(ctx)
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  
   340  	oldHash, err := oldFks.HashOf(ctx, oldRoot.VRW())
   341  	if err != nil {
   342  		return nil, err
   343  	}
   344  
   345  	newHash, err := newFks.HashOf(ctx, newRoot.VRW())
   346  	if err != nil {
   347  		return nil, err
   348  	}
   349  
   350  	changedHash, err := changedFks.HashOf(ctx, changedRoot.VRW())
   351  	if err != nil {
   352  		return nil, err
   353  	}
   354  
   355  	if oldHash == changedHash {
   356  		return newFks, nil
   357  	} else if oldHash == newHash {
   358  		return changedFks, nil
   359  	} else {
   360  		// Both roots have modified the foreign keys. We need to do more work to merge them together into a new foreign
   361  		// key collection.
   362  		return mergeForeignKeyChanges(ctx, oldFks, newRoot, newFks, changedRoot, changedFks, force)
   363  	}
   364  }
   365  
   366  // mergeForeignKeyChanges merges the foreign key changes from the old and changed roots into a new foreign key
   367  // collection, or returns an error if the changes are incompatible. Changes are incompatible if the changed root
   368  // and new root both altered foreign keys on the same table.
   369  func mergeForeignKeyChanges(
   370  	ctx context.Context,
   371  	oldFks *doltdb.ForeignKeyCollection,
   372  	newRoot doltdb.RootValue,
   373  	newFks *doltdb.ForeignKeyCollection,
   374  	changedRoot doltdb.RootValue,
   375  	changedFks *doltdb.ForeignKeyCollection,
   376  	force bool,
   377  ) (*doltdb.ForeignKeyCollection, error) {
   378  	fksByTable := make(map[string][]doltdb.ForeignKey)
   379  
   380  	conflicts := set.NewEmptyStrSet()
   381  	tblNames, err := newRoot.GetTableNames(ctx, doltdb.DefaultSchemaName)
   382  	if err != nil {
   383  		return nil, err
   384  	}
   385  
   386  	for _, tblName := range tblNames {
   387  		oldFksForTable, _ := oldFks.KeysForTable(tblName)
   388  		newFksForTable, _ := newFks.KeysForTable(tblName)
   389  		changedFksForTable, _ := changedFks.KeysForTable(tblName)
   390  
   391  		oldHash, err := doltdb.CombinedHash(oldFksForTable)
   392  		if err != nil {
   393  			return nil, err
   394  		}
   395  		newHash, err := doltdb.CombinedHash(newFksForTable)
   396  		if err != nil {
   397  			return nil, err
   398  		}
   399  		changedHash, err := doltdb.CombinedHash(changedFksForTable)
   400  		if err != nil {
   401  			return nil, err
   402  		}
   403  
   404  		if oldHash == changedHash {
   405  			fksByTable[tblName] = append(fksByTable[tblName], newFksForTable...)
   406  		} else if oldHash == newHash {
   407  			fksByTable[tblName] = append(fksByTable[tblName], changedFksForTable...)
   408  		} else if force {
   409  			fksByTable[tblName] = append(fksByTable[tblName], newFksForTable...)
   410  		} else {
   411  			conflicts.Add(tblName)
   412  		}
   413  	}
   414  
   415  	tblNames, err = changedRoot.GetTableNames(ctx, doltdb.DefaultSchemaName)
   416  	if err != nil {
   417  		return nil, err
   418  	}
   419  
   420  	for _, tblName := range tblNames {
   421  		if _, exists := fksByTable[tblName]; !exists {
   422  			oldKeys, _ := oldFks.KeysForTable(tblName)
   423  			oldHash, err := doltdb.CombinedHash(oldKeys)
   424  			if err != nil {
   425  				return nil, err
   426  			}
   427  
   428  			changedKeys, _ := changedFks.KeysForTable(tblName)
   429  			changedHash, err := doltdb.CombinedHash(changedKeys)
   430  			if err != nil {
   431  				return nil, err
   432  			}
   433  
   434  			if oldHash == emptyHash {
   435  				fksByTable[tblName] = append(fksByTable[tblName], changedKeys...)
   436  			} else if force {
   437  				fksByTable[tblName] = append(fksByTable[tblName], oldKeys...)
   438  			} else if oldHash != changedHash {
   439  				conflicts.Add(tblName)
   440  			}
   441  		}
   442  	}
   443  
   444  	if conflicts.Size() > 0 {
   445  		return nil, ErrCheckoutWouldOverwrite{conflicts.AsSlice()}
   446  	}
   447  
   448  	fks := make([]doltdb.ForeignKey, 0)
   449  	for _, v := range fksByTable {
   450  		fks = append(fks, v...)
   451  	}
   452  
   453  	return doltdb.NewForeignKeyCollection(fks...)
   454  }
   455  
   456  // writeTableHashes writes new table hash values for the root given and returns it.
   457  // This is an inexpensive and convenient way of replacing all the tables at once.
   458  func writeTableHashes(ctx context.Context, head doltdb.RootValue, tblHashes map[string]hash.Hash) (doltdb.RootValue, error) {
   459  	names, err := head.GetTableNames(ctx, doltdb.DefaultSchemaName)
   460  	if err != nil {
   461  		return nil, err
   462  	}
   463  
   464  	var toDrop []string
   465  	for _, name := range names {
   466  		if _, ok := tblHashes[name]; !ok {
   467  			toDrop = append(toDrop, name)
   468  		}
   469  	}
   470  
   471  	head, err = head.RemoveTables(ctx, false, false, toDrop...)
   472  	if err != nil {
   473  		return nil, err
   474  	}
   475  
   476  	for k, v := range tblHashes {
   477  		if v == emptyHash {
   478  			continue
   479  		}
   480  
   481  		head, err = head.SetTableHash(ctx, k, v)
   482  		if err != nil {
   483  			return nil, err
   484  		}
   485  	}
   486  
   487  	return head, nil
   488  }
   489  
   490  // CheckoutWouldStompWorkingSetChanges checks that the current working set is "compatible" with the dest working set.
   491  // This means that if both working sets are present (ie there are changes on both source and dest branches),
   492  // we check if the changes are identical before allowing a clobbering checkout.
   493  // Working set errors are ignored by this function, because they are properly handled elsewhere.
   494  func CheckoutWouldStompWorkingSetChanges(ctx context.Context, sourceRoots, destRoots doltdb.Roots) (bool, error) {
   495  
   496  	wouldStomp := doRootsHaveIncompatibleChanges(sourceRoots, destRoots)
   497  
   498  	if !wouldStomp {
   499  		return false, nil
   500  	}
   501  
   502  	// In some cases, a working set differs from its head only by the feature version.
   503  	// If this is the case, moving the working set is safe.
   504  	modifiedSourceRoots, err := clearFeatureVersion(ctx, sourceRoots)
   505  	if err != nil {
   506  		return true, err
   507  	}
   508  
   509  	modifiedDestRoots, err := clearFeatureVersion(ctx, destRoots)
   510  	if err != nil {
   511  		return true, err
   512  	}
   513  
   514  	return doRootsHaveIncompatibleChanges(modifiedSourceRoots, modifiedDestRoots), nil
   515  }
   516  
   517  func doRootsHaveIncompatibleChanges(sourceRoots, destRoots doltdb.Roots) bool {
   518  	sourceHasChanges, sourceWorkingHash, sourceStagedHash, err := RootHasUncommittedChanges(sourceRoots)
   519  	if err != nil {
   520  		return false
   521  	}
   522  
   523  	destHasChanges, destWorkingHash, destStagedHash, err := RootHasUncommittedChanges(destRoots)
   524  	if err != nil {
   525  		return false
   526  	}
   527  
   528  	// This is a stomping checkout operation if both the source and dest have uncommitted changes, and they're not the
   529  	// same uncommitted changes
   530  	return sourceHasChanges && destHasChanges && (sourceWorkingHash != destWorkingHash || sourceStagedHash != destStagedHash)
   531  }
   532  
   533  // clearFeatureVersion creates a new version of the provided roots where all three roots have the same
   534  // feature version. By hashing these new roots, we can easily determine whether the roots differ only by
   535  // their feature version.
   536  func clearFeatureVersion(ctx context.Context, roots doltdb.Roots) (doltdb.Roots, error) {
   537  	currentBranchFeatureVersion, _, err := roots.Head.GetFeatureVersion(ctx)
   538  	if err != nil {
   539  		return doltdb.Roots{}, err
   540  	}
   541  
   542  	modifiedWorking, err := roots.Working.SetFeatureVersion(currentBranchFeatureVersion)
   543  	if err != nil {
   544  		return doltdb.Roots{}, err
   545  	}
   546  
   547  	modifiedStaged, err := roots.Staged.SetFeatureVersion(currentBranchFeatureVersion)
   548  	if err != nil {
   549  		return doltdb.Roots{}, err
   550  	}
   551  
   552  	return doltdb.Roots{
   553  		Head:    roots.Head,
   554  		Working: modifiedWorking,
   555  		Staged:  modifiedStaged,
   556  	}, nil
   557  }
   558  
   559  // RootHasUncommittedChanges returns whether the roots given have uncommitted changes, and the hashes of
   560  // the working and staged roots are identical. This function will ignore any difference in feature
   561  // versions between the root values.
   562  func RootHasUncommittedChanges(roots doltdb.Roots) (hasChanges bool, workingHash hash.Hash, stagedHash hash.Hash, err error) {
   563  	roots, err = clearFeatureVersion(context.Background(), roots)
   564  	if err != nil {
   565  		return false, hash.Hash{}, hash.Hash{}, err
   566  	}
   567  
   568  	headHash, err := roots.Head.HashOf()
   569  	if err != nil {
   570  		return false, hash.Hash{}, hash.Hash{}, err
   571  	}
   572  
   573  	workingHash, err = roots.Working.HashOf()
   574  	if err != nil {
   575  		return false, hash.Hash{}, hash.Hash{}, err
   576  	}
   577  
   578  	stagedHash, err = roots.Staged.HashOf()
   579  	if err != nil {
   580  		return false, hash.Hash{}, hash.Hash{}, err
   581  	}
   582  
   583  	hasChanges = workingHash != stagedHash || stagedHash != headHash
   584  	return hasChanges, workingHash, stagedHash, nil
   585  }