This commit is contained in:
Lunny Xiao 2025-03-11 23:35:31 +00:00 committed by GitHub
commit 85d86b1c8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1161 additions and 562 deletions

View File

@ -93,3 +93,65 @@
is_deleted: false
deleted_by_id: 0
deleted_unix: 0
-
id: 16
repo_id: 1
name: 'DefaultBranch'
commit_id: '90c1019714259b24fb81711d4416ac0f18667dfa'
commit_message: 'add license'
commit_time: 1709259547
pusher_id: 1
is_deleted: false
-
id: 17
repo_id: 1
name: 'develop'
commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
commit_message: 'first commit'
commit_time: 978307100
pusher_id: 1
is_deleted: false
-
id: 18
repo_id: 11
name: 'develop'
commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
commit_message: 'Initial commit'
commit_time: 1489956479
pusher_id: 1
is_deleted: false
-
id: 19
repo_id: 10
name: 'DefaultBranch'
commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
commit_message: 'Initial commit'
commit_time: 1489956479
pusher_id: 1
is_deleted: false
-
id: 20
repo_id: 1
name: 'pr-to-update'
commit_id: '62fb502a7172d4453f0322a2cc85bddffa57f07a'
commit_message: 'add WoW File'
commit_time: 1579200695
pusher_id: 1
is_deleted: false
-
id: 21
repo_id: 10
name: 'develop'
commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
commit_message: 'Initial commit'
commit_time: 1489927679
pusher_id: 12
is_deleted: false
deleted_by_id: 0
deleted_unix: 0

View File

