Compare commits

...

40 Commits

Author SHA1 Message Date
Lunny Xiao
fb9782590b
Merge 0bf39c72e7 into bc50431e8b 2025-10-27 00:06:57 +08:00
Lunny Xiao
bc50431e8b
Upgrade go mail to 0.7.2 (#35748) 2025-10-26 09:52:01 -04:00
Lunny Xiao
0bf39c72e7 Merge branch 'lunny/merge_tree_conflict_check' of github.com:lunny/gitea into lunny/merge_tree_conflict_check 2025-10-25 20:47:09 -07:00
Lunny Xiao
053817ae0e
Fix test 2025-10-25 20:46:52 -07:00
Lunny Xiao
562ab41116 Merge branch 'main' into lunny/merge_tree_conflict_check 2025-10-25 20:42:07 -07:00
GiteaBot
2a6af15448 [skip ci] Updated translations via Crowdin 2025-10-26 00:38:59 +00:00
Lunny Xiao
2609edba48
Merge branch 'main' into lunny/merge_tree_conflict_check 2025-10-25 10:10:00 -07:00
Lunny Xiao
3845774049
improvements 2025-10-21 20:02:13 -07:00
Lunny Xiao
27848790aa
Merge branch 'main' into lunny/merge_tree_conflict_check 2025-10-21 19:51:03 -07:00
Lunny Xiao
59ea28c5dd
Merge branch 'main' into lunny/merge_tree_conflict_check 2025-10-21 11:41:44 -07:00
Lunny Xiao
da5fb5c8a6
Merge branch 'main' into lunny/merge_tree_conflict_check 2025-10-17 20:03:17 -07:00
Lunny Xiao
0d28912f52 Merge branch 'lunny/merge_tree_conflict_check' of github.com:lunny/gitea into lunny/merge_tree_conflict_check 2025-10-14 17:24:24 -07:00
Lunny Xiao
9318bbe846 Merge branch 'main' into lunny/merge_tree_conflict_check 2025-10-14 17:24:03 -07:00
Lunny Xiao
d68ad1b582
improvements 2025-10-14 17:24:00 -07:00
Lunny Xiao
d34e640ac6
Merge branch 'main' into lunny/merge_tree_conflict_check 2025-10-14 16:28:39 -07:00
Lunny Xiao
09f519e0f0 Merge branch 'main' into lunny/merge_tree_conflict_check 2025-10-09 20:49:27 -07:00
Lunny Xiao
146e816810
Fix test 2025-10-09 20:49:10 -07:00
Lunny Xiao
b61a5ae83a
remove unnecessary code 2025-10-09 18:25:40 -07:00
Lunny Xiao
703a0c54bd Merge branch 'main' into lunny/merge_tree_conflict_check 2025-10-09 18:23:30 -07:00
Lunny Xiao
dc0abc4b5c
Fix test 2025-10-09 18:23:17 -07:00
Lunny Xiao
acb99d46dd
remove unused comment 2025-10-09 16:44:18 -07:00
Lunny Xiao
0a9eff3b4b Merge branch 'main' into lunny/merge_tree_conflict_check 2025-10-09 15:31:17 -07:00
Lunny Xiao
07f6a8b7df
improvements 2025-10-09 15:31:06 -07:00
Lunny Xiao
5b1229e528 Merge branch 'main' into lunny/merge_tree_conflict_check 2025-10-07 22:00:16 -07:00
Lunny Xiao
793cbf72e6 Merge branch 'main' into lunny/merge_tree_conflict_check 2025-10-06 18:40:28 -07:00
Lunny Xiao
22f0aa25b7
Fix test 2025-10-06 18:40:22 -07:00
Lunny Xiao
6e96a4afc1
remove unnecessary check 2025-10-06 15:38:20 -07:00
Lunny Xiao
9fad9fb43d Merge branch 'main' into lunny/merge_tree_conflict_check 2025-10-06 14:58:34 -07:00
Lunny Xiao
4b8c0471e3
make test happy 2025-10-06 14:00:32 -07:00
Lunny Xiao
e8636b7a4e
Add both mergetree and tmprepo for rebase and retarget tests 2025-10-03 18:05:48 -07:00
Lunny Xiao
ed5a7493a0 Merge branch 'main' into lunny/merge_tree_conflict_check 2025-10-03 18:02:26 -07:00
Lunny Xiao
af79992734
add tests for mergeable tmprepo checking 2025-10-03 18:02:03 -07:00
Lunny Xiao
4d4f3ae2ee Merge branch 'main' into lunny/merge_tree_conflict_check 2025-09-29 16:40:28 -07:00
Lunny Xiao
76da4bf759
fix bug 2025-09-29 15:54:26 -07:00
Lunny Xiao
72a154a9b5
fix bug 2025-09-29 14:01:27 -07:00
Lunny Xiao
7e973b35d5
improvements 2025-09-27 18:39:16 -07:00
Lunny Xiao
3d0222c159
allow empty pull request 2025-09-26 22:36:15 -07:00
Lunny Xiao
2f74aecf2e
remove unused functions 2025-09-26 21:52:18 -07:00
Lunny Xiao
21cc4aaf07
improvements 2025-09-26 21:00:31 -07:00
Lunny Xiao
dda296addc
Fix lint 2025-09-26 20:37:13 -07:00
47 changed files with 903 additions and 291 deletions

2
go.mod
View File

@ -109,7 +109,7 @@ require (
github.com/ulikunitz/xz v0.5.15
github.com/urfave/cli-docs/v3 v3.0.0-alpha6
github.com/urfave/cli/v3 v3.4.1
github.com/wneessen/go-mail v0.7.1
github.com/wneessen/go-mail v0.7.2
github.com/xeipuuv/gojsonschema v1.2.0
github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.7.13

4
go.sum
View File

@ -768,8 +768,8 @@ github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZ
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/wneessen/go-mail v0.7.1 h1:rvy63sp14N06/kdGqCYwW8Na5gDCXjTQM1E7So4PuKk=
github.com/wneessen/go-mail v0.7.1/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=

View File

@ -4,7 +4,6 @@
package git
import (
"os"
"path/filepath"
"strings"
"testing"
@ -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)
}

View File

@ -7,6 +7,7 @@ import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
@ -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() {
@ -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
@ -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

View File

@ -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
@ -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
}

View File

@ -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)

View File

@ -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
}

16
modules/gitrepo/fetch.go Normal file
View File

@ -0,0 +1,16 @@
// 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 {
return RunCmd(ctx, repo, gitcmd.NewCommand("fetch", "--no-tags").
AddDynamicArguments(repoPath(remoteRepo)).
AddDynamicArguments(commitID))
}

