Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions modules/git/commit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package git

import (
"os"
"path/filepath"
"strings"
"testing"
Expand Down Expand Up @@ -339,18 +338,3 @@ func TestGetCommitFileStatusMerges(t *testing.T) {
assert.Equal(t, expected.Removed, commitFileStatus.Removed)
assert.Equal(t, expected.Modified, commitFileStatus.Modified)
}

func Test_GetCommitBranchStart(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
repo, err := OpenRepository(t.Context(), bareRepo1Path)
assert.NoError(t, err)
defer repo.Close()
commit, err := repo.GetBranchCommit("branch1")
assert.NoError(t, err)
assert.Equal(t, "2839944139e0de9737a044f78b0e4b40d989a9e3", commit.ID.String())

startCommitID, err := repo.GetCommitBranchStart(os.Environ(), "branch1", commit.ID.String())
assert.NoError(t, err)
assert.NotEmpty(t, startCommitID)
assert.Equal(t, "95bb4d39648ee7e325106df01a621c530863a653", startCommitID)
}
27 changes: 13 additions & 14 deletions modules/git/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -288,20 +289,18 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
}

// GetAffectedFiles returns the affected files between two commits
func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID string, env []string) ([]string, error) {
if oldCommitID == emptySha1ObjectID.String() || oldCommitID == emptySha256ObjectID.String() {
startCommitID, err := repo.GetCommitBranchStart(env, branchName, newCommitID)
if err != nil {
return nil, err
}
if startCommitID == "" {
return nil, fmt.Errorf("cannot find the start commit of %s", newCommitID)
}
oldCommitID = startCommitID
func GetAffectedFiles(ctx context.Context, repoPath, oldCommitID, newCommitID string, env []string) ([]string, error) {
if oldCommitID == emptySha1ObjectID.String() {
oldCommitID = emptySha1ObjectID.Type().EmptyTree().String()
} else if oldCommitID == emptySha256ObjectID.String() {
oldCommitID = emptySha256ObjectID.Type().EmptyTree().String()
} else if oldCommitID == "" {
return nil, errors.New("oldCommitID is empty")
}

stdoutReader, stdoutWriter, err := os.Pipe()
if err != nil {
log.Error("Unable to create os.Pipe for %s", repo.Path)
log.Error("Unable to create os.Pipe for %s", repoPath)
return nil, err
}
defer func() {
Expand All @@ -314,7 +313,7 @@ func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID str
// Run `git diff --name-only` to get the names of the changed files
err = gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID).
WithEnv(env).
WithDir(repo.Path).
WithDir(repoPath).
WithStdout(stdoutWriter).
WithPipelineFunc(func(ctx context.Context, cancel context.CancelFunc) error {
// Close the writer end of the pipe to begin processing
Expand All @@ -334,9 +333,9 @@ func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID str
}
return scanner.Err()
}).
Run(repo.Ctx)
Run(ctx)
if err != nil {
log.Error("Unable to get affected files for commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err)
log.Error("Unable to get affected files for commits from %s to %s in %s: %v", oldCommitID, newCommitID, repoPath, err)
}

return affectedFiles, err
Expand Down
2 changes: 2 additions & 0 deletions modules/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type Features struct {
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
SupportedObjectFormats []ObjectFormat // sha1, sha256
SupportCheckAttrOnBare bool // >= 2.40
SupportGitMergeTree bool // >= 2.38
}

var defaultFeatures *Features
Expand Down Expand Up @@ -75,6 +76,7 @@ func loadGitVersionFeatures() (*Features, error) {
features.SupportedObjectFormats = append(features.SupportedObjectFormats, Sha256ObjectFormat)
}
features.SupportCheckAttrOnBare = features.CheckVersionAtLeast("2.40")
features.SupportGitMergeTree = features.CheckVersionAtLeast("2.38")
return features, nil
}

Expand Down
11 changes: 11 additions & 0 deletions modules/git/gitcmd/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,17 @@ func IsErrorExitCode(err error, code int) bool {
return false
}

func ExitCode(err error) (int, bool) {
if err == nil {
return 0, true
}
var exitError *exec.ExitError
if errors.As(err, &exitError) {
return exitError.ExitCode(), true
}
return 0, false
}

// RunStdString runs the command and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr).
func (c *Command) RunStdString(ctx context.Context) (stdout, stderr string, runErr RunStdError) {
stdoutBytes, stderrBytes, runErr := c.WithParentCallerInfo().runStdBytes(ctx)
Expand Down
31 changes: 0 additions & 31 deletions modules/git/repo_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -580,34 +580,3 @@ func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error
}
return nil
}

// GetCommitBranchStart returns the commit where the branch diverged
func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID string) (string, error) {
cmd := gitcmd.NewCommand("log", prettyLogFormat)
cmd.AddDynamicArguments(endCommitID)

stdout, _, runErr := cmd.WithDir(repo.Path).
WithEnv(env).
RunStdBytes(repo.Ctx)
if runErr != nil {
return "", runErr
}

parts := bytes.SplitSeq(bytes.TrimSpace(stdout), []byte{'\n'})

// check the commits one by one until we find a commit contained by another branch
// and we think this commit is the divergence point
for commitID := range parts {
branches, err := repo.getBranches(env, string(commitID), 2)
if err != nil {
return "", err
}
for _, b := range branches {
if b != branch {
return string(commitID), nil
}
}
}

return "", nil
}
17 changes: 17 additions & 0 deletions modules/gitrepo/fetch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package gitrepo

