summaryrefslogtreecommitdiffstats
path: root/routers/web/repo/issue.go
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2022-04-08 02:59:56 +0800
committerGitHub <noreply@github.com>2022-04-07 20:59:56 +0200
commit783a02188970ba5800514f7c64f6a818f65c04a1 (patch)
tree8c7dc3bbe00abbc245203f94c437f157168cc54a /routers/web/repo/issue.go
parentbb7e0619c3356227d6c5826cb789841f6bc4a05a (diff)
downloadgitea-783a02188970ba5800514f7c64f6a818f65c04a1.tar.gz
gitea-783a02188970ba5800514f7c64f6a818f65c04a1.zip
Never use /api/v1 from Gitea UI Pages (#19318)
Reusing `/api/v1` from Gitea UI Pages have pros and cons. Pros: 1) Less code copy Cons: 1) API/v1 have to support shared session with page requests. 2) You need to consider for each other when you want to change something about api/v1 or page. This PR moves all dependencies to API/v1 from UI Pages. Partially replace #16052
Diffstat (limited to 'routers/web/repo/issue.go')
-rw-r--r--routers/web/repo/issue.go382
1 files changed, 382 insertions, 0 deletions
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index a1a7200ba4..1fd60812f9 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -16,6 +16,7 @@ import (
"path"
"strconv"
"strings"
+ "time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
@@ -36,6 +37,7 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates/vars"
+ "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -1762,6 +1764,20 @@ func getActionIssues(ctx *context.Context) []*models.Issue {
return issues
}
+// GetIssueInfo get an issue of a repository
+func GetIssueInfo(ctx *context.Context) {
+ issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ if err != nil {
+ if models.IsErrIssueNotExist(err) {
+ ctx.Error(http.StatusNotFound)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
+ }
+ return
+ }
+ ctx.JSON(http.StatusOK, convert.ToAPIIssue(issue))
+}
+
// UpdateIssueTitle change issue's title
func UpdateIssueTitle(ctx *context.Context) {
issue := GetActionIssue(ctx)
@@ -1856,6 +1872,40 @@ func UpdateIssueContent(ctx *context.Context) {
})
}
+// UpdateIssueDeadline updates an issue deadline
+func UpdateIssueDeadline(ctx *context.Context) {
+ form := web.GetForm(ctx).(*api.EditDeadlineOption)
+ issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+ if err != nil {
+ if models.IsErrIssueNotExist(err) {
+ ctx.NotFound("GetIssueByIndex", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
+ }
+ return
+ }
+
+ if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
+ ctx.Error(http.StatusForbidden, "", "Not repo writer")
+ return
+ }
+
+ var deadlineUnix timeutil.TimeStamp
+ var deadline time.Time
+ if form.Deadline != nil && !form.Deadline.IsZero() {
+ deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
+ 23, 59, 59, 0, time.Local)
+ deadlineUnix = timeutil.TimeStamp(deadline.Unix())
+ }
+
+ if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
+ ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error())
+ return
+ }
+
+ ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline})
+}
+
// UpdateIssueMilestone change issue's milestone
func UpdateIssueMilestone(ctx *context.Context) {
issues := getActionIssues(ctx)
@@ -2052,6 +2102,338 @@ func UpdatePullReviewRequest(ctx *context.Context) {
})
}
+// SearchIssues searches for issues across the repositories that the user has access to
+func SearchIssues(ctx *context.Context) {
+ before, since, err := context.GetQueryBeforeSince(ctx)
+ if err != nil {
+ ctx.Error(http.StatusUnprocessableEntity, err.Error())
+ return
+ }
+
+ var isClosed util.OptionalBool
+ switch ctx.FormString("state") {
+ case "closed":
+ isClosed = util.OptionalBoolTrue
+ case "all":
+ isClosed = util.OptionalBoolNone
+ default:
+ isClosed = util.OptionalBoolFalse
+ }
+
+ // find repos user can access (for issue search)
+ opts := &models.SearchRepoOptions{
+ Private: false,
+ AllPublic: true,
+ TopicOnly: false,
+ Collaborate: util.OptionalBoolNone,
+ // This needs to be a column that is not nil in fixtures or
+ // MySQL will return different results when sorting by null in some cases
+ OrderBy: db.SearchOrderByAlphabetically,
+ Actor: ctx.Doer,
+ }
+ if ctx.IsSigned {
+ opts.Private = true
+ opts.AllLimited = true
+ }
+ if ctx.FormString("owner") != "" {
+ owner, err := user_model.GetUserByName(ctx.FormString("owner"))
+ if err != nil {
+ if user_model.IsErrUserNotExist(err) {
+ ctx.Error(http.StatusBadRequest, "Owner not found", err.Error())
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
+ }
+ return
+ }
+ opts.OwnerID = owner.ID
+ opts.AllLimited = false
+ opts.AllPublic = false
+ opts.Collaborate = util.OptionalBoolFalse
+ }
+ if ctx.FormString("team") != "" {
+ if ctx.FormString("owner") == "" {
+ ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
+ return
+ }
+ team, err := organization.GetTeam(opts.OwnerID, ctx.FormString("team"))
+ if err != nil {
+ if organization.IsErrTeamNotExist(err) {
+ ctx.Error(http.StatusBadRequest, "Team not found", err.Error())
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
+ }
+ return
+ }
+ opts.TeamID = team.ID
+ }
+
+ repoIDs, _, err := models.SearchRepositoryIDs(opts)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "SearchRepositoryByName", err.Error())
+ return
+ }
+
+ var issues []*models.Issue
+ var filteredCount int64
+
+ keyword := ctx.FormTrim("q")
+ if strings.IndexByte(keyword, 0) >= 0 {
+ keyword = ""
+ }
+ var issueIDs []int64
+ if len(keyword) > 0 && len(repoIDs) > 0 {
+ if issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, repoIDs, keyword); err != nil {
+ ctx.Error(http.StatusInternalServerError, "SearchIssuesByKeyword", err.Error())
+ return
+ }
+ }
+
+ var isPull util.OptionalBool
+ switch ctx.FormString("type") {
+ case "pulls":
+ isPull = util.OptionalBoolTrue
+ case "issues":
+ isPull = util.OptionalBoolFalse
+ default:
+ isPull = util.OptionalBoolNone
+ }
+
+ labels := ctx.FormTrim("labels")
+ var includedLabelNames []string
+ if len(labels) > 0 {
+ includedLabelNames = strings.Split(labels, ",")
+ }
+
+ milestones := ctx.FormTrim("milestones")
+ var includedMilestones []string
+ if len(milestones) > 0 {
+ includedMilestones = strings.Split(milestones, ",")
+ }
+
+ // this api is also used in UI,
+ // so the default limit is set to fit UI needs
+ limit := ctx.FormInt("limit")
+ if limit == 0 {
+ limit = setting.UI.IssuePagingNum
+ } else if limit > setting.API.MaxResponseItems {
+ limit = setting.API.MaxResponseItems
+ }
+
+ // Only fetch the issues if we either don't have a keyword or the search returned issues
+ // This would otherwise return all issues if no issues were found by the search.
+ if len(keyword) == 0 || len(issueIDs) > 0 || len(includedLabelNames) > 0 || len(includedMilestones) > 0 {
+ issuesOpt := &models.IssuesOptions{
+ ListOptions: db.ListOptions{
+ Page: ctx.FormInt("page"),
+ PageSize: limit,
+ },
+ RepoIDs: repoIDs,
+ IsClosed: isClosed,
+ IssueIDs: issueIDs,
+ IncludedLabelNames: includedLabelNames,
+ IncludeMilestones: includedMilestones,
+ SortType: "priorityrepo",
+ PriorityRepoID: ctx.FormInt64("priority_repo_id"),
+ IsPull: isPull,
+ UpdatedBeforeUnix: before,
+ UpdatedAfterUnix: since,
+ }
+
+ ctxUserID := int64(0)
+ if ctx.IsSigned {
+ ctxUserID = ctx.Doer.ID
+ }
+
+ // Filter for: Created by User, Assigned to User, Mentioning User, Review of User Requested
+ if ctx.FormBool("created") {
+ issuesOpt.PosterID = ctxUserID
+ }
+ if ctx.FormBool("assigned") {
+ issuesOpt.AssigneeID = ctxUserID
+ }
+ if ctx.FormBool("mentioned") {
+ issuesOpt.MentionedID = ctxUserID
+ }
+ if ctx.FormBool("review_requested") {
+ issuesOpt.ReviewRequestedID = ctxUserID
+ }
+
+ if issues, err = models.Issues(issuesOpt); err != nil {
+ ctx.Error(http.StatusInternalServerError, "Issues", err.Error())
+ return
+ }
+
+ issuesOpt.ListOptions = db.ListOptions{
+ Page: -1,
+ }
+ if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
+ ctx.Error(http.StatusInternalServerError, "CountIssues", err.Error())
+ return
+ }
+ }
+
+ ctx.SetTotalCountHeader(filteredCount)
+ ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
+}
+
+func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
+ userName := ctx.FormString(queryName)
+ if len(userName) == 0 {
+ return 0
+ }
+
+ user, err := user_model.GetUserByName(userName)
+ if user_model.IsErrUserNotExist(err) {
+ ctx.NotFound("", err)
+ return 0
+ }
+
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return 0
+ }
+
+ return user.ID
+}
+
+// ListIssues list the issues of a repository
+func ListIssues(ctx *context.Context) {
+ before, since, err := context.GetQueryBeforeSince(ctx)
+ if err != nil {
+ ctx.Error(http.StatusUnprocessableEntity, err.Error())
+ return
+ }
+
+ var isClosed util.OptionalBool
+ switch ctx.FormString("state") {
+ case "closed":
+ isClosed = util.OptionalBoolTrue
+ case "all":
+ isClosed = util.OptionalBoolNone
+ default:
+ isClosed = util.OptionalBoolFalse
+ }
+
+ var issues []*models.Issue
+ var filteredCount int64
+
+ keyword := ctx.FormTrim("q")
+ if strings.IndexByte(keyword, 0) >= 0 {
+ keyword = ""
+ }
+ var issueIDs []int64
+ var labelIDs []int64
+ if len(keyword) > 0 {
+ issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, []int64{ctx.Repo.Repository.ID}, keyword)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ }
+
+ if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
+ labelIDs, err = models.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ }
+
+ var mileIDs []int64
+ if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 {
+ for i := range part {
+ // uses names and fall back to ids
+ // non existent milestones are discarded
+ mile, err := models.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, part[i])
+ if err == nil {
+ mileIDs = append(mileIDs, mile.ID)
+ continue
+ }
+ if !models.IsErrMilestoneNotExist(err) {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ id, err := strconv.ParseInt(part[i], 10, 64)
+ if err != nil {
+ continue
+ }
+ mile, err = models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, id)
+ if err == nil {
+ mileIDs = append(mileIDs, mile.ID)
+ continue
+ }
+ if models.IsErrMilestoneNotExist(err) {
+ continue
+ }
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ }
+ }
+
+ listOptions := db.ListOptions{
+ Page: ctx.FormInt("page"),
+ PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
+ }
+
+ var isPull util.OptionalBool
+ switch ctx.FormString("type") {
+ case "pulls":
+ isPull = util.OptionalBoolTrue
+ case "issues":
+ isPull = util.OptionalBoolFalse
+ default:
+ isPull = util.OptionalBoolNone
+ }
+
+ // FIXME: we should be more efficient here
+ createdByID := getUserIDForFilter(ctx, "created_by")
+ if ctx.Written() {
+ return
+ }
+ assignedByID := getUserIDForFilter(ctx, "assigned_by")
+ if ctx.Written() {
+ return
+ }
+ mentionedByID := getUserIDForFilter(ctx, "mentioned_by")
+ if ctx.Written() {
+ return
+ }
+
+ // Only fetch the issues if we either don't have a keyword or the search returned issues
+ // This would otherwise return all issues if no issues were found by the search.
+ if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
+ issuesOpt := &models.IssuesOptions{
+ ListOptions: listOptions,
+ RepoIDs: []int64{ctx.Repo.Repository.ID},
+ IsClosed: isClosed,
+ IssueIDs: issueIDs,
+ LabelIDs: labelIDs,
+ MilestoneIDs: mileIDs,
+ IsPull: isPull,
+ UpdatedBeforeUnix: before,
+ UpdatedAfterUnix: since,
+ PosterID: createdByID,
+ AssigneeID: assignedByID,
+ MentionedID: mentionedByID,
+ }
+
+ if issues, err = models.Issues(issuesOpt); err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ issuesOpt.ListOptions = db.ListOptions{
+ Page: -1,
+ }
+ if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ }
+
+ ctx.SetTotalCountHeader(filteredCount)
+ ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
+}
+
// UpdateIssueStatus change issue's status
func UpdateIssueStatus(ctx *context.Context) {
issues := getActionIssues(ctx)