1// Copyright 2019 The Gitea Authors. 2// All rights reserved. 3// Use of this source code is governed by a MIT-style 4// license that can be found in the LICENSE file. 5 6package pull 7 8import ( 9 "context" 10 "fmt" 11 "os" 12 "strconv" 13 "strings" 14 15 "code.gitea.io/gitea/models" 16 "code.gitea.io/gitea/models/db" 17 repo_model "code.gitea.io/gitea/models/repo" 18 "code.gitea.io/gitea/models/unit" 19 user_model "code.gitea.io/gitea/models/user" 20 "code.gitea.io/gitea/modules/git" 21 "code.gitea.io/gitea/modules/graceful" 22 "code.gitea.io/gitea/modules/log" 23 "code.gitea.io/gitea/modules/notification" 24 "code.gitea.io/gitea/modules/queue" 25 "code.gitea.io/gitea/modules/timeutil" 26 "code.gitea.io/gitea/modules/util" 27) 28 29// prQueue represents a queue to handle update pull request tests 30var prQueue queue.UniqueQueue 31 32// AddToTaskQueue adds itself to pull request test task queue. 33func AddToTaskQueue(pr *models.PullRequest) { 34 err := prQueue.PushFunc(strconv.FormatInt(pr.ID, 10), func() error { 35 pr.Status = models.PullRequestStatusChecking 36 err := pr.UpdateColsIfNotMerged("status") 37 if err != nil { 38 log.Error("AddToTaskQueue.UpdateCols[%d].(add to queue): %v", pr.ID, err) 39 } else { 40 log.Trace("Adding PR ID: %d to the test pull requests queue", pr.ID) 41 } 42 return err 43 }) 44 if err != nil && err != queue.ErrAlreadyInQueue { 45 log.Error("Error adding prID %d to the test pull requests queue: %v", pr.ID, err) 46 } 47} 48 49// checkAndUpdateStatus checks if pull request is possible to leaving checking status, 50// and set to be either conflict or mergeable. 51func checkAndUpdateStatus(pr *models.PullRequest) { 52 // Status is not changed to conflict means mergeable. 53 if pr.Status == models.PullRequestStatusChecking { 54 pr.Status = models.PullRequestStatusMergeable 55 } 56 57 // Make sure there is no waiting test to process before leaving the checking status. 58 has, err := prQueue.Has(strconv.FormatInt(pr.ID, 10)) 59 if err != nil { 60 log.Error("Unable to check if the queue is waiting to reprocess pr.ID %d. Error: %v", pr.ID, err) 61 } 62 63 if !has { 64 if err := pr.UpdateColsIfNotMerged("merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil { 65 log.Error("Update[%d]: %v", pr.ID, err) 66 } 67 } 68} 69 70// getMergeCommit checks if a pull request got merged 71// Returns the git.Commit of the pull request if merged 72func getMergeCommit(pr *models.PullRequest) (*git.Commit, error) { 73 if pr.BaseRepo == nil { 74 var err error 75 pr.BaseRepo, err = repo_model.GetRepositoryByID(pr.BaseRepoID) 76 if err != nil { 77 return nil, fmt.Errorf("GetRepositoryByID: %v", err) 78 } 79 } 80 81 indexTmpPath, err := os.MkdirTemp(os.TempDir(), "gitea-"+pr.BaseRepo.Name) 82 if err != nil { 83 return nil, fmt.Errorf("Failed to create temp dir for repository %s: %v", pr.BaseRepo.RepoPath(), err) 84 } 85 defer func() { 86 if err := util.RemoveAll(indexTmpPath); err != nil { 87 log.Warn("Unable to remove temporary index path: %s: Error: %v", indexTmpPath, err) 88 } 89 }() 90 91 headFile := pr.GetGitRefName() 92 93 // Check if a pull request is merged into BaseBranch 94 _, err = git.NewCommand("merge-base", "--is-ancestor", headFile, pr.BaseBranch). 95 RunInDirWithEnv(pr.BaseRepo.RepoPath(), []string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()}) 96 if err != nil { 97 // Errors are signaled by a non-zero status that is not 1 98 if strings.Contains(err.Error(), "exit status 1") { 99 return nil, nil 100 } 101 return nil, fmt.Errorf("git merge-base --is-ancestor: %v", err) 102 } 103 104 commitIDBytes, err := os.ReadFile(pr.BaseRepo.RepoPath() + "/" + headFile) 105 if err != nil { 106 return nil, fmt.Errorf("ReadFile(%s): %v", headFile, err) 107 } 108 commitID := string(commitIDBytes) 109 if len(commitID) < 40 { 110 return nil, fmt.Errorf(`ReadFile(%s): invalid commit-ID "%s"`, headFile, commitID) 111 } 112 cmd := commitID[:40] + ".." + pr.BaseBranch 113 114 // Get the commit from BaseBranch where the pull request got merged 115 mergeCommit, err := git.NewCommand("rev-list", "--ancestry-path", "--merges", "--reverse", cmd). 116 RunInDirWithEnv("", []string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()}) 117 if err != nil { 118 return nil, fmt.Errorf("git rev-list --ancestry-path --merges --reverse: %v", err) 119 } else if len(mergeCommit) < 40 { 120 // PR was maybe fast-forwarded, so just use last commit of PR 121 mergeCommit = commitID[:40] 122 } 123 124 gitRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath()) 125 if err != nil { 126 return nil, fmt.Errorf("OpenRepository: %v", err) 127 } 128 defer gitRepo.Close() 129 130 commit, err := gitRepo.GetCommit(mergeCommit[:40]) 131 if err != nil { 132 return nil, fmt.Errorf("GetMergeCommit[%v]: %v", mergeCommit[:40], err) 133 } 134 135 return commit, nil 136} 137 138// manuallyMerged checks if a pull request got manually merged 139// When a pull request got manually merged mark the pull request as merged 140func manuallyMerged(pr *models.PullRequest) bool { 141 if err := pr.LoadBaseRepo(); err != nil { 142 log.Error("PullRequest[%d].LoadBaseRepo: %v", pr.ID, err) 143 return false 144 } 145 146 if unit, err := pr.BaseRepo.GetUnit(unit.TypePullRequests); err == nil { 147 config := unit.PullRequestsConfig() 148 if !config.AutodetectManualMerge { 149 return false 150 } 151 } else { 152 log.Error("PullRequest[%d].BaseRepo.GetUnit(unit.TypePullRequests): %v", pr.ID, err) 153 return false 154 } 155 156 commit, err := getMergeCommit(pr) 157 if err != nil { 158 log.Error("PullRequest[%d].getMergeCommit: %v", pr.ID, err) 159 return false 160 } 161 if commit != nil { 162 pr.MergedCommitID = commit.ID.String() 163 pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix()) 164 pr.Status = models.PullRequestStatusManuallyMerged 165 merger, _ := user_model.GetUserByEmail(commit.Author.Email) 166 167 // When the commit author is unknown set the BaseRepo owner as merger 168 if merger == nil { 169 if pr.BaseRepo.Owner == nil { 170 if err = pr.BaseRepo.GetOwner(db.DefaultContext); err != nil { 171 log.Error("BaseRepo.GetOwner[%d]: %v", pr.ID, err) 172 return false 173 } 174 } 175 merger = pr.BaseRepo.Owner 176 } 177 pr.Merger = merger 178 pr.MergerID = merger.ID 179 180 if merged, err := pr.SetMerged(); err != nil { 181 log.Error("PullRequest[%d].setMerged : %v", pr.ID, err) 182 return false 183 } else if !merged { 184 return false 185 } 186 187 notification.NotifyMergePullRequest(pr, merger) 188 189 log.Info("manuallyMerged[%d]: Marked as manually merged into %s/%s by commit id: %s", pr.ID, pr.BaseRepo.Name, pr.BaseBranch, commit.ID.String()) 190 return true 191 } 192 return false 193} 194 195// InitializePullRequests checks and tests untested patches of pull requests. 196func InitializePullRequests(ctx context.Context) { 197 prs, err := models.GetPullRequestIDsByCheckStatus(models.PullRequestStatusChecking) 198 if err != nil { 199 log.Error("Find Checking PRs: %v", err) 200 return 201 } 202 for _, prID := range prs { 203 select { 204 case <-ctx.Done(): 205 return 206 default: 207 if err := prQueue.PushFunc(strconv.FormatInt(prID, 10), func() error { 208 log.Trace("Adding PR ID: %d to the pull requests patch checking queue", prID) 209 return nil 210 }); err != nil { 211 log.Error("Error adding prID: %s to the pull requests patch checking queue %v", prID, err) 212 } 213 } 214 } 215} 216 217// handle passed PR IDs and test the PRs 218func handle(data ...queue.Data) { 219 for _, datum := range data { 220 id, _ := strconv.ParseInt(datum.(string), 10, 64) 221 222 log.Trace("Testing PR ID %d from the pull requests patch checking queue", id) 223 224 pr, err := models.GetPullRequestByID(id) 225 if err != nil { 226 log.Error("GetPullRequestByID[%s]: %v", datum, err) 227 continue 228 } else if pr.HasMerged { 229 continue 230 } else if manuallyMerged(pr) { 231 continue 232 } else if err = TestPatch(pr); err != nil { 233 log.Error("testPatch[%d]: %v", pr.ID, err) 234 pr.Status = models.PullRequestStatusError 235 if err := pr.UpdateCols("status"); err != nil { 236 log.Error("update pr [%d] status to PullRequestStatusError failed: %v", pr.ID, err) 237 } 238 continue 239 } 240 checkAndUpdateStatus(pr) 241 } 242} 243 244// CheckPrsForBaseBranch check all pulls with bseBrannch 245func CheckPrsForBaseBranch(baseRepo *repo_model.Repository, baseBranchName string) error { 246 prs, err := models.GetUnmergedPullRequestsByBaseInfo(baseRepo.ID, baseBranchName) 247 if err != nil { 248 return err 249 } 250 251 for _, pr := range prs { 252 AddToTaskQueue(pr) 253 } 254 255 return nil 256} 257 258// Init runs the task queue to test all the checking status pull requests 259func Init() error { 260 prQueue = queue.CreateUniqueQueue("pr_patch_checker", handle, "") 261 262 if prQueue == nil { 263 return fmt.Errorf("Unable to create pr_patch_checker Queue") 264 } 265 266 go graceful.GetManager().RunWithShutdownFns(prQueue.Run) 267 go graceful.GetManager().RunWithShutdownContext(InitializePullRequests) 268 return nil 269} 270