From 7fe084000861e6b19fd3dfc5f29205cd1b83cad7 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 7 Dec 2025 22:14:02 -0800 Subject: [PATCH 01/12] Refactor compare router param parse --- routers/api/v1/repo/compare.go | 14 +--- routers/api/v1/repo/pull.go | 110 ++++++++++++++------------ routers/common/compare.go | 113 +++++++++++++++++++++++++++ routers/web/repo/compare.go | 136 +++++++++++++-------------------- 4 files changed, 228 insertions(+), 145 deletions(-) diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 6d427c8073422..9472bb7d3f9ba 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -5,7 +5,6 @@ package repo import ( "net/http" - "strings" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/gitrepo" @@ -52,18 +51,7 @@ 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} - } - } - } - - compareResult, closer := parseCompareInfo(ctx, api.CreatePullRequestOption{Base: infos[0], Head: infos[1]}) + compareResult, closer := parseCompareInfo(ctx, ctx.PathParam("*")) if ctx.Written() { return } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 073c7842424af..87bcdd26580ba 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -30,6 +30,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "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" @@ -413,7 +414,7 @@ func CreatePullRequest(ctx *context.APIContext) { ) // Get repo/branch information - compareResult, closer := parseCompareInfo(ctx, form) + compareResult, closer := parseCompareInfo(ctx, form.Base+".."+form.Head) if ctx.Written() { return } @@ -1065,61 +1066,76 @@ type parseCompareInfoResult struct { } // 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<-head: master...head:feature - // same repo: master...feature +func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *parseCompareInfoResult, closer func()) { 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 { + compareReq, err := common.ParseCompareRouterParam(baseRepo, compareParam) + if err != nil { 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) + var headRepo *repo_model.Repository + if compareReq.HeadOwner == "" { + if compareReq.HeadRepoName == "" { + headRepo = ctx.Repo.Repository + } else { + ctx.APIErrorNotFound() 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 + } else { + var headUser *user_model.User + if compareReq.HeadOwner == ctx.Repo.Owner.Name { + headUser = ctx.Repo.Owner + } else { + headUser, err = user_model.GetUserByName(ctx, compareReq.HeadOwner) + if err != nil { + if user_model.IsErrUserNotExist(err) { + ctx.APIErrorNotFound("GetUserByName") + } else { + ctx.APIErrorInternal(err) + } + return nil, nil + } + } + if compareReq.HeadRepoName == "" { + headRepo = repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID) + if headRepo == nil && headUser.ID != baseRepo.OwnerID { + 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 + } + } else { + if compareReq.HeadOwner == ctx.Repo.Owner.Name && compareReq.HeadRepoName == ctx.Repo.Repository.Name { + headRepo = ctx.Repo.Repository + } else { + headRepo, err = repo_model.GetRepositoryByName(ctx, headUser.ID, compareReq.HeadRepoName) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + ctx.APIErrorNotFound("GetRepositoryByName") + } else { + ctx.APIErrorInternal(err) + } + return nil, nil + } + } } - // Assign headRepo so it can be used below. - headRepo = baseRepo.BaseRepo } + isSameRepo := baseRepo.ID == headRepo.ID + 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 { @@ -1162,10 +1178,10 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) return nil, nil } - baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefToGuess) - headRef := headGitRepo.UnstableGuessRefByShortName(headRefToGuess) + baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(compareReq.BaseOriRef) + headRef := headGitRepo.UnstableGuessRefByShortName(compareReq.HeadOriRef) - log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.RelativePath(), baseRefToGuess, baseRef, headRefToGuess, headRef) + log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.RelativePath(), compareReq.BaseOriRef, baseRef, compareReq.HeadOriRef, 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()) diff --git a/routers/common/compare.go b/routers/common/compare.go index fda31a07ba736..386d54b8b9d93 100644 --- a/routers/common/compare.go +++ b/routers/common/compare.go @@ -4,9 +4,12 @@ package common import ( + "strings" + 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/util" pull_service "code.gitea.io/gitea/services/pull" ) @@ -20,3 +23,113 @@ type CompareInfo struct { HeadBranch string DirectComparison bool } + +type CompareRouterReq struct { + BaseOriRef string + HeadOwner string + HeadRepoName string + HeadOriRef string + CaretTimes int // ^ times after base ref + DotTimes int +} + +func (cr *CompareRouterReq) DirectComparison() bool { + return cr.DotTimes == 2 +} + +func (cr *CompareRouterReq) 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] +} + +// ParseCompareRouterParam Get compare information from the router parameter. +// 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<-head: master...head:feature +// same repo: master...feature +func ParseCompareRouterParam(baseRepo *repo_model.Repository, routerParam string) (*CompareRouterReq, error) { + if routerParam == "" { + return &CompareRouterReq{ + BaseOriRef: baseRepo.DefaultBranch, + HeadOwner: baseRepo.Owner.Name, + HeadRepoName: baseRepo.Name, + HeadOriRef: baseRepo.DefaultBranch, + DotTimes: 2, + }, nil + } + + var basePart, headPart string + dotTimes := 3 + parts := strings.Split(routerParam, "...") + if len(parts) > 2 { + return nil, util.NewInvalidArgumentErrorf("invalid compare router: %s", routerParam) + } + if len(parts) != 2 { + parts = strings.Split(routerParam, "..") + if len(parts) == 1 { + headOwnerName, headRepoName, headRef := parseHead(routerParam) + return &CompareRouterReq{ + BaseOriRef: baseRepo.DefaultBranch, + HeadOriRef: headRef, + HeadOwner: headOwnerName, + HeadRepoName: headRepoName, + DotTimes: dotTimes, + }, nil + } else if len(parts) > 2 { + return nil, util.NewInvalidArgumentErrorf("invalid compare router: %s", routerParam) + } + dotTimes = 2 + } + basePart, headPart = parts[0], parts[1] + + baseRef, caretTimes := parseBase(basePart) + headOwnerName, headRepoName, headRef := parseHead(headPart) + + return &CompareRouterReq{ + BaseOriRef: baseRef, + HeadOriRef: headRef, + HeadOwner: headOwnerName, + HeadRepoName: headRepoName, + CaretTimes: caretTimes, + DotTimes: dotTimes, + }, nil +} diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 7750278a8d4bd..4f3e5206eef52 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -195,71 +195,27 @@ func setCsvCompareContext(ctx *context.Context) { func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { baseRepo := ctx.Repo.Repository ci := &common.CompareInfo{} - fileOnly := ctx.FormBool("file-only") - // 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<-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} - } - } + compareReq, err := common.ParseCompareRouterParam(baseRepo, ctx.PathParam("*")) + if err != nil { + ctx.ServerError("GetUserByName", err) + return nil } - 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 compareReq.HeadOwner == "" { + if compareReq.HeadRepoName == "" { + ci.HeadRepo = baseRepo + } else { + ctx.NotFound(nil) + return nil + } + } else { + var headUser *user_model.User + if compareReq.HeadOwner == ctx.Repo.Owner.Name { + headUser = ctx.Repo.Owner + } else { + headUser, err = user_model.GetUserByName(ctx, compareReq.HeadOwner) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.NotFound(nil) @@ -268,37 +224,47 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { } 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) { + } + if compareReq.HeadRepoName == "" { + ci.HeadRepo = repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID) + if ci.HeadRepo == nil && headUser.ID != baseRepo.OwnerID { + err = baseRepo.GetBaseRepo(ctx) + if err != nil { + ctx.ServerError("GetBaseRepo", err) + return 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.NotFound(nil) - } else { - ctx.ServerError("GetRepositoryByOwnerAndName", err) + return nil } - return nil + // Assign headRepo so it can be used below. + ci.HeadRepo = baseRepo.BaseRepo } - if err := ci.HeadRepo.LoadOwner(ctx); err != nil { - if user_model.IsErrUserNotExist(err) { - ctx.NotFound(nil) - } else { - ctx.ServerError("GetUserByName", err) + } else { + if compareReq.HeadOwner == ctx.Repo.Owner.Name && compareReq.HeadRepoName == ctx.Repo.Repository.Name { + ci.HeadRepo = ctx.Repo.Repository + } else { + ci.HeadRepo, err = repo_model.GetRepositoryByName(ctx, headUser.ID, compareReq.HeadRepoName) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + ctx.NotFound(nil) + } else { + ctx.ServerError("GetRepositoryByName", err) + } + return nil } - 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 } + ci.BaseBranch = compareReq.BaseOriRef + ci.HeadBranch = compareReq.HeadOriRef + isSameRepo := baseRepo.ID == ci.HeadRepo.ID + + ctx.Data["BaseName"] = baseRepo.OwnerName + ctx.Data["BaseBranch"] = ci.BaseBranch ctx.Data["HeadUser"] = ci.HeadUser ctx.Data["HeadBranch"] = ci.HeadBranch ctx.Repo.PullRequest.SameRepo = isSameRepo From 3c51f3825365b571c9262616de2c4c2687e71176 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 7 Dec 2025 22:55:36 -0800 Subject: [PATCH 02/12] Refactor compare router param parse --- routers/api/v1/repo/pull.go | 9 +- routers/common/compare.go | 17 +--- routers/common/compare_test.go | 151 +++++++++++++++++++++++++++++++++ routers/web/repo/compare.go | 7 +- 4 files changed, 163 insertions(+), 21 deletions(-) create mode 100644 routers/common/compare_test.go diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 87bcdd26580ba..815540957badc 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -28,6 +28,7 @@ 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" @@ -1068,7 +1069,7 @@ type parseCompareInfoResult struct { // parseCompareInfo returns non-nil if it succeeds, it always writes to the context and returns nil if it fails func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *parseCompareInfoResult, closer func()) { baseRepo := ctx.Repo.Repository - compareReq, err := common.ParseCompareRouterParam(baseRepo, compareParam) + compareReq, err := common.ParseCompareRouterParam(compareParam) if err != nil { ctx.APIErrorNotFound() return nil, nil @@ -1178,8 +1179,8 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *par return nil, nil } - baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(compareReq.BaseOriRef) - headRef := headGitRepo.UnstableGuessRefByShortName(compareReq.HeadOriRef) + baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(util.Iif(compareReq.BaseOriRef == "", baseRepo.DefaultBranch, compareReq.BaseOriRef)) + headRef := headGitRepo.UnstableGuessRefByShortName(util.Iif(compareReq.HeadOriRef == "", headRepo.DefaultBranch, compareReq.HeadOriRef)) log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.RelativePath(), compareReq.BaseOriRef, baseRef, compareReq.HeadOriRef, headRef) @@ -1191,7 +1192,7 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *par return nil, nil } - compareInfo, err := pull_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseRef.ShortName(), headRef.ShortName(), false, false) + compareInfo, err := pull_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseRef.ShortName(), headRef.ShortName(), compareReq.DirectComparison(), false) if err != nil { ctx.APIErrorInternal(err) return nil, nil diff --git a/routers/common/compare.go b/routers/common/compare.go index 386d54b8b9d93..94c0474b94a23 100644 --- a/routers/common/compare.go +++ b/routers/common/compare.go @@ -34,11 +34,7 @@ type CompareRouterReq struct { } func (cr *CompareRouterReq) DirectComparison() bool { - return cr.DotTimes == 2 -} - -func (cr *CompareRouterReq) CompareDots() string { - return strings.Repeat(".", cr.DotTimes) + return cr.DotTimes == 2 || cr.CaretTimes == 0 } func parseBase(base string) (string, int) { @@ -86,15 +82,9 @@ func parseHead(head string) (string, string, string) { // format: ...[:] // base<-head: master...head:feature // same repo: master...feature -func ParseCompareRouterParam(baseRepo *repo_model.Repository, routerParam string) (*CompareRouterReq, error) { +func ParseCompareRouterParam(routerParam string) (*CompareRouterReq, error) { if routerParam == "" { - return &CompareRouterReq{ - BaseOriRef: baseRepo.DefaultBranch, - HeadOwner: baseRepo.Owner.Name, - HeadRepoName: baseRepo.Name, - HeadOriRef: baseRepo.DefaultBranch, - DotTimes: 2, - }, nil + return &CompareRouterReq{}, nil } var basePart, headPart string @@ -108,7 +98,6 @@ func ParseCompareRouterParam(baseRepo *repo_model.Repository, routerParam string if len(parts) == 1 { headOwnerName, headRepoName, headRef := parseHead(routerParam) return &CompareRouterReq{ - BaseOriRef: baseRepo.DefaultBranch, HeadOriRef: headRef, HeadOwner: headOwnerName, HeadRepoName: headRepoName, diff --git a/routers/common/compare_test.go b/routers/common/compare_test.go new file mode 100644 index 0000000000000..cb041ae85a5f2 --- /dev/null +++ b/routers/common/compare_test.go @@ -0,0 +1,151 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestCompareRouterReq(t *testing.T) { + unittest.PrepareTestEnv(t) + + kases := []struct { + router string + CompareRouterReq *CompareRouterReq + }{ + { + router: "", + CompareRouterReq: &CompareRouterReq{ + BaseOriRef: "", + HeadOriRef: "", + DotTimes: 0, + }, + }, + { + router: "main...develop", + CompareRouterReq: &CompareRouterReq{ + BaseOriRef: "main", + HeadOriRef: "develop", + DotTimes: 3, + }, + }, + { + router: "main..develop", + CompareRouterReq: &CompareRouterReq{ + BaseOriRef: "main", + HeadOriRef: "develop", + DotTimes: 2, + }, + }, + { + router: "main^...develop", + CompareRouterReq: &CompareRouterReq{ + BaseOriRef: "main", + HeadOriRef: "develop", + CaretTimes: 1, + DotTimes: 3, + }, + }, + { + router: "main^^^^^...develop", + CompareRouterReq: &CompareRouterReq{ + BaseOriRef: "main", + HeadOriRef: "develop", + CaretTimes: 5, + DotTimes: 3, + }, + }, + { + router: "develop", + CompareRouterReq: &CompareRouterReq{ + HeadOriRef: "develop", + DotTimes: 3, + }, + }, + { + router: "lunny/forked_repo:develop", + CompareRouterReq: &CompareRouterReq{ + HeadOwner: "lunny", + HeadRepoName: "forked_repo", + HeadOriRef: "develop", + DotTimes: 3, + }, + }, + { + router: "main...lunny/forked_repo:develop", + CompareRouterReq: &CompareRouterReq{ + BaseOriRef: "main", + HeadOwner: "lunny", + HeadRepoName: "forked_repo", + HeadOriRef: "develop", + DotTimes: 3, + }, + }, + { + router: "main...lunny/forked_repo:develop", + CompareRouterReq: &CompareRouterReq{ + BaseOriRef: "main", + HeadOwner: "lunny", + HeadRepoName: "forked_repo", + HeadOriRef: "develop", + DotTimes: 3, + }, + }, + { + router: "main^...lunny/forked_repo:develop", + CompareRouterReq: &CompareRouterReq{ + BaseOriRef: "main", + HeadOwner: "lunny", + HeadRepoName: "forked_repo", + HeadOriRef: "develop", + DotTimes: 3, + CaretTimes: 1, + }, + }, + { + router: "v1.0...v1.1", + CompareRouterReq: &CompareRouterReq{ + BaseOriRef: "v1.0", + HeadOriRef: "v1.1", + DotTimes: 3, + }, + }, + { + router: "teabot-patch-1...v0.0.1", + CompareRouterReq: &CompareRouterReq{ + BaseOriRef: "teabot-patch-1", + HeadOriRef: "v0.0.1", + DotTimes: 3, + }, + }, + { + router: "teabot:feature1", + CompareRouterReq: &CompareRouterReq{ + HeadOwner: "teabot", + HeadOriRef: "feature1", + DotTimes: 3, + }, + }, + { + router: "8eb19a5ae19abae15c0666d4ab98906139a7f439...283c030497b455ecfa759d4649f9f8b45158742e", + CompareRouterReq: &CompareRouterReq{ + BaseOriRef: "8eb19a5ae19abae15c0666d4ab98906139a7f439", + HeadOriRef: "283c030497b455ecfa759d4649f9f8b45158742e", + DotTimes: 3, + }, + }, + } + + for _, kase := range kases { + t.Run(kase.router, func(t *testing.T) { + r, err := ParseCompareRouterParam(kase.router) + assert.NoError(t, err) + assert.Equal(t, kase.CompareRouterReq, r) + }) + } +} diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 4f3e5206eef52..c84eb09c4402f 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -197,7 +197,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { ci := &common.CompareInfo{} fileOnly := ctx.FormBool("file-only") - compareReq, err := common.ParseCompareRouterParam(baseRepo, ctx.PathParam("*")) + compareReq, err := common.ParseCompareRouterParam(ctx.PathParam("*")) if err != nil { ctx.ServerError("GetUserByName", err) return nil @@ -259,8 +259,9 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { } } } - ci.BaseBranch = compareReq.BaseOriRef - ci.HeadBranch = compareReq.HeadOriRef + ci.BaseBranch = util.Iif(compareReq.BaseOriRef == "", baseRepo.DefaultBranch, compareReq.BaseOriRef) + ci.HeadBranch = util.Iif(compareReq.HeadOriRef == "", ci.HeadRepo.DefaultBranch, compareReq.HeadOriRef) + ci.DirectComparison = compareReq.DirectComparison() isSameRepo := baseRepo.ID == ci.HeadRepo.ID ctx.Data["BaseName"] = baseRepo.OwnerName From a2c8e9c073932410d2775be737efffbd0fefc5ae Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 8 Dec 2025 10:30:09 -0800 Subject: [PATCH 03/12] Fix test --- routers/api/v1/repo/pull.go | 6 +++--- routers/web/repo/compare.go | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 815540957badc..54d8f23710eb9 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1077,12 +1077,12 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *par var headRepo *repo_model.Repository if compareReq.HeadOwner == "" { - if compareReq.HeadRepoName == "" { - headRepo = ctx.Repo.Repository - } else { + if compareReq.HeadRepoName != "" { // unsupported syntax ctx.APIErrorNotFound() return nil, nil } + + headRepo = ctx.Repo.Repository } else { var headUser *user_model.User if compareReq.HeadOwner == ctx.Repo.Owner.Name { diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index c84eb09c4402f..3340a8d7ab299 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -204,18 +204,18 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { } if compareReq.HeadOwner == "" { - if compareReq.HeadRepoName == "" { - ci.HeadRepo = baseRepo - } else { + if compareReq.HeadRepoName != "" { // unsupported syntax ctx.NotFound(nil) return nil } + + ci.HeadUser = baseRepo.Owner + ci.HeadRepo = baseRepo } else { - var headUser *user_model.User if compareReq.HeadOwner == ctx.Repo.Owner.Name { - headUser = ctx.Repo.Owner + ci.HeadUser = ctx.Repo.Owner } else { - headUser, err = user_model.GetUserByName(ctx, compareReq.HeadOwner) + ci.HeadUser, err = user_model.GetUserByName(ctx, compareReq.HeadOwner) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.NotFound(nil) @@ -226,8 +226,8 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { } } if compareReq.HeadRepoName == "" { - ci.HeadRepo = repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID) - if ci.HeadRepo == nil && headUser.ID != baseRepo.OwnerID { + ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ID) + if ci.HeadRepo == nil && ci.HeadUser.ID != baseRepo.OwnerID { err = baseRepo.GetBaseRepo(ctx) if err != nil { ctx.ServerError("GetBaseRepo", err) @@ -235,7 +235,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { } // Check if baseRepo's base repository is the same as headUser's repository. - if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID { + if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != ci.HeadUser.ID { log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID) ctx.NotFound(nil) return nil @@ -247,7 +247,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { if compareReq.HeadOwner == ctx.Repo.Owner.Name && compareReq.HeadRepoName == ctx.Repo.Repository.Name { ci.HeadRepo = ctx.Repo.Repository } else { - ci.HeadRepo, err = repo_model.GetRepositoryByName(ctx, headUser.ID, compareReq.HeadRepoName) + ci.HeadRepo, err = repo_model.GetRepositoryByName(ctx, ci.HeadUser.ID, compareReq.HeadRepoName) if err != nil { if repo_model.IsErrRepoNotExist(err) { ctx.NotFound(nil) From 44925e3fe9d0399dc643414d65ed62a536b216a7 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 8 Dec 2025 18:29:33 -0800 Subject: [PATCH 04/12] Update routers/common/compare_test.go Co-authored-by: techknowlogick Signed-off-by: Lunny Xiao --- routers/common/compare_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/common/compare_test.go b/routers/common/compare_test.go index cb041ae85a5f2..a55f6607aec74 100644 --- a/routers/common/compare_test.go +++ b/routers/common/compare_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. +// Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package common From 8a83a64ebffab848c9ab24a0397ff47bbb669d85 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 10 Dec 2025 11:07:46 -0800 Subject: [PATCH 05/12] Fix --- routers/api/v1/repo/pull.go | 34 ++++++++++++++++++++-------------- routers/web/repo/compare.go | 34 ++++++++++++++++++++-------------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 54d8f23710eb9..a90dba12c0b2a 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1099,22 +1099,28 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *par } } if compareReq.HeadRepoName == "" { - headRepo = repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID) - if headRepo == nil && headUser.ID != baseRepo.OwnerID { - err = baseRepo.GetBaseRepo(ctx) - if err != nil { - ctx.APIErrorInternal(err) - return nil, nil - } + if headUser.ID == baseRepo.OwnerID { + headRepo = baseRepo + } else { + // TODO: forked's fork + headRepo = repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID) + if headRepo == nil { + // TODO: based's base? + 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 + // 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 } - // Assign headRepo so it can be used below. - headRepo = baseRepo.BaseRepo } } else { if compareReq.HeadOwner == ctx.Repo.Owner.Name && compareReq.HeadRepoName == ctx.Repo.Repository.Name { diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 3340a8d7ab299..b325b030a46fb 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -226,22 +226,28 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { } } if compareReq.HeadRepoName == "" { - ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ID) - if ci.HeadRepo == nil && ci.HeadUser.ID != baseRepo.OwnerID { - err = baseRepo.GetBaseRepo(ctx) - if err != nil { - ctx.ServerError("GetBaseRepo", err) - return nil - } + if ci.HeadUser.ID == baseRepo.OwnerID { + ci.HeadRepo = baseRepo + } else { + // TODO: forked's fork + ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ID) + if ci.HeadRepo == nil { + // TODO: based's base? + err = baseRepo.GetBaseRepo(ctx) + if err != nil { + ctx.ServerError("GetBaseRepo", err) + return nil + } - // Check if baseRepo's base repository is the same as headUser's repository. - if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != ci.HeadUser.ID { - log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID) - ctx.NotFound(nil) - return nil + // Check if baseRepo's base repository is the same as headUser's repository. + if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != ci.HeadUser.ID { + log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID) + ctx.NotFound(nil) + return nil + } + // Assign headRepo so it can be used below. + ci.HeadRepo = baseRepo.BaseRepo } - // Assign headRepo so it can be used below. - ci.HeadRepo = baseRepo.BaseRepo } } else { if compareReq.HeadOwner == ctx.Repo.Owner.Name && compareReq.HeadRepoName == ctx.Repo.Repository.Name { From dcb8e478e010de5a146f913100b41d8405b7c22f Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 10 Dec 2025 11:40:33 -0800 Subject: [PATCH 06/12] Fix bug --- routers/common/compare.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/common/compare.go b/routers/common/compare.go index 94c0474b94a23..f2def833e2711 100644 --- a/routers/common/compare.go +++ b/routers/common/compare.go @@ -34,7 +34,7 @@ type CompareRouterReq struct { } func (cr *CompareRouterReq) DirectComparison() bool { - return cr.DotTimes == 2 || cr.CaretTimes == 0 + return cr.DotTimes == 2 || cr.DotTimes == 0 } func parseBase(base string) (string, int) { From aebb29ccc95ad3808457b76847fc6201cbef5497 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 10 Dec 2025 14:37:51 -0800 Subject: [PATCH 07/12] Fix bug and add test --- models/user/user.go | 11 ++++ routers/api/v1/repo/pull.go | 40 ++++++------ routers/common/compare.go | 54 +++++++++++++++- routers/web/repo/compare.go | 53 +++++++-------- tests/integration/pull_create_test.go | 92 +++++++++++++++++++++++++++ 5 files changed, 198 insertions(+), 52 deletions(-) diff --git a/models/user/user.go b/models/user/user.go index 925be83713cd2..764890e26e8ac 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1461,3 +1461,14 @@ func GetUserOrOrgIDByName(ctx context.Context, name string) (int64, error) { } return id, nil } + +func GetUserOrOrgByName(ctx context.Context, name string) (*User, error) { + var u User + has, err := db.GetEngine(ctx).Table("user").Where("name = ?", name).Get(&u) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("user or org with name %s: %w", name, util.ErrNotExist) + } + return &u, nil +} diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index a90dba12c0b2a..fe395dd58774c 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1075,6 +1075,12 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *par return nil, nil } + // remove the check when we support compare with carets + if compareReq.CaretTimes > 0 { + ctx.APIErrorNotFound("Unsupported compare syntax with carets") + return nil, nil + } + var headRepo *repo_model.Repository if compareReq.HeadOwner == "" { if compareReq.HeadRepoName != "" { // unsupported syntax @@ -1084,11 +1090,11 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *par headRepo = ctx.Repo.Repository } else { - var headUser *user_model.User + var headOwner *user_model.User if compareReq.HeadOwner == ctx.Repo.Owner.Name { - headUser = ctx.Repo.Owner + headOwner = ctx.Repo.Owner } else { - headUser, err = user_model.GetUserByName(ctx, compareReq.HeadOwner) + headOwner, err = user_model.GetUserOrOrgByName(ctx, compareReq.HeadOwner) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.APIErrorNotFound("GetUserByName") @@ -1099,34 +1105,24 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *par } } if compareReq.HeadRepoName == "" { - if headUser.ID == baseRepo.OwnerID { + if headOwner.ID == baseRepo.OwnerID { headRepo = baseRepo } else { - // TODO: forked's fork - headRepo = repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID) + headRepo, err = common.FindHeadRepo(ctx, baseRepo, headOwner.ID) + if err != nil { + ctx.APIErrorInternal(err) + return nil, nil + } if headRepo == nil { - // TODO: based's base? - 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 + ctx.HTTPError(http.StatusBadRequest, "The user "+headOwner.Name+" does not have a fork of the base repository") + return nil, nil } } } else { if compareReq.HeadOwner == ctx.Repo.Owner.Name && compareReq.HeadRepoName == ctx.Repo.Repository.Name { headRepo = ctx.Repo.Repository } else { - headRepo, err = repo_model.GetRepositoryByName(ctx, headUser.ID, compareReq.HeadRepoName) + headRepo, err = repo_model.GetRepositoryByName(ctx, headOwner.ID, compareReq.HeadRepoName) if err != nil { if repo_model.IsErrRepoNotExist(err) { ctx.APIErrorNotFound("GetRepositoryByName") diff --git a/routers/common/compare.go b/routers/common/compare.go index f2def833e2711..f2194095b4d89 100644 --- a/routers/common/compare.go +++ b/routers/common/compare.go @@ -4,6 +4,7 @@ package common import ( + "context" "strings" repo_model "code.gitea.io/gitea/models/repo" @@ -15,7 +16,7 @@ import ( // CompareInfo represents the collected results from ParseCompareInfo type CompareInfo struct { - HeadUser *user_model.User + HeadOwner *user_model.User HeadRepo *repo_model.Repository HeadGitRepo *git.Repository CompareInfo *pull_service.CompareInfo @@ -122,3 +123,54 @@ func ParseCompareRouterParam(routerParam string) (*CompareRouterReq, error) { DotTimes: dotTimes, }, nil } + +// maxForkTraverseLevel defines the maximum levels to traverse when searching for the head repository. +const maxForkTraverseLevel = 10 + +// FindHeadRepo tries to find the head repository based on the base repository and head user ID. +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, maxForkTraverseLevel) + } + curRepo = curRepo.BaseRepo + } + return curRepo, nil + } + + return findHeadRepoFromRootBase(ctx, baseRepo, headUserID, maxForkTraverseLevel) +} + +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 + } + + firstLevelForkedRepos, err := repo_model.GetRepositoriesByForkID(ctx, baseRepo.ID) + if err != nil { + return nil, err + } + for _, repo := range firstLevelForkedRepos { + forked, err := findHeadRepoFromRootBase(ctx, repo, headUserID, traverseLevel-1) + if err != nil { + return nil, err + } + if forked != nil { + return forked, nil + } + } + return nil, nil +} diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index b325b030a46fb..c7c45935665fe 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -202,6 +202,11 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { ctx.ServerError("GetUserByName", err) return nil } + // remove the check when we support compare with carets + if compareReq.CaretTimes > 0 { + ctx.HTTPError(http.StatusBadRequest, "Unsupported compare syntax with carets") + return nil + } if compareReq.HeadOwner == "" { if compareReq.HeadRepoName != "" { // unsupported syntax @@ -209,13 +214,13 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { return nil } - ci.HeadUser = baseRepo.Owner + ci.HeadOwner = baseRepo.Owner ci.HeadRepo = baseRepo } else { if compareReq.HeadOwner == ctx.Repo.Owner.Name { - ci.HeadUser = ctx.Repo.Owner + ci.HeadOwner = ctx.Repo.Owner } else { - ci.HeadUser, err = user_model.GetUserByName(ctx, compareReq.HeadOwner) + ci.HeadOwner, err = user_model.GetUserOrOrgByName(ctx, compareReq.HeadOwner) if err != nil { if user_model.IsErrUserNotExist(err) { ctx.NotFound(nil) @@ -226,34 +231,24 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { } } if compareReq.HeadRepoName == "" { - if ci.HeadUser.ID == baseRepo.OwnerID { + if ci.HeadOwner.ID == baseRepo.OwnerID { ci.HeadRepo = baseRepo } else { - // TODO: forked's fork - ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ID) + ci.HeadRepo, err = common.FindHeadRepo(ctx, baseRepo, ci.HeadOwner.ID) + if err != nil { + ctx.ServerError("FindHeadRepo", err) + return nil + } if ci.HeadRepo == nil { - // TODO: based's base? - err = baseRepo.GetBaseRepo(ctx) - if err != nil { - ctx.ServerError("GetBaseRepo", err) - return nil - } - - // Check if baseRepo's base repository is the same as headUser's repository. - if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != ci.HeadUser.ID { - log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID) - ctx.NotFound(nil) - return nil - } - // Assign headRepo so it can be used below. - ci.HeadRepo = baseRepo.BaseRepo + ctx.HTTPError(http.StatusBadRequest, "The user "+ci.HeadOwner.Name+" does not have a fork of the base repository") + return nil } } } else { if compareReq.HeadOwner == ctx.Repo.Owner.Name && compareReq.HeadRepoName == ctx.Repo.Repository.Name { ci.HeadRepo = ctx.Repo.Repository } else { - ci.HeadRepo, err = repo_model.GetRepositoryByName(ctx, ci.HeadUser.ID, compareReq.HeadRepoName) + ci.HeadRepo, err = repo_model.GetRepositoryByName(ctx, ci.HeadOwner.ID, compareReq.HeadRepoName) if err != nil { if repo_model.IsErrRepoNotExist(err) { ctx.NotFound(nil) @@ -272,7 +267,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { ctx.Data["BaseName"] = baseRepo.OwnerName ctx.Data["BaseBranch"] = ci.BaseBranch - ctx.Data["HeadUser"] = ci.HeadUser + ctx.Data["HeadUser"] = ci.HeadOwner ctx.Data["HeadBranch"] = ci.HeadBranch ctx.Repo.PullRequest.SameRepo = isSameRepo @@ -344,27 +339,27 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { 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 { + if !has && rootRepo != nil && rootRepo.OwnerID == ci.HeadOwner.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 { + if !has && ownForkRepo != nil && ownForkRepo.OwnerID == ci.HeadOwner.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) + ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadOwner.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) + ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadOwner.ID, baseRepo.ForkID) has = ci.HeadRepo != nil } @@ -679,10 +674,10 @@ func PrepareCompareDiff( } ctx.Data["title"] = title - ctx.Data["Username"] = ci.HeadUser.Name + ctx.Data["Username"] = ci.HeadOwner.Name ctx.Data["Reponame"] = ci.HeadRepo.Name - setCompareContext(ctx, beforeCommit, headCommit, ci.HeadUser.Name, repo.Name) + setCompareContext(ctx, beforeCommit, headCommit, ci.HeadOwner.Name, repo.Name) return false } diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go index ddafdf33b837d..dc27f6a7819f2 100644 --- a/tests/integration/pull_create_test.go +++ b/tests/integration/pull_create_test.go @@ -4,6 +4,7 @@ package integration import ( + "encoding/base64" "fmt" "net/http" "net/http/httptest" @@ -17,7 +18,9 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git/gitcmd" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -295,6 +298,95 @@ func TestPullCreatePrFromBaseToFork(t *testing.T) { }) } +func TestCreatePullRequestFromNestedOrgForks(t *testing.T) { + onGiteaRun(t, func(t *testing.T, _ *url.URL) { + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization) + + const ( + baseOrg = "test-fork-org1" + midForkOrg = "test-fork-org2" + leafForkOrg = "test-fork-org3" + repoName = "test-fork-repo" + patchBranch = "teabot-patch-1" + ) + + createOrg := func(name string) { + req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{ + UserName: name, + Visibility: "public", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + } + + createOrg(baseOrg) + createOrg(midForkOrg) + createOrg(leafForkOrg) + + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/repos", baseOrg), &api.CreateRepoOption{ + Name: repoName, + AutoInit: true, + DefaultBranch: "main", + Private: false, + Readme: "Default", + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusCreated) + var baseRepo api.Repository + DecodeJSON(t, resp, &baseRepo) + assert.Equal(t, "main", baseRepo.DefaultBranch) + + forkIntoOrg := func(srcOrg, dstOrg string) api.Repository { + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", srcOrg, repoName), &api.CreateForkOption{ + Organization: util.ToPointer(dstOrg), + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusAccepted) + var forkRepo api.Repository + DecodeJSON(t, resp, &forkRepo) + assert.NotNil(t, forkRepo.Owner) + if forkRepo.Owner != nil { + assert.Equal(t, dstOrg, forkRepo.Owner.UserName) + } + return forkRepo + } + + forkIntoOrg(baseOrg, midForkOrg) + forkIntoOrg(midForkOrg, leafForkOrg) + + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", leafForkOrg, repoName, "patch-from-org3.txt"), &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + BranchName: "main", + NewBranchName: patchBranch, + Message: "create patch from org3", + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte("patch content")), + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + + prPayload := map[string]string{ + "head": fmt.Sprintf("%s:%s", leafForkOrg, patchBranch), + "base": "main", + "title": "test creating pull from test-fork-org3 to test-fork-org1", + } + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/pulls", baseOrg, repoName), prPayload).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusCreated) + var pr api.PullRequest + DecodeJSON(t, resp, &pr) + assert.Equal(t, prPayload["title"], pr.Title) + if assert.NotNil(t, pr.Head) { + assert.Equal(t, patchBranch, pr.Head.Ref) + if assert.NotNil(t, pr.Head.Repository) { + assert.Equal(t, fmt.Sprintf("%s/%s", leafForkOrg, repoName), pr.Head.Repository.FullName) + } + } + if assert.NotNil(t, pr.Base) { + assert.Equal(t, "main", pr.Base.Ref) + if assert.NotNil(t, pr.Base.Repository) { + assert.Equal(t, fmt.Sprintf("%s/%s", baseOrg, repoName), pr.Base.Repository.FullName) + } + } + }) +} + func TestPullCreateParallel(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { sessionFork := loginUser(t, "user1") From c3c4e3c9c29bde4e082dd44fb3f3bee43e67b97a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 10 Dec 2025 15:51:01 -0800 Subject: [PATCH 08/12] remove unnecessary code --- models/user/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/user/user.go b/models/user/user.go index 764890e26e8ac..7b50008848320 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1464,7 +1464,7 @@ func GetUserOrOrgIDByName(ctx context.Context, name string) (int64, error) { func GetUserOrOrgByName(ctx context.Context, name string) (*User, error) { var u User - has, err := db.GetEngine(ctx).Table("user").Where("name = ?", name).Get(&u) + has, err := db.GetEngine(ctx).Where("name = ?", name).Get(&u) if err != nil { return nil, err } else if !has { From 220a908e5f563928c987eac3f52c7ccf56a9dcfa Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 10 Dec 2025 16:18:43 -0800 Subject: [PATCH 09/12] Remove unnecessary permission check --- routers/api/v1/repo/pull.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index fe395dd58774c..541ea639bd5db 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1162,9 +1162,9 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *par 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") + if !permBase.CanRead(unit.TypeCode) { + log.Trace("Permission Denied: User %-v cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase) + ctx.APIErrorNotFound("can't read baseRepo UnitTypeCode") return nil, nil } From 3bd496b59f36112f7a96434c6f2d52d4a01b3124 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 10 Dec 2025 19:03:30 -0800 Subject: [PATCH 10/12] Prevent to create duplicated pull request --- options/locale/locale_en-US.ini | 1 + routers/web/repo/pull.go | 11 +++++++++++ tests/integration/pull_create_test.go | 10 +++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 981d9de2f8623..6ac612a481d13 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1862,6 +1862,7 @@ pulls.desc = Enable pull requests and code reviews. pulls.new = New Pull Request pulls.new.blocked_user = Cannot create pull request because you are blocked by the repository owner. pulls.new.must_collaborator = You must be a collaborator to create pull request. +pulls.new.already_existed = A pull request between these branches already exists pulls.edit.already_changed = Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes. pulls.view = View Pull Request pulls.compare_changes = New Pull Request diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 4353e00840f92..488389e204963 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1340,6 +1340,17 @@ func CompareAndPullRequestPost(ctx *context.Context) { return } + // Check if a pull request already exists with the same head and base branch. + pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, repo.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub) + if err != nil && !issues_model.IsErrPullRequestNotExist(err) { + ctx.ServerError("GetUnmergedPullRequest", err) + return + } + if pr != nil { + ctx.JSONError(ctx.Tr("repo.pulls.new.already_existed")) + return + } + content := form.Content if filename := ctx.Req.Form.Get("template-file"); filename != "" { if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil { diff --git a/tests/integration/pull_create_test.go b/tests/integration/pull_create_test.go index dc27f6a7819f2..ff60b70cf9a86 100644 --- a/tests/integration/pull_create_test.go +++ b/tests/integration/pull_create_test.go @@ -156,8 +156,16 @@ func TestPullCreate(t *testing.T) { url := test.RedirectURL(resp) assert.Regexp(t, "^/user2/repo1/pulls/[0-9]*$", url) + // test create the pull request again and it should fail now + link := "/user2/repo1/compare/master...user1/repo1:master" + req := NewRequestWithValues(t, "POST", link, map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "title": "This is a pull title", + }) + session.MakeRequest(t, req, http.StatusBadRequest) + // check .diff can be accessed and matches performed change - req := NewRequest(t, "GET", url+".diff") + req = NewRequest(t, "GET", url+".diff") resp = session.MakeRequest(t, req, http.StatusOK) assert.Regexp(t, `\+Hello, World \(Edited\)`, resp.Body) assert.Regexp(t, "^diff", resp.Body) From 6453e4ab86d6f9a9948fbfebc1324ddb083cf67f Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 11 Dec 2025 19:17:10 -0800 Subject: [PATCH 11/12] some improvements --- models/user/user.go | 5 +++-- routers/api/v1/repo/pull.go | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/models/user/user.go b/models/user/user.go index 7b50008848320..1797d3eefc2b5 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -1462,13 +1462,14 @@ func GetUserOrOrgIDByName(ctx context.Context, name string) (int64, error) { return id, nil } +// GetUserOrOrgByName returns the user or org by name func GetUserOrOrgByName(ctx context.Context, name string) (*User, error) { var u User - has, err := db.GetEngine(ctx).Where("name = ?", name).Get(&u) + has, err := db.GetEngine(ctx).Where("lower_name = ?", strings.ToLower(name)).Get(&u) if err != nil { return nil, err } else if !has { - return nil, fmt.Errorf("user or org with name %s: %w", name, util.ErrNotExist) + return nil, ErrUserNotExist{Name: name} } return &u, nil } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 541ea639bd5db..b1fb0daa7a19a 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1097,7 +1097,7 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *par headOwner, err = user_model.GetUserOrOrgByName(ctx, compareReq.HeadOwner) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.APIErrorNotFound("GetUserByName") + ctx.APIErrorNotFound("GetUserOrOrgByName") } else { ctx.APIErrorInternal(err) } @@ -1114,7 +1114,7 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *par return nil, nil } if headRepo == nil { - ctx.HTTPError(http.StatusBadRequest, "The user "+headOwner.Name+" does not have a fork of the base repository") + ctx.APIError(http.StatusBadRequest, "The user "+headOwner.Name+" does not have a fork of the base repository") return nil, nil } } From eb40447ffdee9620c89e865400d922be8fe7bd6a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 11 Dec 2025 19:34:18 -0800 Subject: [PATCH 12/12] remove duplicated code --- routers/api/v1/repo/pull.go | 63 +++++++------------------------------ routers/common/compare.go | 48 ++++++++++++++++++++++++++++ routers/web/repo/compare.go | 63 +++++++------------------------------ 3 files changed, 71 insertions(+), 103 deletions(-) diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index b1fb0daa7a19a..c9fcea8569ca8 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1081,58 +1081,17 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *par return nil, nil } - var headRepo *repo_model.Repository - if compareReq.HeadOwner == "" { - if compareReq.HeadRepoName != "" { // unsupported syntax - ctx.APIErrorNotFound() - return nil, nil - } - - headRepo = ctx.Repo.Repository - } else { - var headOwner *user_model.User - if compareReq.HeadOwner == ctx.Repo.Owner.Name { - headOwner = ctx.Repo.Owner - } else { - headOwner, err = user_model.GetUserOrOrgByName(ctx, compareReq.HeadOwner) - if err != nil { - if user_model.IsErrUserNotExist(err) { - ctx.APIErrorNotFound("GetUserOrOrgByName") - } else { - ctx.APIErrorInternal(err) - } - return nil, nil - } - } - if compareReq.HeadRepoName == "" { - if headOwner.ID == baseRepo.OwnerID { - headRepo = baseRepo - } else { - headRepo, err = common.FindHeadRepo(ctx, baseRepo, headOwner.ID) - if err != nil { - ctx.APIErrorInternal(err) - return nil, nil - } - if headRepo == nil { - ctx.APIError(http.StatusBadRequest, "The user "+headOwner.Name+" does not have a fork of the base repository") - return nil, nil - } - } - } else { - if compareReq.HeadOwner == ctx.Repo.Owner.Name && compareReq.HeadRepoName == ctx.Repo.Repository.Name { - headRepo = ctx.Repo.Repository - } else { - headRepo, err = repo_model.GetRepositoryByName(ctx, headOwner.ID, compareReq.HeadRepoName) - if err != nil { - if repo_model.IsErrRepoNotExist(err) { - ctx.APIErrorNotFound("GetRepositoryByName") - } else { - ctx.APIErrorInternal(err) - } - return nil, nil - } - } - } + headOwner, headRepo, err := common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq) + switch { + case errors.Is(err, util.ErrInvalidArgument): + ctx.APIError(http.StatusBadRequest, err.Error()) + return nil, nil + case err != nil: + ctx.APIErrorInternal(err) + return nil, nil + case headOwner == nil || headRepo == nil: + ctx.APIErrorNotFound() + return nil, nil } isSameRepo := baseRepo.ID == headRepo.ID diff --git a/routers/common/compare.go b/routers/common/compare.go index f2194095b4d89..ae1444b30a0be 100644 --- a/routers/common/compare.go +++ b/routers/common/compare.go @@ -174,3 +174,51 @@ func findHeadRepoFromRootBase(ctx context.Context, baseRepo *repo_model.Reposito } return nil, nil } + +func GetHeadOwnerAndRepo(ctx context.Context, baseRepo *repo_model.Repository, compareReq *CompareRouterReq) (headOwner *user_model.User, headRepo *repo_model.Repository, err error) { + if compareReq.HeadOwner == "" { + if compareReq.HeadRepoName != "" { // unsupported syntax + return nil, nil, nil + } + + return baseRepo.Owner, baseRepo, nil + } + + if compareReq.HeadOwner == baseRepo.Owner.Name { + headOwner = baseRepo.Owner + } else { + headOwner, err = user_model.GetUserOrOrgByName(ctx, compareReq.HeadOwner) + if err != nil { + if user_model.IsErrUserNotExist(err) { + return nil, nil, nil + } + return nil, nil, err + } + } + if compareReq.HeadRepoName == "" { + if headOwner.ID == baseRepo.OwnerID { + headRepo = baseRepo + } else { + headRepo, err = FindHeadRepo(ctx, baseRepo, headOwner.ID) + if err != nil { + return nil, nil, err + } + if headRepo == nil { + return nil, nil, util.ErrorWrap(util.ErrInvalidArgument, "the user %s does not have a fork of the base repository", headOwner.Name) + } + } + } else { + if compareReq.HeadOwner == baseRepo.Owner.Name && compareReq.HeadRepoName == baseRepo.Name { + headRepo = baseRepo + } else { + headRepo, err = repo_model.GetRepositoryByName(ctx, headOwner.ID, compareReq.HeadRepoName) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + return nil, nil, nil + } + return nil, nil, err + } + } + } + return headOwner, headRepo, nil +} diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index c7c45935665fe..b3ed7944197f7 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -208,58 +208,19 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo { return nil } - if compareReq.HeadOwner == "" { - if compareReq.HeadRepoName != "" { // unsupported syntax - ctx.NotFound(nil) - return nil - } - - ci.HeadOwner = baseRepo.Owner - ci.HeadRepo = baseRepo - } else { - if compareReq.HeadOwner == ctx.Repo.Owner.Name { - ci.HeadOwner = ctx.Repo.Owner - } else { - ci.HeadOwner, err = user_model.GetUserOrOrgByName(ctx, compareReq.HeadOwner) - if err != nil { - if user_model.IsErrUserNotExist(err) { - ctx.NotFound(nil) - } else { - ctx.ServerError("GetUserByName", err) - } - return nil - } - } - if compareReq.HeadRepoName == "" { - if ci.HeadOwner.ID == baseRepo.OwnerID { - ci.HeadRepo = baseRepo - } else { - ci.HeadRepo, err = common.FindHeadRepo(ctx, baseRepo, ci.HeadOwner.ID) - if err != nil { - ctx.ServerError("FindHeadRepo", err) - return nil - } - if ci.HeadRepo == nil { - ctx.HTTPError(http.StatusBadRequest, "The user "+ci.HeadOwner.Name+" does not have a fork of the base repository") - return nil - } - } - } else { - if compareReq.HeadOwner == ctx.Repo.Owner.Name && compareReq.HeadRepoName == ctx.Repo.Repository.Name { - ci.HeadRepo = ctx.Repo.Repository - } else { - ci.HeadRepo, err = repo_model.GetRepositoryByName(ctx, ci.HeadOwner.ID, compareReq.HeadRepoName) - if err != nil { - if repo_model.IsErrRepoNotExist(err) { - ctx.NotFound(nil) - } else { - ctx.ServerError("GetRepositoryByName", err) - } - return nil - } - } - } + ci.HeadOwner, ci.HeadRepo, err = common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq) + switch { + case errors.Is(err, util.ErrInvalidArgument): + ctx.HTTPError(http.StatusBadRequest, err.Error()) + return nil + case err != nil: + ctx.ServerError("GetHeadOwnerAndRepo", err) + return nil + case ci.HeadOwner == nil || ci.HeadRepo == nil: + ctx.NotFound(nil) + return nil } + ci.BaseBranch = util.Iif(compareReq.BaseOriRef == "", baseRepo.DefaultBranch, compareReq.BaseOriRef) ci.HeadBranch = util.Iif(compareReq.HeadOriRef == "", ci.HeadRepo.DefaultBranch, compareReq.HeadOriRef) ci.DirectComparison = compareReq.DirectComparison()