@ -4,12 +4,19 @@
package repo
import (
"errors"
"net/http"
"strings"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@ -52,30 +59,67 @@ func CompareDiff(ctx *context.APIContext) {
}
}
infoPath := ctx.PathParam("*")
infos := []string{ctx.Repo.Repository.DefaultBranch, ctx.Repo.Repository.DefaultBranch}
if infoPath != "" {
infos = strings.SplitN(infoPath, "...", 2)
if len(infos) != 2 {
if infos = strings.SplitN(infoPath, "..", 2); len(infos) != 2 {
infos = []string{ctx.Repo.Repository.DefaultBranch, infoPath}
pathParam := ctx.PathParam("*")
baseRepo := ctx.Repo.Repository
ci, err := common.ParseComparePathParams(ctx, pathParam, baseRepo, ctx.Repo.GitRepo)
if err != nil {
switch {
case user_model.IsErrUserNotExist(err):
ctx.APIErrorNotFound("GetUserByName")
case repo_model.IsErrRepoNotExist(err):
ctx.APIErrorNotFound("GetRepositoryByOwnerAndName")
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIErrorNotFound("ParseComparePathParams")
case git.IsErrNotExist(err):
ctx.APIErrorNotFound("ParseComparePathParams")
default:
ctx.APIError(http.StatusInternalServerError, err)
}
return
}
defer ci.Close()
// remove the check when we support compare with carets
if ci.CaretTimes > 0 {
ctx.APIErrorNotFound("Unsupported compare")
return
}
if !ci.IsSameRepo() {
// user should have permission to read headrepo's codes
permHead, err := access_model.GetUserRepoPermission(ctx, ci.HeadRepo, ctx.Doer)
if err != nil {
ctx.APIError(http.StatusInternalServerError, err)
return
}
if !permHead.CanRead(unit.TypeCode) {
if log.IsTrace() {
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
ctx.Doer,
ci.HeadRepo,
permHead)
}
ctx.APIErrorNotFound("Can't read headRepo UnitTypeCode")
return
}
}
compareResult, closer := parseCompareInfo(ctx, api.CreatePullRequestOption{Base: infos[0], Head: infos[1]})
if ctx.Written() {
ctx.Repo.PullRequest.SameRepo = ci.IsSameRepo()
log.Trace("Repo path: %q, base branch: %q, head branch: %q", ctx.Repo.GitRepo.Path, ci.BaseOriRef, ci.HeadOriRef)
ci.CompareInfo, err = ci.HeadGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), ci.BaseOriRef, ci.HeadOriRef, false, false)
if err != nil {
ctx.APIError(http.StatusInternalServerError, err)
return
}
defer closer()
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
files := ctx.FormString("files") == "" || ctx.FormBool("files")
apiCommits := make([]*api.Commit, 0, len(compareResult.compareInfo.Commits))
apiCommits := make([]*api.Commit, 0, len(ci.CompareInfo.Commits))
userCache := make(map[string]*user_model.User)
for i := 0; i < len(compareResult.compareInfo.Commits); i++ {
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareResult.compareInfo.Commits[i], userCache,
for i := 0; i < len(ci.CompareInfo.Commits); i++ {
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ci.CompareInfo.Commits[i], userCache,
convert.ToCommitOptions{
Stat: true,
Verification: verification,
@ -89,7 +133,7 @@ func CompareDiff(ctx *context.APIContext) {
}
ctx.JSON(http.StatusOK, &api.Compare{
TotalCommits: len(compareResult.compareInfo.Commits),
TotalCommits: len(ci.CompareInfo.Commits),
Commits: apiCommits,
})
}

View File

@ -27,8 +27,10 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/automerge"
"code.gitea.io/gitea/services/context"
@ -398,28 +400,72 @@ func CreatePullRequest(ctx *context.APIContext) {
}
var (
repo = ctx.Repo.Repository
baseRepo = ctx.Repo.Repository
labelIDs []int64
milestoneID int64
)
// Get repo/branch information
compareResult, closer := parseCompareInfo(ctx, form)
if ctx.Written() {
baseGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, baseRepo)
if err != nil {
ctx.APIError(http.StatusInternalServerError, err)
return
}
defer closer()
defer closer.Close()
if !compareResult.baseRef.IsBranch() || !compareResult.headRef.IsBranch() {
ctx.APIError(http.StatusUnprocessableEntity, "Invalid PullRequest: base and head must be branches")
// Get repo/branch information
ci, err := common.ParseComparePathParams(ctx, form.Base+"..."+form.Head, baseRepo, baseGitRepo)
if err != nil {
switch {
case user_model.IsErrUserNotExist(err):
ctx.APIErrorNotFound("GetUserByName")
case repo_model.IsErrRepoNotExist(err):
ctx.APIErrorNotFound("GetRepositoryByOwnerAndName")
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIErrorNotFound("ParseComparePathParams")
case git.IsErrNotExist(err):
ctx.APIErrorNotFound("ParseComparePathParams")
default:
ctx.APIError(http.StatusInternalServerError, err)
}
return
}
defer ci.Close()
if !ci.IsPull() {
ctx.APIError(http.StatusUnprocessableEntity, "Bad base or head refs, Only support branch to branch comparison")
return
}
// we just need to check the head repository's permission here because the base
// repository's permission is already checked in api.go with
// mustAllowPulls, reqRepoReader(unit.TypeCode)
if !ci.IsSameRepo() {
// user should have permission to read headrepo's codes
permHead, err := access_model.GetUserRepoPermission(ctx, ci.HeadRepo, ctx.Doer)
if err != nil {
ctx.APIError(http.StatusInternalServerError, err)
return
}
if !permHead.CanRead(unit.TypeCode) {
if log.IsTrace() {
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
ctx.Doer,
ci.HeadRepo,
permHead)
}
ctx.APIErrorNotFound("Can't read headRepo UnitTypeCode")
return
}
}
ci.CompareInfo, err = ci.HeadGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), ci.BaseOriRef, ci.HeadOriRef, false, false)
if err != nil {
ctx.APIError(http.StatusInternalServerError, err)
return
}
// Check if another PR exists with the same targets
existingPr, err := issues_model.GetUnmergedPullRequest(ctx, compareResult.headRepo.ID, ctx.Repo.Repository.ID,
compareResult.headRef.ShortName(), compareResult.baseRef.ShortName(),
issues_model.PullRequestFlowGithub,
)
existingPr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, baseRepo.ID, ci.HeadOriRef, ci.BaseOriRef, issues_model.PullRequestFlowGithub)
if err != nil {
if !issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorInternal(err)
@ -439,7 +485,7 @@ func CreatePullRequest(ctx *context.APIContext) {
}
if len(form.Labels) > 0 {
labels, err := issues_model.GetLabelsInRepoByIDs(ctx, ctx.Repo.Repository.ID, form.Labels)
labels, err := issues_model.GetLabelsInRepoByIDs(ctx, baseRepo.ID, form.Labels)
if err != nil {
ctx.APIErrorInternal(err)
return
@ -466,7 +512,7 @@ func CreatePullRequest(ctx *context.APIContext) {
}
if form.Milestone > 0 {
milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone)
milestone, err := issues_model.GetMilestoneByRepoID(ctx, baseRepo.ID, form.Milestone)
if err != nil {
if issues_model.IsErrMilestoneNotExist(err) {
ctx.APIErrorNotFound()
@ -485,7 +531,7 @@ func CreatePullRequest(ctx *context.APIContext) {
}
prIssue := &issues_model.Issue{
RepoID: repo.ID,
RepoID: baseRepo.ID,
Title: form.Title,
PosterID: ctx.Doer.ID,
Poster: ctx.Doer,
@ -495,13 +541,13 @@ func CreatePullRequest(ctx *context.APIContext) {
DeadlineUnix: deadlineUnix,
}
pr := &issues_model.PullRequest{
HeadRepoID: compareResult.headRepo.ID,
BaseRepoID: repo.ID,
HeadBranch: compareResult.headRef.ShortName(),
BaseBranch: compareResult.baseRef.ShortName(),
HeadRepo: compareResult.headRepo,
BaseRepo: repo,
MergeBase: compareResult.compareInfo.MergeBase,
HeadRepoID: ci.HeadRepo.ID,
BaseRepoID: baseRepo.ID,
HeadBranch: ci.HeadOriRef,
BaseBranch: ci.BaseOriRef,
HeadRepo: ci.HeadRepo,
BaseRepo: baseRepo,
MergeBase: ci.CompareInfo.MergeBase,
Type: issues_model.PullRequestGitea,
}
@ -523,19 +569,19 @@ func CreatePullRequest(ctx *context.APIContext) {
return
}
valid, err := access_model.CanBeAssigned(ctx, assignee, repo, true)
valid, err := access_model.CanBeAssigned(ctx, assignee, baseRepo, true)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !valid {
ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: baseRepo.Name})
return
}
}
prOpts := &pull_service.NewPullRequestOptions{
Repo: repo,
Repo: baseRepo,
Issue: prIssue,
LabelIDs: labelIDs,
PullRequest: pr,
@ -559,7 +605,7 @@ func CreatePullRequest(ctx *context.APIContext) {
return
}
log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
log.Trace("Pull request created: %d/%d", baseRepo.ID, prIssue.ID)
ctx.JSON(http.StatusCreated, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
}
@ -1068,135 +1114,6 @@ func MergePullRequest(ctx *context.APIContext) {
ctx.Status(http.StatusOK)
}
type parseCompareInfoResult struct {
headRepo *repo_model.Repository
headGitRepo *git.Repository
compareInfo *git.CompareInfo
baseRef git.RefName
headRef git.RefName
}
// parseCompareInfo returns non-nil if it succeeds, it always writes to the context and returns nil if it fails
func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (result *parseCompareInfoResult, closer func()) {
var err error
// Get compared branches information
// format: <base branch>...[<head repo>:]<head branch>
// base<-head: master...head:feature
// same repo: master...feature
baseRepo := ctx.Repo.Repository
baseRefToGuess := form.Base
headUser := ctx.Repo.Owner
headRefToGuess := form.Head
if headInfos := strings.Split(form.Head, ":"); len(headInfos) == 1 {
// If there is no head repository, it means pull request between same repository.
// Do nothing here because the head variables have been assigned above.
} else if len(headInfos) == 2 {
// There is a head repository (the head repository could also be the same base repo)
headRefToGuess = headInfos[1]
headUser, err = user_model.GetUserByName(ctx, headInfos[0])
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound("GetUserByName")
} else {
ctx.APIErrorInternal(err)
}
return nil, nil
}
} else {
ctx.APIErrorNotFound()
return nil, nil
}
isSameRepo := ctx.Repo.Owner.ID == headUser.ID
// Check if current user has fork of repository or in the same repository.
headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID)
if headRepo == nil && !isSameRepo {
err = baseRepo.GetBaseRepo(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
}
// Check if baseRepo's base repository is the same as headUser's repository.
if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID {
log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
ctx.APIErrorNotFound("GetBaseRepo")
return nil, nil
}
// Assign headRepo so it can be used below.
headRepo = baseRepo.BaseRepo
}
var headGitRepo *git.Repository
if isSameRepo {
headRepo = ctx.Repo.Repository
headGitRepo = ctx.Repo.GitRepo
closer = func() {} // no need to close the head repo because it shares the base repo
} else {
headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
}
closer = func() { _ = headGitRepo.Close() }
}
defer func() {
if result == nil && !isSameRepo {
_ = headGitRepo.Close()
}
}()
// user should have permission to read baseRepo's codes and pulls, NOT headRepo's
permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
}
if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase)
ctx.APIErrorNotFound("Can't read pulls or can't read UnitTypeCode")
return nil, nil
}
// user should have permission to read headRepo's codes
// TODO: could the logic be simplified if the headRepo is the same as the baseRepo? Need to think more about it.
permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
}
if !permHead.CanRead(unit.TypeCode) {
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", ctx.Doer, headRepo, permHead)
ctx.APIErrorNotFound("Can't read headRepo UnitTypeCode")
return nil, nil
}
baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefToGuess)
headRef := headGitRepo.UnstableGuessRefByShortName(headRefToGuess)
log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.GitRepo.Path, baseRefToGuess, baseRef, headRefToGuess, headRef)
baseRefValid := baseRef.IsBranch() || baseRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName), baseRef.ShortName())
headRefValid := headRef.IsBranch() || headRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(headRepo.ObjectFormatName), headRef.ShortName())
// Check if base&head ref are valid.
if !baseRefValid || !headRefValid {
ctx.APIErrorNotFound()
return nil, nil
}
compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseRef.ShortName(), headRef.ShortName(), false, false)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
}
result = &parseCompareInfoResult{headRepo: headRepo, headGitRepo: headGitRepo, compareInfo: compareInfo, baseRef: baseRef, headRef: headRef}
return result, closer
}
// UpdatePullRequest merge PR's baseBranch into headBranch
func UpdatePullRequest(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/update repository repoUpdatePullRequest

View File

@ -4,18 +4,343 @@
package common
import (
"context"
"strings"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/util"
)
type CompareRouter struct {
BaseOriRef string
BaseFullRef git.RefName
HeadOwnerName string
HeadRepoName string
HeadOriRef string
HeadFullRef git.RefName
CaretTimes int // ^ times after base ref
DotTimes int // 2(..) or 3(...)
}
func (cr *CompareRouter) DirectComparison() bool {
return cr.DotTimes == 2
}
func (cr *CompareRouter) CompareDots() string {
return strings.Repeat(".", cr.DotTimes)
}
func parseBase(base string) (string, int) {
parts := strings.SplitN(base, "^", 2)
if len(parts) == 1 {
return base, 0
}
return parts[0], len(parts[1]) + 1
}
func parseHead(head string) (string, string, string) {
paths := strings.SplitN(head, ":", 2)
if len(paths) == 1 {
return "", "", paths[0]
}
ownerRepo := strings.SplitN(paths[0], "/", 2)
if len(ownerRepo) == 1 {
return paths[0], "", paths[1]
}
return ownerRepo[0], ownerRepo[1], paths[1]
}
func parseCompareRouter(router string) (*CompareRouter, error) {
var basePart, headPart string
dotTimes := 3
parts := strings.Split(router, "...")
if len(parts) > 2 {
return nil, util.NewInvalidArgumentErrorf("invalid compare router: %s", router)
}
if len(parts) != 2 {
parts = strings.Split(router, "..")
if len(parts) == 1 {
headOwnerName, headRepoName, headRef := parseHead(router)
return &CompareRouter{
HeadOriRef: headRef,
HeadOwnerName: headOwnerName,
HeadRepoName: headRepoName,
DotTimes: dotTimes,
}, nil
} else if len(parts) > 2 {
return nil, util.NewInvalidArgumentErrorf("invalid compare router: %s", router)
}
dotTimes = 2
}
basePart, headPart = parts[0], parts[1]
baseRef, caretTimes := parseBase(basePart)
headOwnerName, headRepoName, headRef := parseHead(headPart)
return &CompareRouter{
BaseOriRef: baseRef,
HeadOriRef: headRef,
HeadOwnerName: headOwnerName,
HeadRepoName: headRepoName,
CaretTimes: caretTimes,
DotTimes: dotTimes,
}, nil
}
// CompareInfo represents the collected results from ParseCompareInfo
type CompareInfo struct {
HeadUser *user_model.User
HeadRepo *repo_model.Repository
HeadGitRepo *git.Repository
CompareInfo *git.CompareInfo
BaseBranch string
HeadBranch string
DirectComparison bool
*CompareRouter
BaseRepo *repo_model.Repository
HeadUser *user_model.User
HeadRepo *repo_model.Repository
HeadGitRepo *git.Repository
CompareInfo *git.CompareInfo
close func()
IsBaseCommit bool
IsHeadCommit bool
}
func (cr *CompareInfo) IsSameRepo() bool {
return cr.HeadRepo.ID == cr.BaseRepo.ID
}
func (cr *CompareInfo) IsSameRef() bool {
return cr.IsSameRepo() && cr.BaseOriRef == cr.HeadOriRef
}
// display pull related information or not
func (cr *CompareInfo) IsPull() bool {
return cr.CaretTimes == 0 && !cr.DirectComparison() &&
cr.BaseFullRef.IsBranch() && (cr.HeadRepo == nil || cr.HeadFullRef.IsBranch())
}
func (cr *CompareInfo) Close() {
if cr.close != nil {
cr.close()
}
}
// detectFullRef detects a short name as a branch, tag or commit's full ref name and type.
// It's the same job as git.UnstableGuessRefByShortName but with a database read instead of git read.
func detectFullRef(ctx context.Context, repoID int64, gitRepo *git.Repository, oriRef string) (git.RefName, bool, error) {
b, err := git_model.GetBranch(ctx, repoID, oriRef)
if err != nil && !git_model.IsErrBranchNotExist(err) {
return "", false, err
}
if b != nil && !b.IsDeleted {
return git.RefNameFromBranch(oriRef), false, nil
}
rel, err := repo_model.GetRelease(ctx, repoID, oriRef)
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
return "", false, err
}
if rel != nil && rel.Sha1 != "" {
return git.RefNameFromTag(oriRef), false, nil
}
commitObjectID, err := gitRepo.ConvertToGitID(oriRef)
if err != nil {
return "", false, err
}
return git.RefName(commitObjectID.String()), true, nil
}
func findHeadRepo(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64) (*repo_model.Repository, error) {
if baseRepo.IsFork {
curRepo := baseRepo
for curRepo.OwnerID != headUserID { // We assume the fork deepth is not too deep.
if err := curRepo.GetBaseRepo(ctx); err != nil {
return nil, err
}
if curRepo.BaseRepo == nil {
return findHeadRepoFromRootBase(ctx, curRepo, headUserID, 3)
}
curRepo = curRepo.BaseRepo
}
return curRepo, nil
}
return findHeadRepoFromRootBase(ctx, baseRepo, headUserID, 3)
}
func findHeadRepoFromRootBase(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64, traverseLevel int) (*repo_model.Repository, error) {
if traverseLevel == 0 {
return nil, nil
}
// test if we are lucky
repo, err := repo_model.GetUserFork(ctx, baseRepo.ID, headUserID)
if err != nil {
return nil, err
}
if repo != nil {
return repo, nil
}
firstLevelForkedRepo, err := repo_model.GetRepositoriesByForkID(ctx, baseRepo.ID)
if err != nil {
return nil, err
}
for _, repo := range firstLevelForkedRepo {
forked, err := findHeadRepoFromRootBase(ctx, repo, headUserID, traverseLevel-1)
if err != nil {
return nil, err
}
if forked != nil {
return forked, nil
}
}
return nil, nil
}
func getRootRepo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) {
curRepo := repo
for curRepo.IsFork {
if err := curRepo.GetBaseRepo(ctx); err != nil {
return nil, err
}
if curRepo.BaseRepo == nil {
break
}
curRepo = curRepo.BaseRepo
}
return curRepo, nil
}
// ParseComparePathParams Get compare information
// A full compare url is of the form:
//
// 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch}
// 2. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}:{:headBranch}
// 3. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}/{:headRepoName}:{:headBranch}
// 4. /{:baseOwner}/{:baseRepoName}/compare/{:headBranch}
// 5. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}:{:headBranch}
// 6. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}/{:headRepoName}:{:headBranch}
//
// Here we obtain the infoPath "{:baseBranch}...[{:headOwner}/{:headRepoName}:]{:headBranch}" as ctx.PathParam("*")
// with the :baseRepo in ctx.Repo.
//
// Note: Generally :headRepoName is not provided here - we are only passed :headOwner.
//
// How do we determine the :headRepo?
//
// 1. If :headOwner is not set then the :headRepo = :baseRepo
// 2. If :headOwner is set - then look for the fork of :baseRepo owned by :headOwner
// 3. But... :baseRepo could be a fork of :headOwner's repo - so check that
// 4. Now, :baseRepo and :headRepos could be forks of the same repo - so check that
//
// format: <base branch>...[<head repo>:]<head branch>
// base<-head: master...head:feature
// same repo: master...feature
func ParseComparePathParams(ctx context.Context, pathParam string, baseRepo *repo_model.Repository, baseGitRepo *git.Repository) (*CompareInfo, error) {
ci := &CompareInfo{BaseRepo: baseRepo}
var err error
if pathParam == "" {
ci.CompareRouter = &CompareRouter{
HeadOriRef: baseRepo.DefaultBranch,
DotTimes: 3,
}
} else {
ci.CompareRouter, err = parseCompareRouter(pathParam)
if err != nil {
return nil, err
}
}
if ci.BaseOriRef == "" {
ci.BaseOriRef = baseRepo.DefaultBranch
}
if (ci.HeadOwnerName == "" && ci.HeadRepoName == "") ||
(ci.HeadOwnerName == baseRepo.Owner.Name && ci.HeadRepoName == baseRepo.Name) {
ci.HeadOwnerName = baseRepo.Owner.Name
ci.HeadRepoName = baseRepo.Name
ci.HeadUser = baseRepo.Owner
ci.HeadRepo = baseRepo
ci.HeadGitRepo = baseGitRepo
} else {
if ci.HeadOwnerName == baseRepo.Owner.Name {
ci.HeadUser = baseRepo.Owner
if ci.HeadRepoName == "" {
ci.HeadRepoName = baseRepo.Name
ci.HeadRepo = baseRepo
}
} else {
ci.HeadUser, err = user_model.GetUserByName(ctx, ci.HeadOwnerName)
if err != nil {
return nil, err
}
}
if ci.HeadRepo == nil {
if ci.HeadRepoName != "" {
ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, ci.HeadOwnerName, ci.HeadRepoName)
} else {
ci.HeadRepo, err = findHeadRepo(ctx, baseRepo, ci.HeadUser.ID)
}
if err != nil {
return nil, err
}
}
if ci.HeadRepo != nil {
ci.HeadRepo.Owner = ci.HeadUser
ci.HeadGitRepo, err = gitrepo.OpenRepository(ctx, ci.HeadRepo)
if err != nil {
return nil, err
}
ci.close = func() {
if ci.HeadGitRepo != nil {
ci.HeadGitRepo.Close()
}
}
}
}
ci.BaseFullRef, ci.IsBaseCommit, err = detectFullRef(ctx, baseRepo.ID, baseGitRepo, ci.BaseOriRef)
if err != nil {
ci.Close()
return nil, err
}
if ci.HeadRepo != nil {
ci.HeadFullRef, ci.IsHeadCommit, err = detectFullRef(ctx, ci.HeadRepo.ID, ci.HeadGitRepo, ci.HeadOriRef)
if err != nil {
ci.Close()
return nil, err
}
}
return ci, nil
}
func (cr *CompareInfo) LoadRootRepoAndOwnForkRepo(ctx context.Context, baseRepo *repo_model.Repository, doer *user_model.User) (*repo_model.Repository, *repo_model.Repository, error) {
// find root repo
var rootRepo *repo_model.Repository
var err error
if !baseRepo.IsFork {
rootRepo = baseRepo
} else {
if !cr.HeadRepo.IsFork {
rootRepo = cr.HeadRepo
} else {
rootRepo, err = getRootRepo(ctx, baseRepo)
if err != nil {
return nil, nil, err
}
}
}
// find ownfork repo
var ownForkRepo *repo_model.Repository
if doer != nil && cr.HeadRepo.OwnerID != doer.ID && baseRepo.OwnerID != doer.ID {
ownForkRepo, err = findHeadRepo(ctx, baseRepo, doer.ID)
if err != nil {
return nil, nil, err
}
}
return rootRepo, ownForkRepo, nil
}

