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 (
"context"
"fmt"
"io"
"os"
2021-12-19 04:19:25 +00:00
"path/filepath"
2019-12-13 22:21:06 +00:00
"strings"
2023-01-16 16:00:22 +08:00
git_model "code.gitea.io/gitea/models/git"
2022-06-13 17:37:59 +08:00
issues_model "code.gitea.io/gitea/models/issues"
2019-12-13 22:21:06 +00:00
"code.gitea.io/gitea/modules/git"
2025-09-15 23:33:12 -07:00
"code.gitea.io/gitea/modules/git/gitcmd"
2024-01-28 04:09:51 +08:00
"code.gitea.io/gitea/modules/gitrepo"
2025-09-14 02:01:00 +08:00
"code.gitea.io/gitea/modules/glob"
2019-12-13 22:21:06 +00:00
"code.gitea.io/gitea/modules/log"
2021-12-19 04:19:25 +00:00
"code.gitea.io/gitea/modules/process"
2020-08-11 21:05:34 +01:00
"code.gitea.io/gitea/modules/util"
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
}
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 ( )
2025-07-16 21:33:33 +08:00
compareArg := pr . MergeBase + "..." + pr . GetGitHeadRefName ( )
2024-12-23 22:29:34 -08:00
switch {
case patch :
err = gitRepo . GetPatch ( compareArg , w )
case binary :
err = gitRepo . GetDiffBinary ( compareArg , w )
default :
err = gitRepo . GetDiff ( compareArg , w )
}
if err != nil {
log . Error ( "unable to get patch file from %s to %s in %s Error: %v" , pr . MergeBase , pr . HeadBranch , pr . BaseRepo . FullName ( ) , err )
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
}
2026-01-27 11:57:20 -08:00
func checkPullRequestBranchMergeable ( ctx context . Context , pr * issues_model . PullRequest ) error {
ctx , _ , finished := process . GetManager ( ) . AddContext ( ctx , fmt . Sprintf ( "checkPullRequestBranchMergeable: %s" , pr ) )
2022-01-19 23:26:57 +00:00
defer finished ( )
2026-01-27 11:57:20 -08:00
if git . DefaultFeatures ( ) . SupportGitMergeTree {
return checkPullRequestMergeableByMergeTree ( ctx , pr )
}
return checkPullRequestMergeableByTmpRepo ( ctx , pr )
}
func checkPullRequestMergeableByTmpRepo ( ctx context . Context , pr * issues_model . PullRequest ) error {
2023-03-07 20:07:35 +00:00
prCtx , cancel , err := createTemporaryRepoForPR ( ctx , pr )
2019-12-13 22:21:06 +00:00
if err != nil {
2023-08-10 10:39:21 +08:00
if ! git_model . IsErrBranchNotExist ( err ) {
log . Error ( "CreateTemporaryRepoForPR %-v: %v" , pr , err )
}
2019-12-13 22:21:06 +00:00
return err
}
2023-03-07 20:07:35 +00:00
defer cancel ( )
2019-12-13 22:21:06 +00:00
2023-03-07 20:07:35 +00:00
gitRepo , err := git . OpenRepository ( ctx , prCtx . tmpBasePath )
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
}
defer gitRepo . Close ( )
2020-10-14 02:50:57 +08:00
// 1. update merge base
2026-01-27 11:57:20 -08:00
pr . MergeBase , _ , err = gitcmd . NewCommand ( "merge-base" , "--" , tmpRepoBaseBranch , tmpRepoTrackingBranch ) . WithDir ( prCtx . tmpBasePath ) . RunStdString ( ctx )
2019-12-13 22:21:06 +00:00
if err != nil {
var err2 error
2026-01-27 11:57:20 -08:00
pr . MergeBase , err2 = gitRepo . GetRefCommitID ( git . BranchPrefix + tmpRepoBaseBranch )
2019-12-13 22:21:06 +00:00
if err2 != nil {
2022-10-24 21:29:17 +02:00
return 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 )
2026-01-27 11:57:20 -08:00
if pr . HeadCommitID , err = gitRepo . GetRefCommitID ( git . BranchPrefix + tmpRepoTrackingBranch ) ; err != nil {
2022-07-13 10:22:51 +02:00
return fmt . Errorf ( "GetBranchCommitID: can't find commit ID for head: %w" , err )
}
if pr . HeadCommitID == pr . MergeBase {
pr . Status = issues_model . PullRequestStatusAncestor
return nil
}
2020-10-14 02:50:57 +08:00
// 2. Check for conflicts
2026-01-27 11:57:20 -08:00
conflicts , err := checkConflictsByTmpRepo ( ctx , pr , gitRepo , prCtx . tmpBasePath )
if err != nil {
2020-10-14 02:50:57 +08:00
return err
}
2026-01-27 11:57:20 -08:00
pr . ChangedProtectedFiles = nil
if conflicts || pr . Status == issues_model . PullRequestStatusEmpty {
return nil
2020-10-14 02:50:57 +08:00
}
2026-01-27 11:57:20 -08:00
// 3. Check for protected files changes
if err = checkPullFilesProtection ( ctx , pr , gitRepo , tmpRepoTrackingBranch ) ; err != nil {
return fmt . Errorf ( "pr.CheckPullFilesProtection(): %w" , err )
2020-10-14 02:50:57 +08:00
}
2022-06-13 17:37:59 +08:00
pr . Status = issues_model . PullRequestStatusMergeable
2020-10-14 02:50:57 +08:00
return nil
}
2021-12-19 04:19:25 +00:00
type errMergeConflict struct {
filename string
}
func ( e * errMergeConflict ) Error ( ) string {
2025-04-01 12:14:01 +02:00
return "conflict detected at: " + e . filename
2021-12-19 04:19:25 +00:00
}
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
2025-10-07 02:06:51 -07:00
root , _ , err := gitcmd . NewCommand ( "unpack-file" ) . AddDynamicArguments ( file . stage1 . sha ) . WithDir ( tmpBasePath ) . RunStdString ( ctx )
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 ) )
} ( )
2025-10-07 02:06:51 -07:00
base , _ , err := gitcmd . NewCommand ( "unpack-file" ) . AddDynamicArguments ( file . stage2 . sha ) . WithDir ( tmpBasePath ) . RunStdString ( ctx )
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 )
} ( )
2025-10-07 02:06:51 -07:00
head , _ , err := gitcmd . NewCommand ( "unpack-file" ) . AddDynamicArguments ( file . stage3 . sha ) . WithDir ( tmpBasePath ) . RunStdString ( ctx )
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 ...
2025-10-07 02:06:51 -07:00
_ , _ , conflictErr := gitcmd . NewCommand ( "merge-file" ) . AddDynamicArguments ( base , root , head ) . WithDir ( tmpBasePath ) . RunStdString ( ctx )
2021-12-19 04:19:25 +00:00
if conflictErr != nil {
return & errMergeConflict { file . stage2 . path }
}
// base now contains the merged data
2025-10-07 02:06:51 -07:00
hash , _ , err := gitcmd . NewCommand ( "hash-object" , "-w" , "--path" ) . AddDynamicArguments ( file . stage2 . path , base ) . WithDir ( tmpBasePath ) . RunStdString ( ctx )
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
func AttemptThreeWayMerge ( ctx context . Context , gitPath string , gitRepo * git . Repository , base , ours , theirs , description string ) ( bool , [ ] string , error ) {
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
2026-01-19 07:10:33 +08:00
if err := gitcmd . NewCommand ( "read-tree" , "-m" ) . AddDynamicArguments ( base , ours , theirs ) . WithDir ( gitPath ) . RunWithStderr ( ctx ) ; 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 )
2022-02-09 20:28:55 +00:00
go unmergedFiles ( ctx , gitPath , 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
2024-07-04 20:57:11 +02:00
if err := attemptMerge ( ctx , file , gitPath , & 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
}
2026-01-27 11:57:20 -08:00
func checkConflictsByTmpRepo ( ctx context . Context , pr * issues_model . PullRequest , gitRepo * git . Repository , tmpBasePath string ) ( bool , error ) {
// 1. checkConflictsByTmpRepo resets the conflict status - therefore - reset the conflict status
2022-03-29 17:42:34 +01:00
pr . ConflictedFiles = nil
// 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 )
2022-12-19 11:37:15 +00:00
conflict , conflictFiles , err := AttemptThreeWayMerge ( ctx ,
2026-01-27 11:57:20 -08:00
tmpBasePath , gitRepo , pr . MergeBase , tmpRepoBaseBranch , tmpRepoTrackingBranch , 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-10-07 02:06:51 -07:00
treeHash , _ , err = gitcmd . NewCommand ( "write-tree" ) . WithDir ( tmpBasePath ) . RunStdString ( ctx )
2021-12-19 04:19:25 +00:00
if err != nil {
2025-10-07 02:06:51 -07:00
lsfiles , _ , _ := gitcmd . NewCommand ( "ls-files" , "-u" ) . WithDir ( tmpBasePath ) . RunStdString ( ctx )
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 )
2026-01-27 11:57:20 -08:00
baseTree , err := gitRepo . GetTree ( tmpRepoBaseBranch )
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
}
2022-12-19 11:37:15 +00:00
// 3. OK the three-way merge method has detected conflicts
2026-03-24 02:23:42 +08:00
pr . Status = issues_model . PullRequestStatusConflict
pr . ConflictedFiles = conflictFiles
log . Trace ( "Found %d files conflicted: %v" , len ( pr . ConflictedFiles ) , pr . ConflictedFiles )
return true , nil
2020-10-14 02:50:57 +08:00
}
2024-12-20 10:05:29 -08:00
// ErrFilePathProtected represents a "FilePathProtected" kind of error.
type ErrFilePathProtected struct {
Message string
Path string
}
// IsErrFilePathProtected checks if an error is an ErrFilePathProtected.
func IsErrFilePathProtected ( err error ) bool {
_ , ok := err . ( ErrFilePathProtected )
return ok
}
func ( err ErrFilePathProtected ) Error ( ) string {
if err . Message != "" {
return err . Message
}
return fmt . Sprintf ( "path is protected and can not be changed [path: %s]" , err . Path )
}
func ( err ErrFilePathProtected ) Unwrap ( ) error {
return util . ErrPermissionDenied
}
2020-10-14 02:50:57 +08:00
// CheckFileProtection check file Protection
2024-08-06 21:32:49 +08:00
func CheckFileProtection ( repo * git . Repository , branchName , 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
}
2024-08-06 21:32:49 +08:00
affectedFiles , err := git . GetAffectedFiles ( repo , branchName , 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 {
2024-12-20 10:05:29 -08:00
err = ErrFilePathProtected {
2021-09-11 16:21:17 +02:00
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
2024-08-06 21:32:49 +08:00
func CheckUnprotectedFiles ( repo * git . Repository , branchName , 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
}
2024-08-06 21:32:49 +08:00
affectedFiles , err := git . GetAffectedFiles ( repo , branchName , 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
2026-01-27 11:57:20 -08:00
func checkPullFilesProtection ( ctx context . Context , pr * issues_model . PullRequest , gitRepo * git . Repository , headRef string ) 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
}
2026-01-27 11:57:20 -08:00
pr . ChangedProtectedFiles , err = CheckFileProtection ( gitRepo , pr . HeadBranch , pr . MergeBase , headRef , pb . GetProtectedFilePatterns ( ) , 10 , os . Environ ( ) )
2024-12-20 10:05:29 -08:00
if err != nil && ! IsErrFilePathProtected ( err ) {
2020-10-14 02:50:57 +08:00
return err
}
2026-01-27 11:57:20 -08:00
if len ( pr . ChangedProtectedFiles ) > 0 {
log . Trace ( "Found %d protected files changed in PR %s#%d" , len ( pr . ChangedProtectedFiles ) , pr . BaseRepo . FullName ( ) , pr . Index )
}
2019-12-13 22:21:06 +00:00
return nil
}