import (
"context"

"code.gitea.io/gitea/modules/git/gitcmd"
)

func FetchRemoteCommit(ctx context.Context, repo, remoteRepo Repository, commitID string) error {
_, err := RunCmdString(ctx, repo, gitcmd.NewCommand("fetch", "--no-tags").
AddDynamicArguments(repoPath(remoteRepo)).
AddDynamicArguments(commitID))
return err
}
74 changes: 74 additions & 0 deletions modules/gitrepo/merge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package gitrepo

import (
"bytes"
"context"
"errors"
"fmt"
"strings"

"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
)

func MergeBase(ctx context.Context, repo Repository, commit1, commit2 string) (string, error) {
mergeBase, err := RunCmdString(ctx, repo, gitcmd.NewCommand("merge-base", "--").
AddDynamicArguments(commit1, commit2))
if err != nil {
return "", fmt.Errorf("get merge-base of %s and %s failed: %w", commit1, commit2, err)
}
return strings.TrimSpace(mergeBase), nil
}

// parseMergeTreeOutput parses the output of git merge-tree --write-tree -z --name-only --no-messages
// For a successful merge, the output is a simply one line <OID of toplevel tree>NUL
// Whereas for a conflicted merge, the output is:
// <OID of toplevel tree>NUL
// <Conflicted file name 1>NUL
// <Conflicted file name 2>NUL
// ...
// ref: https://git-scm.com/docs/git-merge-tree/2.38.0#OUTPUT
func parseMergeTreeOutput(output string) (string, []string, error) {
fields := strings.Split(strings.TrimSuffix(output, "\x00"), "\x00")
if len(fields) == 0 {
return "", nil, errors.New("unexpected empty output")
}
if len(fields) == 1 {
return strings.TrimSpace(fields[0]), nil, nil
}
return strings.TrimSpace(fields[0]), fields[1:], nil
}