View File

@ -0,0 +1,496 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"testing"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"github.com/stretchr/testify/assert"
)
func TestCompareRouters(t *testing.T) {
kases := []struct {
router string
compareRouter *CompareRouter
}{
{
router: "",
compareRouter: &CompareRouter{
BaseOriRef: "",
HeadOriRef: "",
DotTimes: 3,
},
},
{
router: "main...develop",
compareRouter: &CompareRouter{
BaseOriRef: "main",
HeadOriRef: "develop",
DotTimes: 3,
},
},
{
router: "main..develop",
compareRouter: &CompareRouter{
BaseOriRef: "main",
HeadOriRef: "develop",
DotTimes: 2,
},
},
{
router: "main^...develop",
compareRouter: &CompareRouter{
BaseOriRef: "main",
HeadOriRef: "develop",
CaretTimes: 1,
DotTimes: 3,
},
},
{
router: "main^^^^^...develop",
compareRouter: &CompareRouter{
BaseOriRef: "main",
HeadOriRef: "develop",
CaretTimes: 5,
DotTimes: 3,
},
},
{
router: "develop",
compareRouter: &CompareRouter{
HeadOriRef: "develop",
DotTimes: 3,
},
},
{
router: "lunny/forked_repo:develop",
compareRouter: &CompareRouter{
HeadOwnerName: "lunny",
HeadRepoName: "forked_repo",
HeadOriRef: "develop",
DotTimes: 3,
},
},
{
router: "main...lunny/forked_repo:develop",
compareRouter: &CompareRouter{
BaseOriRef: "main",
HeadOwnerName: "lunny",
HeadRepoName: "forked_repo",
HeadOriRef: "develop",
DotTimes: 3,
},
},
{
router: "main...lunny/forked_repo:develop",
compareRouter: &CompareRouter{
BaseOriRef: "main",
HeadOwnerName: "lunny",
HeadRepoName: "forked_repo",
HeadOriRef: "develop",
DotTimes: 3,
},
},
{
router: "main^...lunny/forked_repo:develop",
compareRouter: &CompareRouter{
BaseOriRef: "main",
HeadOwnerName: "lunny",
HeadRepoName: "forked_repo",
HeadOriRef: "develop",
DotTimes: 3,
CaretTimes: 1,
},
},
{
router: "v1.0...v1.1",
compareRouter: &CompareRouter{
BaseOriRef: "v1.0",
HeadOriRef: "v1.1",
DotTimes: 3,
},
},
{
router: "teabot-patch-1...v0.0.1",
compareRouter: &CompareRouter{
BaseOriRef: "teabot-patch-1",
HeadOriRef: "v0.0.1",
DotTimes: 3,
},
},
{
router: "teabot:feature1",
compareRouter: &CompareRouter{
HeadOwnerName: "teabot",
HeadOriRef: "feature1",
DotTimes: 3,
},
},
{
router: "8eb19a5ae19abae15c0666d4ab98906139a7f439...283c030497b455ecfa759d4649f9f8b45158742e",
compareRouter: &CompareRouter{
BaseOriRef: "8eb19a5ae19abae15c0666d4ab98906139a7f439",
HeadOriRef: "283c030497b455ecfa759d4649f9f8b45158742e",
DotTimes: 3,
},
},
}
for _, kase := range kases {
t.Run(kase.router, func(t *testing.T) {
r, err := parseCompareRouter(kase.router)
assert.NoError(t, err)
assert.EqualValues(t, kase.compareRouter, r)
})
}
}
func Test_ParseComparePathParams(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.NotNil(t, repo1)
assert.NoError(t, repo1.LoadOwner(db.DefaultContext))
gitRepo1, err := gitrepo.OpenRepository(t.Context(), repo1)
assert.NoError(t, err)
defer gitRepo1.Close()
repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
assert.NotNil(t, repo10)
assert.NoError(t, repo10.LoadOwner(db.DefaultContext))
gitRepo10, err := gitrepo.OpenRepository(t.Context(), repo10)
assert.NoError(t, err)
defer gitRepo10.Close()
repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
assert.NotNil(t, repo11)
assert.NoError(t, repo11.LoadOwner(db.DefaultContext))
gitRepo11, err := gitrepo.OpenRepository(t.Context(), repo11)
assert.NoError(t, err)
defer gitRepo11.Close()
assert.True(t, repo11.IsFork) // repo11 is a fork of repo10
kases := []struct {
repoName string
hasClose bool
router string
compareInfo *CompareInfo
}{
{
repoName: "repo1",
router: "",
compareInfo: &CompareInfo{
CompareRouter: &CompareRouter{
BaseOriRef: "master",
BaseFullRef: git.RefNameFromBranch("master"),
HeadOriRef: "master",
HeadFullRef: git.RefNameFromBranch("master"),
HeadOwnerName: repo1.OwnerName,
HeadRepoName: repo1.Name,
DotTimes: 3,
},
BaseRepo: repo1,
HeadUser: repo1.Owner,
HeadRepo: repo1,
HeadGitRepo: gitRepo1,
},
},
{
repoName: "repo1",
router: "master...branch2",
compareInfo: &CompareInfo{
CompareRouter: &CompareRouter{
BaseOriRef: "master",
BaseFullRef: git.RefNameFromBranch("master"),
HeadOriRef: "branch2",
HeadFullRef: git.RefNameFromBranch("branch2"),
HeadOwnerName: repo1.OwnerName,
HeadRepoName: repo1.Name,
DotTimes: 3,
},
BaseRepo: repo1,
HeadUser: repo1.Owner,
HeadRepo: repo1,
HeadGitRepo: gitRepo1,
},
},
{
repoName: "repo1",
router: "DefaultBranch..branch2",
compareInfo: &CompareInfo{
CompareRouter: &CompareRouter{
BaseOriRef: "DefaultBranch",
BaseFullRef: git.RefNameFromBranch("DefaultBranch"),
HeadOriRef: "branch2",
HeadFullRef: git.RefNameFromBranch("branch2"),
HeadOwnerName: repo1.Owner.Name,
HeadRepoName: repo1.Name,
DotTimes: 2,
},
BaseRepo: repo1,
HeadUser: repo1.Owner,
HeadRepo: repo1,
HeadGitRepo: gitRepo1,
},
},
{
repoName: "repo1",
router: "DefaultBranch^...branch2",
compareInfo: &CompareInfo{
CompareRouter: &CompareRouter{
BaseOriRef: "DefaultBranch",
BaseFullRef: git.RefNameFromBranch("DefaultBranch"),
HeadOriRef: "branch2",
HeadFullRef: git.RefNameFromBranch("branch2"),
HeadOwnerName: repo1.Owner.Name,
HeadRepoName: repo1.Name,
CaretTimes: 1,
DotTimes: 3,
},
BaseRepo: repo1,
HeadUser: repo1.Owner,
HeadRepo: repo1,
HeadGitRepo: gitRepo1,
},
},
{
repoName: "repo1",
router: "branch2",
compareInfo: &CompareInfo{
CompareRouter: &CompareRouter{
BaseOriRef: repo1.DefaultBranch,
BaseFullRef: git.RefNameFromBranch(repo1.DefaultBranch),
HeadOriRef: "branch2",
HeadOwnerName: repo1.Owner.Name,
HeadRepoName: repo1.Name,
HeadFullRef: git.RefNameFromBranch("branch2"),
DotTimes: 3,
},
BaseRepo: repo1,
HeadUser: repo1.Owner,
HeadRepo: repo1,
HeadGitRepo: gitRepo1,
},
},
{
repoName: "repo10",
hasClose: true,
router: "user13/repo11:develop",
compareInfo: &CompareInfo{
CompareRouter: &CompareRouter{
BaseOriRef: repo10.DefaultBranch,
BaseFullRef: git.RefNameFromBranch(repo10.DefaultBranch),
HeadOwnerName: "user13",
HeadRepoName: "repo11",
HeadOriRef: "develop",
HeadFullRef: git.RefNameFromBranch("develop"),
DotTimes: 3,
},
BaseRepo: repo10,
HeadUser: repo11.Owner,
HeadRepo: repo11,
HeadGitRepo: gitRepo11,
},
},
{
repoName: "repo10",
hasClose: true,
router: "master...user13/repo11:develop",
compareInfo: &CompareInfo{
CompareRouter: &CompareRouter{
BaseOriRef: "master",
BaseFullRef: git.RefNameFromBranch("master"),
HeadOwnerName: "user13",
HeadRepoName: "repo11",
HeadOriRef: "develop",
HeadFullRef: git.RefNameFromBranch("develop"),
DotTimes: 3,
},
BaseRepo: repo10,
HeadUser: repo11.Owner,
HeadRepo: repo11,
HeadGitRepo: gitRepo11,
},
},
{
repoName: "repo10",
hasClose: true,
router: "DefaultBranch^...user13/repo11:develop",
compareInfo: &CompareInfo{
CompareRouter: &CompareRouter{
BaseOriRef: "DefaultBranch",
BaseFullRef: git.RefNameFromBranch("DefaultBranch"),
HeadOwnerName: "user13",
HeadRepoName: "repo11",
HeadOriRef: "develop",
HeadFullRef: git.RefNameFromBranch("develop"),
DotTimes: 3,
CaretTimes: 1,
},
BaseRepo: repo10,
HeadUser: repo11.Owner,
HeadRepo: repo11,
HeadGitRepo: gitRepo11,
},
},
{
repoName: "repo11",
hasClose: true,
router: "user12/repo10:master",
compareInfo: &CompareInfo{
CompareRouter: &CompareRouter{
BaseOriRef: repo11.DefaultBranch,
BaseFullRef: git.RefNameFromBranch(repo11.DefaultBranch),
HeadOwnerName: "user12",
HeadRepoName: "repo10",
HeadOriRef: "master",
HeadFullRef: git.RefNameFromBranch("master"),
DotTimes: 3,
},
BaseRepo: repo11,
HeadUser: repo10.Owner,
HeadRepo: repo10,
HeadGitRepo: gitRepo10,
},
},
{
repoName: "repo1",
router: "master...v1.1",
compareInfo: &CompareInfo{
CompareRouter: &CompareRouter{
BaseOriRef: "master",
BaseFullRef: git.RefNameFromBranch("master"),
HeadOwnerName: repo1.Owner.Name,
HeadRepoName: repo1.Name,
HeadOriRef: "v1.1",
HeadFullRef: git.RefNameFromTag("v1.1"),
DotTimes: 3,
},
BaseRepo: repo1,
HeadUser: repo1.Owner,
HeadRepo: repo1,
HeadGitRepo: gitRepo1,
},
},
{
repoName: "repo10",
hasClose: true,
router: "user13:develop",
compareInfo: &CompareInfo{
CompareRouter: &CompareRouter{
BaseOriRef: repo10.DefaultBranch,
BaseFullRef: git.RefNameFromBranch(repo10.DefaultBranch),
HeadOwnerName: "user13",
HeadOriRef: "develop",
HeadFullRef: git.RefNameFromBranch("develop"),
DotTimes: 3,
},
BaseRepo: repo10,
HeadUser: repo11.Owner,
HeadRepo: repo11,
HeadGitRepo: gitRepo11,
},
},
{
repoName: "repo1",
router: "65f1bf27bc3bf70f64657658635e66094edbcb4d...90c1019714259b24fb81711d4416ac0f18667dfa",
compareInfo: &CompareInfo{
CompareRouter: &CompareRouter{
BaseOriRef: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
BaseFullRef: git.RefName("65f1bf27bc3bf70f64657658635e66094edbcb4d"),
HeadOwnerName: repo1.Owner.Name,
HeadRepoName: repo1.Name,
HeadOriRef: "90c1019714259b24fb81711d4416ac0f18667dfa",
HeadFullRef: git.RefName("90c1019714259b24fb81711d4416ac0f18667dfa"),
DotTimes: 3,
},
BaseRepo: repo1,
HeadUser: repo1.Owner,
HeadRepo: repo1,
HeadGitRepo: gitRepo1,
IsBaseCommit: true,
IsHeadCommit: true,
},
},
{
repoName: "repo1",
router: "5c050d3b6d2db231ab1f64e324f1b6b9a0b181c2^...985f0301dba5e7b34be866819cd15ad3d8f508ee",
compareInfo: &CompareInfo{
CompareRouter: &CompareRouter{
BaseOriRef: "5c050d3b6d2db231ab1f64e324f1b6b9a0b181c2",
BaseFullRef: git.RefName("5c050d3b6d2db231ab1f64e324f1b6b9a0b181c2"),
HeadOwnerName: repo1.Owner.Name,
HeadRepoName: repo1.Name,
HeadOriRef: "985f0301dba5e7b34be866819cd15ad3d8f508ee",
HeadFullRef: git.RefName("985f0301dba5e7b34be866819cd15ad3d8f508ee"),
DotTimes: 3,
CaretTimes: 1,
},
BaseRepo: repo1,
HeadUser: repo1.Owner,
HeadRepo: repo1,
HeadGitRepo: gitRepo1,
IsBaseCommit: true,
IsHeadCommit: true,
},
},
{
repoName: "repo1",
hasClose: true,
router: "user12/repo10:master",
compareInfo: &CompareInfo{
CompareRouter: &CompareRouter{
BaseOriRef: repo11.DefaultBranch,
BaseFullRef: git.RefNameFromBranch(repo11.DefaultBranch),
HeadOwnerName: "user12",
HeadRepoName: "repo10",
HeadOriRef: "master",
HeadFullRef: git.RefNameFromBranch("master"),
DotTimes: 3,
},
BaseRepo: repo1,
HeadUser: repo10.Owner,
HeadRepo: repo10,
HeadGitRepo: gitRepo10,
},
},
}
for _, kase := range kases {
t.Run(kase.router, func(t *testing.T) {
var baseRepo *repo_model.Repository
var baseGitRepo *git.Repository
if kase.repoName == "repo1" {
baseRepo = repo1
baseGitRepo = gitRepo1
} else if kase.repoName == "repo10" {
baseRepo = repo10
baseGitRepo = gitRepo10
} else if kase.repoName == "repo11" {
baseRepo = repo11
baseGitRepo = gitRepo11
} else {
t.Fatalf("unknown repo name: %s", kase.router)
}
r, err := ParseComparePathParams(t.Context(), kase.router, baseRepo, baseGitRepo)
assert.NoError(t, err)
if kase.hasClose {
assert.NotNil(t, r.close)
r.close = nil // close is a function, so we can't compare it
}
assert.EqualValues(t, *kase.compareInfo.CompareRouter, *r.CompareRouter)
assert.EqualValues(t, *kase.compareInfo.BaseRepo, *r.BaseRepo)
assert.EqualValues(t, *kase.compareInfo.HeadUser, *r.HeadUser)
assert.EqualValues(t, *kase.compareInfo.HeadRepo, *r.HeadRepo)
assert.EqualValues(t, kase.compareInfo.HeadGitRepo.Path, r.HeadGitRepo.Path)
assert.EqualValues(t, kase.compareInfo.IsBaseCommit, r.IsBaseCommit)
assert.EqualValues(t, kase.compareInfo.IsHeadCommit, r.IsHeadCommit)
})
}
}

