github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/cherry_pick/cherry_pick.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 cherry_pick 16 17 import ( 18 "errors" 19 "fmt" 20 21 "github.com/dolthub/go-mysql-server/sql" 22 23 "github.com/dolthub/dolt/go/libraries/doltcore/diff" 24 "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" 25 "github.com/dolthub/dolt/go/libraries/doltcore/env/actions" 26 "github.com/dolthub/dolt/go/libraries/doltcore/merge" 27 "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" 28 ) 29 30 // ErrCherryPickUncommittedChanges is returned when a cherry-pick is attempted without a clean working set. 31 var ErrCherryPickUncommittedChanges = errors.New("cannot cherry-pick with uncommitted changes") 32 33 // CherryPickOptions specifies optional parameters specifying how a cherry-pick is performed. 34 type CherryPickOptions struct { 35 // Amend controls whether the commit at HEAD is amended and combined with the commit to be cherry-picked. 36 Amend bool 37 38 // CommitMessage is optional, and controls the message for the new commit. 39 CommitMessage string 40 } 41 42 // CherryPick replays a commit, specified by |options.Commit|, and applies it as a new commit to the current HEAD. If 43 // successful, the hash of the new commit is returned. If the cherry-pick results in merge conflicts, the merge result 44 // is returned. If any unexpected error occur, it is returned. 45 func CherryPick(ctx *sql.Context, commit string, options CherryPickOptions) (string, *merge.Result, error) { 46 doltSession := dsess.DSessFromSess(ctx.Session) 47 dbName := ctx.GetCurrentDatabase() 48 49 roots, ok := doltSession.GetRoots(ctx, dbName) 50 if !ok { 51 return "", nil, fmt.Errorf("failed to get roots for current session") 52 } 53 54 mergeResult, commitMsg, err := cherryPick(ctx, doltSession, roots, dbName, commit) 55 if err != nil { 56 return "", mergeResult, err 57 } 58 59 // If we're amending the previous commit and a new commit message hasn't been provided, 60 // grab the previous commit message and reuse it. 61 if options.Amend && options.CommitMessage == "" { 62 commitMsg, err = previousCommitMessage(ctx) 63 if err != nil { 64 return "", nil, err 65 } 66 } 67 68 newWorkingRoot := mergeResult.Root 69 err = doltSession.SetWorkingRoot(ctx, dbName, newWorkingRoot) 70 if err != nil { 71 return "", nil, err 72 } 73 74 err = stageCherryPickedTables(ctx, mergeResult.Stats) 75 if err != nil { 76 return "", nil, err 77 } 78 79 // If there were merge conflicts, just return the merge result. 80 if mergeResult.HasMergeArtifacts() { 81 return "", mergeResult, nil 82 } 83 84 commitProps := actions.CommitStagedProps{ 85 Date: ctx.QueryTime(), 86 Name: ctx.Client().User, 87 Email: fmt.Sprintf("%s@%s", ctx.Client().User, ctx.Client().Address), 88 Message: commitMsg, 89 } 90 91 if options.CommitMessage != "" { 92 commitProps.Message = options.CommitMessage 93 } 94 if options.Amend { 95 commitProps.Amend = true 96 } 97 98 // NOTE: roots are old here (after staging the tables) and need to be refreshed 99 roots, ok = doltSession.GetRoots(ctx, dbName) 100 if !ok { 101 return "", nil, fmt.Errorf("failed to get roots for current session") 102 } 103 104 pendingCommit, err := doltSession.NewPendingCommit(ctx, dbName, roots, commitProps) 105 if err != nil { 106 return "", nil, err 107 } 108 if pendingCommit == nil { 109 return "", nil, errors.New("nothing to commit") 110 } 111 112 newCommit, err := doltSession.DoltCommit(ctx, dbName, doltSession.GetTransaction(), pendingCommit) 113 if err != nil { 114 return "", nil, err 115 } 116 117 h, err := newCommit.HashOf() 118 if err != nil { 119 return "", nil, err 120 } 121 122 return h.String(), nil, nil 123 } 124 125 func previousCommitMessage(ctx *sql.Context) (string, error) { 126 doltSession := dsess.DSessFromSess(ctx.Session) 127 headCommit, err := doltSession.GetHeadCommit(ctx, ctx.GetCurrentDatabase()) 128 if err != nil { 129 return "", err 130 } 131 headCommitMeta, err := headCommit.GetCommitMeta(ctx) 132 if err != nil { 133 return "", err 134 } 135 136 return headCommitMeta.Description, nil 137 } 138 139 // AbortCherryPick aborts a cherry-pick merge, if one is in progress. If unable to abort for any reason 140 // (e.g. if there is not cherry-pick merge in progress), an error is returned. 141 func AbortCherryPick(ctx *sql.Context, dbName string) error { 142 doltSession := dsess.DSessFromSess(ctx.Session) 143 144 ws, err := doltSession.WorkingSet(ctx, dbName) 145 if err != nil { 146 return fmt.Errorf("fatal: unable to load working set: %v", err) 147 } 148 149 if !ws.MergeActive() { 150 return fmt.Errorf("error: There is no cherry-pick merge to abort") 151 } 152 153 roots, ok := doltSession.GetRoots(ctx, dbName) 154 if !ok { 155 return fmt.Errorf("fatal: unable to load roots for %s", dbName) 156 } 157 158 newWs, err := merge.AbortMerge(ctx, ws, roots) 159 if err != nil { 160 return fmt.Errorf("fatal: unable to abort merge: %v", err) 161 } 162 163 return doltSession.SetWorkingSet(ctx, dbName, newWs) 164 } 165 166 // cherryPick checks that the current working set is clean, verifies the cherry-pick commit is not a merge commit 167 // or a commit without parent commit, performs merge and returns the new working set root value and 168 // the commit message of cherry-picked commit as the commit message of the new commit created during this command. 169 func cherryPick(ctx *sql.Context, dSess *dsess.DoltSession, roots doltdb.Roots, dbName, cherryStr string) (*merge.Result, string, error) { 170 // check for clean working set 171 wsOnlyHasIgnoredTables, err := diff.WorkingSetContainsOnlyIgnoredTables(ctx, roots) 172 if err != nil { 173 return nil, "", err 174 } 175 if !wsOnlyHasIgnoredTables { 176 return nil, "", ErrCherryPickUncommittedChanges 177 } 178 179 headRootHash, err := roots.Head.HashOf() 180 if err != nil { 181 return nil, "", err 182 } 183 184 workingRootHash, err := roots.Working.HashOf() 185 if err != nil { 186 return nil, "", err 187 } 188 189 doltDB, ok := dSess.GetDoltDB(ctx, dbName) 190 if !ok { 191 return nil, "", fmt.Errorf("failed to get DoltDB") 192 } 193 194 dbData, ok := dSess.GetDbData(ctx, dbName) 195 if !ok { 196 return nil, "", fmt.Errorf("failed to get dbData") 197 } 198 199 cherryCommitSpec, err := doltdb.NewCommitSpec(cherryStr) 200 if err != nil { 201 return nil, "", err 202 } 203 headRef, err := dbData.Rsr.CWBHeadRef() 204 if err != nil { 205 return nil, "", err 206 } 207 optCmt, err := doltDB.Resolve(ctx, cherryCommitSpec, headRef) 208 if err != nil { 209 return nil, "", err 210 } 211 cherryCommit, ok := optCmt.ToCommit() 212 if !ok { 213 return nil, "", doltdb.ErrGhostCommitEncountered 214 } 215 216 if len(cherryCommit.DatasParents()) > 1 { 217 return nil, "", fmt.Errorf("cherry-picking a merge commit is not supported") 218 } 219 if len(cherryCommit.DatasParents()) == 0 { 220 return nil, "", fmt.Errorf("cherry-picking a commit without parents is not supported") 221 } 222 223 cherryRoot, err := cherryCommit.GetRootValue(ctx) 224 if err != nil { 225 return nil, "", err 226 } 227 228 // When cherry-picking, we need to use the parent of the cherry-picked commit as the ancestor. This 229 // ensures that only the delta from the cherry-pick commit is applied. 230 optCmt, err = doltDB.ResolveParent(ctx, cherryCommit, 0) 231 if err != nil { 232 return nil, "", err 233 } 234 parentCommit, ok := optCmt.ToCommit() 235 if !ok { 236 return nil, "", doltdb.ErrGhostCommitEncountered 237 } 238 239 parentRoot, err := parentCommit.GetRootValue(ctx) 240 if err != nil { 241 return nil, "", err 242 } 243 244 dbState, ok, err := dSess.LookupDbState(ctx, dbName) 245 if err != nil { 246 return nil, "", err 247 } else if !ok { 248 return nil, "", sql.ErrDatabaseNotFound.New(dbName) 249 } 250 251 mo := merge.MergeOpts{ 252 IsCherryPick: true, 253 KeepSchemaConflicts: false, 254 } 255 result, err := merge.MergeRoots(ctx, roots.Working, cherryRoot, parentRoot, cherryCommit, parentCommit, dbState.EditOpts(), mo) 256 if err != nil { 257 return result, "", err 258 } 259 260 workingRootHash, err = result.Root.HashOf() 261 if err != nil { 262 return nil, "", err 263 } 264 265 // If the cherry-pick modifies a deleted table, we don't have a good way to surface that. Abort. 266 for _, schConflict := range result.SchemaConflicts { 267 if schConflict.ModifyDeleteConflict { 268 return nil, "", schConflict 269 } 270 } 271 272 if headRootHash.Equal(workingRootHash) { 273 return nil, "", fmt.Errorf("no changes were made, nothing to commit") 274 } 275 276 cherryCommitMeta, err := cherryCommit.GetCommitMeta(ctx) 277 if err != nil { 278 return nil, "", err 279 } 280 281 // If any of the merge stats show a data or schema conflict or a constraint 282 // violation, record that a merge is in progress. 283 for _, stats := range result.Stats { 284 if stats.HasArtifacts() { 285 ws, err := dSess.WorkingSet(ctx, dbName) 286 if err != nil { 287 return nil, "", err 288 } 289 newWorkingSet := ws.StartCherryPick(cherryCommit, cherryStr) 290 err = dSess.SetWorkingSet(ctx, dbName, newWorkingSet) 291 if err != nil { 292 return nil, "", err 293 } 294 295 break 296 } 297 } 298 299 return result, cherryCommitMeta.Description, nil 300 } 301 302 // stageCherryPickedTables stages the tables from |mergeStats| that don't have any merge artifacts – i.e. 303 // tables that don't have any data or schema conflicts and don't have any constraint violations. 304 func stageCherryPickedTables(ctx *sql.Context, mergeStats map[string]*merge.MergeStats) (err error) { 305 tablesToAdd := make([]string, 0, len(mergeStats)) 306 for tableName, mergeStats := range mergeStats { 307 if mergeStats.HasArtifacts() { 308 continue 309 } 310 311 // Find any tables being deleted and make sure we stage those tables first 312 if mergeStats.Operation == merge.TableRemoved { 313 tablesToAdd = append([]string{tableName}, tablesToAdd...) 314 } else { 315 tablesToAdd = append(tablesToAdd, tableName) 316 } 317 } 318 319 doltSession := dsess.DSessFromSess(ctx.Session) 320 dbName := ctx.GetCurrentDatabase() 321 roots, ok := doltSession.GetRoots(ctx, dbName) 322 if !ok { 323 return fmt.Errorf("unable to get roots for database '%s' from session", dbName) 324 } 325 326 roots, err = actions.StageTables(ctx, roots, tablesToAdd, true) 327 if err != nil { 328 return err 329 } 330 331 return doltSession.SetRoots(ctx, dbName, roots) 332 }