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

     1  // Copyright 2020 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  	"fmt"
    20  	"time"
    21  
    22  	"github.com/dolthub/dolt/go/store/datas"
    23  
    24  	"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
    25  	"github.com/dolthub/dolt/go/libraries/doltcore/env"
    26  	"github.com/dolthub/dolt/go/libraries/doltcore/ref"
    27  	"github.com/dolthub/dolt/go/libraries/doltcore/schema"
    28  	"github.com/dolthub/dolt/go/libraries/utils/argparser"
    29  )
    30  
    31  // resetHardTables resolves a new HEAD commit from a refSpec and updates working set roots by
    32  // resetting the table contexts for tracked tables. New tables are ignored. Returns new HEAD
    33  // Commit and Roots.
    34  func resetHardTables(ctx context.Context, dbData env.DbData, cSpecStr string, roots doltdb.Roots) (*doltdb.Commit, doltdb.Roots, error) {
    35  	ddb := dbData.Ddb
    36  	rsr := dbData.Rsr
    37  
    38  	var newHead *doltdb.Commit
    39  	if cSpecStr != "" {
    40  		cs, err := doltdb.NewCommitSpec(cSpecStr)
    41  		if err != nil {
    42  			return nil, doltdb.Roots{}, err
    43  		}
    44  
    45  		headRef, err := rsr.CWBHeadRef()
    46  		if err != nil {
    47  			return nil, doltdb.Roots{}, err
    48  		}
    49  		optCmt, err := ddb.Resolve(ctx, cs, headRef)
    50  		if err != nil {
    51  			return nil, doltdb.Roots{}, err
    52  		}
    53  
    54  		var ok bool
    55  		if newHead, ok = optCmt.ToCommit(); !ok {
    56  			return nil, doltdb.Roots{}, doltdb.ErrGhostCommitEncountered
    57  		}
    58  
    59  		roots.Head, err = newHead.GetRootValue(ctx)
    60  		if err != nil {
    61  			return nil, doltdb.Roots{}, err
    62  		}
    63  	}
    64  
    65  	// mirroring Git behavior, untracked tables are ignored on 'reset --hard',
    66  	// save the state of these tables and apply them to |newHead|'s root.
    67  	//
    68  	// as a special case, if an untracked table has a tag collision with any
    69  	// tables in |newHead| we silently drop it from the new working set.
    70  	// these tag collision is typically cause by table renames (bug #751).
    71  
    72  	untracked, err := doltdb.GetAllSchemas(ctx, roots.Working)
    73  	if err != nil {
    74  		return nil, doltdb.Roots{}, err
    75  	}
    76  	// untracked tables exist in |working| but not in |staged|
    77  	staged, err := roots.Staged.GetTableNames(ctx, doltdb.DefaultSchemaName)
    78  	if err != nil {
    79  		return nil, doltdb.Roots{}, err
    80  	}
    81  	for _, name := range staged {
    82  		delete(untracked, name)
    83  	}
    84  
    85  	newWkRoot := roots.Head
    86  
    87  	ws, err := doltdb.GetAllSchemas(ctx, newWkRoot)
    88  	if err != nil {
    89  		return nil, doltdb.Roots{}, err
    90  	}
    91  	tags := mapColumnTags(ws)
    92  
    93  	for name, sch := range untracked {
    94  		for _, pk := range sch.GetAllCols().GetColumns() {
    95  			if _, ok := tags[pk.Tag]; ok {
    96  				// |pk.Tag| collides with a schema in |newWkRoot|
    97  				delete(untracked, name)
    98  			}
    99  		}
   100  	}
   101  
   102  	for name := range untracked {
   103  		tbl, _, err := roots.Working.GetTable(ctx, doltdb.TableName{Name: name})
   104  		if err != nil {
   105  			return nil, doltdb.Roots{}, err
   106  		}
   107  		newWkRoot, err = newWkRoot.PutTable(ctx, doltdb.TableName{Name: name}, tbl)
   108  		if err != nil {
   109  			return nil, doltdb.Roots{}, fmt.Errorf("failed to write table back to database: %s", err)
   110  		}
   111  	}
   112  
   113  	// need to save the state of files that aren't tracked
   114  	untrackedTables := make(map[string]*doltdb.Table)
   115  	wTblNames, err := roots.Working.GetTableNames(ctx, doltdb.DefaultSchemaName)
   116  
   117  	if err != nil {
   118  		return nil, doltdb.Roots{}, err
   119  	}
   120  
   121  	for _, tblName := range wTblNames {
   122  		untrackedTables[tblName], _, err = roots.Working.GetTable(ctx, doltdb.TableName{Name: tblName})
   123  
   124  		if err != nil {
   125  			return nil, doltdb.Roots{}, err
   126  		}
   127  	}
   128  
   129  	headTblNames, err := roots.Staged.GetTableNames(ctx, doltdb.DefaultSchemaName)
   130  
   131  	if err != nil {
   132  		return nil, doltdb.Roots{}, err
   133  	}
   134  
   135  	for _, tblName := range headTblNames {
   136  		delete(untrackedTables, tblName)
   137  	}
   138  
   139  	roots.Working = newWkRoot
   140  	roots.Staged = roots.Head
   141  
   142  	return newHead, roots, nil
   143  }
   144  
   145  // ResetHardTables resets the tables in working, staged, and head based on the given parameters. Returns the new
   146  // head commit and resulting roots
   147  func ResetHardTables(ctx context.Context, dbData env.DbData, cSpecStr string, roots doltdb.Roots) (*doltdb.Commit, doltdb.Roots, error) {
   148  	return resetHardTables(ctx, dbData, cSpecStr, roots)
   149  }
   150  
   151  // ResetHard resets the working, staged, and head to the ones in the provided roots and head ref.
   152  // The reset can be performed on a non-current branch and working set.
   153  // Returns an error if the reset fails.
   154  func ResetHard(
   155  	ctx context.Context,
   156  	dbData env.DbData,
   157  	doltDb *doltdb.DoltDB,
   158  	username, email string,
   159  	cSpecStr string,
   160  	roots doltdb.Roots,
   161  	headRef ref.DoltRef,
   162  	ws *doltdb.WorkingSet,
   163  ) error {
   164  
   165  	newHead, roots, err := resetHardTables(ctx, dbData, cSpecStr, roots)
   166  	if err != nil {
   167  		return err
   168  	}
   169  
   170  	currentWs, err := doltDb.ResolveWorkingSet(ctx, ws.Ref())
   171  	if err != nil {
   172  		return err
   173  	}
   174  
   175  	h, err := currentWs.HashOf()
   176  	if err != nil {
   177  		return err
   178  	}
   179  
   180  	// TODO - refactor this to ensure the update to the head and working set are transactional.
   181  	err = doltDb.UpdateWorkingSet(ctx, ws.Ref(), ws.WithWorkingRoot(roots.Working).WithStagedRoot(roots.Staged).ClearMerge().ClearRebase(), h, &datas.WorkingSetMeta{
   182  		Name:        username,
   183  		Email:       email,
   184  		Timestamp:   uint64(time.Now().Unix()),
   185  		Description: "reset hard",
   186  	}, nil)
   187  	if err != nil {
   188  		return err
   189  	}
   190  
   191  	if newHead != nil {
   192  		err = doltDb.SetHeadToCommit(ctx, headRef, newHead)
   193  		if err != nil {
   194  			return err
   195  		}
   196  	}
   197  
   198  	return nil
   199  }
   200  
   201  func ResetSoftTables(ctx context.Context, dbData env.DbData, apr *argparser.ArgParseResults, roots doltdb.Roots) (doltdb.Roots, error) {
   202  	tables, err := getUnionedTables(ctx, apr.Args, roots.Staged, roots.Head)
   203  	if err != nil {
   204  		return doltdb.Roots{}, err
   205  	}
   206  
   207  	err = ValidateTables(context.TODO(), tables, roots.Staged, roots.Head)
   208  	if err != nil {
   209  		return doltdb.Roots{}, err
   210  	}
   211  
   212  	roots.Staged, err = MoveTablesBetweenRoots(ctx, tables, roots.Head, roots.Staged)
   213  	if err != nil {
   214  		return doltdb.Roots{}, err
   215  	}
   216  
   217  	return roots, nil
   218  }
   219  
   220  // ResetSoft resets the staged value from HEAD for the tables given and returns the updated roots.
   221  func ResetSoft(ctx context.Context, dbData env.DbData, tables []string, roots doltdb.Roots) (doltdb.Roots, error) {
   222  	tables, err := getUnionedTables(ctx, tables, roots.Staged, roots.Head)
   223  	if err != nil {
   224  		return doltdb.Roots{}, err
   225  	}
   226  
   227  	err = ValidateTables(context.TODO(), tables, roots.Staged, roots.Head)
   228  	if err != nil {
   229  		return doltdb.Roots{}, err
   230  	}
   231  	return resetStaged(ctx, roots, tables)
   232  }
   233  
   234  // ResetSoftToRef matches the `git reset --soft <REF>` pattern. It returns a new Roots with the Staged and Head values
   235  // set to the commit specified by the spec string. The Working root is not set
   236  func ResetSoftToRef(ctx context.Context, dbData env.DbData, cSpecStr string) (doltdb.Roots, error) {
   237  	cs, err := doltdb.NewCommitSpec(cSpecStr)
   238  	if err != nil {
   239  		return doltdb.Roots{}, err
   240  	}
   241  
   242  	headRef, err := dbData.Rsr.CWBHeadRef()
   243  	if err != nil {
   244  		return doltdb.Roots{}, err
   245  	}
   246  	optCmt, err := dbData.Ddb.Resolve(ctx, cs, headRef)
   247  	if err != nil {
   248  		return doltdb.Roots{}, err
   249  	}
   250  	newHead, ok := optCmt.ToCommit()
   251  	if !ok {
   252  		return doltdb.Roots{}, doltdb.ErrGhostCommitEncountered
   253  	}
   254  
   255  	foundRoot, err := newHead.GetRootValue(ctx)
   256  	if err != nil {
   257  		return doltdb.Roots{}, err
   258  	}
   259  
   260  	// Update the head to this commit
   261  	if err = dbData.Ddb.SetHeadToCommit(ctx, headRef, newHead); err != nil {
   262  		return doltdb.Roots{}, err
   263  	}
   264  
   265  	return doltdb.Roots{
   266  		Head:   foundRoot,
   267  		Staged: foundRoot,
   268  	}, err
   269  }
   270  
   271  func getUnionedTables(ctx context.Context, tables []string, stagedRoot, headRoot doltdb.RootValue) ([]string, error) {
   272  	if len(tables) == 0 || (len(tables) == 1 && tables[0] == ".") {
   273  		var err error
   274  		tables, err = doltdb.UnionTableNames(ctx, stagedRoot, headRoot)
   275  
   276  		if err != nil {
   277  			return nil, err
   278  		}
   279  	}
   280  
   281  	return tables, nil
   282  }
   283  
   284  func resetStaged(ctx context.Context, roots doltdb.Roots, tbls []string) (doltdb.Roots, error) {
   285  	newStaged, err := MoveTablesBetweenRoots(ctx, tbls, roots.Head, roots.Staged)
   286  	if err != nil {
   287  		return doltdb.Roots{}, err
   288  	}
   289  
   290  	roots.Staged = newStaged
   291  	return roots, nil
   292  }
   293  
   294  // IsValidRef validates whether the input parameter is a valid cString
   295  // TODO: this doesn't belong in this package
   296  func IsValidRef(ctx context.Context, cSpecStr string, ddb *doltdb.DoltDB, rsr env.RepoStateReader) (bool, error) {
   297  	// The error return value is only for propagating unhandled errors from rsr.CWBHeadRef()
   298  	// All other errors merely indicate an invalid ref spec.
   299  	// TODO: It's much better to enumerate the expected errors, to make sure we don't suppress any unexpected ones.
   300  	cs, err := doltdb.NewCommitSpec(cSpecStr)
   301  	if err != nil {
   302  		return false, nil
   303  	}
   304  
   305  	headRef, err := rsr.CWBHeadRef()
   306  	if err == doltdb.ErrOperationNotSupportedInDetachedHead {
   307  		// This is safe because ddb.Resolve checks if headRef is nil, but only when the value is actually needed.
   308  		// Basically, this guarantees that resolving "HEAD" or similar will return an error but other resolves will work.
   309  		headRef = nil
   310  	} else if err != nil {
   311  		return false, err
   312  	}
   313  
   314  	_, err = ddb.Resolve(ctx, cs, headRef)
   315  	if err != nil {
   316  		return false, nil
   317  	}
   318  
   319  	return true, nil
   320  }
   321  
   322  // CleanUntracked deletes untracked tables from the working root.
   323  // Evaluates untracked tables as: all working tables - all staged tables.
   324  func CleanUntracked(ctx context.Context, roots doltdb.Roots, tables []string, dryrun bool, force bool) (doltdb.Roots, error) {
   325  	untrackedTables := make(map[string]struct{})
   326  
   327  	var err error
   328  	if len(tables) == 0 {
   329  		tables, err = roots.Working.GetTableNames(ctx, doltdb.DefaultSchemaName)
   330  		if err != nil {
   331  			return doltdb.Roots{}, nil
   332  		}
   333  	}
   334  
   335  	for i := range tables {
   336  		name := tables[i]
   337  		_, _, err = roots.Working.GetTable(ctx, doltdb.TableName{Name: name})
   338  		if err != nil {
   339  			return doltdb.Roots{}, err
   340  		}
   341  		untrackedTables[name] = struct{}{}
   342  	}
   343  
   344  	// untracked tables = working tables - staged tables
   345  	headTblNames, err := roots.Staged.GetTableNames(ctx, doltdb.DefaultSchemaName)
   346  	if err != nil {
   347  		return doltdb.Roots{}, err
   348  	}
   349  
   350  	for _, name := range headTblNames {
   351  		delete(untrackedTables, name)
   352  	}
   353  
   354  	newRoot := roots.Working
   355  	var toDelete []string
   356  	for t := range untrackedTables {
   357  		toDelete = append(toDelete, t)
   358  	}
   359  
   360  	newRoot, err = newRoot.RemoveTables(ctx, force, force, toDelete...)
   361  	if err != nil {
   362  		return doltdb.Roots{}, fmt.Errorf("failed to remove tables; %w", err)
   363  	}
   364  
   365  	if dryrun {
   366  		return roots, nil
   367  	}
   368  	roots.Working = newRoot
   369  
   370  	return roots, nil
   371  }
   372  
   373  // mapColumnTags takes a map from table name to schema.Schema and generates
   374  // a map from column tags to table names (see RootValue.GetAllSchemas).
   375  func mapColumnTags(tables map[string]schema.Schema) (m map[uint64]string) {
   376  	m = make(map[uint64]string, len(tables))
   377  	for tbl, sch := range tables {
   378  		for _, tag := range sch.GetAllCols().Tags {
   379  			m[tag] = tbl
   380  		}
   381  	}
   382  	return
   383  }