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
This commit is contained in:
Lunny Xiao 2022-04-08 02:59:56 +08:00 committed by GitHub
parent bb7e0619c3
commit 783a021889
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1082 additions and 74 deletions

View file

@ -26,7 +26,7 @@ func NewAvailable(ctx *context.APIContext) {
}
func getFindNotificationOptions(ctx *context.APIContext) *models.FindNotificationOptions {
before, since, err := utils.GetQueryBeforeSince(ctx)
before, since, err := context.GetQueryBeforeSince(ctx.Context)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return nil

View file

@ -111,7 +111,7 @@ func SearchIssues(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/IssueList"
before, since, err := utils.GetQueryBeforeSince(ctx)
before, since, err := context.GetQueryBeforeSince(ctx.Context)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
@ -359,7 +359,7 @@ func ListIssues(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/IssueList"
before, since, err := utils.GetQueryBeforeSince(ctx)
before, since, err := context.GetQueryBeforeSince(ctx.Context)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return

View file

@ -58,7 +58,7 @@ func ListIssueComments(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/CommentList"
before, since, err := utils.GetQueryBeforeSince(ctx)
before, since, err := context.GetQueryBeforeSince(ctx.Context)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
@ -150,7 +150,7 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/TimelineList"
before, since, err := utils.GetQueryBeforeSince(ctx)
before, since, err := context.GetQueryBeforeSince(ctx.Context)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
@ -253,7 +253,7 @@ func ListRepoIssueComments(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/CommentList"
before, since, err := utils.GetQueryBeforeSince(ctx)
before, since, err := context.GetQueryBeforeSince(ctx.Context)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return

View file

@ -103,7 +103,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
opts.UserID = user.ID
}
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil {
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
}
@ -522,7 +522,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
}
var err error
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil {
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
}
@ -597,7 +597,7 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
}
var err error
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil {
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
}

View file

@ -31,23 +31,6 @@ import (
repo_service "code.gitea.io/gitea/services/repository"
)
var searchOrderByMap = map[string]map[string]db.SearchOrderBy{
"asc": {
"alpha": db.SearchOrderByAlphabetically,
"created": db.SearchOrderByOldest,
"updated": db.SearchOrderByLeastUpdated,
"size": db.SearchOrderBySize,
"id": db.SearchOrderByID,
},
"desc": {
"alpha": db.SearchOrderByAlphabeticallyReverse,
"created": db.SearchOrderByNewest,
"updated": db.SearchOrderByRecentUpdated,
"size": db.SearchOrderBySizeReverse,
"id": db.SearchOrderByIDReverse,
},
}
// Search repositories via options
func Search(ctx *context.APIContext) {
// swagger:operation GET /repos/search repository repoSearch
@ -193,7 +176,7 @@ func Search(ctx *context.APIContext) {
if len(sortOrder) == 0 {
sortOrder = "asc"
}
if searchModeMap, ok := searchOrderByMap[sortOrder]; ok {
if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok {
if orderBy, ok := searchModeMap[sortMode]; ok {
opts.OrderBy = orderBy
} else {

View file

@ -0,0 +1,19 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package utils
import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert"
)
// GetListOptions returns list options using the page and limit parameters
func GetListOptions(ctx *context.APIContext) db.ListOptions {
return db.ListOptions{
Page: ctx.FormInt("page"),
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
}
}

View file

@ -1,68 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package utils
import (
"net/url"
"strings"
"time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert"
)
// GetQueryBeforeSince return parsed time (unix format) from URL query's before and since
func GetQueryBeforeSince(ctx *context.APIContext) (before, since int64, err error) {
qCreatedBefore, err := prepareQueryArg(ctx, "before")
if err != nil {
return 0, 0, err
}
qCreatedSince, err := prepareQueryArg(ctx, "since")
if err != nil {
return 0, 0, err
}
before, err = parseTime(qCreatedBefore)
if err != nil {
return 0, 0, err
}
since, err = parseTime(qCreatedSince)
if err != nil {
return 0, 0, err
}
return before, since, nil
}
// parseTime parse time and return unix timestamp
func parseTime(value string) (int64, error) {
if len(value) != 0 {
t, err := time.Parse(time.RFC3339, value)
if err != nil {
return 0, err
}
if !t.IsZero() {
return t.Unix(), nil
}
}
return 0, nil
}
// prepareQueryArg unescape and trim a query arg
func prepareQueryArg(ctx *context.APIContext, name string) (value string, err error) {
value, err = url.PathUnescape(ctx.FormString(name))
value = strings.TrimSpace(value)
return
}
// GetListOptions returns list options using the page and limit parameters
func GetListOptions(ctx *context.APIContext) db.ListOptions {
return db.ListOptions{
Page: ctx.FormInt("page"),
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
}
}

View file

@ -0,0 +1,42 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package explore
import (
"net/http"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert"
api "code.gitea.io/gitea/modules/structs"
)
// TopicSearch search for creating topic
func TopicSearch(ctx *context.Context) {
opts := &repo_model.FindTopicOptions{
Keyword: ctx.FormString("q"),
ListOptions: db.ListOptions{
Page: ctx.FormInt("page"),
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
},
}
topics, total, err := repo_model.FindTopics(opts)
if err != nil {
ctx.Error(http.StatusInternalServerError)
return
}
topicResponses := make([]*api.TopicResponse, len(topics))
for i, topic := range topics {
topicResponses[i] = convert.ToTopicResponse(topic)
}
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, map[string]interface{}{
"topics": topicResponses,
})
}

View file

@ -13,6 +13,7 @@ import (
"strings"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
@ -20,7 +21,9 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/utils"
"code.gitea.io/gitea/services/forms"
@ -329,6 +332,51 @@ func TeamRepositories(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplTeamRepositories)
}
// SearchTeam api for searching teams
func SearchTeam(ctx *context.Context) {
listOptions := db.ListOptions{
Page: ctx.FormInt("page"),
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
}
opts := &organization.SearchTeamOptions{
UserID: ctx.Doer.ID,
Keyword: ctx.FormTrim("q"),
OrgID: ctx.Org.Organization.ID,
IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"),
ListOptions: listOptions,
}
teams, maxResults, err := organization.SearchTeam(opts)
if err != nil {
log.Error("SearchTeam failed: %v", err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": "SearchTeam internal failure",
})
return
}
apiTeams := make([]*api.Team, len(teams))
for i := range teams {
if err := teams[i].GetUnits(); err != nil {
log.Error("Team GetUnits failed: %v", err)
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": "SearchTeam failed to get units",
})
return
}
apiTeams[i] = convert.ToTeam(teams[i])
}
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
"data": apiTeams,
})
}
// EditTeam render team edit page
func EditTeam(ctx *context.Context) {
ctx.Data["Title"] = ctx.Org.Organization.FullName

View file

@ -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)

