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