75
modules/gitrepo/merge.go Normal file
View File

@ -0,0 +1,75 @@
// 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")
switch len(fields) {
case 0:
return "", nil, errors.New("unexpected empty output")
case 1:
return strings.TrimSpace(fields[0]), nil, nil
default:
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)
}
}

View File

@ -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)
}

View File

@ -3586,6 +3586,7 @@ variables.update.success=Proměnná byla upravena.
logs.always_auto_scroll=Vždy automaticky posouvat logy
logs.always_expand_running=Vždy rozšířit běžící logy
[projects]
deleted.display_name=Odstraněný projekt
type-1.display_name=Samostatný projekt

View File

@ -3645,6 +3645,7 @@ variables.update.success=Die Variable wurde bearbeitet.
logs.always_auto_scroll=Autoscroll für Logs immer aktivieren
logs.always_expand_running=Laufende Logs immer erweitern
[projects]
deleted.display_name=Gelöschtes Projekt
type-1.display_name=Individuelles Projekt

View File

@ -3280,6 +3280,7 @@ variables.update.failed=Αποτυχία επεξεργασίας μεταβλη
variables.update.success=Η μεταβλητή έχει τροποποιηθεί.
[projects]
type-1.display_name=Ατομικό Έργο
type-2.display_name=Έργο Αποθετηρίου

View File

@ -3257,6 +3257,7 @@ variables.update.failed=Error al editar la variable.
variables.update.success=La variable ha sido editada.
[projects]
type-1.display_name=Proyecto individual
type-2.display_name=Proyecto repositorio

View File

@ -2446,6 +2446,7 @@ runs.commit=کامیت
[projects]
[git.filemode]

View File

@ -1693,6 +1693,7 @@ runs.commit=Commit
[projects]
[git.filemode]

View File

@ -3906,6 +3906,7 @@ variables.update.success=La variable a bien été modifiée.
logs.always_auto_scroll=Toujours faire défiler les journaux automatiquement
logs.always_expand_running=Toujours développer les journaux en cours
[projects]
deleted.display_name=Projet supprimé
type-1.display_name=Projet personnel

View File

@ -3914,6 +3914,7 @@ variables.update.success=Tá an t-athróg curtha in eagar.
logs.always_auto_scroll=Logchomhaid scrollaithe uathoibríoch i gcónaí
logs.always_expand_running=Leathnaigh logs reatha i gcónaí
[projects]
deleted.display_name=Tionscadal scriosta
type-1.display_name=Tionscadal Aonair

View File

@ -1605,6 +1605,7 @@ runs.commit=Commit
[projects]
[git.filemode]

View File

@ -1428,6 +1428,7 @@ variables.update.failed=Gagal mengedit variabel.
variables.update.success=Variabel telah diedit.
[projects]
type-1.display_name=Proyek Individu
type-2.display_name=Proyek Repositori

View File

@ -1334,6 +1334,7 @@ runs.commit=Framlag
[projects]
[git.filemode]

View File

@ -2706,6 +2706,7 @@ runs.commit=Commit
[projects]
[git.filemode]

View File

@ -3910,6 +3910,7 @@ variables.update.success=変数を更新しました。
logs.always_auto_scroll=常にログを自動スクロール
logs.always_expand_running=常に実行中のログを展開
[projects]
deleted.display_name=削除されたプロジェクト
type-1.display_name=個人プロジェクト

View File

@ -1554,6 +1554,7 @@ runs.commit=커밋
[projects]
[git.filemode]

View File

@ -3282,6 +3282,7 @@ variables.update.failed=Neizdevās labot mainīgo.
variables.update.success=Mainīgais tika labots.
[projects]
type-1.display_name=Individuālais projekts
type-2.display_name=Repozitorija projekts

View File

@ -2458,6 +2458,7 @@ runs.commit=Commit
[projects]
[git.filemode]

View File

@ -2347,6 +2347,7 @@ runs.commit=Commit
[projects]
[git.filemode]

View File

@ -3615,6 +3615,7 @@ variables.update.failed=Falha ao editar a variável.
variables.update.success=A variável foi editada.
[projects]
deleted.display_name=Excluir Projeto
type-1.display_name=Projeto Individual

View File

@ -3914,6 +3914,7 @@ variables.update.success=A variável foi editada.
logs.always_auto_scroll=Rolar registos de forma automática e permanente
logs.always_expand_running=Expandir sempre os registos que vão rolando
[projects]
deleted.display_name=Planeamento eliminado
type-1.display_name=Planeamento individual

View File

@ -3225,6 +3225,7 @@ variables.update.failed=Не удалось изменить переменну
variables.update.success=Переменная изменена.
[projects]
type-1.display_name=Индивидуальный проект
type-2.display_name=Проект репозитория

View File

@ -2391,6 +2391,7 @@ runs.commit=කැප
[projects]
[git.filemode]

View File

@ -1292,6 +1292,7 @@ runners.labels=Štítky
[projects]
[git.filemode]

View File

@ -1968,6 +1968,7 @@ runs.commit=Commit
[projects]
[git.filemode]

View File

@ -3907,6 +3907,7 @@ variables.update.success=Değişken düzenlendi.
logs.always_auto_scroll=Günlükleri her zaman otomatik kaydır
logs.always_expand_running=Çalıştırma günlüklerini her zaman genişlet
[projects]
deleted.display_name=Silinmiş Proje
type-1.display_name=Kişisel Proje

View File

@ -3428,6 +3428,7 @@ variables.update.success=Змінну відредаговано.
logs.always_auto_scroll=Завжди автоматично прокручувати журнали
logs.always_expand_running=Завжди розгортати поточні журнали
[projects]
deleted.display_name=Видалений проєкт
type-1.display_name=Індивідуальний проєкт

View File

@ -3911,6 +3911,7 @@ variables.update.success=变量已编辑。
logs.always_auto_scroll=总是自动滚动日志
logs.always_expand_running=总是展开运行日志
[projects]
deleted.display_name=已删除项目
type-1.display_name=个人项目

View File

@ -980,6 +980,7 @@ runners.task_list.repository=儲存庫
[projects]
[git.filemode]

View File

@ -3554,6 +3554,7 @@ variables.update.failed=編輯變數失敗。
variables.update.success=已編輯變數。
[projects]
deleted.display_name=已刪除的專案
type-1.display_name=個人專案

View File

