diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml
index 6536e1dda7b84..14e7b71ad714e 100644
--- a/models/fixtures/branch.yml
+++ b/models/fixtures/branch.yml
@@ -201,3 +201,52 @@
   is_deleted: false
   deleted_by_id: 0
   deleted_unix: 0
+
+-
+  id: 25
+  repo_id: 1
+  name: 'DefaultBranch'
+  commit_id: '90c1019714259b24fb81711d4416ac0f18667dfa'
+  commit_message: 'add license'
+  commit_time: 1709259547
+  pusher_id: 1
+  is_deleted: false
+
+-
+  id: 26
+  repo_id: 11
+  name: 'develop'
+  commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
+  commit_message: 'Initial commit'
+  commit_time: 1489956479
+  pusher_id: 1
+  is_deleted: false
+
+-
+  id: 27
+  repo_id: 10
+  name: 'DefaultBranch'
+  commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
+  commit_message: 'Initial commit'
+  commit_time: 1489956479
+  pusher_id: 1
+  is_deleted: false
+
+-
+  id: 28
+  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: 29
+  repo_id: 10
+  name: 'develop'
+  commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
+  commit_message: 'Initial commit'
+  commit_time: 1489927679
+  pusher_id: 12
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 c0b1810191982..49d02b03862f5 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 branch>...[<head repo>:]<head branch>
-	// base<-head: master...head:feature
-	// same repo: master...feature
-	baseRepo := ctx.Repo.Repository
-	baseRefToGuess := form.Base
-
-	headUser := ctx.Repo.Owner
-	headRefToGuess := form.Head
-	if headInfos := strings.Split(form.Head, ":"); len(headInfos) == 1 {
-		// If there is no head repository, it means pull request between same repository.
-		// Do nothing here because the head variables have been assigned above.
-	} else if len(headInfos) == 2 {
-		// There is a head repository (the head repository could also be the same base repo)
-		headRefToGuess = headInfos[1]
-		headUser, err = user_model.GetUserByName(ctx, headInfos[0])
-		if err != nil {
-			if user_model.IsErrUserNotExist(err) {
-				ctx.APIErrorNotFound("GetUserByName")
-			} else {
-				ctx.APIErrorInternal(err)
-			}
-			return nil, nil
-		}
-	} else {
-		ctx.APIErrorNotFound()
-		return nil, nil
-	}
-
-	isSameRepo := ctx.Repo.Owner.ID == headUser.ID
-
-	// Check if current user has fork of repository or in the same repository.
-	headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID)
-	if headRepo == nil && !isSameRepo {
-		err = baseRepo.GetBaseRepo(ctx)
-		if err != nil {
-			ctx.APIErrorInternal(err)
-			return nil, nil
-		}
-
-		// Check if baseRepo's base repository is the same as headUser's repository.
-		if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID {
-			log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
-			ctx.APIErrorNotFound("GetBaseRepo")
-			return nil, nil
-		}
-		// Assign headRepo so it can be used below.
-		headRepo = baseRepo.BaseRepo
-	}
-
-	var headGitRepo *git.Repository
-	if isSameRepo {
-		headRepo = ctx.Repo.Repository
-		headGitRepo = ctx.Repo.GitRepo
-		closer = func() {} // no need to close the head repo because it shares the base repo
-	} else {
-		headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo)
-		if err != nil {
-			ctx.APIErrorInternal(err)
-			return nil, nil
-		}
-		closer = func() { _ = headGitRepo.Close() }
-	}
-	defer func() {
-		if result == nil && !isSameRepo {
-			_ = headGitRepo.Close()
-		}
-	}()
-
-	// user should have permission to read baseRepo's codes and pulls, NOT headRepo's
-	permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
-	if err != nil {
-		ctx.APIErrorInternal(err)
-		return nil, nil
-	}
-
-	if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
-		log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase)
-		ctx.APIErrorNotFound("Can't read pulls or can't read UnitTypeCode")
-		return nil, nil
-	}
-
-	// user should have permission to read headRepo's codes
-	// TODO: could the logic be simplified if the headRepo is the same as the baseRepo? Need to think more about it.
-	permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer)
-	if err != nil {
-		ctx.APIErrorInternal(err)
-		return nil, nil
-	}
-	if !permHead.CanRead(unit.TypeCode) {
-		log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", ctx.Doer, headRepo, permHead)
-		ctx.APIErrorNotFound("Can't read headRepo UnitTypeCode")
-		return nil, nil
-	}
-
-	baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefToGuess)
-	headRef := headGitRepo.UnstableGuessRefByShortName(headRefToGuess)
-
-	log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.GitRepo.Path, baseRefToGuess, baseRef, headRefToGuess, headRef)
-
-	baseRefValid := baseRef.IsBranch() || baseRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName), baseRef.ShortName())
-	headRefValid := headRef.IsBranch() || headRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(headRepo.ObjectFormatName), headRef.ShortName())
-	// Check if base&head ref are valid.
-	if !baseRefValid || !headRefValid {
-		ctx.APIErrorNotFound()
-		return nil, nil
-	}
-
-	compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseRef.ShortName(), headRef.ShortName(), false, false)
-	if err != nil {
-		ctx.APIErrorInternal(err)
-		return nil, nil
-	}
-
-	result = &parseCompareInfoResult{headRepo: headRepo, headGitRepo: headGitRepo, compareInfo: compareInfo, baseRef: baseRef, headRef: headRef}
-	return result, closer
-}
-
 // UpdatePullRequest merge PR's baseBranch into headBranch
 func UpdatePullRequest(ctx *context.APIContext) {
 	// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/update repository repoUpdatePullRequest
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 branch>...[<head repo>:]<head branch>
+// base<-head: master...head:feature
+// same repo: master...feature
+func ParseComparePathParams(ctx context.Context, pathParam string, baseRepo *repo_model.Repository, baseGitRepo *git.Repository) (*CompareInfo, error) {
+	ci := &CompareInfo{BaseRepo: baseRepo}
+	var err error
+
+	if pathParam == "" {
+		ci.CompareRouter = &CompareRouter{
+			HeadOriRef: baseRepo.DefaultBranch,
+			DotTimes:   3,
+		}
+	} else {
+		ci.CompareRouter, err = parseCompareRouter(pathParam)
+		if err != nil {
+			return nil, err
+		}
+	}
+	if ci.BaseOriRef == "" {
+		ci.BaseOriRef = baseRepo.DefaultBranch
+	}
+
+	if (ci.HeadOwnerName == "" && ci.HeadRepoName == "") ||
+		(ci.HeadOwnerName == baseRepo.Owner.Name && ci.HeadRepoName == baseRepo.Name) {
+		ci.HeadOwnerName = baseRepo.Owner.Name
+		ci.HeadRepoName = baseRepo.Name
+		ci.HeadUser = baseRepo.Owner
+		ci.HeadRepo = baseRepo
+		ci.HeadGitRepo = baseGitRepo
+	} else {
+		if ci.HeadOwnerName == baseRepo.Owner.Name {
+			ci.HeadUser = baseRepo.Owner
+			if ci.HeadRepoName == "" {
+				ci.HeadRepoName = baseRepo.Name
+				ci.HeadRepo = baseRepo
+			}
+		} else {
+			ci.HeadUser, err = user_model.GetUserByName(ctx, ci.HeadOwnerName)
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		if ci.HeadRepo == nil {
+			if ci.HeadRepoName != "" {
+				ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, ci.HeadOwnerName, ci.HeadRepoName)
+			} else {
+				ci.HeadRepo, err = findHeadRepo(ctx, baseRepo, ci.HeadUser.ID)
+			}
+			if err != nil {
+				return nil, err
+			}
+		}
+		if ci.HeadRepo != nil {
+			ci.HeadRepo.Owner = ci.HeadUser
+			ci.HeadGitRepo, err = gitrepo.OpenRepository(ctx, ci.HeadRepo)
+			if err != nil {
+				return nil, err
+			}
+			ci.close = func() {
+				if ci.HeadGitRepo != nil {
+					ci.HeadGitRepo.Close()
+				}
+			}
+		}
+	}
+
+	ci.BaseFullRef, ci.IsBaseCommit, err = detectFullRef(ctx, baseRepo.ID, baseGitRepo, ci.BaseOriRef)
+	if err != nil {
+		ci.Close()
+		return nil, err
+	}
+
+	if ci.HeadRepo != nil {
+		ci.HeadFullRef, ci.IsHeadCommit, err = detectFullRef(ctx, ci.HeadRepo.ID, ci.HeadGitRepo, ci.HeadOriRef)
+		if err != nil {
+			ci.Close()
+			return nil, err
+		}
+	}
+	return ci, nil
+}
+
+func (cr *CompareInfo) LoadRootRepoAndOwnForkRepo(ctx context.Context, baseRepo *repo_model.Repository, doer *user_model.User) (*repo_model.Repository, *repo_model.Repository, error) {
+	// find root repo
+	var rootRepo *repo_model.Repository
+	var err error
+	if !baseRepo.IsFork {
+		rootRepo = baseRepo
+	} else {
+		if !cr.HeadRepo.IsFork {
+			rootRepo = cr.HeadRepo
+		} else {
+			rootRepo, err = getRootRepo(ctx, baseRepo)
+			if err != nil {
+				return nil, nil, err
+			}
+		}
+	}
+
+	// find ownfork repo
+	var ownForkRepo *repo_model.Repository
+	if doer != nil && cr.HeadRepo.OwnerID != doer.ID && baseRepo.OwnerID != doer.ID {
+		ownForkRepo, err = findHeadRepo(ctx, baseRepo, doer.ID)
+		if err != nil {
+			return nil, nil, err
+		}
+	}
+
+	return rootRepo, ownForkRepo, nil
 }
diff --git a/routers/common/compare_test.go b/routers/common/compare_test.go
new file mode 100644
index 0000000000000..66a18361e0ad6
--- /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.Equal(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.Equal(t, *kase.compareInfo.CompareRouter, *r.CompareRouter)
+			assert.Equal(t, *kase.compareInfo.BaseRepo, *r.BaseRepo)
+			assert.Equal(t, *kase.compareInfo.HeadUser, *r.HeadUser)
+			assert.Equal(t, *kase.compareInfo.HeadRepo, *r.HeadRepo)
+			assert.Equal(t, kase.compareInfo.HeadGitRepo.Path, r.HeadGitRepo.Path)
+			assert.Equal(t, kase.compareInfo.IsBaseCommit, r.IsBaseCommit)
+			assert.Equal(t, kase.compareInfo.IsHeadCommit, r.IsHeadCommit)
+		})
+	}
+}
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 2c36477e6a85f..55adb77033bcb 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 branch>...[<head repo>:]<head branch>
-	// base<-head: master...head:feature
-	// same repo: master...feature
-
-	var (
-		isSameRepo bool
-		infoPath   string
-		err        error
-	)
-
-	infoPath = ctx.PathParam("*")
-	var infos []string
-	if infoPath == "" {
-		infos = []string{baseRepo.DefaultBranch, baseRepo.DefaultBranch}
-	} else {
-		infos = strings.SplitN(infoPath, "...", 2)
-		if len(infos) != 2 {
-			if infos = strings.SplitN(infoPath, "..", 2); len(infos) == 2 {
-				ci.DirectComparison = true
-				ctx.Data["PageIsComparePull"] = false
-			} else {
-				infos = []string{baseRepo.DefaultBranch, infoPath}
-			}
-		}
-	}
-
-	ctx.Data["BaseName"] = baseRepo.OwnerName
-	ci.BaseBranch = infos[0]
-	ctx.Data["BaseBranch"] = ci.BaseBranch
-
-	// If there is no head repository, it means compare between same repository.
-	headInfos := strings.Split(infos[1], ":")
-	if len(headInfos) == 1 {
-		isSameRepo = true
-		ci.HeadUser = ctx.Repo.Owner
-		ci.HeadBranch = headInfos[0]
-	} else if len(headInfos) == 2 {
-		headInfosSplit := strings.Split(headInfos[0], "/")
-		if len(headInfosSplit) == 1 {
-			ci.HeadUser, err = user_model.GetUserByName(ctx, headInfos[0])
-			if err != nil {
-				if user_model.IsErrUserNotExist(err) {
-					ctx.NotFound(nil)
-				} else {
-					ctx.ServerError("GetUserByName", err)
-				}
-				return nil
-			}
-			ci.HeadBranch = headInfos[1]
-			isSameRepo = ci.HeadUser.ID == ctx.Repo.Owner.ID
-			if isSameRepo {
-				ci.HeadRepo = baseRepo
-			}
-		} else {
-			ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, headInfosSplit[0], headInfosSplit[1])
-			if err != nil {
-				if repo_model.IsErrRepoNotExist(err) {
-					ctx.NotFound(nil)
-				} else {
-					ctx.ServerError("GetRepositoryByOwnerAndName", err)
-				}
-				return nil
-			}
-			if err := ci.HeadRepo.LoadOwner(ctx); err != nil {
-				if user_model.IsErrUserNotExist(err) {
-					ctx.NotFound(nil)
-				} else {
-					ctx.ServerError("GetUserByName", err)
-				}
-				return nil
-			}
-			ci.HeadBranch = headInfos[1]
-			ci.HeadUser = ci.HeadRepo.Owner
-			isSameRepo = ci.HeadRepo.ID == ctx.Repo.Repository.ID
-		}
-	} else {
-		ctx.NotFound(nil)
-		return nil
-	}
-	ctx.Data["HeadUser"] = ci.HeadUser
-	ctx.Data["HeadBranch"] = ci.HeadBranch
-	ctx.Repo.PullRequest.SameRepo = isSameRepo
-
-	// Check if base branch is valid.
-	baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch)
-	baseIsBranch := 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 16e1f2812e596..841e2d1c5c193 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, 6)
+	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, 7)
+	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 cf50d5e639e2d..57e80834cb42f 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