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