@ -244,7 +244,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)
@ -302,7 +302,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{

View File

@ -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 {

155
services/pull/conflicts.go Normal file
View File

@ -0,0 +1,155 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pull
import (
"context"
"errors"
"fmt"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
// checkPullRequestMergeableAndUpdateStatus checks whether a pull request is mergeable and updates its status accordingly.
// It uses 'git merge-tree' if supported by the Git version, otherwise it falls back to using a temporary repository.
// This function updates the pr.Status, pr.MergeBase and pr.ConflictedFiles fields as necessary.
// The pull request parameter may not be created yet in the database, so do not assume it has an ID.
func checkPullRequestMergeableAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) error {
if git.DefaultFeatures().SupportGitMergeTree {
return checkPullRequestMergeableAndUpdateStatusMergeTree(ctx, pr)
}
return checkPullRequestMergeableAndUpdateStatusTmpRepo(ctx, pr)
}
// checkConflictsMergeTree uses git merge-tree to check for conflicts and if none are found checks if the patch is empty
// return true if there is conflicts otherwise return false
// pr.Status and pr.ConflictedFiles will be updated as necessary
func checkConflictsMergeTree(ctx context.Context, pr *issues_model.PullRequest, baseCommitID string) (bool, error) {
treeHash, conflict, conflictFiles, err := gitrepo.MergeTree(ctx, pr.BaseRepo, baseCommitID, pr.HeadCommitID, pr.MergeBase)
if err != nil {
return false, fmt.Errorf("MergeTree: %w", err)
}
if conflict {
pr.Status = issues_model.PullRequestStatusConflict
pr.ConflictedFiles = util.Iif(len(conflictFiles) > 0, conflictFiles, []string{"(no files listed)"})
log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
return true, nil
}
// Detecting whether the pull request has difference via git diff-tree
// it will return exit code 0 if there's no diff and exit code 1 if there's a diff.
gitErr := gitrepo.RunCmd(ctx, pr.BaseRepo, gitcmd.NewCommand("diff-tree", "-r", "--quiet").
AddDynamicArguments(treeHash, pr.MergeBase))
exitCode, ok := gitcmd.ExitCode(gitErr)
if !ok {
return false, fmt.Errorf("run diff-tree failed: %w", gitErr)
}
switch exitCode {
case 0:
log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
pr.Status = issues_model.PullRequestStatusEmpty
case 1:
pr.Status = issues_model.PullRequestStatusMergeable
default:
return false, fmt.Errorf("run diff-tree exit abnormally: %w", gitErr)
}
return false, nil
}
func checkPullRequestMergeableAndUpdateStatusMergeTree(ctx context.Context, pr *issues_model.PullRequest) error {
// 1. Get head commit
if err := pr.LoadHeadRepo(ctx); err != nil {
return err
}
headGitRepo, err := gitrepo.OpenRepository(ctx, pr.HeadRepo)
if err != nil {
return fmt.Errorf("OpenRepository: %w", err)
}
defer headGitRepo.Close()
// 2. Get base commit id
var baseGitRepo *git.Repository
if pr.IsSameRepo() {
baseGitRepo = headGitRepo
} else {
baseGitRepo, err = gitrepo.OpenRepository(ctx, pr.BaseRepo)
if err != nil {
return fmt.Errorf("OpenRepository: %w", err)
}
defer baseGitRepo.Close()
}
// 3. Get head commit id
if pr.Flow == issues_model.PullRequestFlowGithub {
pr.HeadCommitID, err = headGitRepo.GetRefCommitID(git.BranchPrefix + pr.HeadBranch)
if err != nil {
return fmt.Errorf("GetBranchCommitID: can't find commit ID for head: %w", err)
}
} else {
if pr.ID > 0 {
pr.HeadCommitID, err = baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName())
if err != nil {
return fmt.Errorf("GetRefCommitID: can't find commit ID for head: %w", err)
}
} else if pr.HeadCommitID == "" { // for new pull request with agit, the head commit id must be provided
return errors.New("head commit ID is empty for pull request Agit flow")
}
}
// 4. fetch head commit id into the current repository
// it will be checked in 2 weeks by default from git if the pull request created failure.
if !pr.IsSameRepo() {
if err := gitrepo.FetchRemoteCommit(ctx, pr.BaseRepo, pr.HeadRepo, pr.HeadCommitID); err != nil {
return fmt.Errorf("FetchRemoteCommit: %w", err)
}
}
// 5. update merge base
baseCommitID, err := baseGitRepo.GetRefCommitID(git.BranchPrefix + pr.BaseBranch)
if err != nil {
return fmt.Errorf("GetBranchCommitID: can't find commit ID for base: %w", err)
}
pr.MergeBase, err = gitrepo.MergeBase(ctx, pr.BaseRepo, baseCommitID, pr.HeadCommitID)
if err != nil {
log.Error("GetMergeBase: %v and can't find commit ID for base: %v", err, baseCommitID)
pr.Status = issues_model.PullRequestStatusEmpty // if there is no merge base, then it's empty but we still need to allow the pull request created
return nil
}
// 6. if base == head, then it's an ancestor
if pr.HeadCommitID == pr.MergeBase {
pr.Status = issues_model.PullRequestStatusAncestor
return nil
}
// 7. Check for conflicts
conflicted, err := checkConflictsMergeTree(ctx, pr, baseCommitID)
if err != nil {
log.Error("checkConflictsMergeTree: %v", err)
pr.Status = issues_model.PullRequestStatusEmpty // if there is no merge base, then it's empty but we still need to allow the pull request created
}
if conflicted || pr.Status == issues_model.PullRequestStatusEmpty {
return nil
}
// 7. Check for protected files changes
if err = checkPullFilesProtection(ctx, pr, pr.BaseRepo.RepoPath()); err != nil {
return fmt.Errorf("pr.CheckPullFilesProtection(): %v", err)
}
if len(pr.ChangedProtectedFiles) > 0 {
log.Trace("Found %d protected files changed", len(pr.ChangedProtectedFiles))
}
pr.Status = issues_model.PullRequestStatusMergeable
return nil
}

View File

@ -1,5 +1,4 @@
// Copyright 2019 The Gitea Authors.
// All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pull
@ -15,15 +14,14 @@ import (
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/glob"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
@ -67,10 +65,7 @@ var patchErrorSuffices = []string{
": does not exist in index",
}
func testPullRequestBranchMergeable(pr *issues_model.PullRequest) error {
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("testPullRequestBranchMergeable: %s", pr))
defer finished()
func checkPullRequestMergeableAndUpdateStatusTmpRepo(ctx context.Context, pr *issues_model.PullRequest) error {
prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
if err != nil {
if !git_model.IsErrBranchNotExist(err) {
@ -80,10 +75,6 @@ func testPullRequestBranchMergeable(pr *issues_model.PullRequest) error {
}
defer cancel()
return testPullRequestTmpRepoBranchMergeable(ctx, prCtx, pr)
}
func testPullRequestTmpRepoBranchMergeable(ctx context.Context, prCtx *prTmpRepoContext, pr *issues_model.PullRequest) error {
gitRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath)
if err != nil {
return fmt.Errorf("OpenRepository: %w", err)
@ -115,7 +106,7 @@ func testPullRequestTmpRepoBranchMergeable(ctx context.Context, prCtx *prTmpRepo
}
// 3. Check for protected files changes
if err = checkPullFilesProtection(ctx, pr, gitRepo); err != nil {
if err = checkPullFilesProtection(ctx, pr, prCtx.tmpBasePath); err != nil {
return fmt.Errorf("pr.CheckPullFilesProtection(): %v", err)
}
@ -530,11 +521,11 @@ func (err ErrFilePathProtected) Unwrap() error {
}
// CheckFileProtection check file Protection
func CheckFileProtection(repo *git.Repository, branchName, oldCommitID, newCommitID string, patterns []glob.Glob, limit int, env []string) ([]string, error) {
func CheckFileProtection(ctx context.Context, repoPath, oldCommitID, newCommitID string, patterns []glob.Glob, limit int, env []string) ([]string, error) {
if len(patterns) == 0 {
return nil, nil
}
affectedFiles, err := git.GetAffectedFiles(repo, branchName, oldCommitID, newCommitID, env)
affectedFiles, err := git.GetAffectedFiles(ctx, repoPath, oldCommitID, newCommitID, env)
if err != nil {
return nil, err
}
@ -560,11 +551,11 @@ func CheckFileProtection(repo *git.Repository, branchName, oldCommitID, newCommi
}
// CheckUnprotectedFiles check if the commit only touches unprotected files
func CheckUnprotectedFiles(repo *git.Repository, branchName, oldCommitID, newCommitID string, patterns []glob.Glob, env []string) (bool, error) {
func CheckUnprotectedFiles(ctx context.Context, repo *repo_model.Repository, oldCommitID, newCommitID string, patterns []glob.Glob, env []string) (bool, error) {
if len(patterns) == 0 {
return false, nil
}
affectedFiles, err := git.GetAffectedFiles(repo, branchName, oldCommitID, newCommitID, env)
affectedFiles, err := git.GetAffectedFiles(ctx, repo.RepoPath(), oldCommitID, newCommitID, env)
if err != nil {
return false, err
}
@ -585,7 +576,8 @@ func CheckUnprotectedFiles(repo *git.Repository, branchName, oldCommitID, newCom
}
// checkPullFilesProtection check if pr changed protected files and save results
func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) error {
// repoPath might be a temporary path so that we need to pass it in
func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest, repoPath string) error {
if pr.Status == issues_model.PullRequestStatusEmpty {
pr.ChangedProtectedFiles = nil
return nil
@ -601,7 +593,7 @@ func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest,
return nil
}
pr.ChangedProtectedFiles, err = CheckFileProtection(gitRepo, pr.HeadBranch, pr.MergeBase, "tracking", pb.GetProtectedFilePatterns(), 10, os.Environ())
pr.ChangedProtectedFiles, err = CheckFileProtection(ctx, repoPath, pr.MergeBase, pr.HeadCommitID, pb.GetProtectedFilePatterns(), 10, os.Environ())
if err != nil && !IsErrFilePathProtected(err) {
return err
}

View File

@ -30,6 +30,7 @@ import (
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@ -86,16 +87,12 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
}
}
prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
if err != nil {
if !git_model.IsErrBranchNotExist(err) {
log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err)
}
return err
}
defer cancel()
ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("testPullRequestBranchMergeable: %s", pr))
defer finished()
if err := testPullRequestTmpRepoBranchMergeable(ctx, prCtx, pr); err != nil {
// the pull request haven't been created
err := checkPullRequestMergeableAndUpdateStatus(ctx, pr)
if err != nil {
return err
}
@ -290,8 +287,11 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer
oldBranch := pr.BaseBranch
pr.BaseBranch = targetBranch
ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("testPullRequestBranchMergeable: %s", pr))
defer finished()
// Refresh patch
if err := testPullRequestBranchMergeable(pr); err != nil {
if err := checkPullRequestMergeableAndUpdateStatus(ctx, pr); err != nil {
return err
}

View File

@ -13,6 +13,8 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/test"
issues_service "code.gitea.io/gitea/services/issue"
"github.com/stretchr/testify/assert"
@ -96,14 +98,40 @@ func testPullCommentRetarget(t *testing.T, u *url.URL, session *TestSession) {
testWaitForPullRequestStatus(t, &issues_model.Issue{Title: testPRTitle}, issues_model.PullRequestStatusMergeable)
}
func TestPullComment(t *testing.T) {
func TestPullComment_MergeTree(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user1")
testCreateBranch(t, session, "user2", "repo1", "branch/master", "test-branch/rebase", http.StatusSeeOther)
testCreateBranch(t, session, "user2", "repo1", "branch/master", "test-branch/retarget", http.StatusSeeOther)
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
t.Run("RebaseComment", func(t *testing.T) { testPullCommentRebase(t, u, session) })
t.Run("RetargetComment", func(t *testing.T) { testPullCommentRetarget(t, u, session) })
t.Run("RebaseComment_MergeTree", func(t *testing.T) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
testPullCommentRebase(t, u, session)
})
t.Run("RetargetComment_MergeTree", func(t *testing.T) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
testPullCommentRetarget(t, u, session)
})
})
}
func TestPullComment_TmpRepo(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user1")
testCreateBranch(t, session, "user2", "repo1", "branch/master", "test-branch/rebase", http.StatusSeeOther)
testCreateBranch(t, session, "user2", "repo1", "branch/master", "test-branch/retarget", http.StatusSeeOther)
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
t.Run("RebaseComment_TmpRepo", func(t *testing.T) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
testPullCommentRebase(t, u, session)
})
t.Run("RetargetComment_TmpRepo", func(t *testing.T) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
testPullCommentRetarget(t, u, session)
})
})
}

View File

@ -4,9 +4,18 @@
package integration
import (
"fmt"
"net/http"
"net/url"
"path"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/commitstatus"
api "code.gitea.io/gitea/modules/structs"
pull_service "code.gitea.io/gitea/services/pull"
"code.gitea.io/gitea/tests"
@ -39,3 +48,98 @@ func TestListPullCommits(t *testing.T) {
assert.Contains(t, resp.Body.String(), `<td class="lines-code lines-code-new"><code class="code-inner"># repo1</code>`)
})
}
func TestPullCreate_CommitStatus(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
url := path.Join("user1", "repo1", "compare", "master...status1")
req := NewRequestWithValues(t, "POST", url,
map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"title": "pull request from status1",
},
)
session.MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", "/user1/repo1/pulls")
resp := session.MakeRequest(t, req, http.StatusOK)
NewHTMLParser(t, resp.Body)
// Request repository commits page
req = NewRequest(t, "GET", "/user1/repo1/pulls/1/commits")
resp = session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
// Get first commit URL
commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
assert.True(t, exists)
assert.NotEmpty(t, commitURL)
commitID := path.Base(commitURL)
statusList := []commitstatus.CommitStatusState{
commitstatus.CommitStatusPending,
commitstatus.CommitStatusError,
commitstatus.CommitStatusFailure,
commitstatus.CommitStatusSuccess,
commitstatus.CommitStatusWarning,
}
statesIcons := map[commitstatus.CommitStatusState]string{
commitstatus.CommitStatusPending: "octicon-dot-fill",
commitstatus.CommitStatusSuccess: "octicon-check",
commitstatus.CommitStatusError: "gitea-exclamation",
commitstatus.CommitStatusFailure: "octicon-x",
commitstatus.CommitStatusWarning: "gitea-exclamation",
}
testCtx := NewAPITestContext(t, "user1", "repo1", auth_model.AccessTokenScopeWriteRepository)
// Update commit status, and check if icon is updated as well
for _, status := range statusList {
// Call API to add status for commit
t.Run("CreateStatus", doAPICreateCommitStatus(testCtx, commitID, api.CreateStatusOption{
State: status,
TargetURL: "http://test.ci/",
Description: "",
Context: "testci",
}))
req = NewRequest(t, "GET", "/user1/repo1/pulls/1/commits")
resp = session.MakeRequest(t, req, http.StatusOK)
doc = NewHTMLParser(t, resp.Body)
commitURL, exists = doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
assert.True(t, exists)
assert.NotEmpty(t, commitURL)
assert.Equal(t, commitID, path.Base(commitURL))
cls, ok := doc.doc.Find("#commits-table tbody tr td.message .commit-status").Last().Attr("class")
assert.True(t, ok)
assert.Contains(t, cls, statesIcons[status])
}
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"})
css := unittest.AssertExistsAndLoadBean(t, &git_model.CommitStatusSummary{RepoID: repo1.ID, SHA: commitID})
assert.Equal(t, commitstatus.CommitStatusSuccess, css.State)
})
}
func doAPICreateCommitStatus(ctx APITestContext, commitID string, data api.CreateStatusOption) func(*testing.T) {
return func(t *testing.T) {
req := NewRequestWithJSON(
t,
http.MethodPost,
fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s", ctx.Username, ctx.Reponame, commitID),
data,
).AddTokenAuth(ctx.Token)
if ctx.ExpectedCode != 0 {
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
return
}
ctx.Session.MakeRequest(t, req, http.StatusCreated)
}
}

