gitea_hat/routers/hat/repo/pull.go

405 lines
12 KiB
Go

package repo
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
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/services/gitdiff"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"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"
gitea_convert "code.gitea.io/gitea/services/convert"
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_pull_service "code.gitlink.org.cn/Gitlink/gitea_hat.git/services/pull"
)
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.ParamsInt64(":index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
}
return
}
err = pr.LoadIssue(ctx)
if err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
}
issue := pr.Issue
issue.Repo = ctx.Repo.Repository
if err := issue.LoadAttributes(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return
}
if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWrite(unit.TypePullRequests) {
ctx.Status(http.StatusForbidden)
return
}
oldTitle := issue.Title
if len(form.Title) > 0 {
issue.Title = form.Title
}
if len(form.Body) > 0 {
issue.Content = form.Body
}
// 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.Error(http.StatusInternalServerError, "UpdateIssueDeadline", 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.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
} else {
ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
}
return
}
}
if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Milestone != 0 &&
issue.MilestoneID != form.Milestone {
oldMilestoneID := issue.MilestoneID
issue.MilestoneID = form.Milestone
if err = issue_service.ChangeMilestoneAssign(issue, ctx.Doer, oldMilestoneID); err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", 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.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDsError", err)
return
}
if ctx.Repo.Owner.IsOrganization() {
orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx, ctx.Repo.Owner.ID, form.Labels)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsInOrgByIDs", err)
return
}
labels = append(labels, orgLabels...)
}
if err = issues_model.ReplaceIssueLabels(issue, labels, ctx.Doer); err != nil {
ctx.Error(http.StatusInternalServerError, "ReplaceLabelsError", err)
return
}
}
if form.State != nil {
if pr.HasMerged {
ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged")
return
}
issue.IsClosed = gitea_api.StateClosed == gitea_api.StateType(*form.State)
}
statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(ctx, issue, ctx.Doer)
if err != nil {
if issues_model.IsErrDependenciesLeft(err) {
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies")
return
}
ctx.Error(http.StatusInternalServerError, "UpdateIssueByAPI", err)
return
}
if titleChanged {
notify_service.IssueChangeTitle(ctx, ctx.Doer, issue, oldTitle)
}
if statusChangeComment != nil {
notify_service.IssueChangeStatus(ctx, ctx.Doer, "", issue, statusChangeComment, issue.IsClosed)
}
// change pull target branch
if !pr.HasMerged && len(form.Base) != 0 && form.Base != pr.BaseBranch {
if !ctx.Repo.GitRepo.IsBranchExist(form.Base) {
ctx.Error(http.StatusNotFound, "NewBaseBranchNotExist", 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.Error(http.StatusConflict, "IsErrPullRequestAlreadyExists", err)
return
} else if issues_model.IsErrIssueIsClosed(err) {
ctx.Error(http.StatusUnprocessableEntity, "IsErrIssueIsClosed", err)
return
} else if models.IsErrPullRequestHasMerged(err) {
ctx.Error(http.StatusConflict, "IsErrPullRequestHasMerged", err)
return
} else {
ctx.InternalServerError(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(pull_service.ErrUserHasNoPermissionForAction, err) {
ctx.Error(http.StatusForbidden, "SetAllowEdits", fmt.Sprintf("SetAllowEdits: %s", err))
return
}
ctx.ServerError("SetAllowEdits", 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.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", 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.ParamsInt64(":index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
}
return
}
if err = pr.LoadBaseRepo(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
return
}
if err = pr.LoadHeadRepo(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
return
}
apiResult, err := hat_convert.ToAPIPullRequest(ctx, ctx.Repo.GitRepo, pr, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ToAPIPullRequest", 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.ParamsInt64(":index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
}
return
}
issue := pull.Issue
if issue == nil {
ctx.NotFound("issue is not present.")
return
}
gitRepo := ctx.Repo.GitRepo
var commits []*git.Commit
var prInfo *git.CompareInfo
if pull.HasMerged {
var baseCommit string
if pull.MergeBase == "" {
var commitSHA, parentCommit string
commitSHA, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
if err != nil {
commitSHA, err := gitRepo.ReadPatchCommit(pull.Index)
if err == nil {
if err := gitRepo.SetReference(pull.GetGitRefName(), 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 = git.NewCommand(ctx, "rev-list", "-1", "--skip=1").AddDynamicArguments(commitSHA).RunStdString(&git.RunOpts{Dir: gitRepo.Path})
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 = gitRepo.GetCompareInfo(gitRepo.Path, baseCommit, pull.GetGitRefName(), false, false)
} else {
prInfo, err = gitRepo.GetCompareInfo(gitRepo.Path, pull.MergeBase, pull.GetGitRefName(), false, false)
}
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err)
return
}
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
commits = prInfo.Commits
userCommits := user_model.ValidateCommitsWithEmails(ctx, commits)
signCommits := asymkey_model.ParseCommitsWithSignature(ctx, userCommits, ctx.Repo.Repository.GetTrustModel(),
func(user *user_model.User) (bool, error) {
return repo_model.IsOwnerMemberCollaborator(ctx, ctx.Repo.Repository, user.ID)
})
signStatusCommits := git_model.ParseCommitsWithStatus(ctx, signCommits, ctx.Repo.Repository)
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.ParamsInt64(":index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
}
return
}
if err := pr.LoadBaseRepo(ctx); err != nil {
ctx.InternalServerError(err)
return
}
if err := pr.LoadHeadRepo(ctx); err != nil {
ctx.InternalServerError(err)
return
}
baseGitRepo := ctx.Repo.GitRepo
var prInfo *git.CompareInfo
if pr.HasMerged {
prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.MergeBase, pr.GetGitRefName(), true, false)
} else {
prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName(), true, false)
}
if err != nil {
ctx.ServerError("GetCompareInfo", err)
return
}
headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
if err != nil {
ctx.ServerError("GetRefCommitID", err)
return
}
startCommitID := prInfo.MergeBase
endCommitID := headCommitID
diff, err := gitdiff.GetDiff(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")),
})
if err != nil {
ctx.ServerError("GetDiff", err)
return
}
fileDiff := struct {
*gitdiff.Diff
LatestSha string
}{
Diff: diff,
LatestSha: endCommitID,
}
ctx.JSON(http.StatusOK, fileDiff)
}