// MergeTree performs a merge between two commits (baseRef and headRef) with an optional merge base.
// It returns the resulting tree hash, a list of conflicted files (if any), and an error if the operation fails.
// If there are no conflicts, the list of conflicted files will be nil.
func MergeTree(ctx context.Context, repo Repository, baseRef, headRef, mergeBase string) (string, bool, []string, error) {
cmd := gitcmd.NewCommand("merge-tree", "--write-tree", "-z", "--name-only", "--no-messages")
if git.DefaultFeatures().CheckVersionAtLeast("2.40") && mergeBase != "" {
cmd.AddOptionFormat("--merge-base=%s", mergeBase)
}

stdout := &bytes.Buffer{}
gitErr := RunCmd(ctx, repo, cmd.AddDynamicArguments(baseRef, headRef).WithStdout(stdout))
exitCode, ok := gitcmd.ExitCode(gitErr)
if !ok {
return "", false, nil, fmt.Errorf("run merge-tree failed: %w", gitErr)
}

switch exitCode {
case 0, 1:
treeID, conflictedFiles, err := parseMergeTreeOutput(stdout.String())
if err != nil {
return "", false, nil, fmt.Errorf("parse merge-tree output failed: %w", err)
}
// For a successful, non-conflicted merge, the exit status is 0. When the merge has conflicts, the exit status is 1.
// A merge can have conflicts without having individual files conflict
// https://git-scm.com/docs/git-merge-tree/2.38.0#_mistakes_to_avoid
return treeID, exitCode == 1, conflictedFiles, nil
default:
return "", false, nil, fmt.Errorf("run merge-tree exit abnormally: %w", gitErr)
}
}
26 changes: 26 additions & 0 deletions modules/gitrepo/merge_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package gitrepo

import (
"testing"

"github.com/stretchr/testify/assert"
)

func Test_parseMergeTreeOutput(t *testing.T) {
conflictedOutput := "837480c2773160381cbe6bcce90f7732789b5856\x00options/locale/locale_en-US.ini\x00services/webhook/webhook_test.go\x00"
treeID, conflictedFiles, err := parseMergeTreeOutput(conflictedOutput)
assert.NoError(t, err)
assert.Equal(t, "837480c2773160381cbe6bcce90f7732789b5856", treeID)
assert.Len(t, conflictedFiles, 2)
assert.Equal(t, "options/locale/locale_en-US.ini", conflictedFiles[0])
assert.Equal(t, "services/webhook/webhook_test.go", conflictedFiles[1])

nonConflictedOutput := "837480c2773160381cbe6bcce90f7732789b5856\x00"
treeID, conflictedFiles, err = parseMergeTreeOutput(nonConflictedOutput)
assert.NoError(t, err)
assert.Equal(t, "837480c2773160381cbe6bcce90f7732789b5856", treeID)
assert.Empty(t, conflictedFiles)
}
4 changes: 2 additions & 2 deletions routers/private/hook_pre_receive.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r

globs := protectBranch.GetProtectedFilePatterns()
if len(globs) > 0 {
_, err := pull_service.CheckFileProtection(gitRepo, branchName, oldCommitID, newCommitID, globs, 1, ctx.env)
_, err := pull_service.CheckFileProtection(ctx, repo.RepoPath(), oldCommitID, newCommitID, globs, 1, ctx.env)
if err != nil {
if !pull_service.IsErrFilePathProtected(err) {
log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
Expand Down Expand Up @@ -300,7 +300,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
// Allow commits that only touch unprotected files
globs := protectBranch.GetUnprotectedFilePatterns()
if len(globs) > 0 {
unprotectedFilesOnly, err := pull_service.CheckUnprotectedFiles(gitRepo, branchName, oldCommitID, newCommitID, globs, ctx.env)
unprotectedFilesOnly, err := pull_service.CheckUnprotectedFiles(ctx, repo, oldCommitID, newCommitID, globs, ctx.env)
if err != nil {
log.Error("Unable to check file protection for commits from %s to %s in %-v: %v", oldCommitID, newCommitID, repo, err)
ctx.JSON(http.StatusInternalServerError, private.Response{
Expand Down
2 changes: 1 addition & 1 deletion services/pull/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ func checkPullRequestMergeable(id int64) {
return
}

if err := testPullRequestBranchMergeable(pr); err != nil {
if err := checkPullRequestMergeableAndUpdateStatus(ctx, pr); err != nil {
log.Error("testPullRequestTmpRepoBranchMergeable[%-v]: %v", pr, err)
pr.Status = issues_model.PullRequestStatusError
if err := pr.UpdateCols(ctx, "status"); err != nil {
Expand Down
Loading
Loading