2019-12-13 22:21:06 +00:00
// Copyright 2019 The Gitea Authors.
// All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2019-12-13 22:21:06 +00:00
package pull
import (
2025-05-16 12:40:38 +00:00
"bytes"
2019-12-13 22:21:06 +00:00
"context"
"fmt"
"io"
"os"
2021-12-19 04:19:25 +00:00
"path/filepath"
2019-12-13 22:21:06 +00:00
"strings"
2025-03-27 19:40:14 +00:00
"forgejo.org/models"
git_model "forgejo.org/models/git"
issues_model "forgejo.org/models/issues"
"forgejo.org/modules/git"
"forgejo.org/modules/gitrepo"
"forgejo.org/modules/graceful"
"forgejo.org/modules/log"
"forgejo.org/modules/process"
"forgejo.org/modules/util"
2020-10-14 02:50:57 +08:00
"github.com/gobwas/glob"
2019-12-13 22:21:06 +00:00
)
// DownloadDiffOrPatch will write the patch for the pr to the writer
2022-06-13 17:37:59 +08:00
func DownloadDiffOrPatch ( ctx context . Context , pr * issues_model . PullRequest , w io . Writer , patch , binary bool ) error {
2022-11-19 09:12:33 +01:00
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
2020-04-03 14:21:41 +01:00
log . Error ( "Unable to load base repository ID %d for pr #%d [%d]" , pr . BaseRepoID , pr . Index , pr . ID )
2019-12-13 22:21:06 +00:00
return err
}
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
2024-01-28 04:09:51 +08:00
gitRepo , closer , err := gitrepo . RepositoryFromContextOrOpen ( ctx , pr . BaseRepo )
2019-12-13 22:21:06 +00:00
if err != nil {
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "OpenRepository: %w" , err )
2019-12-13 22:21:06 +00:00
}
2022-01-19 23:26:57 +00:00
defer closer . Close ( )
2021-09-27 23:09:49 +02:00
if err := gitRepo . GetDiffOrPatch ( pr . MergeBase , pr . GetGitRefName ( ) , w , patch , binary ) ; err != nil {
2020-01-12 17:36:21 +08:00
log . Error ( "Unable to get patch file from %s to %s in %s Error: %v" , pr . MergeBase , pr . HeadBranch , pr . BaseRepo . FullName ( ) , err )
2022-10-24 21:29:17 +02:00
return fmt . Errorf ( "Unable to get patch file from %s to %s in %s Error: %w" , pr . MergeBase , pr . HeadBranch , pr . BaseRepo . FullName ( ) , err )
2019-12-13 22:21:06 +00:00
}
return nil
}
// TestPatch will test whether a simple patch will apply
2022-06-13 17:37:59 +08:00
func TestPatch ( pr * issues_model . PullRequest ) error {
2023-02-03 23:11:48 +00:00
ctx , _ , finished := process . GetManager ( ) . AddContext ( graceful . GetManager ( ) . HammerContext ( ) , fmt . Sprintf ( "TestPatch: %s" , pr ) )
2022-01-19 23:26:57 +00:00
defer finished ( )
2025-05-16 12:40:38 +00:00
testPatchCtx , err := testPatch ( ctx , pr )
testPatchCtx . close ( )
return err
}
type testPatchContext struct {
headRev string
headIsCommitID bool
baseRev string
env [ ] string
gitRepo * git . Repository
close func ( )
}
// LoadHeadRevision loads the necessary information to access the head revision.
func ( t * testPatchContext ) LoadHeadRevision ( ctx context . Context , pr * issues_model . PullRequest ) error {
// If AGit, then use HeadCommitID if set (AGit flow creates pull request),
// otherwise use the pull request reference.
if pr . Flow == issues_model . PullRequestFlowAGit {
if len ( pr . HeadCommitID ) > 0 {
t . headRev = pr . HeadCommitID
t . headIsCommitID = true
return nil
2023-08-10 10:39:21 +08:00
}
2025-05-16 12:40:38 +00:00
t . headRev = pr . GetGitRefName ( )
return nil
}
// If it is within the same repository, simply return the branch name.
if pr . BaseRepoID == pr . HeadRepoID {
t . headRev = pr . GetGitHeadBranchRefName ( )
return nil
}
// We are in Github flow, head and base repository are different.
// Resolve the head branch to a commitID and return a Git alternate
// environment for the head repository.
gitRepo , err := git . OpenRepository ( ctx , pr . HeadRepo . RepoPath ( ) )
if err != nil {
return err
}
defer gitRepo . Close ( )
headCommitID , err := gitRepo . GetRefCommitID ( pr . GetGitHeadBranchRefName ( ) )
if err != nil {
2019-12-13 22:21:06 +00:00
return err
}
2025-05-16 12:40:38 +00:00
t . headRev = headCommitID
t . headIsCommitID = true
t . env = append ( os . Environ ( ) , ` GIT_ALTERNATE_OBJECT_DIRECTORIES= ` + pr . HeadRepo . RepoPath ( ) + "/objects" )
return nil
2023-08-10 10:39:21 +08:00
}
2025-05-16 12:40:38 +00:00
// getTestPatchCtx constructs a new testpatch context for the given pull request.
func getTestPatchCtx ( ctx context . Context , pr * issues_model . PullRequest ) ( * testPatchContext , error ) {
testPatchCtx := & testPatchContext {
close : func ( ) { } ,
}
if git . SupportGitMergeTree {
if err := pr . LoadBaseRepo ( ctx ) ; err != nil {
return testPatchCtx , fmt . Errorf ( "LoadBaseRepo: %w" , err )
}
if err := pr . LoadHeadRepo ( ctx ) ; err != nil {
return testPatchCtx , fmt . Errorf ( "LoadHeadRepo: %w" , err )
}
if err := testPatchCtx . LoadHeadRevision ( ctx , pr ) ; err != nil {
return testPatchCtx , fmt . Errorf ( "LoadHeadRevision: %w" , err )
}
gitRepo , err := git . OpenRepository ( ctx , pr . BaseRepo . RepoPath ( ) )
if err != nil {
return testPatchCtx , fmt . Errorf ( "OpenRepository: %w" , err )
}
testPatchCtx . baseRev = git . BranchPrefix + pr . BaseBranch
testPatchCtx . gitRepo = gitRepo
testPatchCtx . close = func ( ) {
gitRepo . Close ( )
}
} else {
prCtx , cancel , err := createTemporaryRepoForPR ( ctx , pr )
if err != nil {
return testPatchCtx , fmt . Errorf ( "createTemporaryRepoForPR: %w" , err )
}
testPatchCtx . close = cancel
gitRepo , err := git . OpenRepository ( ctx , prCtx . tmpBasePath )
if err != nil {
return testPatchCtx , fmt . Errorf ( "OpenRepository: %w" , err )
}
testPatchCtx . baseRev = git . BranchPrefix + baseBranch
testPatchCtx . headRev = git . BranchPrefix + trackingBranch
testPatchCtx . gitRepo = gitRepo
testPatchCtx . close = func ( ) {
cancel ( )
gitRepo . Close ( )
}
}
return testPatchCtx , nil
}
func testPatch ( ctx context . Context , pr * issues_model . PullRequest ) ( * testPatchContext , error ) {
testPatchCtx , err := getTestPatchCtx ( ctx , pr )
2019-12-13 22:21:06 +00:00
if err != nil {
2025-05-16 12:40:38 +00:00
return testPatchCtx , fmt . Errorf ( "getTestPatchCtx: %w" , err )
2019-12-13 22:21:06 +00:00
}
2020-10-14 02:50:57 +08:00
// 1. update merge base
2025-05-16 12:40:38 +00:00
pr . MergeBase , _ , err = git . NewCommand ( ctx , "merge-base" ) . AddDashesAndList ( testPatchCtx . baseRev , testPatchCtx . headRev ) . RunStdString ( & git . RunOpts { Dir : testPatchCtx . gitRepo . Path , Env : testPatchCtx . env } )
2019-12-13 22:21:06 +00:00
if err != nil {
var err2 error
2025-05-16 12:40:38 +00:00
pr . MergeBase , err2 = testPatchCtx . gitRepo . GetRefCommitID ( testPatchCtx . baseRev )
2019-12-13 22:21:06 +00:00
if err2 != nil {
2025-05-16 12:40:38 +00:00
return testPatchCtx , fmt . Errorf ( "GetMergeBase: %v and can't find commit ID for base: %w" , err , err2 )
2019-12-13 22:21:06 +00:00
}
}
pr . MergeBase = strings . TrimSpace ( pr . MergeBase )
2025-05-16 12:40:38 +00:00
if testPatchCtx . headIsCommitID {
pr . HeadCommitID = testPatchCtx . headRev
} else {
if pr . HeadCommitID , err = testPatchCtx . gitRepo . GetRefCommitID ( testPatchCtx . headRev ) ; err != nil {
return testPatchCtx , fmt . Errorf ( "GetRefCommitID: can't find commit ID for head: %w" , err )
}
2022-07-13 10:22:51 +02:00
}
2025-05-16 12:40:38 +00:00
// If the head commit is equal to the merge base it roughly means that the
// head commit is a parent of the base commit.
2022-07-13 10:22:51 +02:00
if pr . HeadCommitID == pr . MergeBase {
pr . Status = issues_model . PullRequestStatusAncestor
2025-05-16 12:40:38 +00:00
return testPatchCtx , nil
2022-07-13 10:22:51 +02:00
}
2020-10-14 02:50:57 +08:00
// 2. Check for conflicts
2025-05-16 12:40:38 +00:00
if conflicts , err := checkConflicts ( ctx , pr , testPatchCtx ) ; err != nil || conflicts || pr . Status == issues_model . PullRequestStatusEmpty {
if err != nil {
return testPatchCtx , fmt . Errorf ( "checkConflicts: %w" , err )
}
return testPatchCtx , nil
2020-10-14 02:50:57 +08:00
}
// 3. Check for protected files changes
2025-05-16 12:40:38 +00:00
if err = checkPullFilesProtection ( ctx , pr , testPatchCtx ) ; err != nil {
return testPatchCtx , fmt . Errorf ( "checkPullFilesProtection: %v" , err )
2020-10-14 02:50:57 +08:00
}
if len ( pr . ChangedProtectedFiles ) > 0 {
log . Trace ( "Found %d protected files changed" , len ( pr . ChangedProtectedFiles ) )
}
2022-06-13 17:37:59 +08:00
pr . Status = issues_model . PullRequestStatusMergeable
2020-10-14 02:50:57 +08:00
2025-05-16 12:40:38 +00:00
return testPatchCtx , nil
2020-10-14 02:50:57 +08:00
}
2021-12-19 04:19:25 +00:00
type errMergeConflict struct {
filename string
}
func ( e * errMergeConflict ) Error ( ) string {
return fmt . Sprintf ( "conflict detected at: %s" , e . filename )
}
2024-07-04 20:57:11 +02:00
func attemptMerge ( ctx context . Context , file * unmergedFile , tmpBasePath string , filesToRemove * [ ] string , filesToAdd * [ ] git . IndexObjectInfo ) error {
2022-07-29 00:19:55 +01:00
log . Trace ( "Attempt to merge:\n%v" , file )
2023-12-13 21:02:00 +00:00
2021-12-19 04:19:25 +00:00
switch {
case file . stage1 != nil && ( file . stage2 == nil || file . stage3 == nil ) :
// 1. Deleted in one or both:
//
// Conflict <==> the stage1 !SameAs to the undeleted one
if ( file . stage2 != nil && ! file . stage1 . SameAs ( file . stage2 ) ) || ( file . stage3 != nil && ! file . stage1 . SameAs ( file . stage3 ) ) {
// Conflict!
return & errMergeConflict { file . stage1 . path }
}
// Not a genuine conflict and we can simply remove the file from the index
2024-07-04 20:57:11 +02:00
* filesToRemove = append ( * filesToRemove , file . stage1 . path )
return nil
2021-12-19 04:19:25 +00:00
case file . stage1 == nil && file . stage2 != nil && ( file . stage3 == nil || file . stage2 . SameAs ( file . stage3 ) ) :
// 2. Added in ours but not in theirs or identical in both
//
// Not a genuine conflict just add to the index
2024-07-04 20:57:11 +02:00
* filesToAdd = append ( * filesToAdd , git . IndexObjectInfo { Mode : file . stage2 . mode , Object : git . MustIDFromString ( file . stage2 . sha ) , Filename : file . stage2 . path } )
2021-12-19 04:19:25 +00:00
return nil
case file . stage1 == nil && file . stage2 != nil && file . stage3 != nil && file . stage2 . sha == file . stage3 . sha && file . stage2 . mode != file . stage3 . mode :
// 3. Added in both with the same sha but the modes are different
//
// Conflict! (Not sure that this can actually happen but we should handle)
return & errMergeConflict { file . stage2 . path }
case file . stage1 == nil && file . stage2 == nil && file . stage3 != nil :
// 4. Added in theirs but not ours:
//
// Not a genuine conflict just add to the index
2024-07-04 20:57:11 +02:00
* filesToAdd = append ( * filesToAdd , git . IndexObjectInfo { Mode : file . stage3 . mode , Object : git . MustIDFromString ( file . stage3 . sha ) , Filename : file . stage3 . path } )
return nil
2021-12-19 04:19:25 +00:00
case file . stage1 == nil :
// 5. Created by new in both
//
// Conflict!
return & errMergeConflict { file . stage2 . path }
case file . stage2 != nil && file . stage3 != nil :
// 5. Modified in both - we should try to merge in the changes but first:
//
if file . stage2 . mode == "120000" || file . stage3 . mode == "120000" {
// 5a. Conflicting symbolic link change
return & errMergeConflict { file . stage2 . path }
}
if file . stage2 . mode == "160000" || file . stage3 . mode == "160000" {
// 5b. Conflicting submodule change
return & errMergeConflict { file . stage2 . path }
}
if file . stage2 . mode != file . stage3 . mode {
// 5c. Conflicting mode change
return & errMergeConflict { file . stage2 . path }
}
// Need to get the objects from the object db to attempt to merge
2022-10-23 22:44:45 +08:00
root , _ , err := git . NewCommand ( ctx , "unpack-file" ) . AddDynamicArguments ( file . stage1 . sha ) . RunStdString ( & git . RunOpts { Dir : tmpBasePath } )
2021-12-19 04:19:25 +00:00
if err != nil {
return fmt . Errorf ( "unable to get root object: %s at path: %s for merging. Error: %w" , file . stage1 . sha , file . stage1 . path , err )
}
root = strings . TrimSpace ( root )
defer func ( ) {
_ = util . Remove ( filepath . Join ( tmpBasePath , root ) )
} ( )
2022-10-23 22:44:45 +08:00
base , _ , err := git . NewCommand ( ctx , "unpack-file" ) . AddDynamicArguments ( file . stage2 . sha ) . RunStdString ( & git . RunOpts { Dir : tmpBasePath } )
2021-12-19 04:19:25 +00:00
if err != nil {
return fmt . Errorf ( "unable to get base object: %s at path: %s for merging. Error: %w" , file . stage2 . sha , file . stage2 . path , err )
}
base = strings . TrimSpace ( filepath . Join ( tmpBasePath , base ) )
defer func ( ) {
_ = util . Remove ( base )
} ( )
2022-10-23 22:44:45 +08:00
head , _ , err := git . NewCommand ( ctx , "unpack-file" ) . AddDynamicArguments ( file . stage3 . sha ) . RunStdString ( & git . RunOpts { Dir : tmpBasePath } )
2021-12-19 04:19:25 +00:00
if err != nil {
return fmt . Errorf ( "unable to get head object:%s at path: %s for merging. Error: %w" , file . stage3 . sha , file . stage3 . path , err )
}
head = strings . TrimSpace ( head )
defer func ( ) {
_ = util . Remove ( filepath . Join ( tmpBasePath , head ) )
} ( )
// now git merge-file annoyingly takes a different order to the merge-tree ...
2022-10-23 22:44:45 +08:00
_ , _ , conflictErr := git . NewCommand ( ctx , "merge-file" ) . AddDynamicArguments ( base , root , head ) . RunStdString ( & git . RunOpts { Dir : tmpBasePath } )
2021-12-19 04:19:25 +00:00
if conflictErr != nil {
return & errMergeConflict { file . stage2 . path }
}
// base now contains the merged data
2022-10-23 22:44:45 +08:00
hash , _ , err := git . NewCommand ( ctx , "hash-object" , "-w" , "--path" ) . AddDynamicArguments ( file . stage2 . path , base ) . RunStdString ( & git . RunOpts { Dir : tmpBasePath } )
2021-12-19 04:19:25 +00:00
if err != nil {
return err
}
hash = strings . TrimSpace ( hash )
2024-07-04 20:57:11 +02:00
* filesToAdd = append ( * filesToAdd , git . IndexObjectInfo { Mode : file . stage2 . mode , Object : git . MustIDFromString ( hash ) , Filename : file . stage2 . path } )
return nil
2021-12-19 04:19:25 +00:00
default :
if file . stage1 != nil {
return & errMergeConflict { file . stage1 . path }
} else if file . stage2 != nil {
return & errMergeConflict { file . stage2 . path }
} else if file . stage3 != nil {
return & errMergeConflict { file . stage3 . path }
}
}
return nil
}
2022-02-09 20:28:55 +00:00
// AttemptThreeWayMerge will attempt to three way merge using git read-tree and then follow the git merge-one-file algorithm to attempt to resolve basic conflicts
2025-05-16 12:40:38 +00:00
func AttemptThreeWayMerge ( ctx context . Context , gitRepo * git . Repository , base , ours , theirs , description string ) ( bool , [ ] string , error ) {
2022-02-09 20:28:55 +00:00
ctx , cancel := context . WithCancel ( ctx )
defer cancel ( )
2021-12-19 04:19:25 +00:00
// First we use read-tree to do a simple three-way merge
2025-05-16 12:40:38 +00:00
if _ , _ , err := git . NewCommand ( ctx , "read-tree" , "-m" ) . AddDynamicArguments ( base , ours , theirs ) . RunStdString ( & git . RunOpts { Dir : gitRepo . Path } ) ; err != nil {
2021-12-19 04:19:25 +00:00
log . Error ( "Unable to run read-tree -m! Error: %v" , err )
2022-10-24 21:29:17 +02:00
return false , nil , fmt . Errorf ( "unable to run read-tree -m! Error: %w" , err )
2021-12-19 04:19:25 +00:00
}
2024-07-04 20:57:11 +02:00
var filesToRemove [ ] string
var filesToAdd [ ] git . IndexObjectInfo
2021-12-19 04:19:25 +00:00
// Then we use git ls-files -u to list the unmerged files and collate the triples in unmergedfiles
unmerged := make ( chan * unmergedFile )
2025-05-16 12:40:38 +00:00
go unmergedFiles ( ctx , gitRepo . Path , unmerged )
2021-12-19 04:19:25 +00:00
defer func ( ) {
cancel ( )
for range unmerged {
// empty the unmerged channel
}
} ( )
numberOfConflicts := 0
conflict := false
2022-02-09 20:28:55 +00:00
conflictedFiles := make ( [ ] string , 0 , 5 )
2021-12-19 04:19:25 +00:00
for file := range unmerged {
if file == nil {
break
}
if file . err != nil {
cancel ( )
2022-02-09 20:28:55 +00:00
return false , nil , file . err
2021-12-19 04:19:25 +00:00
}
// OK now we have the unmerged file triplet attempt to merge it
2025-05-16 12:40:38 +00:00
if err := attemptMerge ( ctx , file , gitRepo . Path , & filesToRemove , & filesToAdd ) ; err != nil {
2021-12-19 04:19:25 +00:00
if conflictErr , ok := err . ( * errMergeConflict ) ; ok {
2022-02-09 20:28:55 +00:00
log . Trace ( "Conflict: %s in %s" , conflictErr . filename , description )
2021-12-19 04:19:25 +00:00
conflict = true
if numberOfConflicts < 10 {
2022-02-09 20:28:55 +00:00
conflictedFiles = append ( conflictedFiles , conflictErr . filename )
2021-12-19 04:19:25 +00:00
}
numberOfConflicts ++
continue
}
2022-02-09 20:28:55 +00:00
return false , nil , err
2021-12-19 04:19:25 +00:00
}
}
2024-07-04 20:57:11 +02:00
// Add and remove files in one command, as this is slow with many files otherwise
if err := gitRepo . RemoveFilesFromIndex ( filesToRemove ... ) ; err != nil {
return false , nil , err
}
if err := gitRepo . AddObjectsToIndex ( filesToAdd ... ) ; err != nil {
return false , nil , err
}
2022-02-09 20:28:55 +00:00
return conflict , conflictedFiles , nil
}
2025-05-16 12:40:38 +00:00
// MergeTree runs a 3-way merge between `ours` and `theirs` with
// `base` as the merge base.
//
// It uses git-merge-tree(1) to do this merge without requiring a work-tree and
// can run in a base repository. It returns the object ID of the merge tree, if
// there are any conflicts and conflicted files.
func MergeTree ( ctx context . Context , gitRepo * git . Repository , base , ours , theirs string , env [ ] string ) ( string , bool , [ ] string , error ) {
cmd := git . NewCommand ( ctx , "merge-tree" , "--write-tree" , "-z" , "--name-only" , "--no-messages" )
if git . CheckGitVersionAtLeast ( "2.40" ) == nil {
cmd . AddOptionFormat ( "--merge-base=%s" , base )
}
stdout := & bytes . Buffer { }
gitErr := cmd . AddDynamicArguments ( ours , theirs ) . Run ( & git . RunOpts { Dir : gitRepo . Path , Stdout : stdout , Env : env } )
if gitErr != nil && ! git . IsErrorExitCode ( gitErr , 1 ) {
log . Error ( "Unable to run merge-tree: %v" , gitErr )
return "" , false , nil , fmt . Errorf ( "unable to run merge-tree: %w" , gitErr )
}
// There are two situations that we consider for the output:
// 1. Clean merge and the output is <OID of toplevel tree>NUL
// 2. Merge conflict and the output is <OID of toplevel tree>NUL<Conflicted file info>NUL
treeOID , conflictedFileInfo , _ := strings . Cut ( stdout . String ( ) , "\x00" )
if len ( conflictedFileInfo ) == 0 {
return treeOID , git . IsErrorExitCode ( gitErr , 1 ) , nil , nil
}
// Remove last NULL-byte from conflicted file info, then split with NULL byte as seperator.
return treeOID , true , strings . Split ( conflictedFileInfo [ : len ( conflictedFileInfo ) - 1 ] , "\x00" ) , nil
}
// checkConflicts takes a pull request and checks if merging it would result in
// merge conflicts and checks if the diff is empty; the status is set accordingly.
func checkConflicts ( ctx context . Context , pr * issues_model . PullRequest , testPatchCtx * testPatchContext ) ( bool , error ) {
// Resets the conflict status.
2022-03-29 17:42:34 +01:00
pr . ConflictedFiles = nil
2025-05-16 12:40:38 +00:00
if git . SupportGitMergeTree {
// Check for conflicts via a merge-tree.
treeHash , conflict , conflictFiles , err := MergeTree ( ctx , testPatchCtx . gitRepo , pr . MergeBase , testPatchCtx . baseRev , testPatchCtx . headRev , testPatchCtx . env )
if err != nil {
return false , fmt . Errorf ( "MergeTree: %w" , err )
}
if ! conflict {
// No conflicts were detected, now check if the pull request actually
// contains anything useful via a diff. git-diff-tree(1) with --quiet
// will return exit code 0 if there's no diff and exit code 1 if there's
// a diff.
err := git . NewCommand ( ctx , "diff-tree" , "--quiet" ) . AddDynamicArguments ( treeHash , pr . MergeBase ) . Run ( & git . RunOpts { Dir : testPatchCtx . gitRepo . Path , Env : testPatchCtx . env } )
isEmpty := true
if err != nil {
if git . IsErrorExitCode ( err , 1 ) {
isEmpty = false
} else {
return false , fmt . Errorf ( "DiffTree: %w" , err )
}
}
if isEmpty {
log . Debug ( "PullRequest[%d]: Patch is empty - ignoring" , pr . ID )
pr . Status = issues_model . PullRequestStatusEmpty
}
return false , nil
}
pr . Status = issues_model . PullRequestStatusConflict
pr . ConflictedFiles = conflictFiles
log . Trace ( "Found %d files conflicted: %v" , len ( pr . ConflictedFiles ) , pr . ConflictedFiles )
return true , nil
}
2022-03-29 17:42:34 +01:00
// 2. AttemptThreeWayMerge first - this is much quicker than plain patch to base
2022-02-09 20:28:55 +00:00
description := fmt . Sprintf ( "PR[%d] %s/%s#%d" , pr . ID , pr . BaseRepo . OwnerName , pr . BaseRepo . Name , pr . Index )
2025-05-16 12:40:38 +00:00
conflict , conflictFiles , err := AttemptThreeWayMerge ( ctx , testPatchCtx . gitRepo , pr . MergeBase , testPatchCtx . baseRev , testPatchCtx . headRev , description )
2022-02-09 20:28:55 +00:00
if err != nil {
return false , err
}
2021-12-19 04:19:25 +00:00
if ! conflict {
2022-12-19 11:37:15 +00:00
// No conflicts detected so we need to check if the patch is empty...
// a. Write the newly merged tree and check the new tree-hash
2022-04-01 10:55:30 +08:00
var treeHash string
2025-05-16 12:40:38 +00:00
treeHash , _ , err = git . NewCommand ( ctx , "write-tree" ) . RunStdString ( & git . RunOpts { Dir : testPatchCtx . gitRepo . Path } )
2021-12-19 04:19:25 +00:00
if err != nil {
2025-05-16 12:40:38 +00:00
lsfiles , _ , _ := git . NewCommand ( ctx , "ls-files" , "-u" ) . RunStdString ( & git . RunOpts { Dir : testPatchCtx . gitRepo . Path } )
2022-07-29 00:19:55 +01:00
return false , fmt . Errorf ( "unable to write unconflicted tree: %w\n`git ls-files -u`:\n%s" , err , lsfiles )
2021-12-19 04:19:25 +00:00
}
treeHash = strings . TrimSpace ( treeHash )
2025-05-16 12:40:38 +00:00
baseTree , err := testPatchCtx . gitRepo . GetTree ( testPatchCtx . baseRev )
2021-12-19 04:19:25 +00:00
if err != nil {
return false , err
}
2022-12-19 11:37:15 +00:00
// b. compare the new tree-hash with the base tree hash
2021-12-19 04:19:25 +00:00
if treeHash == baseTree . ID . String ( ) {
log . Debug ( "PullRequest[%d]: Patch is empty - ignoring" , pr . ID )
2022-06-13 17:37:59 +08:00
pr . Status = issues_model . PullRequestStatusEmpty
2021-12-19 04:19:25 +00:00
}
return false , nil
}
2025-05-01 16:35:23 +00:00
pr . Status = issues_model . PullRequestStatusConflict
pr . ConflictedFiles = conflictFiles
2019-12-13 22:21:06 +00:00
2025-05-01 16:35:23 +00:00
log . Trace ( "Found %d files conflicted: %v" , len ( pr . ConflictedFiles ) , pr . ConflictedFiles )
return true , nil
2020-10-14 02:50:57 +08:00
}
// CheckFileProtection check file Protection
2022-01-19 23:26:57 +00:00
func CheckFileProtection ( repo * git . Repository , oldCommitID , newCommitID string , patterns [ ] glob . Glob , limit int , env [ ] string ) ( [ ] string , error ) {
2020-10-14 02:50:57 +08:00
if len ( patterns ) == 0 {
return nil , nil
}
2022-01-19 23:26:57 +00:00
affectedFiles , err := git . GetAffectedFiles ( repo , oldCommitID , newCommitID , env )
2020-10-14 02:50:57 +08:00
if err != nil {
return nil , err
}
changedProtectedFiles := make ( [ ] string , 0 , limit )
2021-09-11 16:21:17 +02:00
for _ , affectedFile := range affectedFiles {
lpath := strings . ToLower ( affectedFile )
for _ , pat := range patterns {
if pat . Match ( lpath ) {
changedProtectedFiles = append ( changedProtectedFiles , lpath )
break
}
}
if len ( changedProtectedFiles ) >= limit {
break
}
}
if len ( changedProtectedFiles ) > 0 {
err = models . ErrFilePathProtected {
Path : changedProtectedFiles [ 0 ] ,
}
2020-10-14 02:50:57 +08:00
}
return changedProtectedFiles , err
}
2021-09-11 16:21:17 +02:00
// CheckUnprotectedFiles check if the commit only touches unprotected files
2022-01-19 23:26:57 +00:00
func CheckUnprotectedFiles ( repo * git . Repository , oldCommitID , newCommitID string , patterns [ ] glob . Glob , env [ ] string ) ( bool , error ) {
2021-09-11 16:21:17 +02:00
if len ( patterns ) == 0 {
return false , nil
}
2022-01-19 23:26:57 +00:00
affectedFiles , err := git . GetAffectedFiles ( repo , oldCommitID , newCommitID , env )
2021-09-11 16:21:17 +02:00
if err != nil {
return false , err
}
for _ , affectedFile := range affectedFiles {
lpath := strings . ToLower ( affectedFile )
unprotected := false
for _ , pat := range patterns {
if pat . Match ( lpath ) {
unprotected = true
break
}
}
if ! unprotected {
return false , nil
}
}
return true , nil
}
2020-10-14 02:50:57 +08:00
// checkPullFilesProtection check if pr changed protected files and save results
2025-05-16 12:40:38 +00:00
func checkPullFilesProtection ( ctx context . Context , pr * issues_model . PullRequest , testPatchCtx * testPatchContext ) error {
2022-06-13 17:37:59 +08:00
if pr . Status == issues_model . PullRequestStatusEmpty {
2022-03-29 17:42:34 +01:00
pr . ChangedProtectedFiles = nil
return nil
}
2023-01-16 16:00:22 +08:00
pb , err := git_model . GetFirstMatchProtectedBranchRule ( ctx , pr . BaseRepoID , pr . BaseBranch )
if err != nil {
2020-10-14 02:50:57 +08:00
return err
}
2023-01-16 16:00:22 +08:00
if pb == nil {
2020-10-14 02:50:57 +08:00
pr . ChangedProtectedFiles = nil
return nil
}
2025-05-16 12:40:38 +00:00
pr . ChangedProtectedFiles , err = CheckFileProtection ( testPatchCtx . gitRepo , pr . MergeBase , testPatchCtx . headRev , pb . GetProtectedFilePatterns ( ) , 10 , testPatchCtx . env )
2020-10-14 02:50:57 +08:00
if err != nil && ! models . IsErrFilePathProtected ( err ) {
return err
}
2019-12-13 22:21:06 +00:00
return nil
}