Skip to content

Commit dda296a

Browse files
committed
Fix lint
1 parent 1f32170 commit dda296a

File tree

13 files changed

+828
-238
lines changed

13 files changed

+828
-238
lines changed

modules/git/diff.go

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"bufio"
88
"bytes"
99
"context"
10+
"errors"
1011
"fmt"
1112
"io"
1213
"os"
@@ -289,20 +290,18 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
289290
}
290291

291292
// GetAffectedFiles returns the affected files between two commits
292-
func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID string, env []string) ([]string, error) {
293-
if oldCommitID == emptySha1ObjectID.String() || oldCommitID == emptySha256ObjectID.String() {
294-
startCommitID, err := repo.GetCommitBranchStart(env, branchName, newCommitID)
295-
if err != nil {
296-
return nil, err
297-
}
298-
if startCommitID == "" {
299-
return nil, fmt.Errorf("cannot find the start commit of %s", newCommitID)
300-
}
301-
oldCommitID = startCommitID
293+
func GetAffectedFiles(ctx context.Context, repoPath, oldCommitID, newCommitID string, env []string) ([]string, error) {
294+
if oldCommitID == emptySha1ObjectID.String() {
295+
oldCommitID = emptySha1ObjectID.Type().EmptyTree().String()
296+
} else if oldCommitID == emptySha256ObjectID.String() {
297+
oldCommitID = emptySha256ObjectID.Type().EmptyTree().String()
298+
} else if oldCommitID == "" {
299+
return nil, errors.New("oldCommitID is empty")
302300
}
301+
303302
stdoutReader, stdoutWriter, err := os.Pipe()
304303
if err != nil {
305-
log.Error("Unable to create os.Pipe for %s", repo.Path)
304+
log.Error("Unable to create os.Pipe for %s", repoPath)
306305
return nil, err
307306
}
308307
defer func() {
@@ -314,9 +313,9 @@ func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID str
314313

315314
// Run `git diff --name-only` to get the names of the changed files
316315
err = gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID).
317-
Run(repo.Ctx, &gitcmd.RunOpts{
316+
Run(ctx, &gitcmd.RunOpts{
318317
Env: env,
319-
Dir: repo.Path,
318+
Dir: repoPath,
320319
Stdout: stdoutWriter,
321320
PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
322321
// Close the writer end of the pipe to begin processing
@@ -338,7 +337,7 @@ func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID str
338337
},
339338
})
340339
if err != nil {
341-
log.Error("Unable to get affected files for commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err)
340+
log.Error("Unable to get affected files for commits from %s to %s in %s: %v", oldCommitID, newCommitID, repoPath, err)
342341
}
343342

344343
return affectedFiles, err

modules/git/git.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type Features struct {
3131
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
3232
SupportedObjectFormats []ObjectFormat // sha1, sha256
3333
SupportCheckAttrOnBare bool // >= 2.40
34+
SupportGitMergeTree bool // >= 2.38
3435
}
3536

3637
var defaultFeatures *Features
@@ -75,6 +76,7 @@ func loadGitVersionFeatures() (*Features, error) {
7576
features.SupportedObjectFormats = append(features.SupportedObjectFormats, Sha256ObjectFormat)
7677
}
7778
features.SupportCheckAttrOnBare = features.CheckVersionAtLeast("2.40")
79+
features.SupportGitMergeTree = features.CheckVersionAtLeast("2.38")
7880
return features, nil
7981
}
8082

modules/gitrepo/fetch.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package gitrepo
5+
6+
import (
7+
"context"
8+
9+
"code.gitea.io/gitea/modules/git/gitcmd"
10+
)
11+
12+
func FetchRemoteCommit(ctx context.Context, repo, remoteRepo Repository, commitID string) error {
13+
_, _, err := gitcmd.NewCommand("fetch", "--no-tags").
14+
AddDynamicArguments(repoPath(remoteRepo)).
15+
AddDynamicArguments(commitID).
16+
RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)})
17+
return err
18+
}

modules/gitrepo/merge.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package gitrepo
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"fmt"
10+
"strings"
11+
12+
"code.gitea.io/gitea/modules/git"
13+
"code.gitea.io/gitea/modules/git/gitcmd"
14+
"code.gitea.io/gitea/modules/log"
15+
)
16+
17+
func MergeBase(ctx context.Context, repo Repository, commit1, commit2 string) (string, error) {
18+
mergeBase, _, err := gitcmd.NewCommand("merge-base", "--").
19+
AddDynamicArguments(commit1, commit2).
20+
RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)})
21+
if err != nil {
22+
return "", fmt.Errorf("unable to get merge-base of %s and %s: %w", commit1, commit2, err)
23+
}
24+
return strings.TrimSpace(mergeBase), nil
25+
}
26+
27+
func MergeTree(ctx context.Context, repo Repository, base, ours, theirs string) (string, bool, []string, error) {
28+
cmd := gitcmd.NewCommand("merge-tree", "--write-tree", "-z", "--name-only", "--no-messages")
29+
// https://git-scm.com/docs/git-merge-tree/2.40.0#_mistakes_to_avoid
30+
if git.DefaultFeatures().CheckVersionAtLeast("2.40") && !git.DefaultFeatures().CheckVersionAtLeast("2.41") {
31+
cmd.AddOptionFormat("--merge-base=%s", base)
32+
}
33+
34+
stdout := &bytes.Buffer{}
35+
gitErr := cmd.AddDynamicArguments(ours, theirs).Run(ctx, &gitcmd.RunOpts{
36+
Dir: repoPath(repo),
37+
Stdout: stdout,
38+
})
39+
if gitErr != nil && !gitcmd.IsErrorExitCode(gitErr, 1) {
40+
log.Error("Unable to run merge-tree: %v", gitErr)
41+
return "", false, nil, fmt.Errorf("unable to run merge-tree: %w", gitErr)
42+
}
43+
44+
// There are two situations that we consider for the output:
45+
// 1. Clean merge and the output is <OID of toplevel tree>NUL
46+
// 2. Merge conflict and the output is <OID of toplevel tree>NUL<Conflicted file info>NUL
47+
treeOID, conflictedFileInfo, _ := strings.Cut(stdout.String(), "\x00")
48+
if len(conflictedFileInfo) == 0 {
49+
return treeOID, gitcmd.IsErrorExitCode(gitErr, 1), nil, nil
50+
}
51+
52+
// Remove last NULL-byte from conflicted file info, then split with NULL byte as seperator.
53+
return treeOID, true, strings.Split(conflictedFileInfo[:len(conflictedFileInfo)-1], "\x00"), nil
54+
}

