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