diff options
author | Lunny Xiao <xiaolunwen@gmail.com> | 2022-04-08 02:59:56 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-07 20:59:56 +0200 |
commit | 783a02188970ba5800514f7c64f6a818f65c04a1 (patch) | |
tree | 8c7dc3bbe00abbc245203f94c437f157168cc54a /routers/web/repo/issue.go | |
parent | bb7e0619c3356227d6c5826cb789841f6bc4a05a (diff) | |
download | gitea-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.go | 382 |
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) |