View file

@ -20,11 +20,14 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/forms"
repo_service "code.gitea.io/gitea/services/repository"
@ -503,3 +506,112 @@ func InitiateDownload(ctx *context.Context) {
"complete": completed,
})
}
// SearchRepo repositories via options
func SearchRepo(ctx *context.Context) {
opts := &models.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: ctx.FormInt("page"),
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
},
Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"),
OwnerID: ctx.FormInt64("uid"),
PriorityOwnerID: ctx.FormInt64("priority_owner_id"),
TeamID: ctx.FormInt64("team_id"),
TopicOnly: ctx.FormBool("topic"),
Collaborate: util.OptionalBoolNone,
Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
Template: util.OptionalBoolNone,
StarredByID: ctx.FormInt64("starredBy"),
IncludeDescription: ctx.FormBool("includeDesc"),
}
if ctx.FormString("template") != "" {
opts.Template = util.OptionalBoolOf(ctx.FormBool("template"))
}
if ctx.FormBool("exclusive") {
opts.Collaborate = util.OptionalBoolFalse
}
mode := ctx.FormString("mode")
switch mode {
case "source":
opts.Fork = util.OptionalBoolFalse
opts.Mirror = util.OptionalBoolFalse
case "fork":
opts.Fork = util.OptionalBoolTrue
case "mirror":
opts.Mirror = util.OptionalBoolTrue
case "collaborative":
opts.Mirror = util.OptionalBoolFalse
opts.Collaborate = util.OptionalBoolTrue
case "":
default:
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid search mode: \"%s\"", mode))
return
}
if ctx.FormString("archived") != "" {
opts.Archived = util.OptionalBoolOf(ctx.FormBool("archived"))
}
if ctx.FormString("is_private") != "" {
opts.IsPrivate = util.OptionalBoolOf(ctx.FormBool("is_private"))
}
sortMode := ctx.FormString("sort")
if len(sortMode) > 0 {
sortOrder := ctx.FormString("order")
if len(sortOrder) == 0 {
sortOrder = "asc"
}
if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok {
if orderBy, ok := searchModeMap[sortMode]; ok {
opts.OrderBy = orderBy
} else {
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort mode: \"%s\"", sortMode))
return
}
} else {
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort order: \"%s\"", sortOrder))
return
}
}
var err error
repos, count, err := models.SearchRepository(opts)
if err != nil {
ctx.JSON(http.StatusInternalServerError, api.SearchError{
OK: false,
Error: err.Error(),
})
return
}
results := make([]*api.Repository, len(repos))
for i, repo := range repos {
if err = repo.GetOwner(ctx); err != nil {
ctx.JSON(http.StatusInternalServerError, api.SearchError{
OK: false,
Error: err.Error(),
})
return
}
accessMode, err := models.AccessLevel(ctx.Doer, repo)
if err != nil {
ctx.JSON(http.StatusInternalServerError, api.SearchError{
OK: false,
Error: err.Error(),
})
}
results[i] = convert.ToRepo(repo, accessMode)
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, api.SearchResults{
OK: true,
Data: results,
})
}