View File

@ -14,6 +14,7 @@ import (
"net/http"
"net/url"
"path/filepath"
"slices"
"strings"
"code.gitea.io/gitea/models/db"
@ -188,254 +189,46 @@ func setCsvCompareContext(ctx *context.Context) {
}
// ParseCompareInfo parse compare info between two commit for preparing comparing references
// Permission check for base repository's code read should be checked before invoking this function
func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
baseRepo := ctx.Repo.Repository
ci := &common.CompareInfo{}
fileOnly := ctx.FormBool("file-only")
pathParam := ctx.PathParam("*")
baseRepo := ctx.Repo.Repository
// Get compared branches information
// A full compare url is of the form:
//
// 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch}
// 2. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}:{:headBranch}
// 3. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}/{:headRepoName}:{:headBranch}
// 4. /{:baseOwner}/{:baseRepoName}/compare/{:headBranch}
// 5. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}:{:headBranch}
// 6. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}/{:headRepoName}:{:headBranch}
//
// Here we obtain the infoPath "{:baseBranch}...[{:headOwner}/{:headRepoName}:]{:headBranch}" as ctx.PathParam("*")
// with the :baseRepo in ctx.Repo.
//
// Note: Generally :headRepoName is not provided here - we are only passed :headOwner.
//
// How do we determine the :headRepo?
//
// 1. If :headOwner is not set then the :headRepo = :baseRepo
// 2. If :headOwner is set - then look for the fork of :baseRepo owned by :headOwner
// 3. But... :baseRepo could be a fork of :headOwner's repo - so check that
// 4. Now, :baseRepo and :headRepos could be forks of the same repo - so check that
//
// format: <base branch>...[<head repo>:]<head branch>
// base<-head: master...head:feature
// same repo: master...feature
var (
isSameRepo bool
infoPath string
err error
)
infoPath = ctx.PathParam("*")
var infos []string
if infoPath == "" {
infos = []string{baseRepo.DefaultBranch, baseRepo.DefaultBranch}
} else {
infos = strings.SplitN(infoPath, "...", 2)
if len(infos) != 2 {
if infos = strings.SplitN(infoPath, "..", 2); len(infos) == 2 {
ci.DirectComparison = true
ctx.Data["PageIsComparePull"] = false
} else {
infos = []string{baseRepo.DefaultBranch, infoPath}
}
}
}
ctx.Data["BaseName"] = baseRepo.OwnerName
ci.BaseBranch = infos[0]
ctx.Data["BaseBranch"] = ci.BaseBranch
// If there is no head repository, it means compare between same repository.
headInfos := strings.Split(infos[1], ":")
if len(headInfos) == 1 {
isSameRepo = true
ci.HeadUser = ctx.Repo.Owner
ci.HeadBranch = headInfos[0]
} else if len(headInfos) == 2 {
headInfosSplit := strings.Split(headInfos[0], "/")
if len(headInfosSplit) == 1 {
ci.HeadUser, err = user_model.GetUserByName(ctx, headInfos[0])
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.NotFound(nil)
} else {
ctx.ServerError("GetUserByName", err)
}
return nil
}
ci.HeadBranch = headInfos[1]
isSameRepo = ci.HeadUser.ID == ctx.Repo.Owner.ID
if isSameRepo {
ci.HeadRepo = baseRepo
}
} else {
ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, headInfosSplit[0], headInfosSplit[1])
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.NotFound(nil)
} else {
ctx.ServerError("GetRepositoryByOwnerAndName", err)
}
return nil
}
if err := ci.HeadRepo.LoadOwner(ctx); err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.NotFound(nil)
} else {
ctx.ServerError("GetUserByName", err)
}
return nil
}
ci.HeadBranch = headInfos[1]
ci.HeadUser = ci.HeadRepo.Owner
isSameRepo = ci.HeadRepo.ID == ctx.Repo.Repository.ID
}
} else {
ctx.NotFound(nil)
return nil
}
ctx.Data["HeadUser"] = ci.HeadUser
ctx.Data["HeadBranch"] = ci.HeadBranch
ctx.Repo.PullRequest.SameRepo = isSameRepo
// Check if base branch is valid.
baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch)
baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(ci.BaseBranch)
baseIsTag := ctx.Repo.GitRepo.IsTagExist(ci.BaseBranch)
if !baseIsCommit && !baseIsBranch && !baseIsTag {
// Check if baseBranch is short sha commit hash
if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(ci.BaseBranch); baseCommit != nil {
ci.BaseBranch = baseCommit.ID.String()
ctx.Data["BaseBranch"] = ci.BaseBranch
baseIsCommit = true
} else if ci.BaseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
if isSameRepo {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadBranch))
} else {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadRepo.FullName()) + ":" + util.PathEscapeSegments(ci.HeadBranch))
}
return nil
} else {
ctx.NotFound(nil)
return nil
}
}
ctx.Data["BaseIsCommit"] = baseIsCommit
ctx.Data["BaseIsBranch"] = baseIsBranch
ctx.Data["BaseIsTag"] = baseIsTag
ctx.Data["IsPull"] = true
// Now we have the repository that represents the base
// The current base and head repositories and branches may not
// actually be the intended branches that the user wants to
// create a pull-request from - but also determining the head
// repo is difficult.
// We will want therefore to offer a few repositories to set as
// our base and head
// 1. First if the baseRepo is a fork get the "RootRepo" it was
// forked from
var rootRepo *repo_model.Repository
if baseRepo.IsFork {
err = baseRepo.GetBaseRepo(ctx)
if err != nil {
if !repo_model.IsErrRepoNotExist(err) {
ctx.ServerError("Unable to find root repo", err)
return nil
}
} else {
rootRepo = baseRepo.BaseRepo
}
}
// 2. Now if the current user is not the owner of the baseRepo,
// check if they have a fork of the base repo and offer that as
// "OwnForkRepo"
var ownForkRepo *repo_model.Repository
if ctx.Doer != nil && baseRepo.OwnerID != ctx.Doer.ID {
repo := repo_model.GetForkedRepo(ctx, ctx.Doer.ID, baseRepo.ID)
if repo != nil {
ownForkRepo = repo
ctx.Data["OwnForkRepo"] = ownForkRepo
}
}
has := ci.HeadRepo != nil
// 3. If the base is a forked from "RootRepo" and the owner of
// the "RootRepo" is the :headUser - set headRepo to that
if !has && rootRepo != nil && rootRepo.OwnerID == ci.HeadUser.ID {
ci.HeadRepo = rootRepo
has = true
}
// 4. If the ctx.Doer has their own fork of the baseRepo and the headUser is the ctx.Doer
// set the headRepo to the ownFork
if !has && ownForkRepo != nil && ownForkRepo.OwnerID == ci.HeadUser.ID {
ci.HeadRepo = ownForkRepo
has = true
}
// 5. If the headOwner has a fork of the baseRepo - use that
if !has {
ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ID)
has = ci.HeadRepo != nil
}
// 6. If the baseRepo is a fork and the headUser has a fork of that use that
if !has && baseRepo.IsFork {
ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ForkID)
has = ci.HeadRepo != nil
}
// 7. Otherwise if we're not the same repo and haven't found a repo give up
if !isSameRepo && !has {
ctx.Data["PageIsComparePull"] = false
}
// 8. Finally open the git repo
if isSameRepo {
ci.HeadRepo = ctx.Repo.Repository
ci.HeadGitRepo = ctx.Repo.GitRepo
} else if has {
ci.HeadGitRepo, err = gitrepo.OpenRepository(ctx, ci.HeadRepo)
if err != nil {
ctx.ServerError("OpenRepository", err)
return nil
}
defer ci.HeadGitRepo.Close()
} else {
ctx.NotFound(nil)
return nil
}
ctx.Data["HeadRepo"] = ci.HeadRepo
ctx.Data["BaseCompareRepo"] = ctx.Repo.Repository
// Now we need to assert that the ctx.Doer has permission to read
// the baseRepo's code and pulls
// (NOT headRepo's)
permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
ci, err := common.ParseComparePathParams(ctx, pathParam, baseRepo, ctx.Repo.GitRepo)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
switch {
case user_model.IsErrUserNotExist(err):
ctx.NotFound(nil)
case repo_model.IsErrRepoNotExist(err):
ctx.NotFound(nil)
case errors.Is(err, util.ErrInvalidArgument):
ctx.NotFound(nil)
case git.IsErrNotExist(err):
ctx.NotFound(nil)
default:
ctx.ServerError("ParseComparePathParams", err)
}
return nil
}
if !permBase.CanRead(unit.TypeCode) {
if log.IsTrace() {
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
ctx.Doer,
baseRepo,
permBase)
}
// remove the check when we support compare with carets
if ci.CaretTimes > 0 {
ctx.NotFound(nil)
return nil
}
if ci.BaseOriRef == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
if ci.IsSameRepo() {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadOriRef))
} else {
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadRepo.FullName()) + ":" + util.PathEscapeSegments(ci.HeadOriRef))
}
return nil
}
// If we're not merging from the same repo:
if !isSameRepo {
if !ci.IsSameRepo() {
// Assert ctx.Doer has permission to read headRepo's codes
permHead, err := access_model.GetUserRepoPermission(ctx, ci.HeadRepo, ctx.Doer)
if err != nil {
@ -455,107 +248,30 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
ctx.Data["CanWriteToHeadRepo"] = permHead.CanWrite(unit.TypeCode)
}
// If we have a rootRepo and it's different from:
// 1. the computed base
// 2. the computed head
// then get the branches of it
if rootRepo != nil &&
rootRepo.ID != ci.HeadRepo.ID &&
rootRepo.ID != baseRepo.ID {
canRead := access_model.CheckRepoUnitUser(ctx, rootRepo, ctx.Doer, unit.TypeCode)
if canRead {
ctx.Data["RootRepo"] = rootRepo
if !fileOnly {
branches, tags, err := getBranchesAndTagsForRepo(ctx, rootRepo)
if err != nil {
ctx.ServerError("GetBranchesForRepo", err)
return nil
}
ctx.Data["PageIsComparePull"] = ci.IsPull() && ctx.Repo.CanReadIssuesOrPulls(true)
ctx.Data["BaseName"] = baseRepo.OwnerName
ctx.Data["BaseBranch"] = ci.BaseOriRef
ctx.Data["HeadUser"] = ci.HeadUser
ctx.Data["HeadBranch"] = ci.HeadOriRef
ctx.Repo.PullRequest.SameRepo = ci.IsSameRepo()
ctx.Data["RootRepoBranches"] = branches
ctx.Data["RootRepoTags"] = tags
}
}
}
ctx.Data["BaseIsCommit"] = ci.IsBaseCommit
ctx.Data["BaseIsBranch"] = ci.BaseFullRef.IsBranch()
ctx.Data["BaseIsTag"] = ci.BaseFullRef.IsTag()
ctx.Data["IsPull"] = true
// If we have a ownForkRepo and it's different from:
// 1. The computed base
// 2. The computed head
// 3. The rootRepo (if we have one)
// then get the branches from it.
if ownForkRepo != nil &&
ownForkRepo.ID != ci.HeadRepo.ID &&
ownForkRepo.ID != baseRepo.ID &&
(rootRepo == nil || ownForkRepo.ID != rootRepo.ID) {
canRead := access_model.CheckRepoUnitUser(ctx, ownForkRepo, ctx.Doer, unit.TypeCode)
if canRead {
ctx.Data["OwnForkRepo"] = ownForkRepo
if !fileOnly {
branches, tags, err := getBranchesAndTagsForRepo(ctx, ownForkRepo)
if err != nil {
ctx.ServerError("GetBranchesForRepo", err)
return nil
}
ctx.Data["OwnForkRepoBranches"] = branches
ctx.Data["OwnForkRepoTags"] = tags
}
}
}
ctx.Data["HeadRepo"] = ci.HeadRepo
ctx.Data["BaseCompareRepo"] = ctx.Repo.Repository
ctx.Data["HeadIsCommit"] = ci.IsHeadCommit
ctx.Data["HeadIsBranch"] = ci.HeadFullRef.IsBranch()
ctx.Data["HeadIsTag"] = ci.HeadFullRef.IsTag()
// Check if head branch is valid.
headIsCommit := ci.HeadGitRepo.IsCommitExist(ci.HeadBranch)
headIsBranch := ci.HeadGitRepo.IsBranchExist(ci.HeadBranch)
headIsTag := ci.HeadGitRepo.IsTagExist(ci.HeadBranch)
if !headIsCommit && !headIsBranch && !headIsTag {
// Check if headBranch is short sha commit hash
if headCommit, _ := ci.HeadGitRepo.GetCommit(ci.HeadBranch); headCommit != nil {
ci.HeadBranch = headCommit.ID.String()
ctx.Data["HeadBranch"] = ci.HeadBranch
headIsCommit = true
} else {
ctx.NotFound(nil)
return nil
}
}
ctx.Data["HeadIsCommit"] = headIsCommit
ctx.Data["HeadIsBranch"] = headIsBranch
ctx.Data["HeadIsTag"] = headIsTag
// Treat as pull request if both references are branches
if ctx.Data["PageIsComparePull"] == nil {
ctx.Data["PageIsComparePull"] = headIsBranch && baseIsBranch
}
if ctx.Data["PageIsComparePull"] == true && !permBase.CanReadIssuesOrPulls(true) {
if log.IsTrace() {
log.Trace("Permission Denied: User: %-v cannot create/read pull requests in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
ctx.Doer,
baseRepo,
permBase)
}
ctx.NotFound(nil)
return nil
}
baseBranchRef := ci.BaseBranch
if baseIsBranch {
baseBranchRef = git.BranchPrefix + ci.BaseBranch
} else if baseIsTag {
baseBranchRef = git.TagPrefix + ci.BaseBranch
}
headBranchRef := ci.HeadBranch
if headIsBranch {
headBranchRef = git.BranchPrefix + ci.HeadBranch
} else if headIsTag {
headBranchRef = git.TagPrefix + ci.HeadBranch
}
ci.CompareInfo, err = ci.HeadGitRepo.GetCompareInfo(baseRepo.RepoPath(), baseBranchRef, headBranchRef, ci.DirectComparison, fileOnly)
ci.CompareInfo, err = ci.HeadGitRepo.GetCompareInfo(baseRepo.RepoPath(), ci.BaseFullRef.String(), ci.HeadFullRef.String(), ci.DirectComparison(), fileOnly)
if err != nil {
ctx.ServerError("GetCompareInfo", err)
return nil
}
if ci.DirectComparison {
if ci.DirectComparison() {
ctx.Data["BeforeCommitID"] = ci.CompareInfo.BaseCommitID
} else {
ctx.Data["BeforeCommitID"] = ci.CompareInfo.MergeBase
@ -583,14 +299,14 @@ func PrepareCompareDiff(
ctx.Data["AfterCommitID"] = headCommitID
if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) ||
if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison()) ||
headCommitID == ci.CompareInfo.BaseCommitID {
ctx.Data["IsNothingToCompare"] = true
if unit, err := repo.GetUnit(ctx, unit.TypePullRequests); err == nil {
config := unit.PullRequestsConfig()
if !config.AutodetectManualMerge {
allowEmptyPr := !(ci.BaseBranch == ci.HeadBranch && ctx.Repo.Repository.Name == ci.HeadRepo.Name)
allowEmptyPr := !(ci.BaseOriRef == ci.HeadOriRef && ctx.Repo.Repository.Name == ci.HeadRepo.Name)
ctx.Data["AllowEmptyPr"] = allowEmptyPr
return !allowEmptyPr
@ -602,7 +318,7 @@ func PrepareCompareDiff(
}
beforeCommitID := ci.CompareInfo.MergeBase
if ci.DirectComparison {
if ci.DirectComparison() {
beforeCommitID = ci.CompareInfo.BaseCommitID
}
@ -623,7 +339,7 @@ func PrepareCompareDiff(
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
MaxFiles: maxFiles,
WhitespaceBehavior: whitespaceBehavior,
DirectComparison: ci.DirectComparison,
DirectComparison: ci.DirectComparison(),
}, ctx.FormStrings("files")...)
if err != nil {
ctx.ServerError("GetDiff", err)
@ -679,7 +395,7 @@ func PrepareCompareDiff(
ctx.Data["content"] = strings.Join(body[1:], "\n")
}
} else {
title = ci.HeadBranch
title = ci.HeadOriRef
}
if len(title) > 255 {
var trailer string
@ -702,14 +418,8 @@ func PrepareCompareDiff(
return false
}
func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repository) (branches, tags []string, err error) {
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
return nil, nil, err
}
defer gitRepo.Close()
branches, err = git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repository) ([]string, []string, error) {
branches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
RepoID: repo.ID,
ListOptions: db.ListOptionsAll,
IsDeletedBranch: optional.Some(false),
@ -717,19 +427,88 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor
if err != nil {
return nil, nil, err
}
tags, err = gitRepo.GetTags(0, 0)
// always put default branch on the top if it exists
if slices.Contains(branches, repo.DefaultBranch) {
branches = util.SliceRemoveAll(branches, repo.DefaultBranch)
branches = append([]string{repo.DefaultBranch}, branches...)
}
tags, err := repo_model.GetTagNamesByRepoID(ctx, repo.ID)
if err != nil {
return nil, nil, err
}
return branches, tags, nil
}
func prepareCompareRepoBranchesTagsDropdowns(ctx *context.Context, ci *common.CompareInfo) {
baseRepo := ctx.Repo.Repository
// For compare repo branches
baseBranches, baseTags, err := getBranchesAndTagsForRepo(ctx, baseRepo)
if err != nil {
ctx.ServerError("getBranchesAndTagsForRepo", err)
return
}
ctx.Data["Branches"] = baseBranches
ctx.Data["Tags"] = baseTags
if ci.IsSameRepo() {
ctx.Data["HeadBranches"] = baseBranches
ctx.Data["HeadTags"] = baseTags
} else {
headBranches, headTags, err := getBranchesAndTagsForRepo(ctx, ci.HeadRepo)
if err != nil {
ctx.ServerError("getBranchesAndTagsForRepo", err)
return
}
ctx.Data["HeadBranches"] = headBranches
ctx.Data["HeadTags"] = headTags
}
rootRepo, ownForkRepo, err := ci.LoadRootRepoAndOwnForkRepo(ctx, baseRepo, ctx.Doer)
if err != nil {
ctx.ServerError("LoadRootRepoAndOwnForkRepo", err)
return
}
if rootRepo != nil &&
rootRepo.ID != ci.HeadRepo.ID &&
rootRepo.ID != baseRepo.ID {
canRead := access_model.CheckRepoUnitUser(ctx, rootRepo, ctx.Doer, unit.TypeCode)
if canRead {
ctx.Data["RootRepo"] = rootRepo
branches, tags, err := getBranchesAndTagsForRepo(ctx, rootRepo)
if err != nil {
ctx.ServerError("GetBranchesForRepo", err)
return
}
ctx.Data["RootRepoBranches"] = branches
ctx.Data["RootRepoTags"] = tags
}
}
if ownForkRepo != nil &&
ownForkRepo.ID != ci.HeadRepo.ID &&
ownForkRepo.ID != baseRepo.ID &&
(rootRepo == nil || ownForkRepo.ID != rootRepo.ID) {
ctx.Data["OwnForkRepo"] = ownForkRepo
branches, tags, err := getBranchesAndTagsForRepo(ctx, ownForkRepo)
if err != nil {
ctx.ServerError("GetBranchesForRepo", err)
return
}
ctx.Data["OwnForkRepoBranches"] = branches
ctx.Data["OwnForkRepoTags"] = tags
}
}
// CompareDiff show different from one commit to another commit
func CompareDiff(ctx *context.Context) {
ci := ParseCompareInfo(ctx)
defer func() {
if ci != nil && ci.HeadGitRepo != nil {
ci.HeadGitRepo.Close()
if ci != nil {
ci.Close()
}
}()
if ctx.Written() {
@ -740,7 +519,7 @@ func CompareDiff(ctx *context.Context) {
ctx.Data["DirectComparison"] = ci.DirectComparison
ctx.Data["OtherCompareSeparator"] = ".."
ctx.Data["CompareSeparator"] = "..."
if ci.DirectComparison {
if ci.DirectComparison() {
ctx.Data["CompareSeparator"] = ".."
ctx.Data["OtherCompareSeparator"] = "..."
}
@ -751,45 +530,19 @@ func CompareDiff(ctx *context.Context) {
return
}
baseTags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetTagNamesByRepoID", err)
return
}
ctx.Data["Tags"] = baseTags
fileOnly := ctx.FormBool("file-only")
if fileOnly {
ctx.HTML(http.StatusOK, tplDiffBox)
return
}
headBranches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
RepoID: ci.HeadRepo.ID,
ListOptions: db.ListOptionsAll,
IsDeletedBranch: optional.Some(false),
})
if err != nil {
ctx.ServerError("GetBranches", err)
return
}
ctx.Data["HeadBranches"] = headBranches
// For compare repo branches
PrepareBranchList(ctx)
prepareCompareRepoBranchesTagsDropdowns(ctx, ci)
if ctx.Written() {
return
}
headTags, err := repo_model.GetTagNamesByRepoID(ctx, ci.HeadRepo.ID)
if err != nil {
ctx.ServerError("GetTagNamesByRepoID", err)
return
}
ctx.Data["HeadTags"] = headTags
if ctx.Data["PageIsComparePull"] == true {
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadOriRef, ci.BaseOriRef, issues_model.PullRequestFlowGithub)
if err != nil {
if !issues_model.IsErrPullRequestNotExist(err) {
ctx.ServerError("GetUnmergedPullRequest", err)
@ -820,11 +573,8 @@ func CompareDiff(ctx *context.Context) {
}
beforeCommitID := ctx.Data["BeforeCommitID"].(string)
afterCommitID := ctx.Data["AfterCommitID"].(string)
separator := ci.CompareDots()
separator := "..."
if ci.DirectComparison {
separator = ".."
}
ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID)
ctx.Data["IsDiffCompare"] = true

View File

@ -1287,8 +1287,8 @@ func CompareAndPullRequestPost(ctx *context.Context) {
ci := ParseCompareInfo(ctx)
defer func() {
if ci != nil && ci.HeadGitRepo != nil {
ci.HeadGitRepo.Close()
if ci != nil {
ci.Close()
}
}()
if ctx.Written() {
@ -1336,8 +1336,8 @@ func CompareAndPullRequestPost(ctx *context.Context) {
pullRequest := &issues_model.PullRequest{
HeadRepoID: ci.HeadRepo.ID,
BaseRepoID: repo.ID,
HeadBranch: ci.HeadBranch,
BaseBranch: ci.BaseBranch,
HeadBranch: ci.HeadOriRef,
BaseBranch: ci.BaseOriRef,
HeadRepo: ci.HeadRepo,
BaseRepo: repo,
MergeBase: ci.CompareInfo.MergeBase,

View File

@ -303,7 +303,7 @@ func TestAPICreateBranchWithSyncBranches(t *testing.T) {
RepoID: 1,
})
assert.NoError(t, err)
assert.Len(t, branches, 4)
assert.Len(t, branches, 7)
// make a broke repository with no branch on database
_, err = db.DeleteByBean(db.DefaultContext, git_model.Branch{RepoID: 1})
@ -320,7 +320,7 @@ func TestAPICreateBranchWithSyncBranches(t *testing.T) {
RepoID: 1,
})
assert.NoError(t, err)
assert.Len(t, branches, 5)
assert.Len(t, branches, 8)
branches, err = db.Find[git_model.Branch](db.DefaultContext, git_model.FindBranchOptions{
RepoID: 1,

View File

@ -29,6 +29,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/queue"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
@ -343,6 +344,10 @@ func TestCantMergeUnrelated(t *testing.T) {
_, _, err = git.NewCommand("branch", "unrelated").AddDynamicArguments(commitSha).RunStdString(git.DefaultContext, &git.RunOpts{Dir: path})
assert.NoError(t, err)
// we created a branch to git repository directly, now we need to do a sync to make it available in the database
_, err = repo_module.SyncRepoBranches(db.DefaultContext, repo1.ID, user1.ID)
assert.NoError(t, err)
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
// Use API to create a conflicting pr