routers/private/hook_pre_receive.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
237237

238238
globs := protectBranch.GetProtectedFilePatterns()
239239
if len(globs) > 0 {
240-
_, err := pull_service.CheckFileProtection(gitRepo, branchName, oldCommitID, newCommitID, globs, 1, ctx.env)
240+
_, err := pull_service.CheckFileProtection(ctx, repo.RepoPath(), oldCommitID, newCommitID, globs, 1, ctx.env)
241241
if err != nil {
242242
if !pull_service.IsErrFilePathProtected(err) {
243243
log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
@@ -295,7 +295,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
295295
// Allow commits that only touch unprotected files
296296
globs := protectBranch.GetUnprotectedFilePatterns()
297297
if len(globs) > 0 {
298-
unprotectedFilesOnly, err := pull_service.CheckUnprotectedFiles(gitRepo, branchName, oldCommitID, newCommitID, globs, ctx.env)
298+
unprotectedFilesOnly, err := pull_service.CheckUnprotectedFiles(ctx, repo, oldCommitID, newCommitID, globs, ctx.env)
299299
if err != nil {
300300
log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
301301
ctx.JSON(http.StatusInternalServerError, private.Response{

services/pull/check.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ func checkPullRequestMergeable(id int64) {
437437
return
438438
}
439439

440-
if err := testPullRequestBranchMergeable(pr); err != nil {
440+
if err := checkPullRequestMergeableAndUpdateStatus(ctx, pr); err != nil {
441441
log.Error("testPullRequestTmpRepoBranchMergeable[%-v]: %v", pr, err)
442442
pr.Status = issues_model.PullRequestStatusError
443443
if err := pr.UpdateCols(ctx, "status"); err != nil {

services/pull/conflicts.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package pull
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
11+
issues_model "code.gitea.io/gitea/models/issues"
12+
"code.gitea.io/gitea/modules/git"
13+
"code.gitea.io/gitea/modules/git/gitcmd"
14+
"code.gitea.io/gitea/modules/gitrepo"
15+
"code.gitea.io/gitea/modules/log"
16+
)
17+
18+
// checkPullRequestMergeableAndUpdateStatus checks whether a pull request is mergeable and updates its status accordingly.
19+
// It uses 'git merge-tree' if supported by the Git version, otherwise it falls back to using a temporary repository.
20+
// This function updates the pr.Status, pr.MergeBase and pr.ConflictedFiles fields as necessary.
21+
func checkPullRequestMergeableAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) error {
22+
if git.DefaultFeatures().SupportGitMergeTree {
23+
return checkPullRequestMergeableAndUpdateStatusMergeTree(ctx, pr)
24+
}
25+
26+
return checkPullRequestMergeableAndUpdateStatusTmpRepo(ctx, pr)
27+
}
28+
29+
// checkConflictsMergeTree uses git merge-tree to check for conflicts and if none are found checks if the patch is empty
30+
// return true if there is conflicts otherwise return false
31+
// pr.Status and pr.ConflictedFiles will be updated as necessary
32+
func checkConflictsMergeTree(ctx context.Context, repoPath string, pr *issues_model.PullRequest, baseCommitID string) (bool, error) {
33+
treeHash, conflict, conflictFiles, err := gitrepo.MergeTree(ctx, pr.BaseRepo, pr.MergeBase, baseCommitID, pr.HeadCommitID)
34+
if err != nil {
35+
return false, fmt.Errorf("MergeTree: %w", err)
36+
}
37+
if conflict {
38+
pr.Status = issues_model.PullRequestStatusConflict
39+
pr.ConflictedFiles = conflictFiles
40+
41+
log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
42+
return true, nil
43+
}
44+
45+
// No conflicts were detected, now check if the pull request actually
46+
// contains anything useful via a diff. git-diff-tree(1) with --quiet
47+
// will return exit code 0 if there's no diff and exit code 1 if there's
48+
// a diff.
49+
isEmpty := true
50+
if err = gitcmd.NewCommand("diff-tree", "--quiet").AddDynamicArguments(treeHash, pr.MergeBase).
51+
Run(ctx, &gitcmd.RunOpts{
52+
Dir: repoPath,
53+
}); err != nil {
54+
if !gitcmd.IsErrorExitCode(err, 1) {
55+
return false, fmt.Errorf("DiffTree: %w", err)
56+
}
57+
isEmpty = false
58+
}
59+
60+
if isEmpty {
61+
log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
62+
pr.Status = issues_model.PullRequestStatusEmpty
63+
}
64+
return false, nil
65+
}
66+
67+
func checkPullRequestMergeableAndUpdateStatusMergeTree(ctx context.Context, pr *issues_model.PullRequest) error {
68+
// 1. Get head commit
69+
if err := pr.LoadHeadRepo(ctx); err != nil {
70+
return err
71+
}
72+
headGitRepo, err := gitrepo.OpenRepository(ctx, pr.HeadRepo)
73+
if err != nil {
74+
return fmt.Errorf("OpenRepository: %w", err)
75+
}
76+
defer headGitRepo.Close()
77+
78+
if pr.Flow == issues_model.PullRequestFlowGithub {
79+
pr.HeadCommitID, err = headGitRepo.GetRefCommitID(git.BranchPrefix + pr.HeadBranch)
80+
if err != nil {
81+
return fmt.Errorf("GetBranchCommitID: can't find commit ID for head: %w", err)
82+
}
83+
} else if pr.HeadCommitID == "" {
84+
return errors.New("head commit ID is empty for pull request Agit flow")
85+
}
86+
87+
// 2. Get base commit id
88+
var baseGitRepo *git.Repository
89+
if pr.IsSameRepo() {
90+
baseGitRepo = headGitRepo
91+
} else {
92+
baseGitRepo, err = gitrepo.OpenRepository(ctx, pr.BaseRepo)
93+
if err != nil {
94+
return fmt.Errorf("OpenRepository: %w", err)
95+
}
96+
defer baseGitRepo.Close()
97+
// 2.1. fetch head commit id into the current repository
98+
// it will be checked in 2 weeks by default from git if the pull request created failure.
99+
if err := gitrepo.FetchRemoteCommit(ctx, pr.BaseRepo, pr.HeadRepo, pr.HeadCommitID); err != nil {
100+
return fmt.Errorf("FetchRemoteCommit: %w", err)
101+
}
102+
}
103+
baseCommitID, err := baseGitRepo.GetRefCommitID(git.BranchPrefix + pr.BaseBranch)
104+
if err != nil {
105+
return fmt.Errorf("GetBranchCommitID: can't find commit ID for base: %w", err)
106+
}
107+
108+
// 3. update merge base
109+
pr.MergeBase, err = gitrepo.MergeBase(ctx, pr.BaseRepo, baseCommitID, pr.HeadCommitID)
110+
if err != nil {
111+
log.Error("GetMergeBase: %v and can't find commit ID for base: %v", err, baseCommitID)
112+
// FIXME: is this the right thing to do?
113+
pr.MergeBase = baseCommitID
114+
}
115+
116+
// 4. if base == head, then it's an ancestor
117+
if pr.HeadCommitID == pr.MergeBase {
118+
pr.Status = issues_model.PullRequestStatusAncestor
119+
return nil
120+
}
121+
122+
// 5. Check for conflicts
123+
conflicted, err := checkConflictsMergeTree(ctx, pr.BaseRepo.RepoPath(), pr, baseCommitID)
124+
if err != nil {
125+
return fmt.Errorf("checkConflictsMergeTree: %w", err)
126+
}
127+
if conflicted || pr.Status == issues_model.PullRequestStatusEmpty {
128+
return nil
129+
}
130+
131+
// 6. Check for protected files changes
132+
if err = checkPullFilesProtection(ctx, pr, pr.BaseRepo.RepoPath()); err != nil {
133+
return fmt.Errorf("pr.CheckPullFilesProtection(): %v", err)
134+
}
135+
if len(pr.ChangedProtectedFiles) > 0 {
136+
log.Trace("Found %d protected files changed", len(pr.ChangedProtectedFiles))
137+
}
138+
139+
pr.Status = issues_model.PullRequestStatusMergeable
140+
return nil
141+
}

0 commit comments

Comments
 (0)