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  }