diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml
index 17b1869ab6364..e8dc4c4a286c0 100644
--- a/models/fixtures/branch.yml
+++ b/models/fixtures/branch.yml
@@ -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
diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go
index 6d427c8073422..26838b0bf6354 100644
--- a/routers/api/v1/repo/compare.go
+++ b/routers/api/v1/repo/compare.go
@@ -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,
})
}
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index f5d0e37c650c4..f49fcef54397d 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -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
)
+ baseGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, baseRepo)
+ if err != nil {
+ ctx.APIError(http.StatusInternalServerError, err)
+ return
+ }
+ defer closer.Close()
+
// Get repo/branch information
- compareResult, closer := parseCompareInfo(ctx, form)
- if ctx.Written() {
+ 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
}
- defer closer()
- if !compareResult.baseRef.IsBranch() || !compareResult.headRef.IsBranch() {
- ctx.APIError(http.StatusUnprocessableEntity, "Invalid PullRequest: base and head must be branches")
+ // 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<-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
diff --git a/routers/common/compare.go b/routers/common/compare.go
index 4d1cc2f0d8908..f7caf42c1032e 100644
--- a/routers/common/compare.go
+++ b/routers/common/compare.go
@@ -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<-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
}
diff --git a/routers/common/compare_test.go b/routers/common/compare_test.go
new file mode 100644
index 0000000000000..5fd80b970175b
--- /dev/null
+++ b/routers/common/compare_test.go
@@ -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)
+ })
+ }
+}
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 4d4969fa87f7d..e2d068c71f638 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -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<-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 := gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, ci.BaseBranch)
- baseIsTag := gitrepo.IsTagExist(ctx, ctx.Repo.Repository, 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 {
+ ci, err := common.ParseComparePathParams(ctx, pathParam, baseRepo, ctx.Repo.GitRepo)
+ if err != nil {
+ switch {
+ case user_model.IsErrUserNotExist(err):
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
+ 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
}
- 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 {
+ // remove the check when we support compare with carets
+ if ci.CaretTimes > 0 {
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)
- if err != nil {
- ctx.ServerError("GetUserRepoPermission", 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)
+ 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))
}
- ctx.NotFound(nil)
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["RootRepoBranches"] = branches
- ctx.Data["RootRepoTags"] = tags
- }
- }
- }
-
- // 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
- }
- }
- }
-
- // Check if head branch is valid.
- headIsCommit := ci.HeadGitRepo.IsCommitExist(ci.HeadBranch)
- headIsBranch := gitrepo.IsBranchExist(ctx, ci.HeadRepo, ci.HeadBranch)
- headIsTag := gitrepo.IsTagExist(ctx, ci.HeadRepo, 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
- }
+ 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()
- 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
- }
+ ctx.Data["BaseIsCommit"] = ci.IsBaseCommit
+ ctx.Data["BaseIsBranch"] = ci.BaseFullRef.IsBranch()
+ ctx.Data["BaseIsTag"] = ci.BaseFullRef.IsTag()
+ ctx.Data["IsPull"] = true
- 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
- }
+ 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()
- 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
@@ -577,14 +293,14 @@ func PrepareCompareDiff(
ctx.Data["AfterCommitID"] = headCommitID
ctx.Data["ExpandNewPrForm"] = ctx.FormBool("expand")
- 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
@@ -596,7 +312,7 @@ func PrepareCompareDiff(
}
beforeCommitID := ci.CompareInfo.MergeBase
- if ci.DirectComparison {
+ if ci.DirectComparison() {
beforeCommitID = ci.CompareInfo.BaseCommitID
}
@@ -617,7 +333,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)
@@ -664,7 +380,7 @@ func PrepareCompareDiff(
ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = len(commits)
- title := ci.HeadBranch
+ title := ci.HeadOriRef
if len(commits) == 1 {
c := commits[0]
title = strings.TrimSpace(c.UserCommit.Summary())
@@ -696,14 +412,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),
@@ -711,19 +421,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() {
@@ -734,7 +513,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"] = "..."
}
@@ -744,45 +523,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)
@@ -813,11 +566,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
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index c72664f8e9035..b9d76d5f577ec 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -1302,8 +1302,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() {
@@ -1351,8 +1351,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,
diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go
index d64dd97f93f0f..127cd3f4f0225 100644
--- a/tests/integration/api_branch_test.go
+++ b/tests/integration/api_branch_test.go
@@ -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,
diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go
index 1c8318db0db91..a01516e3572b3 100644
--- a/tests/integration/pull_merge_test.go
+++ b/tests/integration/pull_merge_test.go
@@ -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