gitea_hat/routers/hat/repo/pull.go

627 lines
18 KiB
Go

package repo
import (
"errors"
"fmt"
"math"
"net/http"
"strconv"
"strings"
"time"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/gitdiff"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
gitea_api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
gitea_convert "code.gitea.io/gitea/services/convert"
git_service "code.gitea.io/gitea/services/git"
issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull"
hat_convert "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/convert"
hat_api "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/structs"
hat_gitdiff "code.gitlink.org.cn/Gitlink/gitea_hat.git/services/gitdiff"
hat_pull_service "code.gitlink.org.cn/Gitlink/gitea_hat.git/services/pull"
)
func ChangePullRequestCloseStatus(ctx *context.APIContext) {
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if err = pr.LoadBaseRepo(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if err = pr.LoadHeadRepo(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
changeStatus := ctx.FormBool("is_closed")
if changeStatus {
_, err = issues_model.CloseIssue(ctx, pr.Issue, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
} else {
_, err = issues_model.ReopenIssue(ctx, pr.Issue, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
if err = hat_pull_service.CheckPullRequestBranchMergeable(ctx, pr); err != nil {
ctx.APIErrorInternal(err)
return
}
// Update Commit Divergence
divergence, err := gitrepo.GetDivergingCommits(ctx, pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
if err != nil {
ctx.APIErrorInternal(err)
return
}
pr.CommitsAhead = divergence.Ahead
pr.CommitsBehind = divergence.Behind
if _, err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files", "base_branch", "commits_ahead", "commits_behind"); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
func CreatePrVersion(ctx *context.APIContext) {
form := web.GetForm(ctx).(*gitea_api.PullRequestPayload)
hat_pull_service.AddToTaskQueue(&issues_model.PullRequest{ID: form.PullRequest.ID}, string(form.Action))
ctx.Status(http.StatusNoContent)
}
func EditPullRequest(ctx *context.APIContext) {
form := web.GetForm(ctx).(*gitea_api.EditPullRequestOption)
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
err = pr.LoadIssue(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
}
issue := pr.Issue
issue.Repo = ctx.Repo.Repository
if err := issue.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWrite(unit.TypePullRequests) {
ctx.Status(http.StatusForbidden)
return
}
if len(form.Title) > 0 {
err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
if form.Body != nil {
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion)
if err != nil {
if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
ctx.APIError(http.StatusBadRequest, err)
return
}
ctx.APIErrorInternal(err)
return
}
}
// Update or remove deadline if set
if form.Deadline != nil || form.RemoveDeadline != nil {
var deadlineUnix timeutil.TimeStamp
if (form.RemoveDeadline == nil || !*form.RemoveDeadline) && !form.Deadline.IsZero() {
deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
23, 59, 59, 0, form.Deadline.Location())
deadlineUnix = timeutil.TimeStamp(deadline.Unix())
}
if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
ctx.APIErrorInternal(err)
return
}
issue.DeadlineUnix = deadlineUnix
}
// Add/delete assignees
// Deleting is done the GitHub way (quote from their api documentation):
// https://developer.github.com/v3/issues/#edit-an-issue
// "assignees" (array): Logins for Users to assign to this issue.
// Pass one or more user logins to replace the set of assignees on this Issue.
// Send an empty array ([]) to clear all assignees from the Issue.
if ctx.Repo.CanWrite(unit.TypePullRequests) && (form.Assignees != nil || len(form.Assignee) > 0) {
err = issue_service.UpdateAssignees(ctx, issue, form.Assignee, form.Assignees, ctx.Doer)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: [name: %s]", err))
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
}
if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Milestone != 0 &&
issue.MilestoneID != form.Milestone {
oldMilestoneID := issue.MilestoneID
issue.MilestoneID = form.Milestone
issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
ctx.APIErrorInternal(err)
return
}
}
if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Labels != nil {
labels, err := issues_model.GetLabelsInRepoByIDs(ctx, ctx.Repo.Repository.ID, form.Labels)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if ctx.Repo.Owner.IsOrganization() {
orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx, ctx.Repo.Owner.ID, form.Labels)
if err != nil {
ctx.APIErrorInternal(err)
return
}
labels = append(labels, orgLabels...)
}
if err = issues_model.ReplaceIssueLabels(ctx, issue, labels, ctx.Doer); err != nil {
ctx.APIErrorInternal(err)
return
}
}
if form.State != nil {
if pr.HasMerged {
ctx.APIError(http.StatusPreconditionFailed, "cannot change state of this pull request, it was already merged")
return
}
state := gitea_api.StateType(*form.State)
closeOrReopenIssue(ctx, issue, state)
if ctx.Written() {
return
}
}
// change pull target branch
if !pr.HasMerged && len(form.Base) != 0 && form.Base != pr.BaseBranch {
if !gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, form.Base) {
ctx.APIError(http.StatusNotFound, fmt.Errorf("new base '%s' not exist", form.Base))
return
}
if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, form.Base); err != nil {
if issues_model.IsErrPullRequestAlreadyExists(err) {
ctx.APIError(http.StatusConflict, err)
return
} else if issues_model.IsErrIssueIsClosed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
} else if pull_service.IsErrPullRequestHasMerged(err) {
ctx.APIError(http.StatusConflict, err)
return
}
ctx.APIErrorInternal(err)
return
}
notify_service.PullRequestChangeTargetBranch(ctx, ctx.Doer, pr, form.Base)
}
// update allow edits
if form.AllowMaintainerEdit != nil {
if err := pull_service.SetAllowEdits(ctx, ctx.Doer, pr, *form.AllowMaintainerEdit); err != nil {
if errors.Is(err, pull_service.ErrUserHasNoPermissionForAction) {
ctx.APIError(http.StatusForbidden, fmt.Sprintf("SetAllowEdits: %s", err))
return
}
ctx.APIErrorInternal(err)
return
}
}
// Refetch from database
pr, err = issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pr.Index)
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
// TODO this should be 200, not 201
ctx.JSON(http.StatusCreated, gitea_convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
}
func GetPullRequest(ctx *context.APIContext) {
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
if err = pr.LoadBaseRepo(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if err = pr.LoadHeadRepo(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
apiResult, err := hat_convert.ToAPIPullRequest(ctx, ctx.Repo.GitRepo, pr, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiResult)
}
type PullRequestCommit struct {
git_model.SignCommitWithStatuses
Sha string
}
func GetPullCommits(ctx *context.APIContext) {
pull, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
issue := pull.Issue
if issue == nil {
ctx.APIErrorNotFound("issue is not present.")
return
}
gitRepo := ctx.Repo.GitRepo
var commits []*git.Commit
var prInfo *git_service.CompareInfo
if pull.HasMerged {
var baseCommit string
if pull.MergeBase == "" {
var commitSHA, parentCommit string
commitSHA, err := gitRepo.GetRefCommitID(pull.GetGitHeadRefName())
if err != nil {
commitSHA, err := gitRepo.ReadPatchCommit(pull.Index)
if err == nil {
if err := gitrepo.UpdateRef(ctx, ctx.Repo.Repository, pull.GetGitHeadRefName(), commitSHA); err != nil {
log.Error("Could not write head file", err)
}
} else {
log.Trace("No history file available for PR %d", pull.Index)
}
}
if commitSHA != "" {
parentCommit, _, err = gitrepo.RunCmdString(ctx, ctx.Repo.Repository,
gitcmd.NewCommand("rev-list", "-1", "--skip=1").AddDynamicArguments(commitSHA))
if err == nil {
parentCommit = strings.TrimSpace(parentCommit)
}
if err != nil || parentCommit == "" {
log.Info("No known parent commit for PR %d, error: %v", pull.Index, err)
parentCommit = commitSHA
}
}
baseCommit = parentCommit
} else {
baseCommit = pull.MergeBase
}
prInfo, err = git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, gitRepo, git.RefName(baseCommit), git.RefName(pull.GetGitHeadRefName()), false, false)
} else {
prInfo, err = git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, gitRepo, git.RefNameFromBranch(pull.BaseBranch), git.RefName(pull.GetGitHeadRefName()), false, false)
}
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
commits = prInfo.Commits
userCommits, err := user_model.ValidateCommitsWithEmails(ctx, commits)
if err != nil {
ctx.APIErrorInternal(err)
return
}
signCommits, err := git_service.ParseCommitsWithSignature(ctx, ctx.Repo.Repository, userCommits, ctx.Repo.Repository.GetTrustModel())
if err != nil {
ctx.APIErrorInternal(err)
return
}
signStatusCommits, err := git_service.ParseCommitsWithStatus(ctx, signCommits, ctx.Repo.Repository)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]PullRequestCommit, 0)
for _, commit := range signStatusCommits {
result = append(result, PullRequestCommit{
SignCommitWithStatuses: *commit,
Sha: commit.ID.String(),
})
}
ctx.JSON(http.StatusOK, result)
}
func GetPullFiles(ctx *context.APIContext) {
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
if err := pr.LoadBaseRepo(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if err := pr.LoadHeadRepo(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
baseGitRepo := ctx.Repo.GitRepo
var prInfo *git_service.CompareInfo
if pr.HasMerged {
prInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefName(pr.MergeBase), git.RefName(pr.GetGitHeadRefName()), false, false)
} else {
prInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), false, false)
}
if err != nil {
ctx.APIErrorInternal(err)
return
}
headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName())
if err != nil {
ctx.APIErrorInternal(err)
return
}
startCommitID := prInfo.MergeBase
endCommitID := headCommitID
diff, err := gitdiff.GetDiffForAPI(ctx, baseGitRepo, &gitdiff.DiffOptions{
BeforeCommitID: startCommitID,
AfterCommitID: endCommitID,
SkipTo: ctx.FormString("skip-to"),
MaxLines: setting.Git.MaxGitDiffLines,
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
MaxFiles: -1,
WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.FormString("whitespace")),
})
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ctx.Repo.Repository, baseGitRepo, startCommitID, endCommitID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err != nil {
ctx.APIErrorInternal(err)
return
}
isNew := ctx.FormString("isNew")
if isNew != "" {
listOptions := utils.GetListOptions(ctx)
totalNumberOfFiles := diffShortStat.NumFiles
totalNumberOfPages := int(math.Ceil(float64(totalNumberOfFiles) / float64(listOptions.PageSize)))
start, limit := listOptions.GetSkipTake()
limit = min(limit, totalNumberOfFiles-start)
limit = max(limit, 0)
apiFiles := make([]*hat_api.ChangedFile, 0, limit)
for i := start; i < start+limit; i++ {
apiFiles = append(apiFiles, hat_convert.ToChangedFile(diff.Files[i], ctx.Repo.Repository, headCommitID))
}
ctx.SetLinkHeader(int64(totalNumberOfFiles), listOptions.PageSize)
ctx.SetTotalCountHeader(int64(totalNumberOfFiles))
ctx.RespHeader().Set("X-Page", strconv.Itoa(listOptions.Page))
ctx.RespHeader().Set("X-PerPage", strconv.Itoa(listOptions.PageSize))
ctx.RespHeader().Set("X-PageCount", strconv.Itoa(totalNumberOfPages))
ctx.RespHeader().Set("X-HasMore", strconv.FormatBool(listOptions.Page < totalNumberOfPages))
ctx.AppendAccessControlExposeHeaders("X-Page", "X-PerPage", "X-PageCount", "X-HasMore")
ctx.JSON(http.StatusOK, struct {
Files []*hat_api.ChangedFile `json:"files"`
TotalAddition int `json:"total_addition"`
TotalDeletion int `json:"total_deletion"`
}{
Files: apiFiles,
TotalAddition: diffShortStat.TotalAddition,
TotalDeletion: diffShortStat.TotalDeletion,
})
} else {
fileDiff := struct {
*gitdiff.Diff
LatestSha string
}{
Diff: diff,
LatestSha: endCommitID,
}
ctx.JSON(http.StatusOK, fileDiff)
}
}
func GetPullFilesByPath(ctx *context.APIContext) {
path := ctx.PathParam("*")
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
if err := pr.LoadBaseRepo(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if err := pr.LoadHeadRepo(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
baseGitRepo := ctx.Repo.GitRepo
var prInfo *git_service.CompareInfo
if pr.HasMerged {
prInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefName(pr.MergeBase), git.RefName(pr.GetGitHeadRefName()), false, false)
} else {
prInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), false, false)
}
if err != nil {
ctx.APIErrorInternal(err)
return
}
headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName())
if err != nil {
ctx.APIErrorInternal(err)
return
}
startCommitID := prInfo.MergeBase
endCommitID := headCommitID
diff, err := gitdiff.GetDiffForAPI(ctx, baseGitRepo, &gitdiff.DiffOptions{
BeforeCommitID: startCommitID,
AfterCommitID: endCommitID,
SkipTo: ctx.FormString("skip-to"),
MaxLines: setting.Git.MaxGitDiffLines,
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
MaxFiles: -1,
WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.FormString("whitespace")),
}, path)
if err != nil {
ctx.APIErrorInternal(err)
return
}
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, startCommitID, endCommitID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
fileDiff := struct {
*hat_gitdiff.Diff
LatestSha string
}{
Diff: &hat_gitdiff.Diff{Diff: diff, NumFiles: diffShortStat.NumFiles, TotalAddition: diffShortStat.TotalAddition, TotalDeletion: diffShortStat.TotalDeletion},
LatestSha: endCommitID,
}
ctx.JSON(http.StatusOK, fileDiff)
}
func closeOrReopenIssue(ctx *context.APIContext, issue *issues_model.Issue, state gitea_api.StateType) {
if state != gitea_api.StateOpen && state != gitea_api.StateClosed {
ctx.APIError(http.StatusPreconditionFailed, fmt.Sprintf("unknown state: %s", state))
return
}
if state == gitea_api.StateClosed && !issue.IsClosed {
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
if issues_model.IsErrDependenciesLeft(err) {
ctx.APIError(http.StatusPreconditionFailed, "cannot close this issue or pull request because it still has open dependencies")
return
}
ctx.APIErrorInternal(err)
return
}
} else if state == gitea_api.StateOpen && issue.IsClosed {
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
ctx.APIErrorInternal(err)
return
}
}
}