Files
gitea/services/convert/pull.go
wxiaoguang 9dc04289aa refactor: make git package handle all git operations (#38543)
before: gitrepo vs git packages
after: git package fully handle all git operations

by the way, use `WithRepo(repo)` instead of `WithDir(repo.Path)` to hide
path details.

benefits:

1. remove all unnecessary wrappers, developers no need to struggle with
"which package should be used"
2. simplify code, RepositoryFacade can (will) be used everywhere, all
"path" details are (will be) hidden
2026-07-20 16:07:38 +00:00

488 lines
14 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package convert
import (
"context"
"fmt"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/cache"
"gitea.dev/modules/cachegroup"
"gitea.dev/modules/git"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/services/gitdiff"
)
// ToAPIPullRequest assumes following fields have been assigned with valid values:
// Required - Issue
// Optional - Merger
func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) *api.PullRequest {
var (
baseBranch string
headBranch string
baseCommit *git.Commit
err error
)
if err = pr.LoadIssue(ctx); err != nil {
log.Error("pr.LoadIssue[%d]: %v", pr.ID, err)
return nil
}
if err = pr.Issue.LoadRepo(ctx); err != nil {
log.Error("pr.Issue.LoadRepo[%d]: %v", pr.ID, err)
return nil
}
apiIssue := ToAPIIssue(ctx, doer, pr.Issue)
if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("GetRepositoryById[%d]: %v", pr.ID, err)
return nil
}
if err := pr.LoadHeadRepo(ctx); err != nil {
log.Error("GetRepositoryById[%d]: %v", pr.ID, err)
return nil
}
var doerID int64
if doer != nil {
doerID = doer.ID
}
repoUserPerm, err := cache.GetWithContextCache(ctx, cachegroup.RepoUserPermission, fmt.Sprintf("%d-%d", pr.BaseRepoID, doerID),
func(ctx context.Context, _ string) (access_model.Permission, error) {
return access_model.GetDoerRepoPermission(ctx, pr.BaseRepo, doer)
},
)
if err != nil {
log.Error("GetDoerRepoPermission[%d]: %v", pr.BaseRepoID, err)
repoUserPerm.AccessMode = perm.AccessModeNone
}
apiPullRequest := &api.PullRequest{
ID: pr.ID,
URL: pr.Issue.HTMLURL(ctx),
Index: pr.Index,
Poster: apiIssue.Poster,
Title: apiIssue.Title,
Body: apiIssue.Body,
Labels: apiIssue.Labels,
Milestone: apiIssue.Milestone,
Assignee: apiIssue.Assignee,
Assignees: util.SliceNilAsEmpty(apiIssue.Assignees),
State: apiIssue.State,
Draft: pr.IsWorkInProgress(ctx),
IsLocked: apiIssue.IsLocked,
Comments: apiIssue.Comments,
ReviewComments: pr.GetReviewCommentsCount(ctx),
HTMLURL: pr.Issue.HTMLURL(ctx),
DiffURL: pr.Issue.DiffURL(),
PatchURL: pr.Issue.PatchURL(),
HasMerged: pr.HasMerged,
MergeBase: pr.MergeBase,
Mergeable: pr.Mergeable(ctx),
Deadline: apiIssue.Deadline,
Created: pr.Issue.CreatedUnix.AsTimePtr(),
Updated: pr.Issue.UpdatedUnix.AsTimePtr(),
PinOrder: util.Iif(apiIssue.PinOrder == -1, 0, apiIssue.PinOrder),
ContentVersion: apiIssue.ContentVersion,
// output "[]" rather than null to align to github outputs
RequestedReviewers: []*api.User{},
RequestedReviewersTeams: []*api.Team{},
AllowMaintainerEdit: pr.AllowMaintainerEdit,
Base: &api.PRBranchInfo{
Name: pr.BaseBranch,
Ref: pr.BaseBranch,
RepoID: pr.BaseRepoID,
Repository: ToRepo(ctx, pr.BaseRepo, repoUserPerm),
},
Head: &api.PRBranchInfo{
Name: pr.HeadBranch,
Ref: pr.GetGitHeadRefName(),
RepoID: -1,
},
}
if err = pr.LoadRequestedReviewers(ctx); err != nil {
log.Error("LoadRequestedReviewers[%d]: %v", pr.ID, err)
return nil
}
if err = pr.LoadRequestedReviewersTeams(ctx); err != nil {
log.Error("LoadRequestedReviewersTeams[%d]: %v", pr.ID, err)
return nil
}
for _, reviewer := range pr.RequestedReviewers {
apiPullRequest.RequestedReviewers = append(apiPullRequest.RequestedReviewers, ToUser(ctx, reviewer, nil))
}
for _, reviewerTeam := range pr.RequestedReviewersTeams {
convertedTeam, err := ToTeam(ctx, reviewerTeam, true)
if err != nil {
log.Error("LoadRequestedReviewersTeams[%d]: %v", pr.ID, err)
return nil
}
apiPullRequest.RequestedReviewersTeams = append(apiPullRequest.RequestedReviewersTeams, convertedTeam)
}
if pr.Issue.ClosedUnix != 0 {
apiPullRequest.Closed = pr.Issue.ClosedUnix.AsTimePtr()
}
gitRepo, err := git.OpenRepository(pr.BaseRepo)
if err != nil {
log.Error("OpenRepository[%s]: %v", pr.BaseRepo.FullName(), err)
return nil
}
defer gitRepo.Close()
exist, err := git_model.IsBranchExist(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
log.Error("GetBranch[%s]: %v", pr.BaseBranch, err)
return nil
}
if exist {
baseCommit, err = gitRepo.GetBranchCommit(ctx, pr.BaseBranch)
if err != nil && !git.IsErrNotExist(err) {
log.Error("GetCommit[%s]: %v", baseBranch, err)
return nil
}
if err == nil {
apiPullRequest.Base.Sha = baseCommit.ID.String()
}
}
if pr.Flow == issues_model.PullRequestFlowAGit {
apiPullRequest.Head.Sha, err = gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName())
if err != nil {
log.Error("GetRefCommitID[%s]: %v", pr.GetGitHeadRefName(), err)
return nil
}
apiPullRequest.Head.RepoID = pr.BaseRepoID
apiPullRequest.Head.Repository = apiPullRequest.Base.Repository
apiPullRequest.Head.Name = ""
}
if pr.HeadRepo != nil && pr.Flow == issues_model.PullRequestFlowGithub {
p, err := access_model.GetDoerRepoPermission(ctx, pr.HeadRepo, doer)
if err != nil {
log.Error("GetDoerRepoPermission[%d]: %v", pr.HeadRepoID, err)
p.AccessMode = perm.AccessModeNone
}
apiPullRequest.Head.RepoID = pr.HeadRepo.ID
apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p)
headGitRepo, err := git.OpenRepository(pr.HeadRepo)
if err != nil {
log.Error("OpenRepository[%s]: %v", pr.HeadRepo.FullName(), err)
return nil
}
defer headGitRepo.Close()
exist, err = git_model.IsBranchExist(ctx, pr.HeadRepoID, pr.HeadBranch)
if err != nil {
log.Error("GetBranch[%s]: %v", pr.HeadBranch, err)
return nil
}
// Outer scope variables to be used in diff calculation
var (
startCommitID string
endCommitID string
)
if !exist {
headCommitID, err := headGitRepo.GetRefCommitID(ctx, apiPullRequest.Head.Ref)
if err != nil && !git.IsErrNotExist(err) {
log.Error("GetCommit[%s]: %v", pr.HeadBranch, err)
return nil
}
if err == nil {
apiPullRequest.Head.Sha = headCommitID
endCommitID = headCommitID
}
} else {
commit, err := headGitRepo.GetBranchCommit(ctx, pr.HeadBranch)
if err != nil && !git.IsErrNotExist(err) {
log.Error("GetCommit[%s]: %v", headBranch, err)
return nil
}
if err == nil {
apiPullRequest.Head.Ref = pr.HeadBranch
apiPullRequest.Head.Sha = commit.ID.String()
endCommitID = commit.ID.String()
}
}
// Calculate diff
startCommitID = pr.MergeBase
diffShortStats, err := gitdiff.GetDiffShortStat(ctx, gitRepo, startCommitID, endCommitID)
if err != nil {
log.Error("GetDiffShortStat: %v", err)
} else {
apiPullRequest.ChangedFiles = &diffShortStats.NumFiles
apiPullRequest.Additions = &diffShortStats.TotalAddition
apiPullRequest.Deletions = &diffShortStats.TotalDeletion
}
}
if len(apiPullRequest.Head.Sha) == 0 && len(apiPullRequest.Head.Ref) != 0 {
baseGitRepo, err := git.OpenRepository(pr.BaseRepo)
if err != nil {
log.Error("OpenRepository[%s]: %v", pr.BaseRepo.FullName(), err)
return nil
}
defer baseGitRepo.Close()
refs, err := baseGitRepo.GetRefsFiltered(ctx, apiPullRequest.Head.Ref)
if err != nil {
log.Error("GetRefsFiltered[%s]: %v", apiPullRequest.Head.Ref, err)
return nil
} else if len(refs) == 0 {
log.Error("unable to resolve PR head ref")
} else {
apiPullRequest.Head.Sha = refs[0].Object.String()
}
}
if pr.HasMerged {
apiPullRequest.Merged = pr.MergedUnix.AsTimePtr()
apiPullRequest.MergedCommitID = &pr.MergedCommitID
apiPullRequest.MergedBy = ToUser(ctx, pr.Merger, nil)
}
return apiPullRequest
}
func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs issues_model.PullRequestList, doer *user_model.User) ([]*api.PullRequest, error) {
for _, pr := range prs {
pr.BaseRepo = baseRepo
if pr.BaseRepoID == pr.HeadRepoID {
pr.HeadRepo = baseRepo
}
}
// NOTE: load head repositories
if err := prs.LoadRepositories(ctx); err != nil {
return nil, err
}
issueList, err := prs.LoadIssues(ctx)
if err != nil {
return nil, err
}
if err := issueList.LoadLabels(ctx); err != nil {
return nil, err
}
if err := issueList.LoadPosters(ctx); err != nil {
return nil, err
}
if err := issueList.LoadAttachments(ctx); err != nil {
return nil, err
}
if err := issueList.LoadMilestones(ctx); err != nil {
return nil, err
}
if err := issueList.LoadAssignees(ctx); err != nil {
return nil, err
}
if err = issueList.LoadPinOrder(ctx); err != nil {
return nil, err
}
reviews, err := prs.LoadReviews(ctx)
if err != nil {
return nil, err
}
if err = reviews.LoadReviewers(ctx); err != nil {
return nil, err
}
reviewersMap := make(map[int64][]*user_model.User)
for _, review := range reviews {
if review.Reviewer != nil {
reviewersMap[review.IssueID] = append(reviewersMap[review.IssueID], review.Reviewer)
}
}
reviewCounts, err := prs.LoadReviewCommentsCounts(ctx)
if err != nil {
return nil, err
}
gitRepo, err := git.OpenRepository(baseRepo)
if err != nil {
return nil, err
}
defer gitRepo.Close()
baseRepoPerm, err := access_model.GetDoerRepoPermission(ctx, baseRepo, doer)
if err != nil {
log.Error("GetDoerRepoPermission[%d]: %v", baseRepo.ID, err)
baseRepoPerm.AccessMode = perm.AccessModeNone
}
apiRepo := ToRepo(ctx, baseRepo, baseRepoPerm)
baseBranchCache := make(map[string]*git_model.Branch)
apiPullRequests := make([]*api.PullRequest, 0, len(prs))
for _, pr := range prs {
apiIssue := ToAPIIssue(ctx, doer, pr.Issue)
apiPullRequest := &api.PullRequest{
ID: pr.ID,
URL: pr.Issue.HTMLURL(ctx),
Index: pr.Index,
Poster: apiIssue.Poster,
Title: apiIssue.Title,
Body: apiIssue.Body,
Labels: apiIssue.Labels,
Milestone: apiIssue.Milestone,
Assignee: apiIssue.Assignee,
Assignees: apiIssue.Assignees,
State: apiIssue.State,
Draft: pr.IsWorkInProgress(ctx),
IsLocked: apiIssue.IsLocked,
Comments: apiIssue.Comments,
ReviewComments: reviewCounts[pr.IssueID],
HTMLURL: pr.Issue.HTMLURL(ctx),
DiffURL: pr.Issue.DiffURL(),
PatchURL: pr.Issue.PatchURL(),
HasMerged: pr.HasMerged,
MergeBase: pr.MergeBase,
Mergeable: pr.Mergeable(ctx),
Deadline: apiIssue.Deadline,
Created: pr.Issue.CreatedUnix.AsTimePtr(),
Updated: pr.Issue.UpdatedUnix.AsTimePtr(),
PinOrder: util.Iif(apiIssue.PinOrder == -1, 0, apiIssue.PinOrder),
ContentVersion: apiIssue.ContentVersion,
AllowMaintainerEdit: pr.AllowMaintainerEdit,
Base: &api.PRBranchInfo{
Name: pr.BaseBranch,
Ref: pr.BaseBranch,
RepoID: pr.BaseRepoID,
Repository: apiRepo,
},
Head: &api.PRBranchInfo{
Name: pr.HeadBranch,
Ref: pr.GetGitHeadRefName(),
RepoID: -1,
},
}
pr.RequestedReviewers = reviewersMap[pr.IssueID]
for _, reviewer := range pr.RequestedReviewers {
apiPullRequest.RequestedReviewers = append(apiPullRequest.RequestedReviewers, ToUser(ctx, reviewer, nil))
}
for _, reviewerTeam := range pr.RequestedReviewersTeams {
convertedTeam, err := ToTeam(ctx, reviewerTeam, true)
if err != nil {
log.Error("LoadRequestedReviewersTeams[%d]: %v", pr.ID, err)
return nil, err
}
apiPullRequest.RequestedReviewersTeams = append(apiPullRequest.RequestedReviewersTeams, convertedTeam)
}
if pr.Issue.ClosedUnix != 0 {
apiPullRequest.Closed = pr.Issue.ClosedUnix.AsTimePtr()
}
baseBranch, ok := baseBranchCache[pr.BaseBranch]
if !ok {
baseBranch, err = git_model.GetBranch(ctx, baseRepo.ID, pr.BaseBranch)
if err == nil {
baseBranchCache[pr.BaseBranch] = baseBranch
} else if !git_model.IsErrBranchNotExist(err) {
return nil, err
}
}
if baseBranch != nil {
apiPullRequest.Base.Sha = baseBranch.CommitID
}
if pr.HeadRepoID == pr.BaseRepoID {
apiPullRequest.Head.Repository = apiPullRequest.Base.Repository
}
// pull request head branch, both repository and branch could not exist
if pr.HeadRepo != nil {
apiPullRequest.Head.RepoID = pr.HeadRepo.ID
exist, err := git_model.IsBranchExist(ctx, pr.HeadRepo.ID, pr.HeadBranch)
if err != nil {
log.Error("IsBranchExist[%d]: %v", pr.HeadRepo.ID, err)
return nil, err
}
if exist {
apiPullRequest.Head.Ref = pr.HeadBranch
}
if pr.HeadRepoID != pr.BaseRepoID {
p, err := access_model.GetDoerRepoPermission(ctx, pr.HeadRepo, doer)
if err != nil {
log.Error("GetDoerRepoPermission[%d]: %v", pr.HeadRepoID, err)
p.AccessMode = perm.AccessModeNone
}
apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p)
}
}
if apiPullRequest.Head.Ref == "" {
apiPullRequest.Head.Ref = pr.GetGitHeadRefName()
}
if pr.Flow == issues_model.PullRequestFlowAGit {
apiPullRequest.Head.Name = ""
}
apiPullRequest.Head.Sha, err = gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName())
if err != nil {
log.Error("GetRefCommitID[%s]: %v", pr.GetGitHeadRefName(), err)
}
if len(apiPullRequest.Head.Sha) == 0 && len(apiPullRequest.Head.Ref) != 0 {
refs, err := gitRepo.GetRefsFiltered(ctx, apiPullRequest.Head.Ref)
if err != nil {
log.Error("GetRefsFiltered[%s]: %v", apiPullRequest.Head.Ref, err)
return nil, err
} else if len(refs) == 0 {
log.Error("unable to resolve PR head ref")
} else {
apiPullRequest.Head.Sha = refs[0].Object.String()
}
}
if pr.HasMerged {
apiPullRequest.Merged = pr.MergedUnix.AsTimePtr()
apiPullRequest.MergedCommitID = &pr.MergedCommitID
apiPullRequest.MergedBy = ToUser(ctx, pr.Merger, nil)
}
// Do not provide "ChangeFiles/Additions/Deletions" for the PR list, because the "diff" is quite slow
// If callers are interested in these values, they should do a separate request to get the PR details
if apiPullRequest.ChangedFiles != nil || apiPullRequest.Additions != nil || apiPullRequest.Deletions != nil {
setting.PanicInDevOrTesting("ChangedFiles/Additions/Deletions should not be set in PR list")
}
apiPullRequests = append(apiPullRequests, apiPullRequest)
}
return apiPullRequests, nil
}