github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/env/environment.go (about) 1 // Copyright 2019-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 env 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "os" 22 "path/filepath" 23 "strings" 24 "time" 25 26 goerrors "gopkg.in/src-d/go-errors.v1" 27 28 "github.com/dolthub/dolt/go/cmd/dolt/errhand" 29 "github.com/dolthub/dolt/go/libraries/doltcore/creds" 30 "github.com/dolthub/dolt/go/libraries/doltcore/dbfactory" 31 "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" 32 "github.com/dolthub/dolt/go/libraries/doltcore/grpcendpoint" 33 "github.com/dolthub/dolt/go/libraries/doltcore/ref" 34 "github.com/dolthub/dolt/go/libraries/doltcore/table/editor" 35 "github.com/dolthub/dolt/go/libraries/utils/concurrentmap" 36 "github.com/dolthub/dolt/go/libraries/utils/config" 37 "github.com/dolthub/dolt/go/libraries/utils/filesys" 38 "github.com/dolthub/dolt/go/store/chunks" 39 "github.com/dolthub/dolt/go/store/datas" 40 "github.com/dolthub/dolt/go/store/hash" 41 "github.com/dolthub/dolt/go/store/types" 42 ) 43 44 const ( 45 DefaultInitBranch = "main" 46 47 DefaultLoginUrl = "https://dolthub.com/settings/credentials" 48 49 DefaultRemotesApiHost = "doltremoteapi.dolthub.com" 50 DefaultRemotesApiPort = "443" 51 52 tempTablesDir = "temptf" 53 ) 54 55 var zeroHashStr = (hash.Hash{}).String() 56 57 var ErrStateUpdate = errors.New("error updating local data repo state") 58 var ErrMarshallingSchema = errors.New("error marshalling schema") 59 var ErrInvalidCredsFile = errors.New("invalid creds file") 60 var ErrRemoteAlreadyExists = errors.New("remote already exists") 61 var ErrInvalidRemoteURL = errors.New("remote URL invalid") 62 var ErrRemoteNotFound = errors.New("remote not found") 63 var ErrInvalidRemoteName = errors.New("remote name invalid") 64 var ErrBackupAlreadyExists = errors.New("backup already exists") 65 var ErrInvalidBackupURL = errors.New("backup URL invalid") 66 var ErrBackupNotFound = errors.New("backup not found") 67 var ErrInvalidBackupName = errors.New("backup name invalid") 68 var ErrFailedToDeleteBackup = errors.New("failed to delete backup") 69 var ErrFailedToReadFromDb = errors.New("failed to read from db") 70 var ErrFailedToDeleteRemote = errors.New("failed to delete remote") 71 var ErrFailedToWriteRepoState = errors.New("failed to write repo state") 72 var ErrRemoteAddressConflict = errors.New("address conflict with a remote") 73 var ErrDoltRepositoryNotFound = errors.New("can no longer find .dolt dir on disk") 74 var ErrFailedToAccessDB = goerrors.NewKind("failed to access '%s' database: can no longer find .dolt dir on disk") 75 var ErrDatabaseIsLocked = errors.New("the database is locked by another dolt process") 76 77 // DoltEnv holds the state of the current environment used by the cli. 78 type DoltEnv struct { 79 Version string 80 81 Config *DoltCliConfig 82 CfgLoadErr error 83 84 RepoState *RepoState 85 RSLoadErr error 86 87 DoltDB *doltdb.DoltDB 88 DBLoadError error 89 90 FS filesys.Filesys 91 urlStr string 92 hdp HomeDirProvider 93 94 UserPassConfig *creds.DoltCredsForPass 95 } 96 97 func (dEnv *DoltEnv) GetRemoteDB(ctx context.Context, format *types.NomsBinFormat, r Remote, withCaching bool) (*doltdb.DoltDB, error) { 98 if withCaching { 99 return r.GetRemoteDB(ctx, format, dEnv) 100 } else { 101 return r.GetRemoteDBWithoutCaching(ctx, format, dEnv) 102 } 103 } 104 105 func (dEnv *DoltEnv) GetConfig() config.ReadableConfig { 106 return dEnv.Config 107 } 108 109 func (dEnv *DoltEnv) UrlStr() string { 110 return dEnv.urlStr 111 } 112 113 func createRepoState(fs filesys.Filesys) (*RepoState, error) { 114 repoState, rsErr := LoadRepoState(fs) 115 116 // deep copy remotes and backups ¯\_(ツ)_/¯ (see commit c59cbead) 117 if repoState != nil { 118 repoState.Remotes = repoState.Remotes.DeepCopy() 119 repoState.Backups = repoState.Backups.DeepCopy() 120 } 121 122 return repoState, rsErr 123 } 124 125 func (dEnv *DoltEnv) ReloadRepoState() error { 126 rs, err := createRepoState(dEnv.FS) 127 if err != nil { 128 return err 129 } 130 dEnv.RepoState = rs 131 return nil 132 } 133 134 func LoadWithoutDB(ctx context.Context, hdp HomeDirProvider, fs filesys.Filesys, version string) *DoltEnv { 135 cfg, cfgErr := LoadDoltCliConfig(hdp, fs) 136 137 repoState, rsErr := createRepoState(fs) 138 139 return &DoltEnv{ 140 Version: version, 141 Config: cfg, 142 CfgLoadErr: cfgErr, 143 RepoState: repoState, 144 RSLoadErr: rsErr, 145 FS: fs, 146 hdp: hdp, 147 } 148 } 149 150 // Load loads the DoltEnv for the .dolt directory determined by resolving the specified urlStr with the specified Filesys. 151 func Load(ctx context.Context, hdp HomeDirProvider, fs filesys.Filesys, urlStr string, version string) *DoltEnv { 152 dEnv := LoadWithoutDB(ctx, hdp, fs, version) 153 154 ddb, dbLoadErr := doltdb.LoadDoltDB(ctx, types.Format_Default, urlStr, fs) 155 156 dEnv.DoltDB = ddb 157 dEnv.DBLoadError = dbLoadErr 158 dEnv.urlStr = urlStr 159 160 if dbLoadErr == nil && dEnv.HasDoltDir() { 161 if !dEnv.HasDoltTempTableDir() { 162 tmpDir, err := dEnv.TempTableFilesDir() 163 if err != nil { 164 dEnv.DBLoadError = err 165 } 166 err = dEnv.FS.MkDirs(tmpDir) 167 dEnv.DBLoadError = err 168 } else { 169 // fire and forget cleanup routine. Will delete as many old temp files as it can during the main commands execution. 170 // The process will not wait for this to finish so this may not always complete. 171 go func() { 172 // TODO dEnv.HasDoltTempTableDir() true but dEnv.TempTableFileDir() panics 173 tmpTableDir, err := dEnv.FS.Abs(filepath.Join(dEnv.urlStr, dbfactory.DoltDir, tempTablesDir)) 174 if err != nil { 175 return 176 } 177 _ = fs.Iter(tmpTableDir, true, func(path string, size int64, isDir bool) (stop bool) { 178 if !isDir { 179 lm, exists := fs.LastModified(path) 180 181 if exists && time.Now().Sub(lm) > (time.Hour*24) { 182 _ = fs.DeleteFile(path) 183 } 184 } 185 186 return false 187 }) 188 }() 189 } 190 } 191 192 if dEnv.RSLoadErr == nil && dbLoadErr == nil { 193 // If the working set isn't present in the DB, create it from the repo state. This step can be removed post 1.0. 194 _, err := dEnv.WorkingSet(ctx) 195 if errors.Is(err, doltdb.ErrWorkingSetNotFound) { 196 _ = dEnv.initWorkingSetFromRepoState(ctx) 197 } else if err != nil { 198 dEnv.RSLoadErr = err 199 } 200 } 201 202 return dEnv 203 } 204 205 func GetDefaultInitBranch(cfg config.ReadableConfig) string { 206 return GetStringOrDefault(cfg, config.InitBranchName, DefaultInitBranch) 207 } 208 209 // Valid returns whether this environment has been properly initialized. This is useful because although every command 210 // gets a DoltEnv, not all of them require it, and we allow invalid dolt envs to be passed around for this reason. 211 func (dEnv *DoltEnv) Valid() bool { 212 return dEnv != nil && dEnv.CfgLoadErr == nil && dEnv.DBLoadError == nil && dEnv.HasDoltDir() && dEnv.HasDoltDataDir() 213 } 214 215 // initWorkingSetFromRepoState sets the working set for the env's head to mirror the contents of the repo state file. 216 // This is only necessary to migrate repos written before this method was introduced, and can be removed after 1.0 217 func (dEnv *DoltEnv) initWorkingSetFromRepoState(ctx context.Context) error { 218 headRef, err := dEnv.RepoStateReader().CWBHeadRef() 219 if err != nil { 220 return err 221 } 222 wsRef, err := ref.WorkingSetRefForHead(headRef) 223 if err != nil { 224 return err 225 } 226 227 headRoot, err := dEnv.HeadRoot(ctx) 228 if err != nil { 229 return err 230 } 231 232 stagedRoot := headRoot 233 if len(dEnv.RepoState.staged) != 0 && dEnv.RepoState.staged != zeroHashStr { 234 stagedHash, ok := hash.MaybeParse(dEnv.RepoState.staged) 235 if !ok { 236 return fmt.Errorf("Corrupt repo, invalid staged hash %s", stagedHash) 237 } 238 239 stagedRoot, err = dEnv.DoltDB.ReadRootValue(ctx, stagedHash) 240 if err != nil { 241 return err 242 } 243 } 244 245 workingRoot := stagedRoot 246 if len(dEnv.RepoState.working) != 0 && dEnv.RepoState.working != zeroHashStr { 247 workingHash, ok := hash.MaybeParse(dEnv.RepoState.working) 248 if !ok { 249 return fmt.Errorf("Corrupt repo, invalid working hash %s", workingHash) 250 } 251 252 workingRoot, err = dEnv.DoltDB.ReadRootValue(ctx, workingHash) 253 if err != nil { 254 return err 255 } 256 } 257 258 mergeState, err := mergeStateToMergeState(ctx, dEnv.RepoState.merge, dEnv.DoltDB) 259 if err != nil { 260 return err 261 } 262 263 ws := doltdb.EmptyWorkingSet(wsRef).WithWorkingRoot(workingRoot).WithStagedRoot(stagedRoot).WithMergeState(mergeState) 264 return dEnv.UpdateWorkingSet(ctx, ws) 265 } 266 267 func mergeStateToMergeState(ctx context.Context, mergeState *mergeState, db *doltdb.DoltDB) (*doltdb.MergeState, error) { 268 if mergeState == nil { 269 return nil, nil 270 } 271 272 cs, err := doltdb.NewCommitSpec(mergeState.Commit) 273 if err != nil { 274 panic("Corrupted repostate. Active merge state is not valid.") 275 } 276 277 optCmt, err := db.Resolve(ctx, cs, nil) 278 if err != nil { 279 return nil, err 280 } 281 282 commit, ok := optCmt.ToCommit() 283 if !ok { 284 return nil, doltdb.ErrGhostCommitEncountered 285 } 286 287 pmwh := hash.Parse(mergeState.PreMergeWorking) 288 pmwr, err := db.ReadRootValue(ctx, pmwh) 289 if err != nil { 290 return nil, err 291 } 292 293 return doltdb.MergeStateFromCommitAndWorking(commit, pmwr), nil 294 } 295 296 // HasDoltDir returns true if the .dolt directory exists and is a valid directory 297 func (dEnv *DoltEnv) HasDoltDir() bool { 298 return dEnv.hasDoltDir("./") 299 } 300 301 func (dEnv *DoltEnv) HasDoltDataDir() bool { 302 exists, isDir := dEnv.FS.Exists(dbfactory.DoltDataDir) 303 return exists && isDir 304 } 305 306 // HasDoltSqlServerInfo returns true if this Dolt environment has a sql-server.info file, indicating 307 // that a sql-server is running from this Dolt environment. 308 func (dEnv *DoltEnv) HasDoltSqlServerInfo() bool { 309 exists, _ := dEnv.FS.Exists(filepath.Join(dbfactory.DoltDir, "sql-server.info")) 310 return exists 311 } 312 313 func (dEnv *DoltEnv) HasDoltTempTableDir() bool { 314 tmpDir, err := dEnv.TempTableFilesDir() 315 if err != nil { 316 return false 317 } 318 ex, _ := dEnv.FS.Exists(tmpDir) 319 320 return ex 321 } 322 323 func mustAbs(dEnv *DoltEnv, path ...string) string { 324 absPath, err := dEnv.FS.Abs(filepath.Join(path...)) 325 326 if err != nil { 327 panic(err) 328 } 329 330 return absPath 331 } 332 333 // GetDoltDir returns the path to the .dolt directory 334 func (dEnv *DoltEnv) GetDoltDir() string { 335 if !dEnv.HasDoltDataDir() { 336 return "" 337 } 338 339 return mustAbs(dEnv, dbfactory.DoltDir) 340 } 341 342 func (dEnv *DoltEnv) hasDoltDir(path string) bool { 343 exists, isDir := dEnv.FS.Exists(mustAbs(dEnv, dbfactory.DoltDir)) 344 return exists && isDir 345 } 346 347 // HasLocalConfig returns true if a repository local config file 348 func (dEnv *DoltEnv) HasLocalConfig() bool { 349 _, ok := dEnv.Config.GetConfig(LocalConfig) 350 351 return ok 352 } 353 354 func (dEnv *DoltEnv) bestEffortDeleteAll(dir string) { 355 fileToIsDir := make(map[string]bool) 356 dEnv.FS.Iter(dir, false, func(path string, size int64, isDir bool) (stop bool) { 357 fileToIsDir[path] = isDir 358 return false 359 }) 360 361 for path, isDir := range fileToIsDir { 362 if isDir { 363 dEnv.FS.Delete(path, true) 364 } else { 365 dEnv.FS.DeleteFile(path) 366 } 367 } 368 } 369 370 // InitRepo takes an empty directory and initializes it with a .dolt directory containing repo state, uncommitted license and readme, and creates a noms 371 // database with dolt structure. 372 func (dEnv *DoltEnv) InitRepo(ctx context.Context, nbf *types.NomsBinFormat, name, email, branchName string) error { // should remove name and email args 373 return dEnv.InitRepoWithTime(ctx, nbf, name, email, branchName, datas.CommitterDate()) 374 } 375 376 func (dEnv *DoltEnv) InitRepoWithTime(ctx context.Context, nbf *types.NomsBinFormat, name, email, branchName string, t time.Time) error { // should remove name and email args 377 return dEnv.InitRepoWithCommitMetaGenerator(ctx, nbf, branchName, datas.MakeCommitMetaGenerator(name, email, t)) 378 } 379 380 func (dEnv *DoltEnv) InitRepoWithCommitMetaGenerator(ctx context.Context, nbf *types.NomsBinFormat, branchName string, commitMeta datas.CommitMetaGenerator) error { 381 doltDir, err := dEnv.createDirectories(".") 382 383 if err != nil { 384 return err 385 } 386 387 err = dEnv.configureRepo(doltDir) 388 389 if err == nil { 390 err = dEnv.InitDBAndRepoStateWithCommitMetaGenerator(ctx, nbf, branchName, commitMeta) 391 } 392 393 if err != nil { 394 dEnv.bestEffortDeleteAll(dbfactory.DoltDir) 395 } 396 397 return err 398 } 399 400 func (dEnv *DoltEnv) InitRepoWithNoData(ctx context.Context, nbf *types.NomsBinFormat) error { 401 doltDir, err := dEnv.createDirectories(".") 402 403 if err != nil { 404 return err 405 } 406 407 err = dEnv.configureRepo(doltDir) 408 409 if err != nil { 410 dEnv.bestEffortDeleteAll(dbfactory.DoltDir) 411 return err 412 } 413 414 dEnv.DoltDB, err = doltdb.LoadDoltDB(ctx, nbf, dEnv.urlStr, dEnv.FS) 415 416 return err 417 } 418 419 func (dEnv *DoltEnv) createDirectories(dir string) (string, error) { 420 absPath, err := dEnv.FS.Abs(dir) 421 422 if err != nil { 423 return "", err 424 } 425 426 exists, isDir := dEnv.FS.Exists(absPath) 427 428 if !exists { 429 return "", fmt.Errorf("'%s' does not exist so could not create '%s", absPath, dbfactory.DoltDataDir) 430 } else if !isDir { 431 return "", fmt.Errorf("'%s' exists but it's a file not a directory", absPath) 432 } 433 434 if dEnv.hasDoltDir(dir) { 435 // Special case a completely empty directory. We can allow that. 436 dotDolt := mustAbs(dEnv, dbfactory.DoltDir) 437 entries, err := os.ReadDir(dotDolt) 438 if err != nil { 439 return "", err 440 } 441 if len(entries) != 0 { 442 return "", fmt.Errorf(".dolt directory already exists at '%s'", dir) 443 } 444 } 445 446 absDataDir := filepath.Join(absPath, dbfactory.DoltDataDir) 447 err = dEnv.FS.MkDirs(absDataDir) 448 449 if err != nil { 450 return "", fmt.Errorf("unable to make directory '%s', cause: %s", absDataDir, err.Error()) 451 } 452 453 tmpDir, err := dEnv.TempTableFilesDir() 454 if err != nil { 455 return "", err 456 } 457 458 err = dEnv.FS.MkDirs(tmpDir) 459 if err != nil { 460 return "", fmt.Errorf("unable to make directory '%s', cause: %s", tmpDir, err.Error()) 461 } 462 463 return filepath.Join(absPath, dbfactory.DoltDir), nil 464 } 465 466 func (dEnv *DoltEnv) configureRepo(doltDir string) error { 467 configDir, err := dEnv.FS.Abs(".") 468 if err != nil { 469 return fmt.Errorf("unable to resolve current path to create repo local config file: %s", err.Error()) 470 } 471 472 err = dEnv.Config.CreateLocalConfig(configDir, map[string]string{}) 473 if err != nil { 474 return fmt.Errorf("failed creating file %s", getLocalConfigPath()) 475 } 476 477 return nil 478 } 479 480 // Inits the dolt DB of this environment with an empty commit at the time given and writes default docs to disk. 481 // Writes new repo state with a main branch and current root hash. 482 func (dEnv *DoltEnv) InitDBAndRepoState(ctx context.Context, nbf *types.NomsBinFormat, name, email, branchName string, t time.Time) error { 483 return dEnv.InitDBAndRepoStateWithCommitMetaGenerator(ctx, nbf, branchName, datas.MakeCommitMetaGenerator(name, email, t)) 484 } 485 486 func (dEnv *DoltEnv) InitDBAndRepoStateWithCommitMetaGenerator(ctx context.Context, nbf *types.NomsBinFormat, branchName string, commitMeta datas.CommitMetaGenerator) error { 487 err := dEnv.InitDBWithCommitMetaGenerator(ctx, nbf, branchName, commitMeta) 488 if err != nil { 489 return err 490 } 491 492 return dEnv.InitializeRepoState(ctx, branchName) 493 } 494 495 // Inits the dolt DB of this environment with an empty commit at the time given and writes default docs to disk. 496 // Does not update repo state. 497 func (dEnv *DoltEnv) InitDBWithTime(ctx context.Context, nbf *types.NomsBinFormat, name, email, branchName string, t time.Time) error { 498 return dEnv.InitDBWithCommitMetaGenerator(ctx, nbf, branchName, datas.MakeCommitMetaGenerator(name, email, t)) 499 } 500 501 func (dEnv *DoltEnv) InitDBWithCommitMetaGenerator(ctx context.Context, nbf *types.NomsBinFormat, branchName string, commitMeta datas.CommitMetaGenerator) error { 502 var err error 503 dEnv.DoltDB, err = doltdb.LoadDoltDB(ctx, nbf, dEnv.urlStr, dEnv.FS) 504 if err != nil { 505 return err 506 } 507 508 err = dEnv.DoltDB.WriteEmptyRepoWithCommitMetaGenerator(ctx, branchName, commitMeta) 509 if err != nil { 510 return fmt.Errorf("%w: %v", doltdb.ErrNomsIO, err) 511 } 512 513 return nil 514 } 515 516 // InitializeRepoState writes a default repo state to disk, consisting of a main branch and current root hash value. 517 func (dEnv *DoltEnv) InitializeRepoState(ctx context.Context, branchName string) error { 518 commit, err := dEnv.DoltDB.ResolveCommitRef(ctx, ref.NewBranchRef(branchName)) 519 if err != nil { 520 return err 521 } 522 523 root, err := commit.GetRootValue(ctx) 524 if err != nil { 525 return err 526 } 527 528 dEnv.RepoState, err = CreateRepoState(dEnv.FS, branchName) 529 if err != nil { 530 return ErrStateUpdate 531 } 532 533 // TODO: combine into one update 534 err = dEnv.UpdateWorkingRoot(ctx, root) 535 if err != nil { 536 return err 537 } 538 539 err = dEnv.UpdateStagedRoot(ctx, root) 540 if err != nil { 541 return err 542 } 543 544 dEnv.RSLoadErr = nil 545 return nil 546 } 547 548 type RootsProvider interface { 549 GetRoots(ctx context.Context) (doltdb.Roots, error) 550 } 551 552 // Roots returns the roots for this environment 553 func (dEnv *DoltEnv) Roots(ctx context.Context) (doltdb.Roots, error) { 554 ws, err := dEnv.WorkingSet(ctx) 555 if err != nil { 556 return doltdb.Roots{}, err 557 } 558 559 headRoot, err := dEnv.HeadRoot(ctx) 560 if err != nil { 561 return doltdb.Roots{}, err 562 } 563 564 return doltdb.Roots{ 565 Head: headRoot, 566 Working: ws.WorkingRoot(), 567 Staged: ws.StagedRoot(), 568 }, nil 569 } 570 571 // RecoveryRoots returns the roots for this environment in the case that the 572 // currently checked out branch has been deleted or HEAD has been updated in a 573 // non-principled way to point to a branch that does not exist. This is used by 574 // `dolt checkout`, in particular, to go forward with a `dolt checkout` of an 575 // existing branch in the degraded state where the current branch was deleted. 576 func (dEnv *DoltEnv) RecoveryRoots(ctx context.Context) (doltdb.Roots, error) { 577 ws, err := dEnv.WorkingSet(ctx) 578 if err != nil { 579 return doltdb.Roots{}, err 580 } 581 582 headRoot, err := dEnv.HeadRoot(ctx) 583 if err == doltdb.ErrBranchNotFound { 584 headRoot = ws.StagedRoot() 585 err = nil 586 } 587 if err != nil { 588 return doltdb.Roots{}, err 589 } 590 591 return doltdb.Roots{ 592 Head: headRoot, 593 Working: ws.WorkingRoot(), 594 Staged: ws.StagedRoot(), 595 }, nil 596 } 597 598 // UpdateRoots updates the working and staged roots for this environment 599 func (dEnv *DoltEnv) UpdateRoots(ctx context.Context, roots doltdb.Roots) error { 600 ws, err := dEnv.WorkingSet(ctx) 601 if err == doltdb.ErrWorkingSetNotFound { 602 // first time updating roots 603 wsRef, err := ref.WorkingSetRefForHead(dEnv.RepoState.CWBHeadRef()) 604 if err != nil { 605 return err 606 } 607 ws = doltdb.EmptyWorkingSet(wsRef) 608 } else if err != nil { 609 return err 610 } 611 612 return dEnv.UpdateWorkingSet(ctx, ws.WithWorkingRoot(roots.Working).WithStagedRoot(roots.Staged)) 613 } 614 615 // WorkingRoot returns the working root for the current working branch 616 func (dEnv *DoltEnv) WorkingRoot(ctx context.Context) (doltdb.RootValue, error) { 617 workingSet, err := dEnv.WorkingSet(ctx) 618 if err != nil { 619 return nil, err 620 } 621 622 return workingSet.WorkingRoot(), nil 623 } 624 625 func (dEnv *DoltEnv) WorkingSet(ctx context.Context) (*doltdb.WorkingSet, error) { 626 return WorkingSet(ctx, dEnv.DoltDB, dEnv.RepoStateReader()) 627 } 628 629 func WorkingSet(ctx context.Context, ddb *doltdb.DoltDB, rsr RepoStateReader) (*doltdb.WorkingSet, error) { 630 headRef, err := rsr.CWBHeadRef() 631 if err != nil { 632 return nil, err 633 } 634 workingSetRef, err := ref.WorkingSetRefForHead(headRef) 635 if err != nil { 636 return nil, err 637 } 638 639 workingSet, err := ddb.ResolveWorkingSet(ctx, workingSetRef) 640 if err != nil { 641 return nil, err 642 } 643 644 return workingSet, nil 645 } 646 647 // UpdateWorkingRoot updates the working root for the current working branch to the root value given. 648 // This method can fail if another client updates the working root at the same time. 649 func (dEnv *DoltEnv) UpdateWorkingRoot(ctx context.Context, newRoot doltdb.RootValue) error { 650 var h hash.Hash 651 var wsRef ref.WorkingSetRef 652 653 ws, err := dEnv.WorkingSet(ctx) 654 if err == doltdb.ErrWorkingSetNotFound { 655 // first time updating root 656 wsRef, err = ref.WorkingSetRefForHead(dEnv.RepoState.CWBHeadRef()) 657 if err != nil { 658 return err 659 } 660 ws = doltdb.EmptyWorkingSet(wsRef).WithWorkingRoot(newRoot).WithStagedRoot(newRoot) 661 } else if err != nil { 662 return err 663 } else { 664 h, err = ws.HashOf() 665 if err != nil { 666 return err 667 } 668 669 wsRef = ws.Ref() 670 } 671 672 return dEnv.DoltDB.UpdateWorkingSet(ctx, wsRef, ws.WithWorkingRoot(newRoot), h, dEnv.workingSetMeta(), nil) 673 } 674 675 // UpdateWorkingSet updates the working set for the current working branch to the value given. 676 // This method can fail if another client updates the working set at the same time. 677 func (dEnv *DoltEnv) UpdateWorkingSet(ctx context.Context, ws *doltdb.WorkingSet) error { 678 currentWs, err := dEnv.WorkingSet(ctx) 679 if err != doltdb.ErrWorkingSetNotFound && err != nil { 680 return err 681 } 682 683 var h hash.Hash 684 if currentWs != nil { 685 h, err = currentWs.HashOf() 686 if err != nil { 687 return err 688 } 689 } 690 691 return dEnv.DoltDB.UpdateWorkingSet(ctx, ws.Ref(), ws, h, dEnv.workingSetMeta(), nil) 692 } 693 694 type repoStateReader struct { 695 *DoltEnv 696 } 697 698 func (r *repoStateReader) CWBHeadRef() (ref.DoltRef, error) { 699 if r.RepoState == nil && r.RSLoadErr != nil { 700 return nil, r.RSLoadErr 701 } 702 return r.RepoState.CWBHeadRef(), nil 703 } 704 705 func (r *repoStateReader) CWBHeadSpec() (*doltdb.CommitSpec, error) { 706 if r.RepoState == nil && r.RSLoadErr != nil { 707 return nil, r.RSLoadErr 708 } 709 return r.RepoState.CWBHeadSpec(), nil 710 } 711 712 func (dEnv *DoltEnv) RepoStateReader() RepoStateReader { 713 return &repoStateReader{dEnv} 714 } 715 716 type repoStateWriter struct { 717 *DoltEnv 718 } 719 720 func (r *repoStateWriter) SetCWBHeadRef(ctx context.Context, marshalableRef ref.MarshalableRef) error { 721 if r.RepoState == nil && r.RSLoadErr != nil { 722 return r.RSLoadErr 723 } 724 725 r.RepoState.Head = marshalableRef 726 err := r.RepoState.Save(r.FS) 727 728 if err != nil { 729 return ErrStateUpdate 730 } 731 732 return nil 733 } 734 735 func (r *repoStateWriter) AddRemote(remote Remote) error { 736 return r.DoltEnv.AddRemote(remote) 737 } 738 739 func (r *repoStateWriter) AddBackup(remote Remote) error { 740 return r.DoltEnv.AddBackup(remote) 741 } 742 743 func (r *repoStateWriter) RemoveRemote(ctx context.Context, name string) error { 744 return r.DoltEnv.RemoveRemote(ctx, name) 745 } 746 747 func (r *repoStateWriter) RemoveBackup(ctx context.Context, name string) error { 748 return r.DoltEnv.RemoveBackup(ctx, name) 749 } 750 751 func (dEnv *DoltEnv) RepoStateWriter() RepoStateWriter { 752 return &repoStateWriter{dEnv} 753 } 754 755 func (dEnv *DoltEnv) HeadRoot(ctx context.Context) (doltdb.RootValue, error) { 756 commit, err := dEnv.HeadCommit(ctx) 757 if err != nil { 758 return nil, err 759 } 760 761 return commit.GetRootValue(ctx) 762 } 763 764 func (dEnv *DoltEnv) HeadCommit(ctx context.Context) (*doltdb.Commit, error) { 765 return dEnv.DoltDB.ResolveCommitRef(ctx, dEnv.RepoState.CWBHeadRef()) 766 } 767 768 func (dEnv *DoltEnv) DbData() DbData { 769 return DbData{ 770 Ddb: dEnv.DoltDB, 771 Rsw: dEnv.RepoStateWriter(), 772 Rsr: dEnv.RepoStateReader(), 773 } 774 } 775 776 // StagedRoot returns the staged root value in the current working set 777 func (dEnv *DoltEnv) StagedRoot(ctx context.Context) (doltdb.RootValue, error) { 778 workingSet, err := dEnv.WorkingSet(ctx) 779 if err != nil { 780 return nil, err 781 } 782 783 return workingSet.StagedRoot(), nil 784 } 785 786 // UpdateStagedRoot updates the staged root for the current working branch. This can fail if multiple clients attempt 787 // to update at the same time. 788 func (dEnv *DoltEnv) UpdateStagedRoot(ctx context.Context, newRoot doltdb.RootValue) error { 789 var h hash.Hash 790 var wsRef ref.WorkingSetRef 791 792 ws, err := dEnv.WorkingSet(ctx) 793 if err == doltdb.ErrWorkingSetNotFound { 794 // first time updating root 795 wsRef, err = ref.WorkingSetRefForHead(dEnv.RepoState.CWBHeadRef()) 796 if err != nil { 797 return err 798 } 799 ws = doltdb.EmptyWorkingSet(wsRef).WithWorkingRoot(newRoot).WithStagedRoot(newRoot) 800 } else if err != nil { 801 return err 802 } else { 803 h, err = ws.HashOf() 804 if err != nil { 805 return err 806 } 807 808 wsRef = ws.Ref() 809 } 810 811 return dEnv.DoltDB.UpdateWorkingSet(ctx, wsRef, ws.WithStagedRoot(newRoot), h, dEnv.workingSetMeta(), nil) 812 } 813 814 func (dEnv *DoltEnv) AbortMerge(ctx context.Context) error { 815 ws, err := dEnv.WorkingSet(ctx) 816 if err != nil { 817 return err 818 } 819 820 h, err := ws.HashOf() 821 if err != nil { 822 return err 823 } 824 825 return dEnv.DoltDB.UpdateWorkingSet(ctx, ws.Ref(), ws.AbortMerge(), h, dEnv.workingSetMeta(), nil) 826 } 827 828 func (dEnv *DoltEnv) workingSetMeta() *datas.WorkingSetMeta { 829 return dEnv.NewWorkingSetMeta("updated from dolt environment") 830 } 831 832 func (dEnv *DoltEnv) NewWorkingSetMeta(message string) *datas.WorkingSetMeta { 833 return &datas.WorkingSetMeta{ 834 Name: dEnv.Config.GetStringOrDefault(config.UserNameKey, ""), 835 Email: dEnv.Config.GetStringOrDefault(config.UserEmailKey, ""), 836 Timestamp: uint64(time.Now().Unix()), 837 Description: message, 838 } 839 } 840 841 func (dEnv *DoltEnv) CredsDir() (string, error) { 842 return getCredsDir(dEnv.hdp) 843 } 844 845 func (dEnv *DoltEnv) UserDoltCreds() (creds.DoltCreds, bool, error) { 846 kid, err := dEnv.Config.GetString(config.UserCreds) 847 848 if err == nil && kid != "" { 849 dir, err := dEnv.CredsDir() 850 851 if err != nil { 852 // not sure why you wouldn't be able to get the creds dir. 853 panic(err) 854 } 855 856 c, err := creds.JWKCredsReadFromFile(dEnv.FS, filepath.Join(dir, kid+".jwk")) 857 return c, c.IsPrivKeyValid() && c.IsPubKeyValid(), err 858 } 859 860 return creds.DoltCreds{}, false, nil 861 } 862 863 // GetGRPCDialParams implements dbfactory.GRPCDialProvider 864 func (dEnv *DoltEnv) GetGRPCDialParams(config grpcendpoint.Config) (dbfactory.GRPCRemoteConfig, error) { 865 return NewGRPCDialProviderFromDoltEnv(dEnv).GetGRPCDialParams(config) 866 } 867 868 func (dEnv *DoltEnv) GetRemotes() (*concurrentmap.Map[string, Remote], error) { 869 if dEnv.RSLoadErr != nil { 870 return nil, dEnv.RSLoadErr 871 } 872 873 return dEnv.RepoState.Remotes, nil 874 } 875 876 // CheckRemoteAddressConflict checks whether any backups or remotes share the given URL. Returns the first remote if multiple match. 877 // Returns NoRemote and false if none match. 878 func CheckRemoteAddressConflict(absUrl string, remotes *concurrentmap.Map[string, Remote], backups *concurrentmap.Map[string, Remote]) (Remote, bool) { 879 if remotes != nil { 880 var rm *Remote 881 remotes.Iter(func(key string, value Remote) bool { 882 if value.Url == absUrl { 883 rm = &value 884 return false 885 } 886 return true 887 }) 888 if rm != nil { 889 return *rm, true 890 } 891 } 892 893 if backups != nil { 894 var rm *Remote 895 backups.Iter(func(key string, value Remote) bool { 896 if value.Url == absUrl { 897 rm = &value 898 return false 899 } 900 return true 901 }) 902 if rm != nil { 903 return *rm, true 904 } 905 } 906 return NoRemote, false 907 } 908 909 func (dEnv *DoltEnv) AddRemote(r Remote) error { 910 if _, ok := dEnv.RepoState.Remotes.Get(r.Name); ok { 911 return ErrRemoteAlreadyExists 912 } 913 914 if strings.IndexAny(r.Name, " \t\n\r./\\!@#$%^&*(){}[],.<>'\"?=+|") != -1 { 915 return ErrInvalidRemoteName 916 } 917 918 _, absRemoteUrl, err := GetAbsRemoteUrl(dEnv.FS, dEnv.Config, r.Url) 919 if err != nil { 920 return fmt.Errorf("%w; %s", ErrInvalidRemoteURL, err.Error()) 921 } 922 923 // can have multiple remotes with the same address, but no conflicting backups 924 if rem, found := CheckRemoteAddressConflict(absRemoteUrl, nil, dEnv.RepoState.Backups); found { 925 return fmt.Errorf("%w: '%s' -> %s", ErrRemoteAddressConflict, rem.Name, rem.Url) 926 } 927 928 r.Url = absRemoteUrl 929 dEnv.RepoState.AddRemote(r) 930 return dEnv.RepoState.Save(dEnv.FS) 931 } 932 933 func (dEnv *DoltEnv) GetBackups() (*concurrentmap.Map[string, Remote], error) { 934 if dEnv.RSLoadErr != nil { 935 return nil, dEnv.RSLoadErr 936 } 937 938 return dEnv.RepoState.Backups, nil 939 } 940 941 func (dEnv *DoltEnv) AddBackup(r Remote) error { 942 if _, ok := dEnv.RepoState.Backups.Get(r.Name); ok { 943 return ErrBackupAlreadyExists 944 } 945 946 if strings.IndexAny(r.Name, " \t\n\r./\\!@#$%^&*(){}[],.<>'\"?=+|") != -1 { 947 return ErrInvalidBackupName 948 } 949 950 _, absRemoteUrl, err := GetAbsRemoteUrl(dEnv.FS, dEnv.Config, r.Url) 951 if err != nil { 952 return fmt.Errorf("%w; %s", ErrInvalidBackupURL, err.Error()) 953 } 954 955 // no conflicting remote or backup addresses 956 if rem, found := CheckRemoteAddressConflict(absRemoteUrl, dEnv.RepoState.Remotes, dEnv.RepoState.Backups); found { 957 return fmt.Errorf("%w: '%s' -> %s", ErrRemoteAddressConflict, rem.Name, rem.Url) 958 } 959 960 r.Url = absRemoteUrl 961 dEnv.RepoState.AddBackup(r) 962 return dEnv.RepoState.Save(dEnv.FS) 963 } 964 965 func (dEnv *DoltEnv) RemoveRemote(ctx context.Context, name string) error { 966 remote, ok := dEnv.RepoState.Remotes.Get(name) 967 if !ok { 968 return ErrRemoteNotFound 969 } 970 971 ddb := dEnv.DoltDB 972 refs, err := ddb.GetRemoteRefs(ctx) 973 if err != nil { 974 return fmt.Errorf("%w: %s", ErrFailedToReadFromDb, err.Error()) 975 } 976 977 for _, r := range refs { 978 rr := r.(ref.RemoteRef) 979 980 if rr.GetRemote() == remote.Name { 981 err = ddb.DeleteBranch(ctx, rr, nil) 982 983 if err != nil { 984 return fmt.Errorf("%w; failed to delete remote tracking ref '%s'; %s", ErrFailedToDeleteRemote, rr.String(), err.Error()) 985 } 986 } 987 } 988 989 dEnv.RepoState.RemoveRemote(remote) 990 err = dEnv.RepoState.Save(dEnv.FS) 991 if err != nil { 992 return ErrFailedToWriteRepoState 993 } 994 995 return nil 996 } 997 998 func (dEnv *DoltEnv) RemoveBackup(ctx context.Context, name string) error { 999 backup, ok := dEnv.RepoState.Backups.Get(name) 1000 if !ok { 1001 return ErrBackupNotFound 1002 } 1003 1004 dEnv.RepoState.RemoveBackup(backup) 1005 1006 err := dEnv.RepoState.Save(dEnv.FS) 1007 if err != nil { 1008 return ErrFailedToWriteRepoState 1009 } 1010 1011 return nil 1012 } 1013 1014 func (dEnv *DoltEnv) GetBranches() (*concurrentmap.Map[string, BranchConfig], error) { 1015 if dEnv.RSLoadErr != nil { 1016 return nil, dEnv.RSLoadErr 1017 } 1018 1019 return dEnv.RepoState.Branches, nil 1020 } 1021 1022 func (dEnv *DoltEnv) UpdateBranch(name string, new BranchConfig) error { 1023 if dEnv.RSLoadErr != nil { 1024 return dEnv.RSLoadErr 1025 } 1026 1027 dEnv.RepoState.Branches.Set(name, new) 1028 1029 err := dEnv.RepoState.Save(dEnv.FS) 1030 if err != nil { 1031 return ErrFailedToWriteRepoState 1032 } 1033 return nil 1034 } 1035 1036 var ErrNotACred = errors.New("not a valid credential key id or public key") 1037 1038 func (dEnv *DoltEnv) FindCreds(credsDir, pubKeyOrId string) (string, error) { 1039 if !creds.B32CredsByteSet.ContainsAll([]byte(pubKeyOrId)) { 1040 return "", creds.ErrBadB32CredsEncoding 1041 } 1042 1043 if len(pubKeyOrId) == creds.B32EncodedPubKeyLen { 1044 pubKeyOrId, _ = creds.PubKeyStrToKIDStr(pubKeyOrId) 1045 } 1046 1047 if len(pubKeyOrId) != creds.B32EncodedKeyIdLen { 1048 return "", ErrNotACred 1049 } 1050 1051 path := mustAbs(dEnv, credsDir, pubKeyOrId+creds.JWKFileExtension) 1052 exists, isDir := dEnv.FS.Exists(path) 1053 1054 if isDir { 1055 return path, filesys.ErrIsDir 1056 } else if !exists { 1057 return "", creds.ErrCredsNotFound 1058 } else { 1059 return path, nil 1060 } 1061 } 1062 1063 func (dEnv *DoltEnv) FindRef(ctx context.Context, refStr string) (ref.DoltRef, error) { 1064 localRef := ref.NewBranchRef(refStr) 1065 if hasRef, err := dEnv.DoltDB.HasRef(ctx, localRef); err != nil { 1066 return nil, err 1067 } else if hasRef { 1068 return localRef, nil 1069 } else { 1070 if strings.HasPrefix(refStr, "remotes/") { 1071 refStr = refStr[len("remotes/"):] 1072 } 1073 1074 slashIdx := strings.IndexRune(refStr, '/') 1075 if slashIdx > 0 { 1076 remoteName := refStr[:slashIdx] 1077 if _, ok := dEnv.RepoState.Remotes.Get(remoteName); ok { 1078 remoteRef, err := ref.NewRemoteRefFromPathStr(refStr) 1079 1080 if err != nil { 1081 return nil, err 1082 } 1083 1084 if hasRef, err = dEnv.DoltDB.HasRef(ctx, remoteRef); err != nil { 1085 return nil, err 1086 } else if hasRef { 1087 return remoteRef, nil 1088 } 1089 } 1090 } 1091 } 1092 1093 return nil, doltdb.ErrBranchNotFound 1094 } 1095 1096 // GetRefSpecs takes an optional remoteName and returns all refspecs associated with that remote. If "" is passed as 1097 // the remoteName then the default remote is used. 1098 func GetRefSpecs(rsr RepoStateReader, remoteName string) ([]ref.RemoteRefSpec, error) { 1099 var remote Remote 1100 var err error 1101 1102 remotes, err := rsr.GetRemotes() 1103 if err != nil { 1104 return nil, err 1105 } 1106 if remoteName == "" { 1107 remote, err = GetDefaultRemote(rsr) 1108 } else if r, ok := remotes.Get(remoteName); ok { 1109 remote = r 1110 } else { 1111 err = ErrInvalidRepository.New(remoteName) 1112 } 1113 1114 if err != nil { 1115 return nil, err 1116 } 1117 1118 var refSpecs []ref.RemoteRefSpec 1119 for _, fs := range remote.FetchSpecs { 1120 rs, err := ref.ParseRefSpecForRemote(remote.Name, fs) 1121 1122 if err != nil { 1123 return nil, errhand.BuildDError("error: for '%s', '%s' is not a valid refspec.", remote.Name, fs).Build() 1124 } 1125 1126 if rrs, ok := rs.(ref.RemoteRefSpec); !ok { 1127 return nil, fmt.Errorf("%w; '%s' is not a valid refspec referring to a remote tracking branch", ref.ErrInvalidRefSpec, remote.Name) 1128 } else if rrs.GetRemote() != remote.Name { 1129 return nil, ErrInvalidRefSpecRemote 1130 } else { 1131 refSpecs = append(refSpecs, rrs) 1132 } 1133 } 1134 1135 return refSpecs, nil 1136 } 1137 1138 var ErrInvalidRefSpecRemote = errors.New("refspec refers to different remote") 1139 var ErrNoRemote = errors.New("no remote") 1140 var ErrUnknownRemote = errors.New("unknown remote") 1141 var ErrCantDetermineDefault = errors.New("unable to determine the default remote") 1142 1143 // GetDefaultRemote gets the default remote for the environment. Not fully implemented yet. Needs to support multiple 1144 // repos and a configurable default. 1145 func GetDefaultRemote(rsr RepoStateReader) (Remote, error) { 1146 remotes, err := rsr.GetRemotes() 1147 if err != nil { 1148 return NoRemote, err 1149 } 1150 1151 remotesLen := remotes.Len() 1152 if remotesLen == 0 { 1153 return NoRemote, ErrNoRemote 1154 } else if remotesLen == 1 { 1155 var remote *Remote 1156 remotes.Iter(func(key string, value Remote) bool { 1157 remote = &value 1158 return false 1159 }) 1160 if remote != nil { 1161 return *remote, nil 1162 } 1163 } 1164 1165 if remote, ok := remotes.Get("origin"); ok { 1166 return remote, nil 1167 } 1168 1169 return NoRemote, ErrCantDetermineDefault 1170 } 1171 1172 // GetUserHomeDir returns the user's home dir 1173 // based on current filesys 1174 func (dEnv *DoltEnv) GetUserHomeDir() (string, error) { 1175 return getHomeDir(dEnv.hdp) 1176 } 1177 1178 func (dEnv *DoltEnv) TempTableFilesDir() (string, error) { 1179 doltDir := dEnv.GetDoltDir() 1180 if doltDir == "" { 1181 return "", ErrDoltRepositoryNotFound 1182 } 1183 1184 absPath, err := dEnv.FS.Abs(filepath.Join(doltDir, tempTablesDir)) 1185 if err != nil { 1186 return "", err 1187 } 1188 1189 return absPath, nil 1190 } 1191 1192 func (dEnv *DoltEnv) DbEaFactory() editor.DbEaFactory { 1193 tmpDir, err := dEnv.TempTableFilesDir() 1194 if err != nil { 1195 return nil 1196 } 1197 return editor.NewDbEaFactory(tmpDir, dEnv.DoltDB.ValueReadWriter()) 1198 } 1199 1200 func (dEnv *DoltEnv) BulkDbEaFactory() editor.DbEaFactory { 1201 tmpDir, err := dEnv.TempTableFilesDir() 1202 if err != nil { 1203 return nil 1204 } 1205 return editor.NewBulkImportTEAFactory(dEnv.DoltDB.ValueReadWriter(), tmpDir) 1206 } 1207 1208 func (dEnv *DoltEnv) IsAccessModeReadOnly() bool { 1209 return dEnv.DoltDB.AccessMode() == chunks.ExclusiveAccessMode_ReadOnly 1210 }