View File

@ -36,9 +36,7 @@ import (
"code.gitea.io/gitea/services/automerge"
"code.gitea.io/gitea/services/automergequeue"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
files_service "code.gitea.io/gitea/services/repository/files"
"github.com/stretchr/testify/assert"
)
@ -523,86 +521,6 @@ func TestCantFastForwardOnlyMergeDiverging(t *testing.T) {
})
}
func TestConflictChecking(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Create new clean repo to test conflict checking.
baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
Name: "conflict-checking",
Description: "Tempo repo",
AutoInit: true,
Readme: "Default",
DefaultBranch: "main",
})
assert.NoError(t, err)
assert.NotEmpty(t, baseRepo)
// create a commit on new branch.
_, err = files_service.ChangeRepoFiles(t.Context(), baseRepo, user, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: "important_file",
ContentReader: strings.NewReader("Just a non-important file"),
},
},
Message: "Add a important file",
OldBranch: "main",
NewBranch: "important-secrets",
})
assert.NoError(t, err)
// create a commit on main branch.
_, err = files_service.ChangeRepoFiles(t.Context(), baseRepo, user, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: "important_file",
ContentReader: strings.NewReader("Not the same content :P"),
},
},
Message: "Add a important file",
OldBranch: "main",
NewBranch: "main",
})
assert.NoError(t, err)
// create Pull to merge the important-secrets branch into main branch.
pullIssue := &issues_model.Issue{
RepoID: baseRepo.ID,
Title: "PR with conflict!",
PosterID: user.ID,
Poster: user,
IsPull: true,
}
pullRequest := &issues_model.PullRequest{
HeadRepoID: baseRepo.ID,
BaseRepoID: baseRepo.ID,
HeadBranch: "important-secrets",
BaseBranch: "main",
HeadRepo: baseRepo,
BaseRepo: baseRepo,
Type: issues_model.PullRequestGitea,
}
prOpts := &pull_service.NewPullRequestOptions{Repo: baseRepo, Issue: pullIssue, PullRequest: pullRequest}
err = pull_service.NewPullRequest(t.Context(), prOpts)
assert.NoError(t, err)
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
assert.NoError(t, issue.LoadPullRequest(t.Context()))
conflictingPR := issue.PullRequest
// Ensure conflictedFiles is populated.
assert.Len(t, conflictingPR.ConflictedFiles, 1)
// Check if status is correct.
assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status)
// Ensure that mergeable returns false
assert.False(t, conflictingPR.Mergeable(t.Context()))
})
}
func TestPullRetargetChildOnBranchDelete(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
session := loginUser(t, "user1")

View File

@ -11,115 +11,20 @@ import (
"strings"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/issues"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/commitstatus"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/services/pull"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
"github.com/stretchr/testify/assert"
)
func TestPullCreate_CommitStatus(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
url := path.Join("user1", "repo1", "compare", "master...status1")
req := NewRequestWithValues(t, "POST", url,
map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"title": "pull request from status1",
},
)
session.MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", "/user1/repo1/pulls")
resp := session.MakeRequest(t, req, http.StatusOK)
NewHTMLParser(t, resp.Body)
// Request repository commits page
req = NewRequest(t, "GET", "/user1/repo1/pulls/1/commits")
resp = session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
// Get first commit URL
commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
assert.True(t, exists)
assert.NotEmpty(t, commitURL)
commitID := path.Base(commitURL)
statusList := []commitstatus.CommitStatusState{
commitstatus.CommitStatusPending,
commitstatus.CommitStatusError,
commitstatus.CommitStatusFailure,
commitstatus.CommitStatusSuccess,
commitstatus.CommitStatusWarning,
}
statesIcons := map[commitstatus.CommitStatusState]string{
commitstatus.CommitStatusPending: "octicon-dot-fill",
commitstatus.CommitStatusSuccess: "octicon-check",
commitstatus.CommitStatusError: "gitea-exclamation",
commitstatus.CommitStatusFailure: "octicon-x",
commitstatus.CommitStatusWarning: "gitea-exclamation",
}
testCtx := NewAPITestContext(t, "user1", "repo1", auth_model.AccessTokenScopeWriteRepository)
// Update commit status, and check if icon is updated as well
for _, status := range statusList {
// Call API to add status for commit
t.Run("CreateStatus", doAPICreateCommitStatus(testCtx, commitID, api.CreateStatusOption{
State: status,
TargetURL: "http://test.ci/",
Description: "",
Context: "testci",
}))
req = NewRequest(t, "GET", "/user1/repo1/pulls/1/commits")
resp = session.MakeRequest(t, req, http.StatusOK)
doc = NewHTMLParser(t, resp.Body)
commitURL, exists = doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
assert.True(t, exists)
assert.NotEmpty(t, commitURL)
assert.Equal(t, commitID, path.Base(commitURL))
cls, ok := doc.doc.Find("#commits-table tbody tr td.message .commit-status").Last().Attr("class")
assert.True(t, ok)
assert.Contains(t, cls, statesIcons[status])
}
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"})
css := unittest.AssertExistsAndLoadBean(t, &git_model.CommitStatusSummary{RepoID: repo1.ID, SHA: commitID})
assert.Equal(t, commitstatus.CommitStatusSuccess, css.State)
})
}
func doAPICreateCommitStatus(ctx APITestContext, commitID string, data api.CreateStatusOption) func(*testing.T) {
return func(t *testing.T) {
req := NewRequestWithJSON(
t,
http.MethodPost,
fmt.Sprintf("/api/v1/repos/%s/%s/statuses/%s", ctx.Username, ctx.Reponame, commitID),
data,
).AddTokenAuth(ctx.Token)
if ctx.ExpectedCode != 0 {
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
return
}
ctx.Session.MakeRequest(t, req, http.StatusCreated)
}
}
func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
// Merge must continue if commits SHA are different, even if content is same
// Reason: gitflow and merging master back into develop, where is high possibility, there are no changes
@ -174,16 +79,16 @@ func TestPullCreate_EmptyChangesWithSameCommits(t *testing.T) {
func TestPullStatusDelayCheck(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
defer test.MockVariableValue(&setting.Repository.PullRequest.DelayCheckForInactiveDays, 1)()
defer test.MockVariableValue(&pull.AddPullRequestToCheckQueue)()
defer test.MockVariableValue(&pull_service.AddPullRequestToCheckQueue)()
session := loginUser(t, "user2")
run := func(t *testing.T, fn func(*testing.T)) (issue3 *issues.Issue, checkedPrID int64) {
pull.AddPullRequestToCheckQueue = func(prID int64) {
run := func(t *testing.T, fn func(*testing.T)) (issue3 *issues_model.Issue, checkedPrID int64) {
pull_service.AddPullRequestToCheckQueue = func(prID int64) {
checkedPrID = prID
}
fn(t)
issue3 = unittest.AssertExistsAndLoadBean(t, &issues.Issue{RepoID: 1, Index: 3})
issue3 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: 1, Index: 3})
_ = issue3.LoadPullRequest(t.Context())
return issue3, checkedPrID
}
@ -201,7 +106,7 @@ func TestPullStatusDelayCheck(t *testing.T) {
// PR issue3 is merageable at the beginning
issue3, checkedPrID := run(t, func(t *testing.T) {})
assert.Equal(t, issues.PullRequestStatusMergeable, issue3.PullRequest.Status)
assert.Equal(t, issues_model.PullRequestStatusMergeable, issue3.PullRequest.Status)
assert.Zero(t, checkedPrID)
assertReloadingInterval(t, "") // the PR is mergeable, so no need to reload the merge box
@ -213,7 +118,7 @@ func TestPullStatusDelayCheck(t *testing.T) {
issue3, checkedPrID = run(t, func(t *testing.T) {
testEditFile(t, session, "user2", "repo1", "master", "README.md", "new content 1")
})
assert.Equal(t, issues.PullRequestStatusChecking, issue3.PullRequest.Status)
assert.Equal(t, issues_model.PullRequestStatusChecking, issue3.PullRequest.Status)
assert.Zero(t, checkedPrID)
assertReloadingInterval(t, "2000") // the PR status is "checking", so try to reload the merge box
@ -222,14 +127,14 @@ func TestPullStatusDelayCheck(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo1/pulls/3")
session.MakeRequest(t, req, http.StatusOK)
})
assert.Equal(t, issues.PullRequestStatusChecking, issue3.PullRequest.Status)
assert.Equal(t, issues_model.PullRequestStatusChecking, issue3.PullRequest.Status)
assert.Equal(t, issue3.PullRequest.ID, checkedPrID)
// when base branch changes, still so no real check
issue3, checkedPrID = run(t, func(t *testing.T) {
testEditFile(t, session, "user2", "repo1", "master", "README.md", "new content 2")
})
assert.Equal(t, issues.PullRequestStatusChecking, issue3.PullRequest.Status)
assert.Equal(t, issues_model.PullRequestStatusChecking, issue3.PullRequest.Status)
assert.Zero(t, checkedPrID)
// then allow to check PRs without delay, when base branch changes, the PRs will be checked
@ -237,7 +142,407 @@ func TestPullStatusDelayCheck(t *testing.T) {
issue3, checkedPrID = run(t, func(t *testing.T) {
testEditFile(t, session, "user2", "repo1", "master", "README.md", "new content 3")
})
assert.Equal(t, issues.PullRequestStatusChecking, issue3.PullRequest.Status)
assert.Equal(t, issues_model.PullRequestStatusChecking, issue3.PullRequest.Status)
assert.Equal(t, issue3.PullRequest.ID, checkedPrID)
})
}
func Test_PullRequestStatusChecking_Mergeable_MergeTree(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
testPullRequestStatusCheckingMergeable(t)
})
}
func Test_PullRequestStatusChecking_Mergeable_TmpRepo(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
testPullRequestStatusCheckingMergeable(t)
})
}
func testPullRequestStatusCheckingMergeable(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user.Name)
// Create new clean repo to test conflict checking.
baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
Name: "conflict-checking",
Description: "Tempo repo",
AutoInit: true,
Readme: "Default",
DefaultBranch: "main",
})
assert.NoError(t, err)
assert.NotEmpty(t, baseRepo)
// create a commit on new branch.
testCreateFile(t, session, baseRepo.OwnerName, baseRepo.Name, "main", "important-secrets", "important_file", "Just a non-important file")
// create Pull to merge the important-secrets branch into main branch.
resp := testPullCreateDirectly(t, session, createPullRequestOptions{
BaseRepoOwner: baseRepo.OwnerName,
BaseRepoName: baseRepo.Name,
BaseBranch: "main",
HeadRepoOwner: baseRepo.OwnerName,
HeadRepoName: baseRepo.Name,
HeadBranch: "important-secrets",
Title: "PR with no conflict",
})
// check the redirected URL
url := test.RedirectURL(resp)
assert.Regexp(t, fmt.Sprintf("^/%s/pulls/[0-9]*$", baseRepo.FullName()), url)
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with no conflict"})
assert.NoError(t, issue.LoadPullRequest(t.Context()))
conflictingPR := issue.PullRequest
// Ensure conflictedFiles is populated.
assert.Empty(t, conflictingPR.ConflictedFiles)
// Check if status is correct.
assert.Equal(t, issues_model.PullRequestStatusMergeable, conflictingPR.Status)
// Ensure that mergeable returns true
assert.True(t, conflictingPR.Mergeable(t.Context()))
}
func Test_PullRequestStatusChecking_Conflicted_MergeTree(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
testPullRequestStatusCheckingConflicted(t)
})
}
func Test_PullRequestStatusChecking_Conflicted_TmpRepo(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
testPullRequestStatusCheckingConflicted(t)
})
}
func testPullRequestStatusCheckingConflicted(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user.Name)
// Create new clean repo to test conflict checking.
baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
Name: "conflict-checking",
Description: "Tempo repo",
AutoInit: true,
Readme: "Default",
DefaultBranch: "main",
})
assert.NoError(t, err)
assert.NotEmpty(t, baseRepo)
// create a commit on new branch.
testCreateFile(t, session, baseRepo.OwnerName, baseRepo.Name, "main", "important-secrets", "important_file", "Just a non-important file")
// create a commit on main branch.
testCreateFile(t, session, baseRepo.OwnerName, baseRepo.Name, "main", "main", "important_file", "Not the same content :P")
// create Pull to merge the important-secrets branch into main branch.
resp := testPullCreateDirectly(t, session, createPullRequestOptions{
BaseRepoOwner: baseRepo.OwnerName,
BaseRepoName: baseRepo.Name,
BaseBranch: "main",
HeadRepoOwner: baseRepo.OwnerName,
HeadRepoName: baseRepo.Name,
HeadBranch: "important-secrets",
Title: "PR with conflict!",
})
// check the redirected URL
url := test.RedirectURL(resp)
assert.Regexp(t, fmt.Sprintf("^/%s/pulls/[0-9]*$", baseRepo.FullName()), url)
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
assert.NoError(t, issue.LoadPullRequest(t.Context()))
conflictingPR := issue.PullRequest
// Ensure conflictedFiles is populated.
assert.Equal(t, []string{"important_file"}, conflictingPR.ConflictedFiles)
// Check if status is correct.
assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status)
// Ensure that mergeable returns false
assert.False(t, conflictingPR.Mergeable(t.Context()))
}
func Test_PullRequestStatusCheckingCrossRepo_Mergeable_MergeTree(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
testPullRequestStatusCheckingCrossRepoMergeable(t, giteaURL)
})
}
func Test_PullRequestStatusCheckingCrossRepo_Mergeable_TmpRepo(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
testPullRequestStatusCheckingCrossRepoMergeable(t, giteaURL)
})
}
func testPullRequestStatusCheckingCrossRepoMergeable(t *testing.T, giteaURL *url.URL) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user.Name)
// Create new clean repo to test conflict checking.
baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
Name: "conflict-checking",
Description: "Tempo repo",
AutoInit: true,
Readme: "Default",
DefaultBranch: "main",
})
assert.NoError(t, err)
assert.NotEmpty(t, baseRepo)
testRepoFork(t, session, baseRepo.OwnerName, baseRepo.Name, "org3", "conflict-checking", "main")
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "org3", Name: "conflict-checking"})
// create a commit on new branch of forked repository
testCreateFile(t, session, forkRepo.OwnerName, forkRepo.Name, "main", "important-secrets", "important_file", "Just a non-important file")
// create Pull to merge the important-secrets branch into main branch.
resp := testPullCreateDirectly(t, session, createPullRequestOptions{
BaseRepoOwner: baseRepo.OwnerName,
BaseRepoName: baseRepo.Name,
BaseBranch: "main",
HeadRepoOwner: forkRepo.OwnerName,
HeadRepoName: forkRepo.Name,
HeadBranch: "important-secrets",
Title: "PR with no conflict",
})
// check the redirected URL
url := test.RedirectURL(resp)
assert.Regexp(t, fmt.Sprintf("^/%s/pulls/[0-9]*$", baseRepo.FullName()), url)
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with no conflict"})
assert.NoError(t, issue.LoadPullRequest(t.Context()))
conflictingPR := issue.PullRequest
// Ensure conflictedFiles is populated.
assert.Empty(t, conflictingPR.ConflictedFiles)
// Check if status is correct.
assert.Equal(t, issues_model.PullRequestStatusMergeable, conflictingPR.Status)
// Ensure that mergeable returns true
assert.True(t, conflictingPR.Mergeable(t.Context()))
}
func Test_PullRequestStatusCheckingCrossRepo_Conflicted_MergeTree(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
testPullRequestStatusCheckingCrossRepoConflicted(t, giteaURL)
})
}
func Test_PullRequestStatusCheckingCrossRepo_Conflicted_TmpRepo(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
testPullRequestStatusCheckingCrossRepoConflicted(t, giteaURL)
})
}
func testPullRequestStatusCheckingCrossRepoConflicted(t *testing.T, giteaURL *url.URL) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user.Name)
// Create new clean repo to test conflict checking.
baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
Name: "conflict-checking",
Description: "Tempo repo",
AutoInit: true,
Readme: "Default",
DefaultBranch: "main",
})
assert.NoError(t, err)
assert.NotEmpty(t, baseRepo)
testRepoFork(t, session, baseRepo.OwnerName, baseRepo.Name, "org3", "conflict-checking", "main")
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "org3", Name: "conflict-checking"})
// create a commit on new branch of forked repository
testCreateFile(t, session, forkRepo.OwnerName, forkRepo.Name, "main", "important-secrets", "important_file", "Just a non-important file")
// create a commit on main branch of base repository.
testCreateFile(t, session, baseRepo.OwnerName, baseRepo.Name, "main", "main", "important_file", "Not the same content :P")
// create Pull to merge the important-secrets branch into main branch.
resp := testPullCreateDirectly(t, session, createPullRequestOptions{
BaseRepoOwner: baseRepo.OwnerName,
BaseRepoName: baseRepo.Name,
BaseBranch: "main",
HeadRepoOwner: forkRepo.OwnerName,
HeadRepoName: forkRepo.Name,
HeadBranch: "important-secrets",
Title: "PR with conflict!",
})
// check the redirected URL
url := test.RedirectURL(resp)
assert.Regexp(t, fmt.Sprintf("^/%s/pulls/[0-9]*$", baseRepo.FullName()), url)
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"})
assert.NoError(t, issue.LoadPullRequest(t.Context()))
conflictingPR := issue.PullRequest
// Ensure conflictedFiles is populated.
assert.Equal(t, []string{"important_file"}, conflictingPR.ConflictedFiles)
// Check if status is correct.
assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status)
// Ensure that mergeable returns false
assert.False(t, conflictingPR.Mergeable(t.Context()))
}
func Test_PullRequest_AGit_StatusChecking_Mergeable_MergeTree(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
testPullRequestAGitStatusCheckingMergeable(t, giteaURL)
})
}
func Test_PullRequest_AGit_StatusChecking_Mergeable_TmpRepo(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
testPullRequestAGitStatusCheckingMergeable(t, giteaURL)
})
}
func testPullRequestAGitStatusCheckingMergeable(t *testing.T, giteaURL *url.URL) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Create new clean repo to test conflict checking.
baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
Name: "conflict-checking",
Description: "Tempo repo",
AutoInit: true,
Readme: "Default",
DefaultBranch: "main",
})
assert.NoError(t, err)
assert.NotEmpty(t, baseRepo)
// add something in local repository and push it to remote
dstPath := t.TempDir()
repoURL := *giteaURL
repoURL.Path = baseRepo.FullName() + ".git"
repoURL.User = url.UserPassword("user2", userPassword)
doGitClone(dstPath, &repoURL)(t)
gitRepo, err := git.OpenRepository(t.Context(), dstPath)
assert.NoError(t, err)
defer gitRepo.Close()
doGitCreateBranch(dstPath, "test-agit-push")(t)
_, err = generateCommitWithNewData(t.Context(), testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-")
assert.NoError(t, err)
// push to create an agit pull request
err = gitcmd.NewCommand("push", "origin",
"-o", "title=agit-test-title", "-o", "description=agit-test-description",
"-o", "topic=head-branch-name",
"HEAD:refs/for/main",
).WithDir(dstPath).Run(t.Context())
assert.NoError(t, err)
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
RepoID: baseRepo.ID,
Title: "agit-test-title",
})
assert.NoError(t, issue.LoadPullRequest(t.Context()))
conflictingPR := issue.PullRequest
// Ensure conflictedFiles is populated.
assert.Empty(t, conflictingPR.ConflictedFiles)
// Check if status is correct.
assert.Equal(t, issues_model.PullRequestStatusMergeable, conflictingPR.Status)
// Ensure that mergeable returns true
assert.True(t, conflictingPR.Mergeable(t.Context()))
}
func Test_PullRequest_AGit_StatusChecking_Conflicted_MergeTree(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, true)()
testPullRequestAGitStatusCheckingConflicted(t, giteaURL)
})
}
func Test_PullRequest_AGit_StatusChecking_Conflicted_TmpRepo(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportGitMergeTree, false)()
testPullRequestAGitStatusCheckingConflicted(t, giteaURL)
})
}
func testPullRequestAGitStatusCheckingConflicted(t *testing.T, giteaURL *url.URL) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Create new clean repo to test conflict checking.
baseRepo, err := repo_service.CreateRepository(t.Context(), user, user, repo_service.CreateRepoOptions{
Name: "conflict-checking",
Description: "Tempo repo",
AutoInit: true,
Readme: "Default",
DefaultBranch: "main",
})
assert.NoError(t, err)
assert.NotEmpty(t, baseRepo)
// add something in local repository and push it to remote
dstPath := t.TempDir()
repoURL := *giteaURL
repoURL.Path = baseRepo.FullName() + ".git"
repoURL.User = url.UserPassword("user2", userPassword)
doGitClone(dstPath, &repoURL)(t)
gitRepo, err := git.OpenRepository(t.Context(), dstPath)
assert.NoError(t, err)
defer gitRepo.Close()
// create agit branch from current commit
doGitCreateBranch(dstPath, "test-agit-push")(t)
doGitCheckoutWriteFileCommit(localGitAddCommitOptions{
LocalRepoPath: dstPath,
CheckoutBranch: "main",
TreeFilePath: "README.md",
TreeFileContent: "Some changes to README file to main cause conflict",
})(t)
err = gitcmd.NewCommand("push", "origin", "main").WithDir(dstPath).Run(t.Context())
assert.NoError(t, err)
// check out back to agit branch and change the same file
doGitCheckoutWriteFileCommit(localGitAddCommitOptions{
LocalRepoPath: dstPath,
CheckoutBranch: "test-agit-push",
TreeFilePath: "README.md",
TreeFileContent: "Some changes to README file for agit branch",
})(t)
// push to create an agit pull request
err = gitcmd.NewCommand("push", "origin",
"-o", "title=agit-test-title", "-o", "description=agit-test-description",
"-o", "topic=head-branch-name",
"HEAD:refs/for/main",
).WithDir(dstPath).Run(t.Context())
assert.NoError(t, err)
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
RepoID: baseRepo.ID,
Title: "agit-test-title",
})
assert.NoError(t, issue.LoadPullRequest(t.Context()))
conflictingPR := issue.PullRequest
// Ensure conflictedFiles is populated.
assert.Equal(t, []string{"README.md"}, conflictingPR.ConflictedFiles)
// Check if status is correct.
assert.Equal(t, issues_model.PullRequestStatusConflict, conflictingPR.Status)
// Ensure that mergeable returns false
assert.False(t, conflictingPR.Mergeable(t.Context()))
}