View file

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
)
const (
@ -191,3 +192,8 @@ func NotificationPurgePost(c *context.Context) {
c.Redirect(setting.AppSubURL+"/notifications", http.StatusSeeOther)
}
// NewAvailable returns the notification counts
func NewAvailable(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, api.NotificationCount{New: models.CountUnread(ctx.Doer)})
}

View file

@ -0,0 +1,44 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package user
import (
"net/http"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert"
)
// Search search users
func Search(ctx *context.Context) {
listOptions := db.ListOptions{
Page: ctx.FormInt("page"),
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
}
users, maxResults, err := user_model.SearchUsers(&user_model.SearchUserOptions{
Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"),
UID: ctx.FormInt64("uid"),
Type: user_model.UserTypeIndividual,
ListOptions: listOptions,
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": err.Error(),
})
return
}
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": true,
"data": convert.ToUsers(ctx.Doer, users),
})
}

View file

@ -0,0 +1,41 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package user
import (
"net/http"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert"
)
// GetStopwatches get all stopwatches
func GetStopwatches(ctx *context.Context) {
sws, err := models.GetUserStopwatches(ctx.Doer.ID, db.ListOptions{
Page: ctx.FormInt("page"),
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
})
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
count, err := models.CountUserStopwatches(ctx.Doer.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
apiSWs, err := convert.ToStopWatches(sws)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiSWs)
}

View file

@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web"
@ -289,8 +290,13 @@ func RegisterRoutes(m *web.Route) {
m.Get("/users", explore.Users)
m.Get("/organizations", explore.Organizations)
m.Get("/code", explore.Code)
m.Get("/topics/search", explore.TopicSearch)
}, ignExploreSignIn)
m.Get("/issues", reqSignIn, user.Issues)
m.Group("/issues", func() {
m.Get("", user.Issues)
m.Get("/search", repo.SearchIssues)
}, reqSignIn)
m.Get("/pulls", reqSignIn, user.Pulls)
m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones)
@ -421,6 +427,8 @@ func RegisterRoutes(m *web.Route) {
m.Post("/forgot_password", auth.ForgotPasswdPost)
m.Post("/logout", auth.SignOut)
m.Get("/task/{task}", user.TaskStatus)
m.Get("/stopwatches", user.GetStopwatches, reqSignIn)
m.Get("/search", user.Search, ignExploreSignIn)
})
// ***** END: User *****
@ -605,6 +613,7 @@ func RegisterRoutes(m *web.Route) {
m.Group("/{org}", func() {
m.Get("/teams/new", org.NewTeam)
m.Post("/teams/new", bindIgnErr(forms.CreateTeamForm{}), org.NewTeamPost)
m.Get("/teams/-/search", org.SearchTeam)
m.Get("/teams/{team}/edit", org.EditTeam)
m.Post("/teams/{team}/edit", bindIgnErr(forms.CreateTeamForm{}), org.EditTeamPost)
m.Post("/teams/{team}/delete", org.DeleteTeam)
@ -669,6 +678,7 @@ func RegisterRoutes(m *web.Route) {
m.Combo("/{repoid}").Get(repo.Fork).
Post(bindIgnErr(forms.CreateRepoForm{}), repo.ForkPost)
}, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader)
m.Get("/search", repo.SearchRepo)
}, reqSignIn)
m.Group("/{username}/-", func() {
@ -811,13 +821,16 @@ func RegisterRoutes(m *web.Route) {
Post(bindIgnErr(forms.CreateIssueForm{}), repo.NewIssuePost)
m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate)
})
m.Get("/search", repo.ListIssues)
}, context.RepoMustNotBeArchived(), reqRepoIssueReader)
// FIXME: should use different URLs but mostly same logic for comments of issue and pull request.
// So they can apply their own enable/disable logic on routers.
m.Group("/{type:issues|pulls}", func() {
m.Group("/{index}", func() {
m.Get("/info", repo.GetIssueInfo)
m.Post("/title", repo.UpdateIssueTitle)
m.Post("/content", repo.UpdateIssueContent)
m.Post("/deadline", bindIgnErr(structs.EditDeadlineOption{}), repo.UpdateIssueDeadline)
m.Post("/watch", repo.IssueWatch)
m.Post("/ref", repo.UpdateIssueRef)
m.Group("/dependency", func() {
@ -1195,6 +1208,7 @@ func RegisterRoutes(m *web.Route) {
m.Get("", user.Notifications)
m.Post("/status", user.NotificationStatusPost)
m.Post("/purge", user.NotificationPurgePost)
m.Get("/new", user.NewAvailable)
}, reqSignIn)
if setting.API.EnableSwagger {