code.gitea.io/gitea@v1.21.7/services/repository/files/patch.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package files
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"strings"
    10  
    11  	"code.gitea.io/gitea/models"
    12  	git_model "code.gitea.io/gitea/models/git"
    13  	repo_model "code.gitea.io/gitea/models/repo"
    14  	user_model "code.gitea.io/gitea/models/user"
    15  	"code.gitea.io/gitea/modules/git"
    16  	"code.gitea.io/gitea/modules/log"
    17  	"code.gitea.io/gitea/modules/structs"
    18  	asymkey_service "code.gitea.io/gitea/services/asymkey"
    19  )
    20  
    21  // ApplyDiffPatchOptions holds the repository diff patch update options
    22  type ApplyDiffPatchOptions struct {
    23  	LastCommitID string
    24  	OldBranch    string
    25  	NewBranch    string
    26  	Message      string
    27  	Content      string
    28  	SHA          string
    29  	Author       *IdentityOptions
    30  	Committer    *IdentityOptions
    31  	Dates        *CommitDateOptions
    32  	Signoff      bool
    33  }
    34  
    35  // Validate validates the provided options
    36  func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_model.Repository, doer *user_model.User) error {
    37  	// If no branch name is set, assume master
    38  	if opts.OldBranch == "" {
    39  		opts.OldBranch = repo.DefaultBranch
    40  	}
    41  	if opts.NewBranch == "" {
    42  		opts.NewBranch = opts.OldBranch
    43  	}
    44  
    45  	gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath())
    46  	if err != nil {
    47  		return err
    48  	}
    49  	defer closer.Close()
    50  
    51  	// oldBranch must exist for this operation
    52  	if _, err := gitRepo.GetBranch(opts.OldBranch); err != nil {
    53  		return err
    54  	}
    55  	// A NewBranch can be specified for the patch to be applied to.
    56  	// Check to make sure the branch does not already exist, otherwise we can't proceed.
    57  	// If we aren't branching to a new branch, make sure user can commit to the given branch
    58  	if opts.NewBranch != opts.OldBranch {
    59  		existingBranch, err := gitRepo.GetBranch(opts.NewBranch)
    60  		if existingBranch != nil {
    61  			return git_model.ErrBranchAlreadyExists{
    62  				BranchName: opts.NewBranch,
    63  			}
    64  		}
    65  		if err != nil && !git.IsErrBranchNotExist(err) {
    66  			return err
    67  		}
    68  	} else {
    69  		protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, opts.OldBranch)
    70  		if err != nil {
    71  			return err
    72  		}
    73  		if protectedBranch != nil {
    74  			protectedBranch.Repo = repo
    75  			if !protectedBranch.CanUserPush(ctx, doer) {
    76  				return models.ErrUserCannotCommit{
    77  					UserName: doer.LowerName,
    78  				}
    79  			}
    80  		}
    81  		if protectedBranch != nil && protectedBranch.RequireSignedCommits {
    82  			_, _, _, err := asymkey_service.SignCRUDAction(ctx, repo.RepoPath(), doer, repo.RepoPath(), opts.OldBranch)
    83  			if err != nil {
    84  				if !asymkey_service.IsErrWontSign(err) {
    85  					return err
    86  				}
    87  				return models.ErrUserCannotCommit{
    88  					UserName: doer.LowerName,
    89  				}
    90  			}
    91  		}
    92  	}
    93  	return nil
    94  }
    95  
    96  // ApplyDiffPatch applies a patch to the given repository
    97  func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) {
    98  	if err := opts.Validate(ctx, repo, doer); err != nil {
    99  		return nil, err
   100  	}
   101  
   102  	message := strings.TrimSpace(opts.Message)
   103  
   104  	author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)
   105  
   106  	t, err := NewTemporaryUploadRepository(ctx, repo)
   107  	if err != nil {
   108  		log.Error("%v", err)
   109  	}
   110  	defer t.Close()
   111  	if err := t.Clone(opts.OldBranch, true); err != nil {
   112  		return nil, err
   113  	}
   114  	if err := t.SetDefaultIndex(); err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	// Get the commit of the original branch
   119  	commit, err := t.GetBranchCommit(opts.OldBranch)
   120  	if err != nil {
   121  		return nil, err // Couldn't get a commit for the branch
   122  	}
   123  
   124  	// Assigned LastCommitID in opts if it hasn't been set
   125  	if opts.LastCommitID == "" {
   126  		opts.LastCommitID = commit.ID.String()
   127  	} else {
   128  		lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID)
   129  		if err != nil {
   130  			return nil, fmt.Errorf("ApplyPatch: Invalid last commit ID: %w", err)
   131  		}
   132  		opts.LastCommitID = lastCommitID.String()
   133  		if commit.ID.String() != opts.LastCommitID {
   134  			return nil, models.ErrCommitIDDoesNotMatch{
   135  				GivenCommitID:   opts.LastCommitID,
   136  				CurrentCommitID: opts.LastCommitID,
   137  			}
   138  		}
   139  	}
   140  
   141  	stdout := &strings.Builder{}
   142  	stderr := &strings.Builder{}
   143  
   144  	cmdApply := git.NewCommand(ctx, "apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary")
   145  	if git.CheckGitVersionAtLeast("2.32") == nil {
   146  		cmdApply.AddArguments("-3")
   147  	}
   148  
   149  	if err := cmdApply.Run(&git.RunOpts{
   150  		Dir:    t.basePath,
   151  		Stdout: stdout,
   152  		Stderr: stderr,
   153  		Stdin:  strings.NewReader(opts.Content),
   154  	}); err != nil {
   155  		return nil, fmt.Errorf("Error: Stdout: %s\nStderr: %s\nErr: %w", stdout.String(), stderr.String(), err)
   156  	}
   157  
   158  	// Now write the tree
   159  	treeHash, err := t.WriteTree()
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	// Now commit the tree
   165  	var commitHash string
   166  	if opts.Dates != nil {
   167  		commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer)
   168  	} else {
   169  		commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff)
   170  	}
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	// Then push this tree to NewBranch
   176  	if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	commit, err = t.GetCommit(commitHash)
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  
   185  	fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil
   186  	verification := GetPayloadCommitVerification(ctx, commit)
   187  	fileResponse := &structs.FileResponse{
   188  		Commit:       fileCommitResponse,
   189  		Verification: verification,
   190  	}
   191  
   192  	return fileResponse, nil
   193  }