code.gitea.io/gitea@v1.21.7/services/repository/files/update.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package files 5 6 import ( 7 "context" 8 "fmt" 9 "io" 10 "path" 11 "strings" 12 "time" 13 14 "code.gitea.io/gitea/models" 15 git_model "code.gitea.io/gitea/models/git" 16 repo_model "code.gitea.io/gitea/models/repo" 17 user_model "code.gitea.io/gitea/models/user" 18 "code.gitea.io/gitea/modules/git" 19 "code.gitea.io/gitea/modules/lfs" 20 "code.gitea.io/gitea/modules/log" 21 "code.gitea.io/gitea/modules/setting" 22 "code.gitea.io/gitea/modules/structs" 23 asymkey_service "code.gitea.io/gitea/services/asymkey" 24 ) 25 26 // IdentityOptions for a person's identity like an author or committer 27 type IdentityOptions struct { 28 Name string 29 Email string 30 } 31 32 // CommitDateOptions store dates for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE 33 type CommitDateOptions struct { 34 Author time.Time 35 Committer time.Time 36 } 37 38 type ChangeRepoFile struct { 39 Operation string 40 TreePath string 41 FromTreePath string 42 ContentReader io.Reader 43 SHA string 44 Options *RepoFileOptions 45 } 46 47 // ChangeRepoFilesOptions holds the repository files update options 48 type ChangeRepoFilesOptions struct { 49 LastCommitID string 50 OldBranch string 51 NewBranch string 52 Message string 53 Files []*ChangeRepoFile 54 Author *IdentityOptions 55 Committer *IdentityOptions 56 Dates *CommitDateOptions 57 Signoff bool 58 } 59 60 type RepoFileOptions struct { 61 treePath string 62 fromTreePath string 63 executable bool 64 } 65 66 // ChangeRepoFiles adds, updates or removes multiple files in the given repository 67 func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (*structs.FilesResponse, error) { 68 // If no branch name is set, assume default branch 69 if opts.OldBranch == "" { 70 opts.OldBranch = repo.DefaultBranch 71 } 72 if opts.NewBranch == "" { 73 opts.NewBranch = opts.OldBranch 74 } 75 76 gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath()) 77 if err != nil { 78 return nil, err 79 } 80 defer closer.Close() 81 82 // oldBranch must exist for this operation 83 if _, err := gitRepo.GetBranch(opts.OldBranch); err != nil && !repo.IsEmpty { 84 return nil, err 85 } 86 87 var treePaths []string 88 for _, file := range opts.Files { 89 // If FromTreePath is not set, set it to the opts.TreePath 90 if file.TreePath != "" && file.FromTreePath == "" { 91 file.FromTreePath = file.TreePath 92 } 93 94 // Check that the path given in opts.treePath is valid (not a git path) 95 treePath := CleanUploadFileName(file.TreePath) 96 if treePath == "" { 97 return nil, models.ErrFilenameInvalid{ 98 Path: file.TreePath, 99 } 100 } 101 // If there is a fromTreePath (we are copying it), also clean it up 102 fromTreePath := CleanUploadFileName(file.FromTreePath) 103 if fromTreePath == "" && file.FromTreePath != "" { 104 return nil, models.ErrFilenameInvalid{ 105 Path: file.FromTreePath, 106 } 107 } 108 109 file.Options = &RepoFileOptions{ 110 treePath: treePath, 111 fromTreePath: fromTreePath, 112 executable: false, 113 } 114 treePaths = append(treePaths, treePath) 115 } 116 117 // A NewBranch can be specified for the file to be created/updated in a new branch. 118 // Check to make sure the branch does not already exist, otherwise we can't proceed. 119 // If we aren't branching to a new branch, make sure user can commit to the given branch 120 if opts.NewBranch != opts.OldBranch { 121 existingBranch, err := gitRepo.GetBranch(opts.NewBranch) 122 if existingBranch != nil { 123 return nil, git_model.ErrBranchAlreadyExists{ 124 BranchName: opts.NewBranch, 125 } 126 } 127 if err != nil && !git.IsErrBranchNotExist(err) { 128 return nil, err 129 } 130 } else if err := VerifyBranchProtection(ctx, repo, doer, opts.OldBranch, treePaths); err != nil { 131 return nil, err 132 } 133 134 message := strings.TrimSpace(opts.Message) 135 136 author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer) 137 138 t, err := NewTemporaryUploadRepository(ctx, repo) 139 if err != nil { 140 log.Error("%v", err) 141 } 142 defer t.Close() 143 hasOldBranch := true 144 if err := t.Clone(opts.OldBranch, true); err != nil { 145 for _, file := range opts.Files { 146 if file.Operation == "delete" { 147 return nil, err 148 } 149 } 150 if !git.IsErrBranchNotExist(err) || !repo.IsEmpty { 151 return nil, err 152 } 153 if err := t.Init(); err != nil { 154 return nil, err 155 } 156 hasOldBranch = false 157 opts.LastCommitID = "" 158 } 159 if hasOldBranch { 160 if err := t.SetDefaultIndex(); err != nil { 161 return nil, err 162 } 163 } 164 165 for _, file := range opts.Files { 166 if file.Operation == "delete" { 167 // Get the files in the index 168 filesInIndex, err := t.LsFiles(file.TreePath) 169 if err != nil { 170 return nil, fmt.Errorf("DeleteRepoFile: %w", err) 171 } 172 173 // Find the file we want to delete in the index 174 inFilelist := false 175 for _, indexFile := range filesInIndex { 176 if indexFile == file.TreePath { 177 inFilelist = true 178 break 179 } 180 } 181 if !inFilelist { 182 return nil, models.ErrRepoFileDoesNotExist{ 183 Path: file.TreePath, 184 } 185 } 186 } 187 } 188 189 if hasOldBranch { 190 // Get the commit of the original branch 191 commit, err := t.GetBranchCommit(opts.OldBranch) 192 if err != nil { 193 return nil, err // Couldn't get a commit for the branch 194 } 195 196 // Assigned LastCommitID in opts if it hasn't been set 197 if opts.LastCommitID == "" { 198 opts.LastCommitID = commit.ID.String() 199 } else { 200 lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID) 201 if err != nil { 202 return nil, fmt.Errorf("ConvertToSHA1: Invalid last commit ID: %w", err) 203 } 204 opts.LastCommitID = lastCommitID.String() 205 206 } 207 208 for _, file := range opts.Files { 209 if err := handleCheckErrors(file, commit, opts, repo); err != nil { 210 return nil, err 211 } 212 } 213 } 214 215 contentStore := lfs.NewContentStore() 216 for _, file := range opts.Files { 217 switch file.Operation { 218 case "create", "update": 219 if err := CreateOrUpdateFile(ctx, t, file, contentStore, repo.ID, hasOldBranch); err != nil { 220 return nil, err 221 } 222 case "delete": 223 // Remove the file from the index 224 if err := t.RemoveFilesFromIndex(file.TreePath); err != nil { 225 return nil, err 226 } 227 default: 228 return nil, fmt.Errorf("invalid file operation: %s %s, supported operations are create, update, delete", file.Operation, file.Options.treePath) 229 } 230 } 231 232 // Now write the tree 233 treeHash, err := t.WriteTree() 234 if err != nil { 235 return nil, err 236 } 237 238 // Now commit the tree 239 var commitHash string 240 if opts.Dates != nil { 241 commitHash, err = t.CommitTreeWithDate(opts.LastCommitID, author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer) 242 } else { 243 commitHash, err = t.CommitTree(opts.LastCommitID, author, committer, treeHash, message, opts.Signoff) 244 } 245 if err != nil { 246 return nil, err 247 } 248 249 // Then push this tree to NewBranch 250 if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { 251 log.Error("%T %v", err, err) 252 return nil, err 253 } 254 255 commit, err := t.GetCommit(commitHash) 256 if err != nil { 257 return nil, err 258 } 259 260 filesResponse, err := GetFilesResponseFromCommit(ctx, repo, commit, opts.NewBranch, treePaths) 261 if err != nil { 262 return nil, err 263 } 264 265 if repo.IsEmpty { 266 if isEmpty, err := gitRepo.IsEmpty(); err == nil && !isEmpty { 267 _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: repo.ID, IsEmpty: false, DefaultBranch: opts.NewBranch}, "is_empty", "default_branch") 268 } 269 } 270 271 return filesResponse, nil 272 } 273 274 // handles the check for various issues for ChangeRepoFiles 275 func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions, repo *repo_model.Repository) error { 276 if file.Operation == "update" || file.Operation == "delete" { 277 fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath) 278 if err != nil { 279 return err 280 } 281 if file.SHA != "" { 282 // If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error 283 if file.SHA != fromEntry.ID.String() { 284 return models.ErrSHADoesNotMatch{ 285 Path: file.Options.treePath, 286 GivenSHA: file.SHA, 287 CurrentSHA: fromEntry.ID.String(), 288 } 289 } 290 } else if opts.LastCommitID != "" { 291 // If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw 292 // an error, but only if we aren't creating a new branch. 293 if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch { 294 if changed, err := commit.FileChangedSinceCommit(file.Options.treePath, opts.LastCommitID); err != nil { 295 return err 296 } else if changed { 297 return models.ErrCommitIDDoesNotMatch{ 298 GivenCommitID: opts.LastCommitID, 299 CurrentCommitID: opts.LastCommitID, 300 } 301 } 302 // The file wasn't modified, so we are good to delete it 303 } 304 } else { 305 // When updating a file, a lastCommitID or SHA needs to be given to make sure other commits 306 // haven't been made. We throw an error if one wasn't provided. 307 return models.ErrSHAOrCommitIDNotProvided{} 308 } 309 file.Options.executable = fromEntry.IsExecutable() 310 } 311 if file.Operation == "create" || file.Operation == "update" { 312 // For the path where this file will be created/updated, we need to make 313 // sure no parts of the path are existing files or links except for the last 314 // item in the path which is the file name, and that shouldn't exist IF it is 315 // a new file OR is being moved to a new path. 316 treePathParts := strings.Split(file.Options.treePath, "/") 317 subTreePath := "" 318 for index, part := range treePathParts { 319 subTreePath = path.Join(subTreePath, part) 320 entry, err := commit.GetTreeEntryByPath(subTreePath) 321 if err != nil { 322 if git.IsErrNotExist(err) { 323 // Means there is no item with that name, so we're good 324 break 325 } 326 return err 327 } 328 if index < len(treePathParts)-1 { 329 if !entry.IsDir() { 330 return models.ErrFilePathInvalid{ 331 Message: fmt.Sprintf("a file exists where you’re trying to create a subdirectory [path: %s]", subTreePath), 332 Path: subTreePath, 333 Name: part, 334 Type: git.EntryModeBlob, 335 } 336 } 337 } else if entry.IsLink() { 338 return models.ErrFilePathInvalid{ 339 Message: fmt.Sprintf("a symbolic link exists where you’re trying to create a subdirectory [path: %s]", subTreePath), 340 Path: subTreePath, 341 Name: part, 342 Type: git.EntryModeSymlink, 343 } 344 } else if entry.IsDir() { 345 return models.ErrFilePathInvalid{ 346 Message: fmt.Sprintf("a directory exists where you’re trying to create a file [path: %s]", subTreePath), 347 Path: subTreePath, 348 Name: part, 349 Type: git.EntryModeTree, 350 } 351 } else if file.Options.fromTreePath != file.Options.treePath || file.Operation == "create" { 352 // The entry shouldn't exist if we are creating new file or moving to a new path 353 return models.ErrRepoFileAlreadyExists{ 354 Path: file.Options.treePath, 355 } 356 } 357 358 } 359 } 360 361 return nil 362 } 363 364 // CreateOrUpdateFile handles creating or updating a file for ChangeRepoFiles 365 func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64, hasOldBranch bool) error { 366 // Get the two paths (might be the same if not moving) from the index if they exist 367 filesInIndex, err := t.LsFiles(file.TreePath, file.FromTreePath) 368 if err != nil { 369 return fmt.Errorf("UpdateRepoFile: %w", err) 370 } 371 // If is a new file (not updating) then the given path shouldn't exist 372 if file.Operation == "create" { 373 for _, indexFile := range filesInIndex { 374 if indexFile == file.TreePath { 375 return models.ErrRepoFileAlreadyExists{ 376 Path: file.TreePath, 377 } 378 } 379 } 380 } 381 382 // Remove the old path from the tree 383 if file.Options.fromTreePath != file.Options.treePath && len(filesInIndex) > 0 { 384 for _, indexFile := range filesInIndex { 385 if indexFile == file.Options.fromTreePath { 386 if err := t.RemoveFilesFromIndex(file.FromTreePath); err != nil { 387 return err 388 } 389 } 390 } 391 } 392 393 treeObjectContentReader := file.ContentReader 394 var lfsMetaObject *git_model.LFSMetaObject 395 if setting.LFS.StartServer && hasOldBranch { 396 // Check there is no way this can return multiple infos 397 filename2attribute2info, err := t.gitRepo.CheckAttribute(git.CheckAttributeOpts{ 398 Attributes: []string{"filter"}, 399 Filenames: []string{file.Options.treePath}, 400 CachedOnly: true, 401 }) 402 if err != nil { 403 return err 404 } 405 406 if filename2attribute2info[file.Options.treePath] != nil && filename2attribute2info[file.Options.treePath]["filter"] == "lfs" { 407 // OK so we are supposed to LFS this data! 408 pointer, err := lfs.GeneratePointer(treeObjectContentReader) 409 if err != nil { 410 return err 411 } 412 lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: repoID} 413 treeObjectContentReader = strings.NewReader(pointer.StringContent()) 414 } 415 } 416 417 // Add the object to the database 418 objectHash, err := t.HashObject(treeObjectContentReader) 419 if err != nil { 420 return err 421 } 422 423 // Add the object to the index 424 if file.Options.executable { 425 if err := t.AddObjectToIndex("100755", objectHash, file.Options.treePath); err != nil { 426 return err 427 } 428 } else { 429 if err := t.AddObjectToIndex("100644", objectHash, file.Options.treePath); err != nil { 430 return err 431 } 432 } 433 434 if lfsMetaObject != nil { 435 // We have an LFS object - create it 436 lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, lfsMetaObject) 437 if err != nil { 438 return err 439 } 440 exist, err := contentStore.Exists(lfsMetaObject.Pointer) 441 if err != nil { 442 return err 443 } 444 if !exist { 445 if err := contentStore.Put(lfsMetaObject.Pointer, file.ContentReader); err != nil { 446 if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); err2 != nil { 447 return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err) 448 } 449 return err 450 } 451 } 452 } 453 454 return nil 455 } 456 457 // VerifyBranchProtection verify the branch protection for modifying the given treePath on the given branch 458 func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, branchName string, treePaths []string) error { 459 protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName) 460 if err != nil { 461 return err 462 } 463 if protectedBranch != nil { 464 protectedBranch.Repo = repo 465 globUnprotected := protectedBranch.GetUnprotectedFilePatterns() 466 globProtected := protectedBranch.GetProtectedFilePatterns() 467 canUserPush := protectedBranch.CanUserPush(ctx, doer) 468 for _, treePath := range treePaths { 469 isUnprotectedFile := false 470 if len(globUnprotected) != 0 { 471 isUnprotectedFile = protectedBranch.IsUnprotectedFile(globUnprotected, treePath) 472 } 473 if !canUserPush && !isUnprotectedFile { 474 return models.ErrUserCannotCommit{ 475 UserName: doer.LowerName, 476 } 477 } 478 if protectedBranch.IsProtectedFile(globProtected, treePath) { 479 return models.ErrFilePathProtected{ 480 Path: treePath, 481 } 482 } 483 } 484 if protectedBranch.RequireSignedCommits { 485 _, _, _, err := asymkey_service.SignCRUDAction(ctx, repo.RepoPath(), doer, repo.RepoPath(), branchName) 486 if err != nil { 487 if !asymkey_service.IsErrWontSign(err) { 488 return err 489 } 490 return models.ErrUserCannotCommit{ 491 UserName: doer.LowerName, 492 } 493 } 494 } 495 } 496 return nil 497 }