初始提交: Gitea 项目代码

This commit is contained in:
root
2026-05-30 22:47:36 +08:00
commit f288f76350
6116 changed files with 776822 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package activitypub
import (
"net/http"
"gitea.dev/services/context"
)
func NotImplemented(ctx *context.APIContext) {
http.Error(ctx.Resp, "Not implemented", http.StatusNotImplemented)
}
+103
View File
@@ -0,0 +1,103 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"gitea.dev/routers/api/v1/shared"
"gitea.dev/services/context"
)
// ListWorkflowJobs Lists all jobs
func ListWorkflowJobs(ctx *context.APIContext) {
// swagger:operation GET /admin/actions/jobs admin listAdminWorkflowJobs
// ---
// summary: Lists all jobs
// produces:
// - application/json
// parameters:
// - name: status
// in: query
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
// type: string
// required: false
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// - name: sort
// in: query
// description: sort jobs by attribute. Supported values are "id". Default is "id"
// type: string
// - name: order
// in: query
// description: sort order, either "asc" (ascending) or "desc" (descending). Default is "asc"
// type: string
// responses:
// "200":
// "$ref": "#/responses/WorkflowJobsList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.ListJobs(ctx, 0, 0, 0, nil)
}
// ListWorkflowRuns Lists all runs
func ListWorkflowRuns(ctx *context.APIContext) {
// swagger:operation GET /admin/actions/runs admin listAdminWorkflowRuns
// ---
// summary: Lists all runs
// produces:
// - application/json
// parameters:
// - name: event
// in: query
// description: workflow event name
// type: string
// required: false
// - name: branch
// in: query
// description: workflow branch
// type: string
// required: false
// - name: status
// in: query
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
// type: string
// required: false
// - name: actor
// in: query
// description: triggered by user
// type: string
// required: false
// - name: head_sha
// in: query
// description: triggering sha of the workflow run
// type: string
// required: false
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/WorkflowRunsList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListRuns(ctx, 0, 0)
}
+180
View File
@@ -0,0 +1,180 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/gitrepo"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
repo_service "gitea.dev/services/repository"
)
// ListUnadoptedRepositories lists the unadopted repositories that match the provided names
func ListUnadoptedRepositories(ctx *context.APIContext) {
// swagger:operation GET /admin/unadopted admin adminUnadoptedList
// ---
// summary: List unadopted repositories
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// - name: pattern
// in: query
// description: pattern of repositories to search for
// type: string
// responses:
// "200":
// "$ref": "#/responses/StringSlice"
// "403":
// "$ref": "#/responses/forbidden"
listOptions := utils.GetListOptions(ctx)
if listOptions.Page == 0 {
listOptions.Page = 1
}
repoNames, count, err := repo_service.ListUnadoptedRepositories(ctx, ctx.FormString("query"), &listOptions)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, repoNames)
}
// AdoptRepository will adopt an unadopted repository
func AdoptRepository(ctx *context.APIContext) {
// swagger:operation POST /admin/unadopted/{owner}/{repo} admin adminAdoptRepository
// ---
// summary: Adopt unadopted files as a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "403":
// "$ref": "#/responses/forbidden"
ownerName := ctx.PathParam("username")
repoName := ctx.PathParam("reponame")
ctxUser, err := user_model.GetUserByName(ctx, ownerName)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound()
return
}
ctx.APIErrorInternal(err)
return
}
// check not a repo
has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName)
if err != nil {
ctx.APIErrorInternal(err)
return
}
exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName)))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if has || !exist {
ctx.APIErrorNotFound()
return
}
if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{
Name: repoName,
IsPrivate: true,
}); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// DeleteUnadoptedRepository will delete an unadopted repository
func DeleteUnadoptedRepository(ctx *context.APIContext) {
// swagger:operation DELETE /admin/unadopted/{owner}/{repo} admin adminDeleteUnadoptedRepository
// ---
// summary: Delete unadopted files
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
ownerName := ctx.PathParam("username")
repoName := ctx.PathParam("reponame")
ctxUser, err := user_model.GetUserByName(ctx, ownerName)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound()
return
}
ctx.APIErrorInternal(err)
return
}
// check not a repo
has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName)
if err != nil {
ctx.APIErrorInternal(err)
return
}
exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName)))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if has || !exist {
ctx.APIErrorNotFound()
return
}
if err := repo_service.DeleteUnadoptedRepository(ctx, ctx.Doer, ctxUser, repoName); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+86
View File
@@ -0,0 +1,86 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
"gitea.dev/modules/log"
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/cron"
)
// ListCronTasks api for getting cron tasks
func ListCronTasks(ctx *context.APIContext) {
// swagger:operation GET /admin/cron admin adminCronList
// ---
// summary: List cron tasks
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/CronList"
// "403":
// "$ref": "#/responses/forbidden"
tasks := cron.ListTasks()
count := len(tasks)
listOpts := utils.GetListOptions(ctx)
tasks = util.PaginateSlice(tasks, listOpts.Page, listOpts.PageSize).(cron.TaskTable)
res := make([]structs.Cron, len(tasks))
for i, task := range tasks {
res[i] = structs.Cron{
Name: task.Name,
Schedule: task.Spec,
Next: task.Next,
Prev: task.Prev,
ExecTimes: task.ExecTimes,
}
}
ctx.SetTotalCountHeader(int64(count))
ctx.JSON(http.StatusOK, res)
}
// PostCronTask api for getting cron tasks
func PostCronTask(ctx *context.APIContext) {
// swagger:operation POST /admin/cron/{task} admin adminCronRun
// ---
// summary: Run cron task
// produces:
// - application/json
// parameters:
// - name: task
// in: path
// description: task to run
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
task := cron.GetTask(ctx.PathParam("task"))
if task == nil {
ctx.APIErrorNotFound()
return
}
task.Run()
log.Trace("Cron Task %s started by admin(%s)", task.Name, ctx.Doer.Name)
ctx.Status(http.StatusNoContent)
}
+87
View File
@@ -0,0 +1,87 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// GetAllEmails
func GetAllEmails(ctx *context.APIContext) {
// swagger:operation GET /admin/emails admin adminGetAllEmails
// ---
// summary: List all emails
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/EmailList"
// "403":
// "$ref": "#/responses/forbidden"
listOptions := utils.GetListOptions(ctx)
emails, maxResults, err := user_model.SearchEmails(ctx, &user_model.SearchEmailOptions{
Keyword: ctx.PathParam("email"),
ListOptions: listOptions,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
results := make([]*api.Email, len(emails))
for i := range emails {
results[i] = convert.ToEmailSearch(emails[i])
}
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &results)
}
// SearchEmail
func SearchEmail(ctx *context.APIContext) {
// swagger:operation GET /admin/emails/search admin adminSearchEmails
// ---
// summary: Search all emails
// produces:
// - application/json
// parameters:
// - name: q
// in: query
// description: keyword
// type: string
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/EmailList"
// "403":
// "$ref": "#/responses/forbidden"
ctx.SetPathParam("email", ctx.FormTrim("q"))
GetAllEmails(ctx)
}
+204
View File
@@ -0,0 +1,204 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"errors"
"net/http"
"gitea.dev/models/webhook"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
webhook_service "gitea.dev/services/webhook"
)
// ListHooks list system's webhooks
func ListHooks(ctx *context.APIContext) {
// swagger:operation GET /admin/hooks admin adminListHooks
// ---
// summary: List system's webhooks
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// - type: string
// enum:
// - system
// - default
// - all
// description: system, default or both kinds of webhooks
// name: type
// default: system
// in: query
//
// responses:
// "200":
// "$ref": "#/responses/HookList"
// for compatibility the default value is true
isSystemWebhook := optional.Some(true)
typeValue := ctx.FormString("type")
switch typeValue {
case "default":
isSystemWebhook = optional.Some(false)
case "all":
isSystemWebhook = optional.None[bool]()
}
listOptions := utils.GetListOptions(ctx)
opts := &webhook.ListSystemWebhookOptions{
ListOptions: listOptions,
IsSystem: isSystemWebhook,
}
sysHooks, total, err := webhook.GetGlobalWebhooks(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
hooks := make([]*api.Hook, len(sysHooks))
for i, hook := range sysHooks {
h, err := webhook_service.ToHook(setting.AppURL+"/-/admin", hook)
if err != nil {
ctx.APIErrorInternal(err)
return
}
hooks[i] = h
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, hooks)
}
// GetHook get an organization's hook by id
func GetHook(ctx *context.APIContext) {
// swagger:operation GET /admin/hooks/{id} admin adminGetHook
// ---
// summary: Get a hook
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the hook to get
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Hook"
hookID := ctx.PathParamInt64("id")
hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
h, err := webhook_service.ToHook("/-/admin/", hook)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, h)
}
// CreateHook create a hook for an organization
func CreateHook(ctx *context.APIContext) {
// swagger:operation POST /admin/hooks admin adminCreateHook
// ---
// summary: Create a hook
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/CreateHookOption"
// responses:
// "201":
// "$ref": "#/responses/Hook"
form := web.GetForm(ctx).(*api.CreateHookOption)
utils.AddSystemHook(ctx, form)
}
// EditHook modify a hook of a repository
func EditHook(ctx *context.APIContext) {
// swagger:operation PATCH /admin/hooks/{id} admin adminEditHook
// ---
// summary: Update a hook
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the hook to update
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditHookOption"
// responses:
// "200":
// "$ref": "#/responses/Hook"
form := web.GetForm(ctx).(*api.EditHookOption)
// TODO in body params
hookID := ctx.PathParamInt64("id")
utils.EditSystemHook(ctx, form, hookID)
}
// DeleteHook delete a system hook
func DeleteHook(ctx *context.APIContext) {
// swagger:operation DELETE /admin/hooks/{id} admin adminDeleteHook
// ---
// summary: Delete a hook
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the hook to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
hookID := ctx.PathParamInt64("id")
if err := webhook.DeleteDefaultSystemWebhook(ctx, hookID); err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
+123
View File
@@ -0,0 +1,123 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
"gitea.dev/models/db"
"gitea.dev/models/organization"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// CreateOrg api for create organization
func CreateOrg(ctx *context.APIContext) {
// swagger:operation POST /admin/users/{username}/orgs admin adminCreateOrg
// ---
// summary: Create an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user who will own the created organization
// type: string
// required: true
// - name: organization
// in: body
// required: true
// schema: { "$ref": "#/definitions/CreateOrgOption" }
// responses:
// "201":
// "$ref": "#/responses/Organization"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateOrgOption)
visibility := api.VisibleTypePublic
if form.Visibility != "" {
visibility = api.VisibilityModes[string(form.Visibility)]
}
org := &organization.Organization{
Name: form.UserName,
FullName: form.FullName,
Description: form.Description,
Website: form.Website,
Location: form.Location,
IsActive: true,
Type: user_model.UserTypeOrganization,
Visibility: visibility,
}
if err := organization.CreateOrganization(ctx, org, ctx.ContextUser); err != nil {
if user_model.IsErrUserAlreadyExist(err) ||
db.IsErrNameReserved(err) ||
db.IsErrNameCharsNotAllowed(err) ||
db.IsErrNamePatternNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.JSON(http.StatusCreated, convert.ToOrganization(ctx, org))
}
// GetAllOrgs API for getting information of all the organizations
func GetAllOrgs(ctx *context.APIContext) {
// swagger:operation GET /admin/orgs admin adminGetAllOrgs
// ---
// summary: List all organizations
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/OrganizationList"
// "403":
// "$ref": "#/responses/forbidden"
listOptions := utils.GetListOptions(ctx)
users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer,
Types: []user_model.UserType{user_model.UserTypeOrganization},
OrderBy: db.SearchOrderByAlphabetically,
ListOptions: listOptions,
Visible: []api.VisibleType{api.VisibleTypePublic, api.VisibleTypeLimited, api.VisibleTypePrivate},
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
orgs := make([]*api.Organization, len(users))
for i := range users {
orgs[i] = convert.ToOrganization(ctx, organization.OrgFromUser(users[i]))
}
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &orgs)
}
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/repo"
"gitea.dev/services/context"
)
// CreateRepo api for creating a repository
func CreateRepo(ctx *context.APIContext) {
// swagger:operation POST /admin/users/{username}/repos admin adminCreateRepo
// ---
// summary: Create a repository on behalf of a user
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user who will own the created repository
// type: string
// required: true
// - name: repository
// in: body
// required: true
// schema: { "$ref": "#/definitions/CreateRepoOption" }
// responses:
// "201":
// "$ref": "#/responses/Repository"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateRepoOption)
repo.CreateUserRepo(ctx, ctx.ContextUser, *form)
}
+126
View File
@@ -0,0 +1,126 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"gitea.dev/routers/api/v1/shared"
"gitea.dev/services/context"
)
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// CreateRegistrationToken returns the token to register global runners
func CreateRegistrationToken(ctx *context.APIContext) {
// swagger:operation POST /admin/actions/runners/registration-token admin adminCreateRunnerRegistrationToken
// ---
// summary: Get a global actions runner registration token
// produces:
// - application/json
// parameters:
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, 0, 0)
}
// ListRunners get all runners
func ListRunners(ctx *context.APIContext) {
// swagger:operation GET /admin/actions/runners admin getAdminRunners
// ---
// summary: Get all runners
// produces:
// - application/json
// parameters:
// - name: disabled
// in: query
// description: filter by disabled status (true or false)
// type: boolean
// required: false
// responses:
// "200":
// "$ref": "#/responses/RunnerList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListRunners(ctx, 0, 0)
}
// GetRunner get a global runner
func GetRunner(ctx *context.APIContext) {
// swagger:operation GET /admin/actions/runners/{runner_id} admin getAdminRunner
// ---
// summary: Get a global runner
// produces:
// - application/json
// parameters:
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.GetRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id"))
}
// DeleteRunner delete a global runner
func DeleteRunner(ctx *context.APIContext) {
// swagger:operation DELETE /admin/actions/runners/{runner_id} admin deleteAdminRunner
// ---
// summary: Delete a global runner
// produces:
// - application/json
// parameters:
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// responses:
// "204":
// description: runner has been deleted
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.DeleteRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id"))
}
// UpdateRunner update a global runner
func UpdateRunner(ctx *context.APIContext) {
// swagger:operation PATCH /admin/actions/runners/{runner_id} admin updateAdminRunner
// ---
// summary: Update a global runner
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditActionRunnerOption"
// responses:
// "200":
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.UpdateRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id"))
}
+570
View File
@@ -0,0 +1,570 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"errors"
"fmt"
"net/http"
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/auth"
"gitea.dev/models/db"
org_model "gitea.dev/models/organization"
packages_model "gitea.dev/models/packages"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/auth/password"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/api/v1/utils"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/context"
"gitea.dev/services/convert"
"gitea.dev/services/mailer"
user_service "gitea.dev/services/user"
)
func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64) {
if sourceID == 0 {
return
}
source, err := auth.GetSourceByID(ctx, sourceID)
if err != nil {
if auth.IsErrSourceNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
u.LoginType = source.Type
u.LoginSource = source.ID
}
// CreateUser create a user
func CreateUser(ctx *context.APIContext) {
// swagger:operation POST /admin/users admin adminCreateUser
// ---
// summary: Create a user
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateUserOption"
// responses:
// "201":
// "$ref": "#/responses/User"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateUserOption)
u := &user_model.User{
Name: form.Username,
FullName: form.FullName,
Email: form.Email,
Passwd: form.Password,
MustChangePassword: true,
LoginType: auth.Plain,
LoginName: form.LoginName,
}
if form.MustChangePassword != nil {
u.MustChangePassword = *form.MustChangePassword
}
parseAuthSource(ctx, u, form.SourceID)
if ctx.Written() {
return
}
if u.LoginType == auth.Plain {
if len(form.Password) < setting.MinPasswordLength {
err := errors.New("PasswordIsRequired")
ctx.APIError(http.StatusBadRequest, err)
return
}
if !password.IsComplexEnough(form.Password) {
err := errors.New("PasswordComplexity")
ctx.APIError(http.StatusBadRequest, err)
return
}
if err := password.IsPwned(ctx, form.Password); err != nil {
if password.IsErrIsPwnedRequest(err) {
log.Error(err.Error())
}
ctx.APIError(http.StatusBadRequest, errors.New("PasswordPwned"))
return
}
}
overwriteDefault := &user_model.CreateUserOverwriteOptions{
IsActive: optional.Some(true),
IsRestricted: optional.FromPtr(form.Restricted),
}
if form.Visibility != "" {
visibility := api.VisibilityModes[string(form.Visibility)]
overwriteDefault.Visibility = &visibility
}
// Update the user creation timestamp. This can only be done after the user
// record has been inserted into the database; the insert intself will always
// set the creation timestamp to "now".
if form.Created != nil {
u.CreatedUnix = timeutil.TimeStamp(form.Created.Unix())
u.UpdatedUnix = u.CreatedUnix
}
if err := user_model.AdminCreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
if user_model.IsErrUserAlreadyExist(err) ||
user_model.IsErrEmailAlreadyUsed(err) ||
db.IsErrNameReserved(err) ||
db.IsErrNameCharsNotAllowed(err) ||
user_model.IsErrEmailCharIsNotSupported(err) ||
user_model.IsErrEmailInvalid(err) ||
db.IsErrNamePatternNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if !user_model.IsEmailDomainAllowed(u.Email) {
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", u.Email))
}
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
// Send email notification.
if form.SendNotify {
mailer.SendRegisterNotifyMail(u)
}
ctx.JSON(http.StatusCreated, convert.ToUser(ctx, u, ctx.Doer))
}
// EditUser api for modifying a user's information
func EditUser(ctx *context.APIContext) {
// swagger:operation PATCH /admin/users/{username} admin adminEditUser
// ---
// summary: Edit an existing user
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user whose data is to be edited
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditUserOption"
// responses:
// "200":
// "$ref": "#/responses/User"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.EditUserOption)
authOpts := &user_service.UpdateAuthOptions{
LoginSource: optional.FromNonDefault(form.SourceID),
LoginName: optional.Some(form.LoginName),
Password: optional.FromNonDefault(form.Password),
MustChangePassword: optional.FromPtr(form.MustChangePassword),
ProhibitLogin: optional.FromPtr(form.ProhibitLogin),
}
if err := user_service.UpdateAuth(ctx, ctx.ContextUser, authOpts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
ctx.APIError(http.StatusBadRequest, fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
case errors.Is(err, password.ErrComplexity):
ctx.APIError(http.StatusBadRequest, err)
case errors.Is(err, password.ErrIsPwned), password.IsErrIsPwnedRequest(err):
ctx.APIError(http.StatusBadRequest, err)
default:
ctx.APIErrorInternal(err)
}
return
}
if form.Email != nil {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
switch {
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
if !user_model.IsEmailDomainAllowed(*form.Email) {
err = fmt.Errorf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email)
}
ctx.APIError(http.StatusBadRequest, err)
case user_model.IsErrEmailAlreadyUsed(err):
ctx.APIError(http.StatusBadRequest, err)
default:
ctx.APIErrorInternal(err)
}
return
}
}
opts := &user_service.UpdateOptions{
FullName: optional.FromPtr(form.FullName),
Website: optional.FromPtr(form.Website),
Location: optional.FromPtr(form.Location),
Description: optional.FromPtr(form.Description),
IsActive: optional.FromPtr(form.Active),
IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin),
Visibility: optional.FromMapLookup(api.VisibilityModes, string(form.Visibility)),
AllowGitHook: optional.FromPtr(form.AllowGitHook),
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
AllowCreateOrganization: optional.FromPtr(form.AllowCreateOrganization),
IsRestricted: optional.FromPtr(form.Restricted),
}
if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil {
if user_model.IsErrDeleteLastAdminUser(err) {
ctx.APIError(http.StatusBadRequest, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.ContextUser, ctx.Doer))
}
// DeleteUser api for deleting a user
func DeleteUser(ctx *context.APIContext) {
// swagger:operation DELETE /admin/users/{username} admin adminDeleteUser
// ---
// summary: Delete a user
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user to delete
// type: string
// required: true
// - name: purge
// in: query
// description: purge the user from the system completely
// type: boolean
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
if ctx.ContextUser.IsOrganization() {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
return
}
// admin should not delete themself
if ctx.ContextUser.ID == ctx.Doer.ID {
ctx.APIError(http.StatusUnprocessableEntity, errors.New("you cannot delete yourself"))
return
}
if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
if repo_model.IsErrUserOwnRepos(err) ||
org_model.IsErrUserHasOrgs(err) ||
packages_model.IsErrUserOwnPackages(err) ||
user_model.IsErrDeleteLastAdminUser(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
log.Trace("Account deleted by admin(%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
ctx.Status(http.StatusNoContent)
}
// CreatePublicKey api for creating a public key to a user
func CreatePublicKey(ctx *context.APIContext) {
// swagger:operation POST /admin/users/{username}/keys admin adminCreatePublicKey
// ---
// summary: Add a public key on behalf of a user
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user who is to receive a public key
// type: string
// required: true
// - name: key
// in: body
// schema:
// "$ref": "#/definitions/CreateKeyOption"
// responses:
// "201":
// "$ref": "#/responses/PublicKey"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateKeyOption)
user.CreateUserPublicKey(ctx, *form, ctx.ContextUser.ID)
}
// DeleteUserPublicKey api for deleting a user's public key
func DeleteUserPublicKey(ctx *context.APIContext) {
// swagger:operation DELETE /admin/users/{username}/keys/{id} admin adminDeleteUserPublicKey
// ---
// summary: Delete a user's public key
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user whose public key is to be deleted
// type: string
// required: true
// - name: id
// in: path
// description: id of the key to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
if err := asymkey_service.DeletePublicKey(ctx, ctx.ContextUser, ctx.PathParamInt64("id")); err != nil {
if asymkey_model.IsErrKeyNotExist(err) {
ctx.APIErrorNotFound()
} else if asymkey_model.IsErrKeyAccessDenied(err) {
ctx.APIError(http.StatusForbidden, "You do not have access to this key")
} else {
ctx.APIErrorInternal(err)
}
return
}
log.Trace("Key deleted by admin(%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
ctx.Status(http.StatusNoContent)
}
// SearchUsers API for getting information of the users according the filter conditions
func SearchUsers(ctx *context.APIContext) {
// swagger:operation GET /admin/users admin adminSearchUsers
// ---
// summary: Search users according filter conditions
// produces:
// - application/json
// parameters:
// - name: source_id
// in: query
// description: ID of the user's login source to search for
// type: integer
// format: int64
// - name: login_name
// in: query
// description: identifier of the user, provided by the external authenticator
// type: string
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// - name: sort
// in: query
// description: sort users by attribute. Supported values are
// "name", "created", "updated" and "id".
// Default is "name"
// type: string
// - name: order
// in: query
// description: sort order, either "asc" (ascending) or "desc" (descending).
// Default is "asc", ignored if "sort" is not specified.
// type: string
// - name: q
// in: query
// description: search term (username, full name, email)
// type: string
// - name: visibility
// in: query
// description: visibility filter. Supported values are
// "public", "limited" and "private".
// type: string
// - name: is_active
// in: query
// description: filter active users
// type: boolean
// - name: is_admin
// in: query
// description: filter admin users
// type: boolean
// - name: is_restricted
// in: query
// description: filter restricted users
// type: boolean
// - name: is_2fa_enabled
// in: query
// description: filter 2FA enabled users
// type: boolean
// - name: is_prohibit_login
// in: query
// description: filter login prohibited users
// type: boolean
// responses:
// "200":
// "$ref": "#/responses/UserList"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
listOptions := utils.GetListOptions(ctx)
orderBy, ok := utils.ResolveSortOrder(ctx, user_model.AdminUserOrderByMap, db.SearchOrderByAlphabetically)
if !ok {
return
}
var visible []api.VisibleType
visibilityParam := ctx.FormString("visibility")
if len(visibilityParam) > 0 {
if visibility, ok := api.VisibilityModes[visibilityParam]; ok {
visible = []api.VisibleType{visibility}
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid visibility: \"%s\"", visibilityParam))
return
}
}
searchOpts := user_model.SearchUserOptions{
Actor: ctx.Doer,
Types: []user_model.UserType{user_model.UserTypeIndividual},
LoginName: ctx.FormTrim("login_name"),
SourceID: ctx.FormInt64("source_id"),
Keyword: ctx.FormTrim("q"),
Visible: visible,
OrderBy: orderBy,
ListOptions: listOptions,
SearchByEmail: true,
}
if ctx.FormString("is_active") != "" {
searchOpts.IsActive = optional.Some(ctx.FormBool("is_active"))
}
if ctx.FormString("is_admin") != "" {
searchOpts.IsAdmin = optional.Some(ctx.FormBool("is_admin"))
}
if ctx.FormString("is_restricted") != "" {
searchOpts.IsRestricted = optional.Some(ctx.FormBool("is_restricted"))
}
if ctx.FormString("is_2fa_enabled") != "" {
searchOpts.IsTwoFactorEnabled = optional.Some(ctx.FormBool("is_2fa_enabled"))
}
if ctx.FormString("is_prohibit_login") != "" {
searchOpts.IsProhibitLogin = optional.Some(ctx.FormBool("is_prohibit_login"))
}
users, maxResults, err := user_model.SearchUsers(ctx, searchOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
results := make([]*api.User, len(users))
for i := range users {
results[i] = convert.ToUser(ctx, users[i], ctx.Doer)
}
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &results)
}
// RenameUser api for renaming a user
func RenameUser(ctx *context.APIContext) {
// swagger:operation POST /admin/users/{username}/rename admin adminRenameUser
// ---
// summary: Rename a user
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: current username of the user
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/RenameUserOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
if ctx.ContextUser.IsOrganization() {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
return
}
newName := web.GetForm(ctx).(*api.RenameUserOption).NewName
// Check if username has been changed
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName, ctx.Doer); err != nil {
if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
+124
View File
@@ -0,0 +1,124 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package admin
import (
"net/http"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
)
// ListUserBadges lists all badges belonging to a user
func ListUserBadges(ctx *context.APIContext) {
// swagger:operation GET /admin/users/{username}/badges admin adminListUserBadges
// ---
// summary: List a user's badges
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user whose badges are to be listed
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/BadgeList"
// "404":
// "$ref": "#/responses/notFound"
badges, maxResults, err := user_model.GetUserBadges(ctx, ctx.ContextUser)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &badges)
}
// AddUserBadges add badges to a user
func AddUserBadges(ctx *context.APIContext) {
// swagger:operation POST /admin/users/{username}/badges admin adminAddUserBadges
// ---
// summary: Add a badge to a user
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user to whom a badge is to be added
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UserBadgeOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
form := web.GetForm(ctx).(*api.UserBadgeOption)
badges := prepareBadgesForReplaceOrAdd(*form)
if err := user_model.AddUserBadges(ctx, ctx.ContextUser, badges); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// DeleteUserBadges delete a badge from a user
func DeleteUserBadges(ctx *context.APIContext) {
// swagger:operation DELETE /admin/users/{username}/badges admin adminDeleteUserBadges
// ---
// summary: Remove a badge from a user
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user whose badge is to be deleted
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UserBadgeOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.UserBadgeOption)
badges := prepareBadgesForReplaceOrAdd(*form)
if err := user_model.RemoveUserBadges(ctx, ctx.ContextUser, badges); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
func prepareBadgesForReplaceOrAdd(form api.UserBadgeOption) []*user_model.Badge {
badges := make([]*user_model.Badge, len(form.BadgeSlugs))
for i, badge := range form.BadgeSlugs {
badges[i] = &user_model.Badge{
Slug: badge,
}
}
return badges
}
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package misc
import (
"net/http"
"gitea.dev/modules/options"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/services/context"
)
// Shows a list of all Gitignore templates
func ListGitignoresTemplates(ctx *context.APIContext) {
// swagger:operation GET /gitignore/templates miscellaneous listGitignoresTemplates
// ---
// summary: Returns a list of all gitignore templates
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/GitignoreTemplateList"
ctx.JSON(http.StatusOK, repo_module.Gitignores)
}
// SHows information about a gitignore template
func GetGitignoreTemplateInfo(ctx *context.APIContext) {
// swagger:operation GET /gitignore/templates/{name} miscellaneous getGitignoreTemplateInfo
// ---
// summary: Returns information about a gitignore template
// produces:
// - application/json
// parameters:
// - name: name
// in: path
// description: name of the template
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/GitignoreTemplateInfo"
// "404":
// "$ref": "#/responses/notFound"
name := util.PathJoinRelX(ctx.PathParam("name"))
text, err := options.Gitignore(name)
if err != nil {
ctx.APIErrorNotFound()
return
}
ctx.JSON(http.StatusOK, &structs.GitignoreTemplateInfo{Name: name, Source: string(text)})
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package misc
import (
"net/http"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/util"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// Shows a list of all Label templates
func ListLabelTemplates(ctx *context.APIContext) {
// swagger:operation GET /label/templates miscellaneous listLabelTemplates
// ---
// summary: Returns a list of all label templates
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/LabelTemplateList"
result := make([]string, len(repo_module.LabelTemplateFiles))
for i := range repo_module.LabelTemplateFiles {
result[i] = repo_module.LabelTemplateFiles[i].DisplayName
}
ctx.JSON(http.StatusOK, result)
}
// Shows all labels in a template
func GetLabelTemplate(ctx *context.APIContext) {
// swagger:operation GET /label/templates/{name} miscellaneous getLabelTemplateInfo
// ---
// summary: Returns all labels in a template
// produces:
// - application/json
// parameters:
// - name: name
// in: path
// description: name of the template
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/LabelTemplateInfo"
// "404":
// "$ref": "#/responses/notFound"
name := util.PathJoinRelX(ctx.PathParam("name"))
labels, err := repo_module.LoadTemplateLabelsByDisplayName(name)
if err != nil {
ctx.APIErrorNotFound()
return
}
ctx.JSON(http.StatusOK, convert.ToLabelTemplateList(labels))
}
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package misc
import (
"fmt"
"net/http"
"net/url"
"gitea.dev/modules/options"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/services/context"
)
// Returns a list of all License templates
func ListLicenseTemplates(ctx *context.APIContext) {
// swagger:operation GET /licenses miscellaneous listLicenseTemplates
// ---
// summary: Returns a list of all license templates
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/LicenseTemplateList"
response := make([]api.LicensesTemplateListEntry, len(repo_module.Licenses))
for i, license := range repo_module.Licenses {
response[i] = api.LicensesTemplateListEntry{
Key: license,
Name: license,
URL: fmt.Sprintf("%sapi/v1/licenses/%s", setting.AppURL, url.PathEscape(license)),
}
}
ctx.JSON(http.StatusOK, response)
}
func GetLicenseTemplateInfo(ctx *context.APIContext) {
// swagger:operation GET /licenses/{name} miscellaneous getLicenseTemplateInfo
// ---
// summary: Returns information about a license template
// produces:
// - application/json
// parameters:
// - name: name
// in: path
// description: name of the license
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/LicenseTemplateInfo"
// "404":
// "$ref": "#/responses/notFound"
name := util.PathJoinRelX(ctx.PathParam("name"))
text, err := options.License(name)
if err != nil {
ctx.APIErrorNotFound()
return
}
response := api.LicenseTemplateInfo{
Key: name,
Name: name,
URL: fmt.Sprintf("%sapi/v1/licenses/%s", setting.AppURL, url.PathEscape(name)),
Body: string(text),
// This is for combatibilty with the GitHub API. This Text is for some reason added to each License response.
Implementation: "Create a text file (typically named LICENSE or LICENSE.txt) in the root of your source code and copy the text of the license into the file",
}
ctx.JSON(http.StatusOK, response)
}
+92
View File
@@ -0,0 +1,92 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package misc
import (
"gitea.dev/modules/markup"
"gitea.dev/modules/markup/markdown"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/common"
"gitea.dev/services/context"
)
// Markup render markup document to HTML
func Markup(ctx *context.APIContext) {
// swagger:operation POST /markup miscellaneous renderMarkup
// ---
// summary: Render a markup document as HTML
// parameters:
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/MarkupOption"
// consumes:
// - application/json
// produces:
// - text/html
// responses:
// "200":
// "$ref": "#/responses/MarkupRender"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.MarkupOption)
mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated
common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, form.FilePath)
}
// Markdown render markdown document to HTML
func Markdown(ctx *context.APIContext) {
// swagger:operation POST /markdown miscellaneous renderMarkdown
// ---
// summary: Render a markdown document as HTML
// parameters:
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/MarkdownOption"
// consumes:
// - application/json
// produces:
// - text/html
// responses:
// "200":
// "$ref": "#/responses/MarkdownRender"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.MarkdownOption)
mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated
common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, "")
}
// MarkdownRaw render raw markdown HTML
func MarkdownRaw(ctx *context.APIContext) {
// swagger:operation POST /markdown/raw miscellaneous renderMarkdownRaw
// ---
// summary: Render raw markdown as HTML
// parameters:
// - name: body
// in: body
// description: Request body to render
// required: true
// schema:
// type: string
// consumes:
// - text/plain
// produces:
// - text/html
// responses:
// "200":
// "$ref": "#/responses/MarkdownRender"
// "422":
// "$ref": "#/responses/validationError"
defer ctx.Req.Body.Close()
if err := markdown.RenderRaw(markup.NewRenderContext(ctx), ctx.Req.Body, ctx.Resp); err != nil {
ctx.APIErrorInternal(err)
return
}
}
+225
View File
@@ -0,0 +1,225 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package misc
import (
go_context "context"
"io"
"net/http"
"os"
"path"
"strings"
"testing"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"gitea.dev/modules/markup"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/test"
"gitea.dev/modules/web"
context_service "gitea.dev/services/context"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
const AppURL = "http://localhost:3000/"
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
FixtureFiles: []string{"repository.yml", "user.yml"},
})
os.Exit(m.Run())
}
func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expectedBody string, expectedCode int) {
setting.AppURL = AppURL
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
context := "/user2/repo1"
if !wiki {
context += path.Join("/src/branch/main", path.Dir(filePath))
}
options := api.MarkupOption{
Mode: mode,
Text: text,
Context: context,
Wiki: wiki,
FilePath: filePath,
}
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
ctx.Repo = &context_service.Repository{}
ctx.Repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
web.SetForm(ctx, &options)
Markup(ctx)
assert.Equal(t, expectedBody, resp.Body.String())
assert.Equal(t, expectedCode, resp.Code)
resp.Body.Reset()
}
func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody string, responseCode int) {
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
setting.AppURL = AppURL
context := "/user2/repo1"
if !wiki {
context += "/src/branch/main"
}
options := api.MarkdownOption{
Mode: mode,
Text: text,
Context: context,
Wiki: wiki,
}
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
web.SetForm(ctx, &options)
Markdown(ctx)
assert.Equal(t, responseBody, resp.Body.String())
assert.Equal(t, responseCode, resp.Code)
resp.Body.Reset()
}
func TestAPI_RenderGFM(t *testing.T) {
unittest.PrepareTestEnv(t)
markup.Init(&markup.RenderHelperFuncs{
IsUsernameMentionable: func(ctx go_context.Context, username string) bool {
return username == "r-lyeh"
},
})
testCasesWiki := []string{
// dear imgui wiki markdown extract: special wiki syntax
`Wiki! Enjoy :)
- [[Links, Language bindings, Engine bindings|Links]]
- [[Tips]]
- Bezier widget (by @r-lyeh) https://github.com/ocornut/imgui/issues/786`,
// rendered
`<p>Wiki! Enjoy :)</p>
<ul>
<li><a href="http://localhost:3000/user2/repo1/wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
<li><a href="http://localhost:3000/user2/repo1/wiki/Tips" rel="nofollow">Tips</a></li>
<li>Bezier widget (by <a href="http://localhost:3000/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li>
</ul>
`,
// Guard wiki sidebar: special syntax
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
// rendered
`<p><a href="http://localhost:3000/user2/repo1/wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
`,
// special syntax
`[[Name|Link]]`,
// rendered
`<p><a href="http://localhost:3000/user2/repo1/wiki/Link" rel="nofollow">Name</a></p>
`,
// empty
``,
// rendered
``,
}
testCasesWikiDocument := []string{
// wine-staging wiki home extract: special wiki syntax, images
`## What is Wine Staging?
**Wine Staging** on website [wine-staging.com](http://wine-staging.com).
## Quick Links
Here are some links to the most important topics. You can find the full list of pages at the sidebar.
[[Configuration]]
[[images/icon-bug.png]]
`,
// rendered
`<h2 id="user-content-what-is-wine-staging">What is Wine Staging?</h2>
<p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
<h2 id="user-content-quick-links">Quick Links</h2>
<p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
<p><a href="http://localhost:3000/user2/repo1/wiki/Configuration" rel="nofollow">Configuration</a>
<a href="http://localhost:3000/user2/repo1/wiki/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
`,
}
for i := 0; i < len(testCasesWiki); i += 2 {
text := testCasesWiki[i]
response := testCasesWiki[i+1]
testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK)
testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK)
testRenderMarkdown(t, "comment", true, text, response, http.StatusOK)
testRenderMarkup(t, "comment", true, "", text, response, http.StatusOK)
testRenderMarkup(t, "file", true, "path/test.md", text, response, http.StatusOK)
}
for i := 0; i < len(testCasesWikiDocument); i += 2 {
text := testCasesWikiDocument[i]
response := testCasesWikiDocument[i+1]
testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK)
testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK)
testRenderMarkup(t, "file", true, "path/test.md", text, response, http.StatusOK)
}
input := "[Link](test.md)\n![Image](image.png)"
testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a>
<a href="http://localhost:3000/user2/repo1/src/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
`, http.StatusOK)
testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a>
<a href="http://localhost:3000/user2/repo1/src/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
`, http.StatusOK)
testRenderMarkup(t, "gfm", false, "", input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a>
<a href="http://localhost:3000/user2/repo1/src/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
`, http.StatusOK)
testRenderMarkup(t, "file", false, "path/new-file.md", input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/path/test.md" rel="nofollow">Link</a>
<a href="http://localhost:3000/user2/repo1/src/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" alt="Image"/></a></p>
`, http.StatusOK)
testRenderMarkup(t, "file", false, "path/test.unknown", "## Test", "unable to find a render\n", http.StatusUnprocessableEntity)
testRenderMarkup(t, "unknown", false, "", "## Test", "unsupported render mode: unknown\n", http.StatusUnprocessableEntity)
}
var simpleCases = []string{
// Guard wiki sidebar: special syntax
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
// rendered
`<p>[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]</p>
`,
// special syntax
`[[Name|Link]]`,
// rendered
`<p>[[Name|Link]]</p>
`,
// empty
``,
// rendered
``,
}
func TestAPI_RenderSimple(t *testing.T) {
setting.AppURL = AppURL
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
options := api.MarkdownOption{
Mode: "markdown",
Text: "",
Context: "/user2/repo1",
}
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
for i := 0; i < len(simpleCases); i += 2 {
options.Text = simpleCases[i]
web.SetForm(ctx, &options)
Markdown(ctx)
assert.Equal(t, simpleCases[i+1], resp.Body.String())
resp.Body.Reset()
}
}
func TestAPI_RenderRaw(t *testing.T) {
setting.AppURL = AppURL
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
for i := 0; i < len(simpleCases); i += 2 {
ctx.Req.Body = io.NopCloser(strings.NewReader(simpleCases[i]))
MarkdownRaw(ctx)
assert.Equal(t, simpleCases[i+1], resp.Body.String())
resp.Body.Reset()
}
}
+101
View File
@@ -0,0 +1,101 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package misc
import (
"gitea.dev/modules/git"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/context"
)
func getSigningKey(ctx *context.APIContext, expectedFormat string) {
// get the global signing key
content, format, err := asymkey_service.PublicSigningKey(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if format == "" {
ctx.APIErrorNotFound("no signing key")
return
} else if format != expectedFormat {
ctx.APIErrorNotFound("signing key format is " + format)
return
}
_, _ = ctx.Write([]byte(content))
}
// SigningKeyGPG returns the public key of the default signing key if it exists
func SigningKeyGPG(ctx *context.APIContext) {
// swagger:operation GET /signing-key.gpg miscellaneous getSigningKey
// ---
// summary: Get default signing-key.gpg
// produces:
// - text/plain
// responses:
// "200":
// description: "GPG armored public key"
// schema:
// type: string
// swagger:operation GET /repos/{owner}/{repo}/signing-key.gpg repository repoSigningKey
// ---
// summary: Get signing-key.gpg for given repository
// produces:
// - text/plain
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// description: "GPG armored public key"
// schema:
// type: string
getSigningKey(ctx, git.SigningKeyFormatOpenPGP)
}
// SigningKeySSH returns the public key of the default signing key if it exists
func SigningKeySSH(ctx *context.APIContext) {
// swagger:operation GET /signing-key.pub miscellaneous getSigningKeySSH
// ---
// summary: Get default signing-key.pub
// produces:
// - text/plain
// responses:
// "200":
// description: "ssh public key"
// schema:
// type: string
// swagger:operation GET /repos/{owner}/{repo}/signing-key.pub repository repoSigningKeySSH
// ---
// summary: Get signing-key.pub for given repository
// produces:
// - text/plain
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// description: "ssh public key"
// schema:
// type: string
getSigningKey(ctx, git.SigningKeyFormatSSH)
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package misc
import (
"net/http"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
"gitea.dev/services/context"
)
// Version shows the version of the Gitea server
func Version(ctx *context.APIContext) {
// swagger:operation GET /version miscellaneous getVersion
// ---
// summary: Returns the version of the Gitea application
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/ServerVersion"
ctx.JSON(http.StatusOK, &structs.ServerVersion{Version: setting.AppVer})
}
+77
View File
@@ -0,0 +1,77 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package notify
import (
"net/http"
"strings"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
)
// NewAvailable check if unread notifications exist
func NewAvailable(ctx *context.APIContext) {
// swagger:operation GET /notifications/new notification notifyNewAvailable
// ---
// summary: Check if unread notifications exist
// responses:
// "200":
// "$ref": "#/responses/NotificationCount"
total, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
UserID: ctx.Doer.ID,
Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread},
})
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
ctx.JSON(http.StatusOK, api.NotificationCount{New: total})
}
func getFindNotificationOptions(ctx *context.APIContext) *activities_model.FindNotificationOptions {
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return nil
}
opts := &activities_model.FindNotificationOptions{
ListOptions: utils.GetListOptions(ctx),
UserID: ctx.Doer.ID,
UpdatedBeforeUnix: before,
UpdatedAfterUnix: since,
}
if !ctx.FormBool("all") {
statuses := ctx.FormStrings("status-types")
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread", "pinned"})
}
subjectTypes := ctx.FormStrings("subject-type")
if len(subjectTypes) != 0 {
opts.Source = subjectToSource(subjectTypes)
}
return opts
}
func subjectToSource(value []string) (result []activities_model.NotificationSource) {
for _, v := range value {
switch strings.ToLower(v) {
case "issue":
result = append(result, activities_model.NotificationSourceIssue)
case "pull":
result = append(result, activities_model.NotificationSourcePullRequest)
case "commit":
result = append(result, activities_model.NotificationSourceCommit)
case "repository":
result = append(result, activities_model.NotificationSourceRepository)
}
}
return result
}
+227
View File
@@ -0,0 +1,227 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package notify
import (
"net/http"
"strings"
"time"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
"gitea.dev/modules/structs"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
func statusStringToNotificationStatus(status string) activities_model.NotificationStatus {
switch strings.ToLower(strings.TrimSpace(status)) {
case "unread":
return activities_model.NotificationStatusUnread
case "read":
return activities_model.NotificationStatusRead
case "pinned":
return activities_model.NotificationStatusPinned
default:
return 0
}
}
func statusStringsToNotificationStatuses(statuses, defaultStatuses []string) []activities_model.NotificationStatus {
if len(statuses) == 0 {
statuses = defaultStatuses
}
results := make([]activities_model.NotificationStatus, 0, len(statuses))
for _, status := range statuses {
notificationStatus := statusStringToNotificationStatus(status)
if notificationStatus > 0 {
results = append(results, notificationStatus)
}
}
return results
}
// ListRepoNotifications list users's notification threads on a specific repo
func ListRepoNotifications(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/notifications notification notifyGetRepoList
// ---
// summary: List users's notification threads on a specific repo
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: all
// in: query
// description: If true, show notifications marked as read. Default value is false
// type: boolean
// - name: status-types
// in: query
// description: "Show notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread & pinned"
// type: array
// collectionFormat: multi
// items:
// type: string
// - name: subject-type
// in: query
// description: "filter notifications by subject type"
// type: array
// collectionFormat: multi
// items:
// type: string
// enum: [issue,pull,commit,repository]
// - name: since
// in: query
// description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: before
// in: query
// description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/NotificationThreadList"
opts := getFindNotificationOptions(ctx)
if ctx.Written() {
return
}
opts.RepoID = ctx.Repo.Repository.ID
totalCount, err := db.Count[activities_model.Notification](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
nl, err := db.Find[activities_model.Notification](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
err = activities_model.NotificationList(nl).LoadAttributes(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(totalCount, opts.PageSize)
ctx.SetTotalCountHeader(totalCount)
ctx.JSON(http.StatusOK, convert.ToNotifications(ctx, nl))
}
// ReadRepoNotifications mark notification threads as read on a specific repo
func ReadRepoNotifications(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/notifications notification notifyReadRepoList
// ---
// summary: Mark notification threads as read, pinned or unread on a specific repo
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: all
// in: query
// description: If true, mark all notifications on this repo. Default value is false
// type: string
// required: false
// - name: status-types
// in: query
// description: "Mark notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread."
// type: array
// collectionFormat: multi
// items:
// type: string
// required: false
// - name: to-status
// in: query
// description: Status to mark notifications as. Defaults to read.
// type: string
// required: false
// - name: last_read_at
// in: query
// description: Describes the last point that notifications were checked. Anything updated since this time will not be updated.
// type: string
// format: date-time
// required: false
// responses:
// "205":
// "$ref": "#/responses/NotificationThreadList"
lastRead := int64(0)
qLastRead := ctx.FormTrim("last_read_at")
if len(qLastRead) > 0 {
tmpLastRead, err := time.Parse(time.RFC3339, qLastRead)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
if !tmpLastRead.IsZero() {
lastRead = tmpLastRead.Unix()
}
}
opts := &activities_model.FindNotificationOptions{
UserID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
UpdatedBeforeUnix: lastRead,
}
if !ctx.FormBool("all") {
statuses := ctx.FormStrings("status-types")
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
}
nl, err := db.Find[activities_model.Notification](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
targetStatus := statusStringToNotificationStatus(ctx.FormString("to-status"))
if targetStatus == 0 {
targetStatus = activities_model.NotificationStatusRead
}
changed := make([]*structs.NotificationThread, 0, len(nl))
for _, n := range nl {
notif, err := activities_model.SetNotificationStatus(ctx, n.ID, ctx.Doer, targetStatus)
if err != nil {
ctx.APIErrorInternal(err)
return
}
_ = notif.LoadAttributes(ctx)
changed = append(changed, convert.ToNotificationThread(ctx, notif))
}
ctx.JSON(http.StatusResetContent, changed)
}
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package notify
import (
"fmt"
"net/http"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// GetThread get notification by ID
func GetThread(ctx *context.APIContext) {
// swagger:operation GET /notifications/threads/{id} notification notifyGetThread
// ---
// summary: Get notification thread by ID
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of notification thread
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/NotificationThread"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
n := getThread(ctx)
if n == nil {
return
}
if err := n.LoadAttributes(ctx); err != nil && !issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToNotificationThread(ctx, n))
}
// ReadThread mark notification as read by ID
func ReadThread(ctx *context.APIContext) {
// swagger:operation PATCH /notifications/threads/{id} notification notifyReadThread
// ---
// summary: Mark notification thread as read by ID
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of notification thread
// type: string
// required: true
// - name: to-status
// in: query
// description: Status to mark notifications as
// type: string
// default: read
// required: false
// responses:
// "205":
// "$ref": "#/responses/NotificationThread"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
n := getThread(ctx)
if n == nil {
return
}
targetStatus := statusStringToNotificationStatus(ctx.FormString("to-status"))
if targetStatus == 0 {
targetStatus = activities_model.NotificationStatusRead
}
notif, err := activities_model.SetNotificationStatus(ctx, n.ID, ctx.Doer, targetStatus)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err = notif.LoadAttributes(ctx); err != nil && !issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusResetContent, convert.ToNotificationThread(ctx, notif))
}
func getThread(ctx *context.APIContext) *activities_model.Notification {
n, err := activities_model.GetNotificationByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if db.IsErrNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return nil
}
if n.UserID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
ctx.APIError(http.StatusForbidden, fmt.Errorf("only user itself and admin are allowed to read/change this thread %d", n.ID))
return nil
}
return n
}
+176
View File
@@ -0,0 +1,176 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package notify
import (
"net/http"
"time"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
"gitea.dev/modules/structs"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListNotifications list users's notification threads
func ListNotifications(ctx *context.APIContext) {
// swagger:operation GET /notifications notification notifyGetList
// ---
// summary: List users's notification threads
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: all
// in: query
// description: If true, show notifications marked as read. Default value is false
// type: boolean
// - name: status-types
// in: query
// description: "Show notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread & pinned."
// type: array
// collectionFormat: multi
// items:
// type: string
// - name: subject-type
// in: query
// description: "filter notifications by subject type"
// type: array
// collectionFormat: multi
// items:
// type: string
// enum: [issue,pull,commit,repository]
// - name: since
// in: query
// description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: before
// in: query
// description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/NotificationThreadList"
opts := getFindNotificationOptions(ctx)
if ctx.Written() {
return
}
totalCount, err := db.Count[activities_model.Notification](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
nl, err := db.Find[activities_model.Notification](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
err = activities_model.NotificationList(nl).LoadAttributes(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(totalCount, opts.PageSize)
ctx.SetTotalCountHeader(totalCount)
ctx.JSON(http.StatusOK, convert.ToNotifications(ctx, nl))
}
// ReadNotifications mark notification threads as read, unread, or pinned
func ReadNotifications(ctx *context.APIContext) {
// swagger:operation PUT /notifications notification notifyReadList
// ---
// summary: Mark notification threads as read, pinned or unread
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: last_read_at
// in: query
// description: Describes the last point that notifications were checked. Anything updated since this time will not be updated.
// type: string
// format: date-time
// required: false
// - name: all
// in: query
// description: If true, mark all notifications on this repo. Default value is false
// type: string
// required: false
// - name: status-types
// in: query
// description: "Mark notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread."
// type: array
// collectionFormat: multi
// items:
// type: string
// required: false
// - name: to-status
// in: query
// description: Status to mark notifications as, Defaults to read.
// type: string
// required: false
// responses:
// "205":
// "$ref": "#/responses/NotificationThreadList"
lastRead := int64(0)
qLastRead := ctx.FormTrim("last_read_at")
if len(qLastRead) > 0 {
tmpLastRead, err := time.Parse(time.RFC3339, qLastRead)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
if !tmpLastRead.IsZero() {
lastRead = tmpLastRead.Unix()
}
}
opts := &activities_model.FindNotificationOptions{
UserID: ctx.Doer.ID,
UpdatedBeforeUnix: lastRead,
}
if !ctx.FormBool("all") {
statuses := ctx.FormStrings("status-types")
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
}
nl, err := db.Find[activities_model.Notification](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
targetStatus := statusStringToNotificationStatus(ctx.FormString("to-status"))
if targetStatus == 0 {
targetStatus = activities_model.NotificationStatusRead
}
changed := make([]*structs.NotificationThread, 0, len(nl))
for _, n := range nl {
notif, err := activities_model.SetNotificationStatus(ctx, n.ID, ctx.Doer, targetStatus)
if err != nil {
ctx.APIErrorInternal(err)
return
}
_ = notif.LoadAttributes(ctx)
changed = append(changed, convert.ToNotificationThread(ctx, notif))
}
ctx.JSON(http.StatusResetContent, changed)
}
+693
View File
@@ -0,0 +1,693 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"errors"
"net/http"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
secret_model "gitea.dev/models/secret"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/shared"
"gitea.dev/routers/api/v1/utils"
actions_service "gitea.dev/services/actions"
"gitea.dev/services/context"
secret_service "gitea.dev/services/secrets"
)
// ListActionsSecrets list an organization's actions secrets
func (Action) ListActionsSecrets(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/secrets organization orgListActionsSecrets
// ---
// summary: List an organization's actions secrets
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/SecretList"
// "404":
// "$ref": "#/responses/notFound"
opts := &secret_model.FindSecretsOptions{
OwnerID: ctx.Org.Organization.ID,
ListOptions: utils.GetListOptions(ctx),
}
secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiSecrets := make([]*api.Secret, len(secrets))
for k, v := range secrets {
apiSecrets[k] = &api.Secret{
Name: v.Name,
Description: v.Description,
Created: v.CreatedUnix.AsTime(),
}
}
ctx.SetLinkHeader(count, opts.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiSecrets)
}
// create or update one secret of the organization
func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/actions/secrets/{secretname} organization updateOrgSecret
// ---
// summary: Create or Update a secret value in an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of organization
// type: string
// required: true
// - name: secretname
// in: path
// description: name of the secret
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateOrUpdateSecretOption"
// responses:
// "201":
// description: response when creating a secret
// "204":
// description: response when updating a secret
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if created {
ctx.Status(http.StatusCreated)
} else {
ctx.Status(http.StatusNoContent)
}
}
// DeleteSecret delete one secret of the organization
func (Action) DeleteSecret(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/actions/secrets/{secretname} organization deleteOrgSecret
// ---
// summary: Delete a secret in an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of organization
// type: string
// required: true
// - name: secretname
// in: path
// description: name of the secret
// type: string
// required: true
// responses:
// "204":
// description: delete one secret of the organization
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// CreateRegistrationToken returns the token to register org runners
func (Action) CreateRegistrationToken(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/actions/runners/registration-token organization orgCreateRunnerRegistrationToken
// ---
// summary: Get an organization's actions runner registration token
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0)
}
// ListVariables list org-level variables
func (Action) ListVariables(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList
// ---
// summary: Get an org-level variables list
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/VariableList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
OwnerID: ctx.Org.Organization.ID,
ListOptions: listOptions,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
variables := make([]*api.ActionVariable, len(vars))
for i, v := range vars {
variables[i] = &api.ActionVariable{
OwnerID: v.OwnerID,
RepoID: v.RepoID,
Name: v.Name,
Data: v.Data,
Description: v.Description,
}
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables)
}
// GetVariable get an org-level variable
func (Action) GetVariable(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable
// ---
// summary: Get an org-level variable
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActionVariable"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
OwnerID: ctx.Org.Organization.ID,
Name: ctx.PathParam("variablename"),
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
variable := &api.ActionVariable{
OwnerID: v.OwnerID,
RepoID: v.RepoID,
Name: v.Name,
Data: v.Data,
Description: v.Description,
}
ctx.JSON(http.StatusOK, variable)
}
// DeleteVariable delete an org-level variable
func (Action) DeleteVariable(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/actions/variables/{variablename} organization deleteOrgVariable
// ---
// summary: Delete an org-level variable
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActionVariable"
// "201":
// description: response when deleting a variable
// "204":
// description: response when deleting a variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
if err := actions_service.DeleteVariableByName(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("variablename")); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// CreateVariable create an org-level variable
func (Action) CreateVariable(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/actions/variables/{variablename} organization createOrgVariable
// ---
// summary: Create an org-level variable
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateVariableOption"
// responses:
// "201":
// description: successfully created the org-level variable
// "400":
// "$ref": "#/responses/error"
// "409":
// description: variable name already exists.
// "500":
// "$ref": "#/responses/error"
opt := web.GetForm(ctx).(*api.CreateVariableOption)
ownerID := ctx.Org.Organization.ID
variableName := ctx.PathParam("variablename")
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
OwnerID: ownerID,
Name: variableName,
})
if err != nil && !errors.Is(err, util.ErrNotExist) {
ctx.APIErrorInternal(err)
return
}
if v != nil && v.ID > 0 {
ctx.APIError(http.StatusConflict, util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
return
}
if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value, opt.Description); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusCreated)
}
// UpdateVariable update an org-level variable
func (Action) UpdateVariable(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/actions/variables/{variablename} organization updateOrgVariable
// ---
// summary: Update an org-level variable
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: variablename
// in: path
// description: name of the variable
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateVariableOption"
// responses:
// "201":
// description: response when updating an org-level variable
// "204":
// description: response when updating an org-level variable
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.UpdateVariableOption)
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
OwnerID: ctx.Org.Organization.ID,
Name: ctx.PathParam("variablename"),
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if opt.Name == "" {
opt.Name = ctx.PathParam("variablename")
}
v.Name = opt.Name
v.Data = opt.Value
v.Description = opt.Description
if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// ListRunners get org-level runners
func (Action) ListRunners(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/runners organization getOrgRunners
// ---
// summary: Get org-level runners
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: disabled
// in: query
// description: filter by disabled status (true or false)
// type: boolean
// required: false
// responses:
// "200":
// "$ref": "#/responses/RunnerList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListRunners(ctx, ctx.Org.Organization.ID, 0)
}
// GetRunner get an org-level runner
func (Action) GetRunner(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/runners/{runner_id} organization getOrgRunner
// ---
// summary: Get an org-level runner
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.GetRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id"))
}
// DeleteRunner delete an org-level runner
func (Action) DeleteRunner(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/actions/runners/{runner_id} organization deleteOrgRunner
// ---
// summary: Delete an org-level runner
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// responses:
// "204":
// description: runner has been deleted
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.DeleteRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id"))
}
// UpdateRunner update an org-level runner
func (Action) UpdateRunner(ctx *context.APIContext) {
// swagger:operation PATCH /orgs/{org}/actions/runners/{runner_id} organization updateOrgRunner
// ---
// summary: Update an org-level runner
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: runner_id
// in: path
// description: id of the runner
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditActionRunnerOption"
// responses:
// "200":
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.UpdateRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id"))
}
func (Action) ListWorkflowJobs(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/jobs organization getOrgWorkflowJobs
// ---
// summary: Get org-level workflow jobs
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: status
// in: query
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
// type: string
// required: false
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/WorkflowJobsList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListJobs(ctx, ctx.Org.Organization.ID, 0, 0, nil)
}
func (Action) ListWorkflowRuns(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/runs organization getOrgWorkflowRuns
// ---
// summary: Get org-level workflow runs
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: event
// in: query
// description: workflow event name
// type: string
// required: false
// - name: branch
// in: query
// description: workflow branch
// type: string
// required: false
// - name: status
// in: query
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
// type: string
// required: false
// - name: actor
// in: query
// description: triggered by user
// type: string
// required: false
// - name: head_sha
// in: query
// description: triggering sha of the workflow run
// type: string
// required: false
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/WorkflowRunsList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListRuns(ctx, ctx.Org.Organization.ID, 0)
}
var _ actions_service.API = new(Action)
// Action implements actions_service.API
type Action struct{}
// NewAction creates a new Action service
func NewAction() actions_service.API {
return Action{}
}
+80
View File
@@ -0,0 +1,80 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"encoding/base64"
"net/http"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
user_service "gitea.dev/services/user"
)
// UpdateAvatarupdates the Avatar of an Organisation
func UpdateAvatar(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/avatar organization orgUpdateAvatar
// ---
// summary: Update Avatar
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateUserAvatarOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.UpdateUserAvatarOption)
content, err := base64.StdEncoding.DecodeString(form.Image)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
err = user_service.UploadAvatar(ctx, ctx.Org.Organization.AsUser(), content)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// DeleteAvatar deletes the Avatar of an Organisation
func DeleteAvatar(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/avatar organization orgDeleteAvatar
// ---
// summary: Delete Avatar
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
err := user_service.DeleteAvatar(ctx, ctx.Org.Organization.AsUser())
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+116
View File
@@ -0,0 +1,116 @@
// Copyright 2024 The Gitea Authors.
// SPDX-License-Identifier: MIT
package org
import (
"gitea.dev/routers/api/v1/shared"
"gitea.dev/services/context"
)
func ListBlocks(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/blocks organization organizationListBlocks
// ---
// summary: List users blocked by the organization
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/UserList"
shared.ListBlocks(ctx, ctx.Org.Organization.AsUser())
}
func CheckUserBlock(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/blocks/{username} organization organizationCheckUserBlock
// ---
// summary: Check if a user is blocked by the organization
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: username
// in: path
// description: username of the user to check
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
shared.CheckUserBlock(ctx, ctx.Org.Organization.AsUser())
}
func BlockUser(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/blocks/{username} organization organizationBlockUser
// ---
// summary: Block a user
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: username
// in: path
// description: username of the user to block
// type: string
// required: true
// - name: note
// in: query
// description: optional note for the block
// type: string
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.BlockUser(ctx, ctx.Org.Organization.AsUser())
}
func UnblockUser(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/blocks/{username} organization organizationUnblockUser
// ---
// summary: Unblock a user
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: username
// in: path
// description: username of the user to unblock
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.UnblockUser(ctx, ctx.Doer, ctx.Org.Organization.AsUser())
}
+189
View File
@@ -0,0 +1,189 @@
// Copyright 2016 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
webhook_service "gitea.dev/services/webhook"
)
// ListHooks list an organization's webhooks
func ListHooks(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/hooks organization orgListHooks
// ---
// summary: List an organization's webhooks
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/HookList"
// "404":
// "$ref": "#/responses/notFound"
utils.ListOwnerHooks(
ctx,
ctx.ContextUser,
)
}
// GetHook get an organization's hook by id
func GetHook(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/hooks/{id} organization orgGetHook
// ---
// summary: Get a hook
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the hook to get
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Hook"
// "404":
// "$ref": "#/responses/notFound"
hook, err := utils.GetOwnerHook(ctx, ctx.ContextUser.ID, ctx.PathParamInt64("id"))
if err != nil {
return
}
apiHook, err := webhook_service.ToHook(ctx.ContextUser.HomeLink(), hook)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiHook)
}
// CreateHook create a hook for an organization
func CreateHook(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/hooks organization orgCreateHook
// ---
// summary: Create a hook
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/CreateHookOption"
// responses:
// "201":
// "$ref": "#/responses/Hook"
// "404":
// "$ref": "#/responses/notFound"
utils.AddOwnerHook(
ctx,
ctx.ContextUser,
web.GetForm(ctx).(*api.CreateHookOption),
)
}
// EditHook modify a hook of an organization
func EditHook(ctx *context.APIContext) {
// swagger:operation PATCH /orgs/{org}/hooks/{id} organization orgEditHook
// ---
// summary: Update a hook
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the hook to update
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditHookOption"
// responses:
// "200":
// "$ref": "#/responses/Hook"
// "404":
// "$ref": "#/responses/notFound"
utils.EditOwnerHook(
ctx,
ctx.ContextUser,
web.GetForm(ctx).(*api.EditHookOption),
ctx.PathParamInt64("id"),
)
}
// DeleteHook delete a hook of an organization
func DeleteHook(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/hooks/{id} organization orgDeleteHook
// ---
// summary: Delete a hook
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the hook to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
utils.DeleteOwnerHook(
ctx,
ctx.ContextUser,
ctx.PathParamInt64("id"),
)
}
+258
View File
@@ -0,0 +1,258 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
"strconv"
"strings"
issues_model "gitea.dev/models/issues"
"gitea.dev/modules/label"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListLabels list all the labels of an organization
func ListLabels(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/labels organization orgListLabels
// ---
// summary: List an organization's labels
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/LabelList"
// "404":
// "$ref": "#/responses/notFound"
labels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), utils.GetListOptions(ctx))
if err != nil {
ctx.APIErrorInternal(err)
return
}
count, err := issues_model.CountLabelsByOrgID(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToLabelList(labels, nil, ctx.Org.Organization.AsUser()))
}
// CreateLabel create a label for a repository
func CreateLabel(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/labels organization orgCreateLabel
// ---
// summary: Create a label for an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateLabelOption"
// responses:
// "201":
// "$ref": "#/responses/Label"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateLabelOption)
form.Color = strings.Trim(form.Color, " ")
color, err := label.NormalizeColor(form.Color)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
form.Color = color
label := &issues_model.Label{
Name: form.Name,
Exclusive: form.Exclusive,
Color: form.Color,
OrgID: ctx.Org.Organization.ID,
Description: form.Description,
}
if err := issues_model.NewLabel(ctx, label); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convert.ToLabel(label, nil, ctx.Org.Organization.AsUser()))
}
// GetLabel get label by organization and label id
func GetLabel(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/labels/{id} organization orgGetLabel
// ---
// summary: Get a single label
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the label to get
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Label"
// "404":
// "$ref": "#/responses/notFound"
var (
label *issues_model.Label
err error
)
strID := ctx.PathParam("id")
if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil {
label, err = issues_model.GetLabelInOrgByName(ctx, ctx.Org.Organization.ID, strID)
} else {
label, err = issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, intID)
}
if err != nil {
if issues_model.IsErrOrgLabelNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.JSON(http.StatusOK, convert.ToLabel(label, nil, ctx.Org.Organization.AsUser()))
}
// EditLabel modify a label for an Organization
func EditLabel(ctx *context.APIContext) {
// swagger:operation PATCH /orgs/{org}/labels/{id} organization orgEditLabel
// ---
// summary: Update a label
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the label to edit
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditLabelOption"
// responses:
// "200":
// "$ref": "#/responses/Label"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.EditLabelOption)
l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrOrgLabelNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
if form.Name != nil {
l.Name = *form.Name
}
if form.Exclusive != nil {
l.Exclusive = *form.Exclusive
}
if form.Color != nil {
color, err := label.NormalizeColor(*form.Color)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
l.Color = color
}
if form.Description != nil {
l.Description = *form.Description
}
l.SetArchived(form.IsArchived != nil && *form.IsArchived)
if err := issues_model.UpdateLabel(ctx, l); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToLabel(l, nil, ctx.Org.Organization.AsUser()))
}
// DeleteLabel delete a label for an organization
func DeleteLabel(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/labels/{id} organization orgDeleteLabel
// ---
// summary: Delete a label
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the label to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
if err := issues_model.DeleteLabel(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id")); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+346
View File
@@ -0,0 +1,346 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
"net/url"
"gitea.dev/models/organization"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
org_service "gitea.dev/services/org"
)
// listMembers list an organization's members
func listMembers(ctx *context.APIContext, isMember bool) {
listOptions := utils.GetListOptions(ctx)
opts := &organization.FindOrgMembersOpts{
Doer: ctx.Doer,
IsDoerMember: isMember,
OrgID: ctx.Org.Organization.ID,
ListOptions: listOptions,
}
count, err := organization.CountOrgMembers(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
members, _, err := organization.FindOrgMembers(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiMembers := make([]*api.User, len(members))
for i, member := range members {
apiMembers[i] = convert.ToUser(ctx, member, ctx.Doer)
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiMembers)
}
// ListMembers list an organization's members
func ListMembers(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/members organization orgListMembers
// ---
// summary: List an organization's members
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/UserList"
// "404":
// "$ref": "#/responses/notFound"
var (
isMember bool
err error
)
if ctx.Doer != nil {
isMember, err = ctx.Org.Organization.IsOrgMember(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
listMembers(ctx, isMember)
}
// ListPublicMembers list an organization's public members
func ListPublicMembers(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/public_members organization orgListPublicMembers
// ---
// summary: List an organization's public members
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/UserList"
// "404":
// "$ref": "#/responses/notFound"
listMembers(ctx, false)
}
// IsMember check if a user is a member of an organization
func IsMember(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/members/{username} organization orgIsMember
// ---
// summary: Check if a user is a member of an organization
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: username
// in: path
// description: username of the user to check for an organization membership
// type: string
// required: true
// responses:
// "204":
// description: user is a member
// "303":
// description: redirection to /orgs/{org}/public_members/{username}
// "404":
// description: user is not a member
userToCheck := user.GetContextUserByPathParam(ctx)
if ctx.Written() {
return
}
if ctx.Doer != nil {
userIsMember, err := ctx.Org.Organization.IsOrgMember(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
} else if userIsMember || ctx.Doer.IsAdmin {
userToCheckIsMember, err := ctx.Org.Organization.IsOrgMember(ctx, userToCheck.ID)
if err != nil {
ctx.APIErrorInternal(err)
} else if userToCheckIsMember {
ctx.Status(http.StatusNoContent)
} else {
ctx.APIErrorNotFound()
}
return
} else if ctx.Doer.ID == userToCheck.ID {
ctx.APIErrorNotFound()
return
}
}
redirectURL := setting.AppSubURL + "/api/v1/orgs/" + url.PathEscape(ctx.Org.Organization.Name) + "/public_members/" + url.PathEscape(userToCheck.Name)
ctx.Redirect(redirectURL)
}
// IsPublicMember check if a user is a public member of an organization
func IsPublicMember(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/public_members/{username} organization orgIsPublicMember
// ---
// summary: Check if a user is a public member of an organization
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: username
// in: path
// description: username of the user to check for a public organization membership
// type: string
// required: true
// responses:
// "204":
// description: user is a public member
// "404":
// description: user is not a public member
userToCheck := user.GetContextUserByPathParam(ctx)
if ctx.Written() {
return
}
is, err := organization.IsPublicMembership(ctx, ctx.Org.Organization.ID, userToCheck.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if is {
ctx.Status(http.StatusNoContent)
} else {
ctx.APIErrorNotFound()
}
}
func checkCanChangeOrgUserStatus(ctx *context.APIContext, targetUser *user_model.User) {
// allow user themselves to change their status, and allow admins to change any user
if targetUser.ID == ctx.Doer.ID || ctx.Doer.IsAdmin {
return
}
// allow org owners to change status of members
isOwner, err := ctx.Org.Organization.IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIError(http.StatusInternalServerError, err)
} else if !isOwner {
ctx.APIError(http.StatusForbidden, "Cannot change member visibility")
}
}
// PublicizeMember make a member's membership public
func PublicizeMember(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/public_members/{username} organization orgPublicizeMember
// ---
// summary: Publicize a user's membership
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: username
// in: path
// description: username of the user whose membership is to be publicized
// type: string
// required: true
// responses:
// "204":
// description: membership publicized
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
userToPublicize := user.GetContextUserByPathParam(ctx)
if ctx.Written() {
return
}
checkCanChangeOrgUserStatus(ctx, userToPublicize)
if ctx.Written() {
return
}
err := organization.ChangeOrgUserStatus(ctx, ctx.Org.Organization.ID, userToPublicize.ID, true)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// ConcealMember make a member's membership not public
func ConcealMember(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/public_members/{username} organization orgConcealMember
// ---
// summary: Conceal a user's membership
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: username
// in: path
// description: username of the user whose membership is to be concealed
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
userToConceal := user.GetContextUserByPathParam(ctx)
if ctx.Written() {
return
}
checkCanChangeOrgUserStatus(ctx, userToConceal)
if ctx.Written() {
return
}
err := organization.ChangeOrgUserStatus(ctx, ctx.Org.Organization.ID, userToConceal.ID, false)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// DeleteMember remove a member from an organization
func DeleteMember(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/members/{username} organization orgDeleteMember
// ---
// summary: Remove a member from an organization
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: username
// in: path
// description: username of the user to remove from the organization
// type: string
// required: true
// responses:
// "204":
// description: member removed
// "404":
// "$ref": "#/responses/notFound"
member := user.GetContextUserByPathParam(ctx)
if ctx.Written() {
return
}
if err := org_service.RemoveOrgUser(ctx, ctx.Org.Organization, member); err != nil {
ctx.APIErrorInternal(err)
}
ctx.Status(http.StatusNoContent)
}
+578
View File
@@ -0,0 +1,578 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
gocontext "context"
"errors"
"fmt"
"net/http"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
system_model "gitea.dev/models/system"
user_model "gitea.dev/models/user"
"gitea.dev/modules/graceful"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
feed_service "gitea.dev/services/feed"
"gitea.dev/services/org"
repo_service "gitea.dev/services/repository"
user_service "gitea.dev/services/user"
)
func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
listOptions := utils.GetListOptions(ctx)
opts := organization.FindOrgOptions{
ListOptions: listOptions,
UserID: u.ID,
IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, u),
}
opts.ApplyPublicOnly(ctx.PublicOnly)
orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiOrgs := make([]*api.Organization, len(orgs))
for i := range orgs {
apiOrgs[i] = convert.ToOrganization(ctx, orgs[i])
}
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &apiOrgs)
}
// ListMyOrgs list all my orgs
func ListMyOrgs(ctx *context.APIContext) {
// swagger:operation GET /user/orgs organization orgListCurrentUserOrgs
// ---
// summary: List the current user's organizations
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/OrganizationList"
// "404":
// "$ref": "#/responses/notFound"
listUserOrgs(ctx, ctx.Doer)
}
// ListUserOrgs list user's orgs
func ListUserOrgs(ctx *context.APIContext) {
// swagger:operation GET /users/{username}/orgs organization orgListUserOrgs
// ---
// summary: List a user's organizations
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user whose organizations are to be listed
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/OrganizationList"
// "404":
// "$ref": "#/responses/notFound"
listUserOrgs(ctx, ctx.ContextUser)
}
// GetUserOrgsPermissions get user permissions in organization
func GetUserOrgsPermissions(ctx *context.APIContext) {
// swagger:operation GET /users/{username}/orgs/{org}/permissions organization orgGetUserPermissions
// ---
// summary: Get user permissions in organization
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of the user whose permissions are to be obtained
// type: string
// required: true
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/OrganizationPermissions"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
var o *user_model.User
if o = user.GetUserByPathParam(ctx, "org"); o == nil {
return
}
op := api.OrganizationPermissions{}
if !organization.HasOrgOrUserVisible(ctx, o, ctx.Doer) {
ctx.APIErrorNotFound("HasOrgOrUserVisible", nil)
return
}
org := organization.OrgFromUser(o)
authorizeLevel, err := org.GetOrgUserMaxAuthorizeLevel(ctx, ctx.ContextUser.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if authorizeLevel > perm.AccessModeNone {
op.CanRead = true
}
if authorizeLevel > perm.AccessModeRead {
op.CanWrite = true
}
if authorizeLevel > perm.AccessModeWrite {
op.IsAdmin = true
}
if authorizeLevel > perm.AccessModeAdmin {
op.IsOwner = true
}
op.CanCreateRepository, err = org.CanCreateOrgRepo(ctx, ctx.ContextUser.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, op)
}
// GetAll return list of all public organizations
func GetAll(ctx *context.APIContext) {
// swagger:operation Get /orgs organization orgGetAll
// ---
// summary: Get list of organizations
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/OrganizationList"
vMode := []api.VisibleType{api.VisibleTypePublic}
if ctx.IsSigned {
vMode = append(vMode, api.VisibleTypeLimited)
if ctx.Doer.IsAdmin {
vMode = append(vMode, api.VisibleTypePrivate)
}
}
listOptions := utils.GetListOptions(ctx)
searchOpts := user_model.SearchUserOptions{
Actor: ctx.Doer,
ListOptions: listOptions,
Types: []user_model.UserType{user_model.UserTypeOrganization},
OrderBy: db.SearchOrderByAlphabetically,
Visible: vMode,
}
searchOpts.ApplyPublicOnly(ctx.PublicOnly)
publicOrgs, maxResults, err := user_model.SearchUsers(ctx, searchOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
orgs := make([]*api.Organization, len(publicOrgs))
for i := range publicOrgs {
orgs[i] = convert.ToOrganization(ctx, organization.OrgFromUser(publicOrgs[i]))
}
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &orgs)
}
// Create api for create organization
func Create(ctx *context.APIContext) {
// swagger:operation POST /orgs organization orgCreate
// ---
// summary: Create an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: organization
// in: body
// required: true
// schema: { "$ref": "#/definitions/CreateOrgOption" }
// responses:
// "201":
// "$ref": "#/responses/Organization"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateOrgOption)
if !ctx.Doer.CanCreateOrganization() {
ctx.APIError(http.StatusForbidden, nil)
return
}
visibility := api.VisibleTypePublic
if form.Visibility != "" {
visibility = api.VisibilityModes[string(form.Visibility)]
}
org := &organization.Organization{
Name: form.UserName,
FullName: form.FullName,
Email: form.Email,
Description: form.Description,
Website: form.Website,
Location: form.Location,
IsActive: true,
Type: user_model.UserTypeOrganization,
Visibility: visibility,
RepoAdminChangeTeamAccess: form.RepoAdminChangeTeamAccess,
}
if err := organization.CreateOrganization(ctx, org, ctx.Doer); err != nil {
if user_model.IsErrUserAlreadyExist(err) ||
db.IsErrNameReserved(err) ||
db.IsErrNameCharsNotAllowed(err) ||
db.IsErrNamePatternNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.JSON(http.StatusCreated, convert.ToOrganization(ctx, org))
}
// Get get an organization
func Get(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org} organization orgGet
// ---
// summary: Get an organization
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization to get
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Organization"
// "404":
// "$ref": "#/responses/notFound"
if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
ctx.APIErrorNotFound("HasOrgOrUserVisible", nil)
return
}
org := convert.ToOrganization(ctx, ctx.Org.Organization)
// Don't show Mail, when User is not logged in
if ctx.Doer == nil {
org.Email = ""
}
ctx.JSON(http.StatusOK, org)
}
func Rename(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/rename organization renameOrg
// ---
// summary: Rename an organization
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: existing org name
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/RenameOrgOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.RenameOrgOption)
orgUser := ctx.Org.Organization.AsUser()
if err := user_service.RenameUser(ctx, orgUser, form.NewName, ctx.Doer); err != nil {
if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// Edit change an organization's information
func Edit(ctx *context.APIContext) {
// swagger:operation PATCH /orgs/{org} organization orgEdit
// ---
// summary: Edit an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization to edit
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/EditOrgOption"
// responses:
// "200":
// "$ref": "#/responses/Organization"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditOrgOption)
if err := org.UpdateOrgEmailAddress(ctx, ctx.Org.Organization, form.Email); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
ctx.APIErrorInternal(err)
return
}
opts := &user_service.UpdateOptions{
FullName: optional.FromPtr(form.FullName),
Description: optional.FromPtr(form.Description),
Website: optional.FromPtr(form.Website),
Location: optional.FromPtr(form.Location),
Visibility: optional.FromMapLookup(api.VisibilityModes, string(optional.FromPtr(form.Visibility).Value())),
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
}
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToOrganization(ctx, ctx.Org.Organization))
}
// Delete an organization
func Delete(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org} organization orgDelete
// ---
// summary: Delete an organization
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: organization that is to be deleted
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
if err := org.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
func ListOrgActivityFeeds(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/activities/feeds organization orgListActivityFeeds
// ---
// summary: List an organization's activity feeds
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the org
// type: string
// required: true
// - name: date
// in: query
// description: the date of the activities to be found
// type: string
// format: date
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ActivityFeedsList"
// "404":
// "$ref": "#/responses/notFound"
includePrivate := false
if ctx.IsSigned {
if ctx.Doer.IsAdmin {
includePrivate = true
} else {
org := organization.OrgFromUser(ctx.ContextUser)
isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
includePrivate = isMember
}
}
listOptions := utils.GetListOptions(ctx)
opts := activities_model.GetFeedsOptions{
RequestedUser: ctx.ContextUser,
Actor: ctx.Doer,
IncludePrivate: includePrivate,
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
feeds, count, err := feed_service.GetFeeds(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
}
func deleteOrgReposBackground(ctx gocontext.Context, org *organization.Organization, repoIDs []int64, doer *user_model.User) {
defer func() {
if r := recover(); r != nil {
log.Error("panic during org repo deletion: %v, stack: %v", r, log.Stack(2))
}
}()
for _, repoID := range repoIDs {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
desc := fmt.Sprintf("Failed to get repository ID %d in org %s: %v", repoID, org.Name, err)
_ = system_model.CreateNotice(ctx, system_model.NoticeRepository, desc)
log.Error("GetRepositoryByID failed: %v", desc)
continue
}
if err := repo_service.DeleteRepository(ctx, doer, repo, true); err != nil {
desc := fmt.Sprintf("Failed to delete repository %s (ID: %d) in org %s: %v", repo.Name, repo.ID, org.Name, err)
_ = system_model.CreateNotice(ctx, system_model.NoticeRepository, desc)
log.Error("DeleteRepository failed: %v", desc)
continue
}
log.Info("Successfully deleted repository %s (ID: %d) in org %s", repo.Name, repo.ID, org.Name)
}
log.Info("Completed deletion of repositories in org %s", org.Name)
}
func DeleteOrgRepos(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/repos organization orgDeleteRepos
// ---
// summary: Delete all repositories in an organization
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "202":
// "$ref": "#/responses/empty"
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// Intentionally it only loads repository IDs to avoid loading too much data into memory
// There is no need to do pagination here as the number of repositories is expected to be manageable
repoIDs, err := repo_model.GetOrgRepositoryIDs(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if len(repoIDs) == 0 {
ctx.Status(http.StatusNoContent)
return
}
// Start deletion (slow) in background with detached context, so it can continue even if the request is canceled
go deleteOrgReposBackground(graceful.GetManager().ShutdownContext(), ctx.Org.Organization, repoIDs, ctx.Doer)
ctx.Status(http.StatusAccepted)
}
+888
View File
@@ -0,0 +1,888 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"errors"
"net/http"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
unit_model "gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
feed_service "gitea.dev/services/feed"
org_service "gitea.dev/services/org"
repo_service "gitea.dev/services/repository"
)
// ListTeams list all the teams of an organization
func ListTeams(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/teams organization orgListTeams
// ---
// summary: List an organization's teams
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/TeamList"
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
teams, count, err := organization.SearchTeam(ctx, &organization.SearchTeamOptions{
ListOptions: listOptions,
OrgID: ctx.Org.Organization.ID,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiTeams, err := convert.ToTeams(ctx, teams, false)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiTeams)
}
// ListUserTeams list all the teams a user belongs to
func ListUserTeams(ctx *context.APIContext) {
// swagger:operation GET /user/teams user userListTeams
// ---
// summary: List all the teams a user belongs to
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/TeamList"
listOptions := utils.GetListOptions(ctx)
teams, count, err := organization.SearchTeam(ctx, &organization.SearchTeamOptions{
ListOptions: listOptions,
UserID: ctx.Doer.ID,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiTeams, err := convert.ToTeams(ctx, teams, true)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiTeams)
}
// GetTeam api for get a team
func GetTeam(ctx *context.APIContext) {
// swagger:operation GET /teams/{id} organization orgGetTeam
// ---
// summary: Get a team
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the team to get
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Team"
// "404":
// "$ref": "#/responses/notFound"
apiTeam, err := convert.ToTeam(ctx, ctx.Org.Team, true)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiTeam)
}
func attachTeamUnits(team *organization.Team, defaultAccessMode perm.AccessMode, units []string) {
unitTypes, _ := unit_model.FindUnitTypes(units...)
team.Units = make([]*organization.TeamUnit, 0, len(units))
for _, tp := range unitTypes {
team.Units = append(team.Units, &organization.TeamUnit{
OrgID: team.OrgID,
Type: tp,
AccessMode: defaultAccessMode,
})
}
}
func attachTeamUnitsMap(team *organization.Team, unitsMap map[string]string) {
team.Units = make([]*organization.TeamUnit, 0, len(unitsMap))
for unitKey, p := range unitsMap {
team.Units = append(team.Units, &organization.TeamUnit{
OrgID: team.OrgID,
Type: unit_model.TypeFromKey(unitKey),
AccessMode: perm.ParseAccessMode(p),
})
}
}
func attachAdminTeamUnits(team *organization.Team) {
team.Units = make([]*organization.TeamUnit, 0, len(unit_model.AllRepoUnitTypes))
for _, ut := range unit_model.AllRepoUnitTypes {
up := perm.AccessModeAdmin
if ut == unit_model.TypeExternalTracker || ut == unit_model.TypeExternalWiki {
up = perm.AccessModeRead
}
team.Units = append(team.Units, &organization.TeamUnit{
OrgID: team.OrgID,
Type: ut,
AccessMode: up,
})
}
}
// CreateTeam api for create a team
func CreateTeam(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/teams organization orgCreateTeam
// ---
// summary: Create a team
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateTeamOption"
// responses:
// "201":
// "$ref": "#/responses/Team"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateTeamOption)
teamPermission := perm.ParseAccessMode(string(form.Permission), perm.AccessModeNone, perm.AccessModeAdmin)
team := &organization.Team{
OrgID: ctx.Org.Organization.ID,
Name: form.Name,
Description: form.Description,
IncludesAllRepositories: form.IncludesAllRepositories,
CanCreateOrgRepo: form.CanCreateOrgRepo,
AccessMode: teamPermission,
}
if team.AccessMode < perm.AccessModeAdmin {
if len(form.UnitsMap) > 0 {
attachTeamUnitsMap(team, form.UnitsMap)
} else if len(form.Units) > 0 {
unitPerm := perm.ParseAccessMode(string(form.Permission), perm.AccessModeRead, perm.AccessModeWrite)
attachTeamUnits(team, unitPerm, form.Units)
} else {
ctx.APIErrorInternal(errors.New("units permission should not be empty"))
return
}
} else {
attachAdminTeamUnits(team)
}
if err := org_service.NewTeam(ctx, team); err != nil {
if organization.IsErrTeamAlreadyExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
apiTeam, err := convert.ToTeam(ctx, team, true)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, apiTeam)
}
// EditTeam api for edit a team
func EditTeam(ctx *context.APIContext) {
// swagger:operation PATCH /teams/{id} organization orgEditTeam
// ---
// summary: Edit a team
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the team to edit
// type: integer
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditTeamOption"
// responses:
// "200":
// "$ref": "#/responses/Team"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditTeamOption)
team := ctx.Org.Team
if err := team.LoadUnits(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if form.CanCreateOrgRepo != nil {
team.CanCreateOrgRepo = team.IsOwnerTeam() || *form.CanCreateOrgRepo
}
if len(form.Name) > 0 {
team.Name = form.Name
}
if form.Description != nil {
team.Description = *form.Description
}
isAuthChanged := false
isIncludeAllChanged := false
if !team.IsOwnerTeam() && len(form.Permission) != 0 {
teamPermission := perm.ParseAccessMode(string(form.Permission), perm.AccessModeNone, perm.AccessModeAdmin)
if team.AccessMode != teamPermission {
isAuthChanged = true
team.AccessMode = teamPermission
}
if form.IncludesAllRepositories != nil {
isIncludeAllChanged = true
team.IncludesAllRepositories = *form.IncludesAllRepositories
}
}
if team.AccessMode < perm.AccessModeAdmin {
if len(form.UnitsMap) > 0 {
attachTeamUnitsMap(team, form.UnitsMap)
} else if len(form.Units) > 0 {
unitPerm := perm.ParseAccessMode(string(form.Permission), perm.AccessModeRead, perm.AccessModeWrite)
attachTeamUnits(team, unitPerm, form.Units)
}
} else {
attachAdminTeamUnits(team)
}
if err := org_service.UpdateTeam(ctx, team, isAuthChanged, isIncludeAllChanged); err != nil {
ctx.APIErrorInternal(err)
return
}
apiTeam, err := convert.ToTeam(ctx, team)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiTeam)
}
// DeleteTeam api for delete a team
func DeleteTeam(ctx *context.APIContext) {
// swagger:operation DELETE /teams/{id} organization orgDeleteTeam
// ---
// summary: Delete a team
// parameters:
// - name: id
// in: path
// description: id of the team to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// description: team deleted
// "404":
// "$ref": "#/responses/notFound"
if err := org_service.DeleteTeam(ctx, ctx.Org.Team); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// GetTeamMembers api for get a team's members
func GetTeamMembers(ctx *context.APIContext) {
// swagger:operation GET /teams/{id}/members organization orgListTeamMembers
// ---
// summary: List a team's members
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the team
// type: integer
// format: int64
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/UserList"
// "404":
// "$ref": "#/responses/notFound"
isMember, err := organization.IsOrganizationMember(ctx, ctx.Org.Team.OrgID, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
} else if !isMember && !ctx.Doer.IsAdmin {
ctx.APIErrorNotFound()
return
}
listOptions := utils.GetListOptions(ctx)
teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
ListOptions: listOptions,
TeamID: ctx.Org.Team.ID,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
members := make([]*api.User, len(teamMembers))
for i, member := range teamMembers {
members[i] = convert.ToUser(ctx, member, ctx.Doer)
}
ctx.SetLinkHeader(int64(ctx.Org.Team.NumMembers), listOptions.PageSize)
ctx.SetTotalCountHeader(int64(ctx.Org.Team.NumMembers))
ctx.JSON(http.StatusOK, members)
}
// GetTeamMember api for get a particular member of team
func GetTeamMember(ctx *context.APIContext) {
// swagger:operation GET /teams/{id}/members/{username} organization orgListTeamMember
// ---
// summary: List a particular member of team
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the team
// type: integer
// format: int64
// required: true
// - name: username
// in: path
// description: username of the user whose data is to be listed
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/User"
// "404":
// "$ref": "#/responses/notFound"
u := user.GetContextUserByPathParam(ctx)
if ctx.Written() {
return
}
teamID := ctx.PathParamInt64("teamid")
isTeamMember, err := organization.IsUserInTeams(ctx, u.ID, []int64{teamID})
if err != nil {
ctx.APIErrorInternal(err)
return
} else if !isTeamMember {
ctx.APIErrorNotFound()
return
}
ctx.JSON(http.StatusOK, convert.ToUser(ctx, u, ctx.Doer))
}
// AddTeamMember api for add a member to a team
func AddTeamMember(ctx *context.APIContext) {
// swagger:operation PUT /teams/{id}/members/{username} organization orgAddTeamMember
// ---
// summary: Add a team member
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the team
// type: integer
// format: int64
// required: true
// - name: username
// in: path
// description: username of the user to add to a team
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
u := user.GetContextUserByPathParam(ctx)
if ctx.Written() {
return
}
if err := org_service.AddTeamMember(ctx, ctx.Org.Team, u); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// RemoveTeamMember api for remove one member from a team
func RemoveTeamMember(ctx *context.APIContext) {
// swagger:operation DELETE /teams/{id}/members/{username} organization orgRemoveTeamMember
// ---
// summary: Remove a team member
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the team
// type: integer
// format: int64
// required: true
// - name: username
// in: path
// description: username of the user to remove from a team
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
u := user.GetContextUserByPathParam(ctx)
if ctx.Written() {
return
}
if err := org_service.RemoveTeamMember(ctx, ctx.Org.Team, u); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// GetTeamRepos api for get a team's repos
func GetTeamRepos(ctx *context.APIContext) {
// swagger:operation GET /teams/{id}/repos organization orgListTeamRepos
// ---
// summary: List a team's repos
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the team
// type: integer
// format: int64
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/RepositoryList"
// "404":
// "$ref": "#/responses/notFound"
team := ctx.Org.Team
listOptions := utils.GetListOptions(ctx)
teamRepos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{
ListOptions: listOptions,
TeamID: team.ID,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
repos := make([]*api.Repository, len(teamRepos))
for i, repo := range teamRepos {
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
repos[i] = convert.ToRepo(ctx, repo, permission)
}
ctx.SetLinkHeader(int64(team.NumRepos), listOptions.PageSize)
ctx.SetTotalCountHeader(int64(team.NumRepos))
ctx.JSON(http.StatusOK, repos)
}
// GetTeamRepo api for get a particular repo of team
func GetTeamRepo(ctx *context.APIContext) {
// swagger:operation GET /teams/{id}/repos/{org}/{repo} organization orgListTeamRepo
// ---
// summary: List a particular repo of team
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the team
// type: integer
// format: int64
// required: true
// - name: org
// in: path
// description: organization that owns the repo to list
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo to list
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Repository"
// "404":
// "$ref": "#/responses/notFound"
repo := getRepositoryByParams(ctx)
if ctx.Written() {
return
}
if !organization.HasTeamRepo(ctx, ctx.Org.Team.OrgID, ctx.Org.Team.ID, repo.ID) {
ctx.APIErrorNotFound()
return
}
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, permission))
}
// getRepositoryByParams get repository by a team's organization ID and repo name
func getRepositoryByParams(ctx *context.APIContext) *repo_model.Repository {
repo, err := repo_model.GetRepositoryByName(ctx, ctx.Org.Team.OrgID, ctx.PathParam("reponame"))
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return nil
}
return repo
}
// AddTeamRepository api for adding a repository to a team
func AddTeamRepository(ctx *context.APIContext) {
// swagger:operation PUT /teams/{id}/repos/{org}/{repo} organization orgAddTeamRepository
// ---
// summary: Add a repository to a team
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the team
// type: integer
// format: int64
// required: true
// - name: org
// in: path
// description: organization that owns the repo to add
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo to add
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
repo := getRepositoryByParams(ctx)
if ctx.Written() {
return
}
if access, err := access_model.AccessLevel(ctx, ctx.Doer, repo); err != nil {
ctx.APIErrorInternal(err)
return
} else if access < perm.AccessModeAdmin {
ctx.APIError(http.StatusForbidden, "Must have admin-level access to the repository")
return
}
if err := repo_service.TeamAddRepository(ctx, ctx.Org.Team, repo); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// RemoveTeamRepository api for removing a repository from a team
func RemoveTeamRepository(ctx *context.APIContext) {
// swagger:operation DELETE /teams/{id}/repos/{org}/{repo} organization orgRemoveTeamRepository
// ---
// summary: Remove a repository from a team
// description: This does not delete the repository, it only removes the
// repository from the team.
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the team
// type: integer
// format: int64
// required: true
// - name: org
// in: path
// description: organization that owns the repo to remove
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo to remove
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
repo := getRepositoryByParams(ctx)
if ctx.Written() {
return
}
if access, err := access_model.AccessLevel(ctx, ctx.Doer, repo); err != nil {
ctx.APIErrorInternal(err)
return
} else if access < perm.AccessModeAdmin {
ctx.APIError(http.StatusForbidden, "Must have admin-level access to the repository")
return
}
if err := repo_service.RemoveRepositoryFromTeam(ctx, ctx.Org.Team, repo.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// SearchTeam api for searching teams
func SearchTeam(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/teams/search organization teamSearch
// ---
// summary: Search for teams within an organization
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: q
// in: query
// description: keywords to search
// type: string
// - name: include_desc
// in: query
// description: include search within team description (defaults to true)
// type: boolean
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// description: "SearchResults of a successful search"
// schema:
// type: object
// properties:
// ok:
// type: boolean
// data:
// type: array
// items:
// "$ref": "#/definitions/Team"
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
opts := &organization.SearchTeamOptions{
Keyword: ctx.FormTrim("q"),
OrgID: ctx.Org.Organization.ID,
IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"),
ListOptions: listOptions,
}
// Only admin is allowed to search for all teams
if !ctx.Doer.IsAdmin {
opts.UserID = ctx.Doer.ID
}
teams, maxResults, err := organization.SearchTeam(ctx, opts)
if err != nil {
log.Error("SearchTeam failed: %v", err)
ctx.JSON(http.StatusInternalServerError, map[string]any{
"ok": false,
"error": "SearchTeam internal failure",
})
return
}
apiTeams, err := convert.ToTeams(ctx, teams, false)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, map[string]any{
"ok": true,
"data": apiTeams,
})
}
func ListTeamActivityFeeds(ctx *context.APIContext) {
// swagger:operation GET /teams/{id}/activities/feeds organization orgListTeamActivityFeeds
// ---
// summary: List a team's activity feeds
// produces:
// - application/json
// parameters:
// - name: id
// in: path
// description: id of the team
// type: integer
// format: int64
// required: true
// - name: date
// in: query
// description: the date of the activities to be found
// type: string
// format: date
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ActivityFeedsList"
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
opts := activities_model.GetFeedsOptions{
RequestedTeam: ctx.Org.Team,
Actor: ctx.Doer,
IncludePrivate: true,
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
feeds, count, err := feed_service.GetFeeds(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
}
+491
View File
@@ -0,0 +1,491 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package packages
import (
"errors"
"net/http"
"gitea.dev/models/packages"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/optional"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
packages_service "gitea.dev/services/packages"
)
// ListPackages gets all packages of an owner
func ListPackages(ctx *context.APIContext) {
// swagger:operation GET /packages/{owner} package listPackages
// ---
// summary: Gets all packages of an owner
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the packages
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// - name: type
// in: query
// description: package type filter
// type: string
// enum: [alpine, cargo, chef, composer, conan, conda, container, cran, debian, generic, go, helm, maven, npm, nuget, pub, pypi, rpm, rubygems, swift, terraform, vagrant]
// - name: q
// in: query
// description: name filter
// type: string
// responses:
// "200":
// "$ref": "#/responses/PackageList"
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
apiPackages, count, err := searchPackages(ctx, &packages.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages.Type(ctx.FormTrim("type")),
Name: packages.SearchValue{Value: ctx.FormTrim("q")},
IsInternal: optional.Some(false),
Paginator: &listOptions,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiPackages)
}
// GetPackage gets a package
func GetPackage(ctx *context.APIContext) {
// swagger:operation GET /packages/{owner}/{type}/{name}/{version} package getPackage
// ---
// summary: Gets a package
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the package
// type: string
// required: true
// - name: type
// in: path
// description: type of the package
// type: string
// required: true
// - name: name
// in: path
// description: name of the package
// type: string
// required: true
// - name: version
// in: path
// description: version of the package
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Package"
// "404":
// "$ref": "#/responses/notFound"
apiPackage, err := convert.ToPackage(ctx, ctx.Package.Descriptor, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiPackage)
}
// DeletePackage deletes a package
func DeletePackage(ctx *context.APIContext) {
// swagger:operation DELETE /packages/{owner}/{type}/{name} package deletePackage
// ---
// summary: Delete a package
// parameters:
// - name: owner
// in: path
// description: owner of the package
// type: string
// required: true
// - name: type
// in: path
// description: type of the package
// type: string
// required: true
// - name: name
// in: path
// description: name of the package
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
err := packages_service.RemovePackage(ctx, ctx.Doer, ctx.Package.Descriptor.Package)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// DeletePackageVersion deletes a package version
func DeletePackageVersion(ctx *context.APIContext) {
// swagger:operation DELETE /packages/{owner}/{type}/{name}/{version} package deletePackageVersion
// ---
// summary: Delete a package version
// parameters:
// - name: owner
// in: path
// description: owner of the package
// type: string
// required: true
// - name: type
// in: path
// description: type of the package
// type: string
// required: true
// - name: name
// in: path
// description: name of the package
// type: string
// required: true
// - name: version
// in: path
// description: version of the package
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
err := packages_service.RemovePackageVersion(ctx, ctx.Doer, ctx.Package.Descriptor.Version)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// ListPackageFiles gets all files of a package
func ListPackageFiles(ctx *context.APIContext) {
// swagger:operation GET /packages/{owner}/{type}/{name}/{version}/files package listPackageFiles
// ---
// summary: Gets all files of a package
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the package
// type: string
// required: true
// - name: type
// in: path
// description: type of the package
// type: string
// required: true
// - name: name
// in: path
// description: name of the package
// type: string
// required: true
// - name: version
// in: path
// description: version of the package
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/PackageFileList"
// "404":
// "$ref": "#/responses/notFound"
apiPackageFiles := make([]*api.PackageFile, 0, len(ctx.Package.Descriptor.Files))
for _, pfd := range ctx.Package.Descriptor.Files {
apiPackageFiles = append(apiPackageFiles, convert.ToPackageFile(pfd))
}
ctx.JSON(http.StatusOK, apiPackageFiles)
}
// ListPackageVersions gets all versions of a package
func ListPackageVersions(ctx *context.APIContext) {
// swagger:operation GET /packages/{owner}/{type}/{name} package listPackageVersions
// ---
// summary: Gets all versions of a package
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the package
// type: string
// required: true
// - name: type
// in: path
// description: type of the package
// type: string
// required: true
// - name: name
// in: path
// description: name of the package
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/PackageList"
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
apiPackages, count, err := searchPackages(ctx, &packages.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages.Type(ctx.PathParam("type")),
Name: packages.SearchValue{Value: ctx.PathParam("name"), ExactMatch: true},
IsInternal: optional.Some(false),
Paginator: &listOptions,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiPackages)
}
// GetLatestPackageVersion gets the latest version of a package
func GetLatestPackageVersion(ctx *context.APIContext) {
// swagger:operation GET /packages/{owner}/{type}/{name}/-/latest package getLatestPackageVersion
// ---
// summary: Gets the latest version of a package
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the package
// type: string
// required: true
// - name: type
// in: path
// description: type of the package
// type: string
// required: true
// - name: name
// in: path
// description: name of the package
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Package"
// "404":
// "$ref": "#/responses/notFound"
pvs, _, err := packages.SearchLatestVersions(ctx, &packages.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages.Type(ctx.PathParam("type")),
Name: packages.SearchValue{Value: ctx.PathParam("name"), ExactMatch: true},
IsInternal: optional.Some(false),
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
if len(pvs) == 0 {
ctx.APIError(http.StatusNotFound, err)
return
}
pd, err := packages.GetPackageDescriptor(ctx, pvs[0])
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiPackage)
}
// LinkPackage sets a repository link for a package
func LinkPackage(ctx *context.APIContext) {
// swagger:operation POST /packages/{owner}/{type}/{name}/-/link/{repo_name} package linkPackage
// ---
// summary: Link a package to a repository
// parameters:
// - name: owner
// in: path
// description: owner of the package
// type: string
// required: true
// - name: type
// in: path
// description: type of the package
// type: string
// required: true
// - name: name
// in: path
// description: name of the package
// type: string
// required: true
// - name: repo_name
// in: path
// description: name of the repository to link.
// type: string
// required: true
// responses:
// "201":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
repo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ctx.PathParam("repo_name"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
err = packages_service.LinkToRepository(ctx, pkg, repo, ctx.Doer)
if err != nil {
switch {
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIError(http.StatusBadRequest, err)
case errors.Is(err, util.ErrPermissionDenied):
ctx.APIError(http.StatusForbidden, err)
default:
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusCreated)
}
// UnlinkPackage sets a repository link for a package
func UnlinkPackage(ctx *context.APIContext) {
// swagger:operation POST /packages/{owner}/{type}/{name}/-/unlink package unlinkPackage
// ---
// summary: Unlink a package from a repository
// parameters:
// - name: owner
// in: path
// description: owner of the package
// type: string
// required: true
// - name: type
// in: path
// description: type of the package
// type: string
// required: true
// - name: name
// in: path
// description: name of the package
// type: string
// required: true
// responses:
// "201":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
err = packages_service.UnlinkFromRepository(ctx, pkg, ctx.Doer)
if err != nil {
switch {
case errors.Is(err, util.ErrPermissionDenied):
ctx.APIError(http.StatusForbidden, err)
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIError(http.StatusBadRequest, err)
default:
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
func searchPackages(ctx *context.APIContext, opts *packages.PackageSearchOptions) ([]*api.Package, int64, error) {
pvs, count, err := packages.SearchVersions(ctx, opts)
if err != nil {
return nil, 0, err
}
pds, err := packages.GetPackageDescriptors(ctx, pvs)
if err != nil {
return nil, 0, err
}
apiPackages := make([]*api.Package, 0, len(pds))
for _, pd := range pds {
apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer)
if err != nil {
return nil, 0, err
}
apiPackages = append(apiPackages, apiPackage)
}
return apiPackages, count, nil
}
File diff suppressed because it is too large Load Diff
+68
View File
@@ -0,0 +1,68 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
actions_model "gitea.dev/models/actions"
"gitea.dev/modules/util"
"gitea.dev/routers/common"
"gitea.dev/services/context"
)
func DownloadActionsRunJobLogs(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs repository downloadActionsRunJobLogs
// ---
// summary: Downloads the job logs for a workflow run
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: job_id
// in: path
// description: id of the job
// type: integer
// required: true
// responses:
// "200":
// description: output blob content
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
jobID := ctx.PathParamInt64("job_id")
curJob, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobID)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if err = curJob.LoadRepo(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
err = common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, curJob)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
}
}
+88
View File
@@ -0,0 +1,88 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"encoding/base64"
"net/http"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
repo_service "gitea.dev/services/repository"
)
// UpdateVatar updates the Avatar of an Repo
func UpdateAvatar(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/avatar repository repoUpdateAvatar
// ---
// summary: Update avatar
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateRepoAvatarOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.UpdateRepoAvatarOption)
content, err := base64.StdEncoding.DecodeString(form.Image)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
err = repo_service.UploadAvatar(ctx, ctx.Repo.Repository, content)
if err != nil {
ctx.APIErrorInternal(err)
}
ctx.Status(http.StatusNoContent)
}
// UpdateAvatar deletes the Avatar of an Repo
func DeleteAvatar(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/avatar repository repoDeleteAvatar
// ---
// summary: Delete avatar
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
err := repo_service.DeleteAvatar(ctx, ctx.Repo.Repository)
if err != nil {
ctx.APIErrorInternal(err)
}
ctx.Status(http.StatusNoContent)
}
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"gitea.dev/services/context"
files_service "gitea.dev/services/repository/files"
)
// GetBlob get the blob of a repository file.
func GetBlob(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/git/blobs/{sha} repository GetBlob
// ---
// summary: Gets the blob of a repository.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: sha
// in: path
// description: sha of the commit
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/GitBlobResponse"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
sha := ctx.PathParam("sha")
if len(sha) == 0 {
ctx.APIError(http.StatusBadRequest, "sha not provided")
return
}
if blob, err := files_service.GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil {
ctx.APIError(http.StatusBadRequest, err)
} else {
ctx.JSON(http.StatusOK, blob)
}
}
File diff suppressed because it is too large Load Diff
+371
View File
@@ -0,0 +1,371 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
"strings"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
pull_service "gitea.dev/services/pull"
repo_service "gitea.dev/services/repository"
)
// ListCollaborators list a repository's collaborators
func ListCollaborators(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/collaborators repository repoListCollaborators
// ---
// summary: List a repository's collaborators
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/UserList"
// "404":
// "$ref": "#/responses/notFound"
collaborators, total, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{
ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
users := make([]*api.User, len(collaborators))
for i, collaborator := range collaborators {
users[i] = convert.ToUser(ctx, collaborator.User, ctx.Doer)
}
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, users)
}
// IsCollaborator check if a user is a collaborator of a repository
func IsCollaborator(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/collaborators/{collaborator} repository repoCheckCollaborator
// ---
// summary: Check if a user is a collaborator of a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: collaborator
// in: path
// description: username of the user to check for being a collaborator
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
user, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
isColab, err := repo_model.IsCollaborator(ctx, ctx.Repo.Repository.ID, user.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if isColab {
ctx.Status(http.StatusNoContent)
} else {
ctx.APIErrorNotFound()
}
}
// AddOrUpdateCollaborator add or update a collaborator to a repository
func AddOrUpdateCollaborator(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/collaborators/{collaborator} repository repoAddCollaborator
// ---
// summary: Add or Update a collaborator to a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: collaborator
// in: path
// description: username of the user to add or update as a collaborator
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/AddCollaboratorOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.AddCollaboratorOption)
collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if !collaborator.IsActive {
ctx.APIErrorInternal(errors.New("collaborator's account is inactive"))
return
}
p := perm.AccessModeWrite
if form.Permission != nil {
p = perm.ParseAccessMode(string(*form.Permission), perm.AccessModeRead, perm.AccessModeWrite, perm.AccessModeAdmin)
}
if err := repo_service.AddOrUpdateCollaborator(ctx, ctx.Repo.Repository, collaborator, p); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// DeleteCollaborator delete a collaborator from a repository
func DeleteCollaborator(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/collaborators/{collaborator} repository repoDeleteCollaborator
// ---
// summary: Delete a collaborator from a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: collaborator
// in: path
// description: username of the collaborator to delete
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// GetRepoPermissions gets repository permissions for a user
func GetRepoPermissions(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/collaborators/{collaborator}/permission repository repoGetRepoPermissions
// ---
// summary: Get repository permissions for a user
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: collaborator
// in: path
// description: username of the collaborator whose permissions are to be obtained
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RepoCollaboratorPermission"
// "404":
// "$ref": "#/responses/notFound"
// "403":
// "$ref": "#/responses/forbidden"
collaboratorUsername := ctx.PathParam("collaborator")
if !ctx.Doer.IsAdmin && !strings.EqualFold(ctx.Doer.LowerName, collaboratorUsername) && !ctx.IsUserRepoAdmin() {
ctx.APIError(http.StatusForbidden, "Only admins can query all permissions, repo admins can query all repo permissions, collaborators can query only their own")
return
}
collaborator, err := user_model.GetUserByName(ctx, collaboratorUsername)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
permission, err := access_model.GetIndividualUserRepoPermission(ctx, ctx.Repo.Repository, collaborator)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToUserAndPermission(ctx, collaborator, ctx.ContextUser, permission.AccessMode))
}
// GetReviewers return all users that can be requested to review in this repo
func GetReviewers(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/reviewers repository repoGetReviewers
// ---
// summary: Return all users that can be requested to review in this repo
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/UserList"
// "404":
// "$ref": "#/responses/notFound"
canChooseReviewer := issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, ctx.Repo.Repository, 0)
if !canChooseReviewer {
ctx.APIError(http.StatusForbidden, errors.New("doer has no permission to get reviewers"))
return
}
reviewers, err := pull_service.GetReviewers(ctx, ctx.Repo.Repository, ctx.Doer.ID, 0)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToUsers(ctx, ctx.Doer, reviewers))
}
// GetAssignees return all users that have write access and can be assigned to issues
func GetAssignees(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/assignees repository repoGetAssignees
// ---
// summary: Return all users that have write access and can be assigned to issues
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/UserList"
// "404":
// "$ref": "#/responses/notFound"
assignees, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToUsers(ctx, ctx.Doer, assignees))
}
+402
View File
@@ -0,0 +1,402 @@
// Copyright 2018 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"math"
"net/http"
"strconv"
"time"
issues_model "gitea.dev/models/issues"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// GetSingleCommit get a commit via sha
func GetSingleCommit(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/git/commits/{sha} repository repoGetSingleCommit
// ---
// summary: Get a single commit from a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: sha
// in: path
// description: a git ref or commit sha
// type: string
// required: true
// - name: stat
// in: query
// description: include diff stats for every commit (disable for speedup, default 'true')
// type: boolean
// - name: verification
// in: query
// description: include verification for every commit (disable for speedup, default 'true')
// type: boolean
// - name: files
// in: query
// description: include a list of affected files for every commit (disable for speedup, default 'true')
// type: boolean
// responses:
// "200":
// "$ref": "#/responses/Commit"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
sha := ctx.PathParam("sha")
if !git.IsValidRefPattern(sha) {
ctx.APIError(http.StatusUnprocessableEntity, "no valid ref or sha: "+sha)
return
}
getCommit(ctx, sha, convert.ParseCommitOptions(ctx))
}
func getCommit(ctx *context.APIContext, identifier string, toCommitOpts convert.ToCommitOptions) {
commit, err := ctx.Repo.GitRepo.GetCommit(identifier)
if err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorNotFound("commit doesn't exist: " + identifier)
return
}
ctx.APIErrorInternal(err)
return
}
json, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, commit, nil, toCommitOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, json)
}
// GetAllCommits get all commits via
func GetAllCommits(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/commits repository repoGetAllCommits
// ---
// summary: Get a list of all commits from a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: sha
// in: query
// description: SHA or branch to start listing commits from (usually 'master')
// type: string
// - name: path
// in: query
// description: filepath of a file/dir
// type: string
// - name: since
// in: query
// description: Only commits after this date will be returned (ISO 8601 format)
// type: string
// format: date-time
// - name: until
// in: query
// description: Only commits before this date will be returned (ISO 8601 format)
// type: string
// format: date-time
// - name: stat
// in: query
// description: include diff stats for every commit (disable for speedup, default 'true')
// type: boolean
// - name: verification
// in: query
// description: include verification for every commit (disable for speedup, default 'true')
// type: boolean
// - name: files
// in: query
// description: include a list of affected files for every commit (disable for speedup, default 'true')
// type: boolean
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results (ignored if used with 'path')
// type: integer
// - name: not
// in: query
// description: commits that match the given specifier will not be listed.
// type: string
// responses:
// "200":
// "$ref": "#/responses/CommitList"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/EmptyRepository"
since := ctx.FormString("since")
until := ctx.FormString("until")
// Validate since/until as ISO 8601 (RFC3339)
if since != "" {
if _, err := time.Parse(time.RFC3339, since); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, "invalid 'since' format, expected ISO 8601 (RFC3339)")
return
}
}
if until != "" {
if _, err := time.Parse(time.RFC3339, until); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, "invalid 'until' format, expected ISO 8601 (RFC3339)")
return
}
}
if ctx.Repo.Repository.IsEmpty {
ctx.JSON(http.StatusConflict, api.APIError{
Message: "Git Repository is empty.",
URL: setting.API.SwaggerURL,
})
return
}
listOptions := utils.GetListOptions(ctx)
if listOptions.Page <= 0 {
listOptions.Page = 1
}
if listOptions.PageSize > setting.Git.CommitsRangeSize {
listOptions.PageSize = setting.Git.CommitsRangeSize
}
sha := ctx.FormString("sha")
path := ctx.FormString("path")
not := ctx.FormString("not")
var (
commitsCountTotal int64
commits []*git.Commit
err error
)
if len(path) == 0 {
var baseCommit *git.Commit
if len(sha) == 0 {
// no sha supplied - use default branch
baseCommit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
ctx.APIErrorInternal(err)
return
}
} else {
// get commit specified by sha
baseCommit, err = ctx.Repo.GitRepo.GetCommit(sha)
if err != nil {
ctx.NotFoundOrServerError(err)
return
}
}
// Total commit count
commitsCountTotal, err = gitrepo.CommitsCount(ctx, ctx.Repo.Repository, gitrepo.CommitsCountOptions{
Not: not,
Revision: []string{baseCommit.ID.String()},
Since: since,
Until: until,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
// Query commits
commits, err = baseCommit.CommitsByRange(listOptions.Page, listOptions.PageSize, not, since, until)
if err != nil {
ctx.APIErrorInternal(err)
return
}
} else {
if len(sha) == 0 {
sha = ctx.Repo.Repository.DefaultBranch
}
commitsCountTotal, err = gitrepo.CommitsCount(ctx, ctx.Repo.Repository,
gitrepo.CommitsCountOptions{
Not: not,
Revision: []string{sha},
RelPath: []string{path},
Since: since,
Until: until,
})
if err != nil {
ctx.APIErrorInternal(err)
return
} else if commitsCountTotal == 0 {
ctx.APIErrorNotFound("FileCommitsCount", nil)
return
}
commits, err = ctx.Repo.GitRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
Revision: sha,
File: path,
Not: not,
Page: listOptions.Page,
Since: since,
Until: until,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
pageCount := int(math.Ceil(float64(commitsCountTotal) / float64(listOptions.PageSize)))
userCache := make(map[string]*user_model.User)
apiCommits := make([]*api.Commit, len(commits))
for i, commit := range commits {
// Create json struct
apiCommits[i], err = convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, commit, userCache, convert.ParseCommitOptions(ctx))
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
ctx.SetLinkHeader(commitsCountTotal, listOptions.PageSize)
ctx.SetTotalCountHeader(commitsCountTotal)
// kept for backwards compatibility
ctx.RespHeader().Set("X-Page", strconv.Itoa(listOptions.Page))
ctx.RespHeader().Set("X-PerPage", strconv.Itoa(listOptions.PageSize))
ctx.RespHeader().Set("X-Total", strconv.FormatInt(commitsCountTotal, 10))
ctx.RespHeader().Set("X-PageCount", strconv.Itoa(pageCount))
ctx.RespHeader().Set("X-HasMore", strconv.FormatBool(listOptions.Page < pageCount))
ctx.AppendAccessControlExposeHeaders("X-Page", "X-PerPage", "X-Total", "X-PageCount", "X-HasMore")
ctx.JSON(http.StatusOK, &apiCommits)
}
// DownloadCommitDiffOrPatch render a commit's raw diff or patch
func DownloadCommitDiffOrPatch(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/git/commits/{sha}.{diffType} repository repoDownloadCommitDiffOrPatch
// ---
// summary: Get a commit's diff or patch
// produces:
// - text/plain
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: sha
// in: path
// description: SHA of the commit to get
// type: string
// required: true
// - name: diffType
// in: path
// description: whether the output is diff or patch
// type: string
// enum: [diff, patch]
// required: true
// responses:
// "200":
// "$ref": "#/responses/string"
// "404":
// "$ref": "#/responses/notFound"
sha := ctx.PathParam("sha")
diffType := git.RawDiffType(ctx.PathParam("diffType"))
if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, diffType, ctx.Resp); err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorNotFound("commit doesn't exist: " + sha)
return
}
ctx.APIErrorInternal(err)
return
}
}
// GetCommitPullRequest returns the merged pull request of the commit
func GetCommitPullRequest(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/commits/{sha}/pull repository repoGetCommitPullRequest
// ---
// summary: Get the merged pull request of the commit
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: sha
// in: path
// description: SHA of the commit to get
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/PullRequest"
// "404":
// "$ref": "#/responses/notFound"
pr, err := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, ctx.PathParam("sha"))
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
}
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
}
+90
View File
@@ -0,0 +1,90 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
user_model "gitea.dev/models/user"
"gitea.dev/modules/gitrepo"
api "gitea.dev/modules/structs"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// CompareDiff compare two branches or commits
func CompareDiff(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/compare/{basehead} repository repoCompareDiff
// ---
// summary: Get commit comparison information
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: basehead
// in: path
// description: compare two branches or commits
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Compare"
// "404":
// "$ref": "#/responses/notFound"
if ctx.Repo.GitRepo == nil {
var err error
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
compareInfo, closer := parseCompareInfo(ctx, ctx.PathParam("*"))
if ctx.Written() {
return
}
defer closer()
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
files := ctx.FormString("files") == "" || ctx.FormBool("files")
apiCommits := make([]*api.Commit, 0, len(compareInfo.Commits))
userCache := make(map[string]*user_model.User)
for i := 0; i < len(compareInfo.Commits); i++ {
apiCommit, err := convert.ToCommit(
ctx,
compareInfo.HeadRepo,
compareInfo.HeadGitRepo,
compareInfo.Commits[i],
userCache,
convert.ToCommitOptions{
Stat: true,
Verification: verification,
Files: files,
},
)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiCommits = append(apiCommits, apiCommit)
}
ctx.JSON(http.StatusOK, &api.Compare{
TotalCommits: len(compareInfo.Commits),
Commits: apiCommits,
})
}
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/util"
"gitea.dev/services/context"
archiver_service "gitea.dev/services/repository/archiver"
)
func serveRepoArchive(ctx *context.APIContext, reqFileName string, paths []string) {
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, reqFileName, paths)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
err = archiver_service.ServeRepoArchive(ctx.Base, aReq)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
} else {
ctx.APIErrorInternal(err)
}
}
}
// DownloadArchive is the GitHub-compatible endpoint to download repository archives
// TODO: The API document is missing: Add github compatible tarball download API endpoints (#32572)
func DownloadArchive(ctx *context.APIContext) {
var tp repo_model.ArchiveType
switch ballType := ctx.PathParam("ball_type"); ballType {
case "tarball":
tp = repo_model.ArchiveTarGz
case "zipball":
tp = repo_model.ArchiveZip
case "bundle":
tp = repo_model.ArchiveBundle
default:
ctx.APIError(http.StatusBadRequest, "Unknown archive type: "+ballType)
return
}
serveRepoArchive(ctx, ctx.PathParam("*")+"."+tp.String(), ctx.FormStrings("path"))
}
+970
View File
@@ -0,0 +1,970 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
git_model "gitea.dev/models/git"
"gitea.dev/modules/git"
"gitea.dev/modules/httpcache"
"gitea.dev/modules/httplib"
"gitea.dev/modules/json"
"gitea.dev/modules/lfs"
"gitea.dev/modules/setting"
"gitea.dev/modules/storage"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/routers/common"
"gitea.dev/services/context"
pull_service "gitea.dev/services/pull"
files_service "gitea.dev/services/repository/files"
)
const giteaObjectTypeHeader = "X-Gitea-Object-Type"
// GetRawFile get a file by path on a repository
func GetRawFile(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/raw/{filepath} repository repoGetRawFile
// ---
// summary: Get a file from a repository
// produces:
// - application/octet-stream
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
// description: path of the file to get, it should be "{ref}/{filepath}". If there is no ref could be inferred, it will be treated as the default branch
// type: string
// required: true
// - name: ref
// in: query
// description: "The name of the commit/branch/tag. Default to the repositorys default branch"
// type: string
// required: false
// responses:
// 200:
// description: Returns raw file content.
// schema:
// type: file
// "404":
// "$ref": "#/responses/notFound"
if ctx.Repo.Repository.IsEmpty {
ctx.APIErrorNotFound()
return
}
blob, entry, lastModified := getBlobForEntry(ctx)
if ctx.Written() {
return
}
ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil {
ctx.APIErrorInternal(err)
}
}
// GetRawFileOrLFS get a file by repo's path, redirecting to LFS if necessary.
func GetRawFileOrLFS(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/media/{filepath} repository repoGetRawFileOrLFS
// ---
// summary: Get a file or it's LFS object from a repository
// produces:
// - application/octet-stream
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
// description: path of the file to get, it should be "{ref}/{filepath}". If there is no ref could be inferred, it will be treated as the default branch
// type: string
// required: true
// - name: ref
// in: query
// description: "The name of the commit/branch/tag. Default to the repositorys default branch"
// type: string
// required: false
// responses:
// 200:
// description: Returns raw file content.
// schema:
// type: file
// "404":
// "$ref": "#/responses/notFound"
if ctx.Repo.Repository.IsEmpty {
ctx.APIErrorNotFound()
return
}
blob, entry, lastModified := getBlobForEntry(ctx)
if ctx.Written() {
return
}
ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
// LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file
if blob.Size() > lfs.MetaFileMaxSize {
// First handle caching for the blob
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
return
}
// If not cached - serve!
if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil {
ctx.APIErrorInternal(err)
}
return
}
// OK, now the blob is known to have at most 1024 (lfs pointer max size) bytes,
// we can simply read this in one go (This saves reading it twice)
lfsPointerBuf, err := blob.GetBlobBytes(lfs.MetaFileMaxSize)
if err != nil {
ctx.APIErrorInternal(err)
return
}
// Check if the blob represents a pointer
pointer, _ := lfs.ReadPointerFromBuffer(lfsPointerBuf)
// if it's not a pointer, just serve the data directly
if !pointer.IsValid() {
_, _ = ctx.Resp.Write(lfsPointerBuf)
return
}
// Now check if there is a MetaObject for this pointer
meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid)
// If there isn't one, just serve the data directly
if errors.Is(err, git_model.ErrLFSObjectNotExist) {
_, _ = ctx.Resp.Write(lfsPointerBuf)
return
} else if err != nil {
ctx.APIErrorInternal(err)
return
}
// Handle caching for the LFS object OID
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`, meta.UpdatedUnix.AsTimePtr()) {
return
}
if setting.LFS.Storage.ServeDirect() {
// If we have a signed url (S3, object storage), redirect to this directly.
u, err := storage.LFS.ServeDirectURL(pointer.RelativePath(), blob.Name(), ctx.Req.Method, nil)
if u != nil && err == nil {
ctx.Redirect(u.String())
return
}
}
lfsDataFile, err := lfs.ReadMetaObject(meta.Pointer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
defer lfsDataFile.Close()
httplib.ServeUserContentByFile(ctx.Base.Req, ctx.Base.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: ctx.Repo.TreePath})
}
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) {
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return nil, nil, nil
}
if entry.IsDir() || entry.IsSubModule() {
ctx.APIErrorNotFound("getBlobForEntry", nil)
return nil, nil, nil
}
latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil, nil
}
when := &latestCommit.Committer.When
return entry.Blob(), entry, when
}
// GetArchive get archive of a repository
func GetArchive(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/archive/{archive} repository repoGetArchive
// ---
// summary: Get an archive of a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: archive
// in: path
// description: the git reference for download with attached archive format (e.g. master.zip)
// type: string
// required: true
// - name: path
// in: query
// type: array
// items:
// type: string
// description: subpath of the repository to download
// collectionFormat: multi
// responses:
// 200:
// description: success
// "404":
// "$ref": "#/responses/notFound"
serveRepoArchive(ctx, ctx.PathParam("*"), ctx.FormStrings("path"))
}
// GetEditorconfig get editor config of a repository
func GetEditorconfig(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/editorconfig/{filepath} repository repoGetEditorConfig
// ---
// summary: Get the EditorConfig definitions of a file in a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
// description: filepath of file to get
// type: string
// required: true
// - name: ref
// in: query
// description: "The name of the commit/branch/tag. Default to the repositorys default branch."
// type: string
// required: false
// responses:
// 200:
// description: success
// "404":
// "$ref": "#/responses/notFound"
ec, _, err := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
if err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
fileName := ctx.PathParam("filename")
def, err := ec.GetDefinitionForFilename(fileName)
if def == nil {
ctx.APIErrorNotFound(err)
return
}
ctx.JSON(http.StatusOK, def)
}
func base64Reader(s string) (io.ReadSeeker, error) {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return nil, err
}
return bytes.NewReader(b), nil
}
func ReqChangeRepoFileOptionsAndCheck(ctx *context.APIContext) {
commonOpts := web.GetForm(ctx).(api.FileOptionsInterface).GetFileOptions()
commonOpts.BranchName = util.IfZero(commonOpts.BranchName, ctx.Repo.Repository.DefaultBranch)
commonOpts.NewBranchName = util.IfZero(commonOpts.NewBranchName, commonOpts.BranchName)
if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, commonOpts.NewBranchName) && !ctx.IsUserSiteAdmin() {
ctx.APIError(http.StatusForbidden, "user should have a permission to write to the target branch")
return
}
changeFileOpts := &files_service.ChangeRepoFilesOptions{
Message: commonOpts.Message,
OldBranch: commonOpts.BranchName,
NewBranch: commonOpts.NewBranchName,
ForcePush: commonOpts.ForcePush,
Committer: &files_service.IdentityOptions{
GitUserName: commonOpts.Committer.Name,
GitUserEmail: commonOpts.Committer.Email,
},
Author: &files_service.IdentityOptions{
GitUserName: commonOpts.Author.Name,
GitUserEmail: commonOpts.Author.Email,
},
Dates: &files_service.CommitDateOptions{
Author: commonOpts.Dates.Author,
Committer: commonOpts.Dates.Committer,
},
Signoff: commonOpts.Signoff,
}
if changeFileOpts.Dates.Author.IsZero() {
changeFileOpts.Dates.Author = time.Now()
}
if changeFileOpts.Dates.Committer.IsZero() {
changeFileOpts.Dates.Committer = time.Now()
}
ctx.Data["__APIChangeRepoFilesOptions"] = changeFileOpts
}
func getAPIChangeRepoFileOptions[T api.FileOptionsInterface](ctx *context.APIContext) (apiOpts T, opts *files_service.ChangeRepoFilesOptions) {
return web.GetForm(ctx).(T), ctx.Data["__APIChangeRepoFilesOptions"].(*files_service.ChangeRepoFilesOptions)
}
// ChangeFiles handles API call for modifying multiple files
func ChangeFiles(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/contents repository repoChangeFiles
// ---
// summary: Modify multiple files in a repository
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/ChangeFilesOptions"
// responses:
// "201":
// "$ref": "#/responses/FilesResponse"
// "403":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/error"
// "423":
// "$ref": "#/responses/repoArchivedError"
apiOpts, opts := getAPIChangeRepoFileOptions[*api.ChangeFilesOptions](ctx)
if ctx.Written() {
return
}
for _, file := range apiOpts.Files {
contentReader, err := base64Reader(file.ContentBase64)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
// FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options
// But the LastCommitID is not provided in the API options, need to fully fix them in API
changeRepoFile := &files_service.ChangeRepoFile{
Operation: file.Operation,
TreePath: file.Path,
FromTreePath: file.FromPath,
ContentReader: contentReader,
SHA: file.SHA,
}
opts.Files = append(opts.Files, changeRepoFile)
}
if opts.Message == "" {
opts.Message = changeFilesCommitMessage(ctx, opts.Files)
}
if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
handleChangeRepoFilesError(ctx, err)
} else {
ctx.JSON(http.StatusCreated, filesResponse)
}
}
// CreateFile handles API call for creating a file
func CreateFile(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
// ---
// summary: Create a file in a repository
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
// description: path of the file to create
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/CreateFileOptions"
// responses:
// "201":
// "$ref": "#/responses/FileResponse"
// "403":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/error"
// "423":
// "$ref": "#/responses/repoArchivedError"
apiOpts, opts := getAPIChangeRepoFileOptions[*api.CreateFileOptions](ctx)
if ctx.Written() {
return
}
contentReader, err := base64Reader(apiOpts.ContentBase64)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
Operation: "create",
TreePath: ctx.PathParam("*"),
ContentReader: contentReader,
})
if opts.Message == "" {
opts.Message = changeFilesCommitMessage(ctx, opts.Files)
}
if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
handleChangeRepoFilesError(ctx, err)
} else {
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
ctx.JSON(http.StatusCreated, fileResponse)
}
}
// UpdateFile handles API call for updating a file
func UpdateFile(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
// ---
// summary: Update a file in a repository if SHA is set, or create the file if SHA is not set
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
// description: path of the file to update
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/UpdateFileOptions"
// responses:
// "200":
// "$ref": "#/responses/FileResponse"
// "201":
// "$ref": "#/responses/FileResponse"
// "403":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/error"
// "423":
// "$ref": "#/responses/repoArchivedError"
apiOpts, opts := getAPIChangeRepoFileOptions[*api.UpdateFileOptions](ctx)
if ctx.Written() {
return
}
contentReader, err := base64Reader(apiOpts.ContentBase64)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
willCreate := apiOpts.SHA == ""
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
Operation: util.Iif(willCreate, "create", "update"),
ContentReader: contentReader,
SHA: apiOpts.SHA,
FromTreePath: apiOpts.FromPath,
TreePath: ctx.PathParam("*"),
})
if opts.Message == "" {
opts.Message = changeFilesCommitMessage(ctx, opts.Files)
}
if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
handleChangeRepoFilesError(ctx, err)
} else {
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
ctx.JSON(util.Iif(willCreate, http.StatusCreated, http.StatusOK), fileResponse)
}
}
func handleChangeRepoFilesError(ctx *context.APIContext, err error) {
if git.IsErrPushRejected(err) {
err := err.(*git.ErrPushRejected)
ctx.APIError(http.StatusForbidden, err.Message)
return
}
if files_service.IsErrUserCannotCommit(err) || pull_service.IsErrFilePathProtected(err) {
ctx.APIError(http.StatusForbidden, err)
return
}
if git_model.IsErrBranchAlreadyExists(err) || files_service.IsErrFilenameInvalid(err) || pull_service.IsErrSHADoesNotMatch(err) ||
files_service.IsErrFilePathInvalid(err) || files_service.IsErrRepoFileAlreadyExists(err) ||
files_service.IsErrCommitIDDoesNotMatch(err) || files_service.IsErrSHAOrCommitIDNotProvided(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
return
}
ctx.APIErrorInternal(err)
}
// format commit message if empty
func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.ChangeRepoFile) string {
var (
createFiles []string
updateFiles []string
deleteFiles []string
)
for _, file := range files {
switch file.Operation {
case "create":
createFiles = append(createFiles, file.TreePath)
case "update", "upload", "rename": // upload and rename works like "update", there is no translation for them at the moment
updateFiles = append(updateFiles, file.TreePath)
case "delete":
deleteFiles = append(deleteFiles, file.TreePath)
}
}
message := ""
if len(createFiles) != 0 {
message += ctx.Locale.TrString("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
}
if len(updateFiles) != 0 {
message += ctx.Locale.TrString("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
}
if len(deleteFiles) != 0 {
message += ctx.Locale.TrString("repo.editor.delete", strings.Join(deleteFiles, ", "))
}
return strings.Trim(message, "\n")
}
// DeleteFile Delete a file in a repository
func DeleteFile(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/contents/{filepath} repository repoDeleteFile
// ---
// summary: Delete a file in a repository
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
// description: path of the file to delete
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/DeleteFileOptions"
// responses:
// "200":
// "$ref": "#/responses/FileDeleteResponse"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/error"
// "423":
// "$ref": "#/responses/repoArchivedError"
apiOpts, opts := getAPIChangeRepoFileOptions[*api.DeleteFileOptions](ctx)
if ctx.Written() {
return
}
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
Operation: "delete",
SHA: apiOpts.SHA,
TreePath: ctx.PathParam("*"),
})
if opts.Message == "" {
opts.Message = changeFilesCommitMessage(ctx, opts.Files)
}
if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
handleChangeRepoFilesError(ctx, err)
} else {
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
}
}
func resolveRefCommit(ctx *context.APIContext, ref string, minCommitIDLen ...int) *utils.RefCommit {
ref = util.IfZero(ref, ctx.Repo.Repository.DefaultBranch)
refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ref, minCommitIDLen...)
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound(err)
} else if err != nil {
ctx.APIErrorInternal(err)
}
return refCommit
}
func GetContentsExt(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/contents-ext/{filepath} repository repoGetContentsExt
// ---
// summary: The extended "contents" API, to get file metadata and/or content, or list a directory.
// description: It guarantees that only one of the response fields is set if the request succeeds.
// Users can pass "includes=file_content" or "includes=lfs_metadata" to retrieve more fields.
// "includes=file_content" only works for single file, if you need to retrieve file contents in batch,
// use "file-contents" API after listing the directory.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
// description: path of the dir, file, symlink or submodule in the repo. Swagger requires path parameter to be "required",
// you can leave it empty or pass a single dot (".") to get the root directory.
// type: string
// required: true
// - name: ref
// in: query
// description: the name of the commit/branch/tag, default to the repositorys default branch.
// type: string
// required: false
// - name: includes
// in: query
// description: By default this API's response only contains file's metadata. Use comma-separated "includes" options to retrieve more fields.
// Option "file_content" will try to retrieve the file content, "lfs_metadata" will try to retrieve LFS metadata,
// "commit_metadata" will try to retrieve commit metadata, and "commit_message" will try to retrieve commit message.
// type: string
// required: false
// responses:
// "200":
// "$ref": "#/responses/ContentsExtResponse"
// "404":
// "$ref": "#/responses/notFound"
if treePath := ctx.PathParam("*"); treePath == "." || treePath == "/" {
ctx.SetPathParam("*", "") // workaround for swagger, it requires path parameter to be "required", but we need to list root directory
}
opts := files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*")}
for includeOpt := range strings.SplitSeq(ctx.FormString("includes"), ",") {
if includeOpt == "" {
continue
}
switch includeOpt {
case "file_content":
opts.IncludeSingleFileContent = true
case "lfs_metadata":
opts.IncludeLfsMetadata = true
case "commit_metadata":
opts.IncludeCommitMetadata = true
case "commit_message":
opts.IncludeCommitMessage = true
default:
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("unknown include option %q", includeOpt))
return
}
}
ctx.JSON(http.StatusOK, getRepoContents(ctx, opts))
}
func GetContents(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents
// ---
// summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir.
// description: This API follows GitHub's design, and it is not easy to use. Recommend users to use the "contents-ext" API instead.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: filepath
// in: path
// description: path of the dir, file, symlink or submodule in the repo
// type: string
// required: true
// - name: ref
// in: query
// description: "The name of the commit/branch/tag. Default to the repositorys default branch."
// type: string
// required: false
// responses:
// "200":
// "$ref": "#/responses/ContentsResponse"
// "404":
// "$ref": "#/responses/notFound"
ret := getRepoContents(ctx, files_service.GetContentsOrListOptions{
TreePath: ctx.PathParam("*"),
IncludeSingleFileContent: true,
IncludeCommitMetadata: true,
})
if ctx.Written() {
return
}
ctx.JSON(http.StatusOK, util.Iif[any](ret.FileContents != nil, ret.FileContents, ret.DirContents))
}
func getRepoContents(ctx *context.APIContext, opts files_service.GetContentsOrListOptions) *api.ContentsExtResponse {
refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref"))
if ctx.Written() {
return nil
}
ret, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts)
if err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorNotFound("GetContentsOrList", err)
return nil
}
ctx.APIErrorInternal(err)
}
return &ret
}
func GetContentsList(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList
// ---
// summary: Gets the metadata of all the entries of the root dir.
// description: This API follows GitHub's design, and it is not easy to use. Recommend users to use our "contents-ext" API instead.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: ref
// in: query
// description: "The name of the commit/branch/tag. Default to the repositorys default branch."
// type: string
// required: false
// responses:
// "200":
// "$ref": "#/responses/ContentsListResponse"
// "404":
// "$ref": "#/responses/notFound"
// same as GetContents(), this function is here because swagger fails if path is empty in GetContents() interface
GetContents(ctx)
}
func GetFileContentsGet(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/file-contents repository repoGetFileContents
// ---
// summary: Get the metadata and contents of requested files
// description: See the POST method. This GET method supports using JSON encoded request body in query parameter.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: ref
// in: query
// description: "The name of the commit/branch/tag. Default to the repositorys default branch."
// type: string
// required: false
// - name: body
// in: query
// description: "The JSON encoded body (see the POST request): {\"files\": [\"filename1\", \"filename2\"]}"
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/ContentsListResponse"
// "404":
// "$ref": "#/responses/notFound"
// The POST method requires "write" permission, so we also support this "GET" method
handleGetFileContents(ctx)
}
func GetFileContentsPost(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/file-contents repository repoGetFileContentsPost
// ---
// summary: Get the metadata and contents of requested files
// description: Uses automatic pagination based on default page size and
// max response size and returns the maximum allowed number of files.
// Files which could not be retrieved are null. Files which are too large
// are being returned with `encoding == null`, `content == null` and `size > 0`,
// they can be requested separately by using the `download_url`.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: ref
// in: query
// description: "The name of the commit/branch/tag. Default to the repositorys default branch."
// type: string
// required: false
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/GetFilesOptions"
// responses:
// "200":
// "$ref": "#/responses/ContentsListResponse"
// "404":
// "$ref": "#/responses/notFound"
// This is actually a "read" request, but we need to accept a "files" list, then POST method seems easy to use.
// But the permission system requires that the caller must have "write" permission to use POST method.
// At the moment, there is no other way to get around the permission check, so there is a "GET" workaround method above.
handleGetFileContents(ctx)
}
func handleGetFileContents(ctx *context.APIContext) {
opts, ok := web.GetForm(ctx).(*api.GetFilesOptions)
if !ok {
err := json.Unmarshal(util.UnsafeStringToBytes(ctx.FormString("body")), &opts)
if err != nil {
ctx.APIError(http.StatusBadRequest, "invalid body parameter")
return
}
}
refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref"))
if ctx.Written() {
return
}
filesResponse := files_service.GetContentsListFromTreePaths(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts.Files)
ctx.JSON(http.StatusOK, util.SliceNilAsEmpty(filesResponse))
}
+179
View File
@@ -0,0 +1,179 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/optional"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
repo_service "gitea.dev/services/repository"
)
// ListForks list a repository's forks
func ListForks(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/forks repository listForks
// ---
// summary: List a repository's forks
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/RepositoryList"
// "404":
// "$ref": "#/responses/notFound"
forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err := repo_model.RepositoryList(forks).LoadOwners(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if err := repo_model.RepositoryList(forks).LoadUnits(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
apiForks := make([]*api.Repository, len(forks))
for i, fork := range forks {
permission, err := access_model.GetDoerRepoPermission(ctx, fork, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiForks[i] = convert.ToRepo(ctx, fork, permission)
}
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, apiForks)
}
func prepareDoerCreateRepoInOrg(ctx *context.APIContext, orgName string) *organization.Organization {
org, err := organization.GetOrgByName(ctx, orgName)
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound()
return nil
} else if err != nil {
ctx.APIErrorInternal(err)
return nil
}
if !organization.HasOrgOrUserVisible(ctx, org.AsUser(), ctx.Doer) {
ctx.APIErrorNotFound()
return nil
}
if !ctx.Doer.IsAdmin {
canCreate, err := org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return nil
}
if !canCreate {
ctx.APIError(http.StatusForbidden, "User is not allowed to create repositories in this organization.")
return nil
}
}
return org
}
// CreateFork create a fork of a repo
func CreateFork(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/forks repository createFork
// ---
// summary: Fork a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo to fork
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo to fork
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateForkOption"
// responses:
// "202":
// "$ref": "#/responses/Repository"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// description: The repository with the same name already exists.
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateForkOption)
forkOwner := ctx.Doer // user/org that will own the fork
if form.Organization != nil {
org := prepareDoerCreateRepoInOrg(ctx, *form.Organization)
if ctx.Written() {
return
}
forkOwner = org.AsUser()
}
repo := ctx.Repo.Repository
name := optional.FromPtr(form.Name).ValueOrDefault(repo.Name)
fork, err := repo_service.ForkRepository(ctx, ctx.Doer, forkOwner, repo_service.ForkRepoOptions{
BaseRepo: repo,
Name: name,
Description: repo.Description,
})
if err != nil {
if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
ctx.APIError(http.StatusConflict, err)
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
// TODO change back to 201
ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, fork, access_model.Permission{AccessMode: perm.AccessModeOwner}))
}
+197
View File
@@ -0,0 +1,197 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
"gitea.dev/modules/git"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListGitHooks list all Git hooks of a repository
func ListGitHooks(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/hooks/git repository repoListGitHooks
// ---
// summary: List the Git hooks in a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/GitHookList"
// "404":
// "$ref": "#/responses/notFound"
hooks, err := ctx.Repo.GitRepo.Hooks()
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiHooks := make([]*api.GitHook, len(hooks))
for i := range hooks {
apiHooks[i] = convert.ToGitHook(hooks[i])
}
ctx.JSON(http.StatusOK, &apiHooks)
}
// GetGitHook get a repo's Git hook by id
func GetGitHook(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/hooks/git/{id} repository repoGetGitHook
// ---
// summary: Get a Git hook
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the hook to get
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/GitHook"
// "404":
// "$ref": "#/responses/notFound"
hookID := ctx.PathParam("id")
hook, err := ctx.Repo.GitRepo.GetHook(hookID)
if err != nil {
if errors.Is(err, git.ErrNotValidHook) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.JSON(http.StatusOK, convert.ToGitHook(hook))
}
// EditGitHook modify a Git hook of a repository
func EditGitHook(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/hooks/git/{id} repository repoEditGitHook
// ---
// summary: Edit a Git hook in a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the hook to get
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditGitHookOption"
// responses:
// "200":
// "$ref": "#/responses/GitHook"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditGitHookOption)
hookID := ctx.PathParam("id")
hook, err := ctx.Repo.GitRepo.GetHook(hookID)
if err != nil {
if errors.Is(err, git.ErrNotValidHook) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
hook.Content = form.Content
if err = hook.Update(); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToGitHook(hook))
}
// DeleteGitHook delete a Git hook of a repository
func DeleteGitHook(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/hooks/git/{id} repository repoDeleteGitHook
// ---
// summary: Delete a Git hook in a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the hook to get
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
hookID := ctx.PathParam("id")
hook, err := ctx.Repo.GitRepo.GetHook(hookID)
if err != nil {
if errors.Is(err, git.ErrNotValidHook) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
hook.Content = ""
if err = hook.Update(); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+108
View File
@@ -0,0 +1,108 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"fmt"
"net/http"
"net/url"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
)
// GetGitAllRefs get ref or an list all the refs of a repository
func GetGitAllRefs(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/git/refs repository repoListAllGitRefs
// ---
// summary: Get specified ref or filtered repository's refs
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// # "$ref": "#/responses/Reference" TODO: swagger doesn't support different output formats by ref
// "$ref": "#/responses/ReferenceList"
// "404":
// "$ref": "#/responses/notFound"
getGitRefsInternal(ctx, "")
}
// GetGitRefs get ref or an filteresd list of refs of a repository
func GetGitRefs(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/git/refs/{ref} repository repoListGitRefs
// ---
// summary: Get specified ref or filtered repository's refs
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: ref
// in: path
// description: part or full name of the ref
// type: string
// required: true
// responses:
// "200":
// # "$ref": "#/responses/Reference" TODO: swagger doesn't support different output formats by ref
// "$ref": "#/responses/ReferenceList"
// "404":
// "$ref": "#/responses/notFound"
getGitRefsInternal(ctx, ctx.PathParam("*"))
}
func getGitRefsInternal(ctx *context.APIContext, filter string) {
refs, lastMethodName, err := utils.GetGitRefs(ctx, filter)
if err != nil {
ctx.APIErrorInternal(fmt.Errorf("%s: %w", lastMethodName, err))
return
}
if len(refs) == 0 {
ctx.APIErrorNotFound()
return
}
apiRefs := make([]*api.Reference, len(refs))
for i := range refs {
apiRefs[i] = &api.Reference{
Ref: refs[i].Name,
URL: ctx.Repo.Repository.APIURL() + "/git/" + util.PathEscapeSegments(refs[i].Name),
Object: &api.GitObject{
SHA: refs[i].Object.String(),
Type: refs[i].Type,
URL: ctx.Repo.Repository.APIURL() + "/git/" + url.PathEscape(refs[i].Type) + "s/" + url.PathEscape(refs[i].Object.String()),
},
}
}
// If single reference is found and it matches filter exactly return it as object
if len(apiRefs) == 1 && apiRefs[0].Ref == filter {
ctx.JSON(http.StatusOK, &apiRefs[0])
return
}
ctx.JSON(http.StatusOK, &apiRefs)
}
+308
View File
@@ -0,0 +1,308 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"gitea.dev/models/db"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
"gitea.dev/models/webhook"
"gitea.dev/modules/git"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
webhook_module "gitea.dev/modules/webhook"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
webhook_service "gitea.dev/services/webhook"
)
// ListHooks list all hooks of a repository
func ListHooks(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/hooks repository repoListHooks
// ---
// summary: List the hooks in a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/HookList"
// "404":
// "$ref": "#/responses/notFound"
opts := &webhook.ListWebhookOptions{
ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID,
}
hooks, count, err := db.FindAndCount[webhook.Webhook](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiHooks := make([]*api.Hook, len(hooks))
for i := range hooks {
apiHooks[i], err = webhook_service.ToHook(ctx.Repo.RepoLink, hooks[i])
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, &apiHooks)
}
// GetHook get a repo's hook by id
func GetHook(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/hooks/{id} repository repoGetHook
// ---
// summary: Get a hook
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the hook to get
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Hook"
// "404":
// "$ref": "#/responses/notFound"
repo := ctx.Repo
hookID := ctx.PathParamInt64("id")
hook, err := utils.GetRepoHook(ctx, repo.Repository.ID, hookID)
if err != nil {
return
}
apiHook, err := webhook_service.ToHook(repo.RepoLink, hook)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiHook)
}
// TestHook tests a hook
func TestHook(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/hooks/{id}/tests repository repoTestHook
// ---
// summary: Test a push webhook
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the hook to test
// type: integer
// format: int64
// required: true
// - name: ref
// in: query
// description: "The name of the commit/branch/tag, indicates which commit will be loaded to the webhook payload."
// type: string
// required: false
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
if ctx.Repo.Commit == nil {
// if repo does not have any commits, then don't send a webhook
ctx.Status(http.StatusNoContent)
return
}
ref := git.BranchPrefix + ctx.Repo.Repository.DefaultBranch
if r := ctx.FormTrim("ref"); r != "" {
ref = r
}
hookID := ctx.PathParamInt64("id")
hook, err := utils.GetRepoHook(ctx, ctx.Repo.Repository.ID, hookID)
if err != nil {
return
}
commit := convert.ToPayloadCommit(ctx, ctx.Repo.Repository, ctx.Repo.Commit)
commitID := ctx.Repo.Commit.ID.String()
if err := webhook_service.PrepareWebhook(ctx, hook, webhook_module.HookEventPush, &api.PushPayload{
Ref: ref,
Before: commitID,
After: commitID,
CompareURL: setting.AppURL + ctx.Repo.Repository.ComposeCompareURL(commitID, commitID),
Commits: []*api.PayloadCommit{commit},
TotalCommits: 1,
HeadCommit: commit,
Repo: convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeNone}),
Pusher: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone),
Sender: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone),
}); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// CreateHook create a hook for a repository
func CreateHook(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/hooks repository repoCreateHook
// ---
// summary: Create a hook
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateHookOption"
// responses:
// "201":
// "$ref": "#/responses/Hook"
// "404":
// "$ref": "#/responses/notFound"
utils.AddRepoHook(ctx, web.GetForm(ctx).(*api.CreateHookOption))
}
// EditHook modify a hook of a repository
func EditHook(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/hooks/{id} repository repoEditHook
// ---
// summary: Edit a hook in a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: index of the hook
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditHookOption"
// responses:
// "200":
// "$ref": "#/responses/Hook"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditHookOption)
hookID := ctx.PathParamInt64("id")
utils.EditRepoHook(ctx, form, hookID)
}
// DeleteHook delete a hook of a repository
func DeleteHook(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/hooks/{id} repository repoDeleteHook
// ---
// summary: Delete a hook in a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the hook to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
if err := webhook.DeleteWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")); err != nil {
if webhook.IsErrWebhookNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"strconv"
"testing"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
"gitea.dev/models/webhook"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
func TestTestHook(t *testing.T) {
unittest.PrepareTestEnv(t)
hook := &webhook.Webhook{
RepoID: 1,
URL: "https://www.example.com/test_hook",
ContentType: webhook.ContentTypeJSON,
Events: `{"push_only":true}`,
IsActive: true,
}
assert.NoError(t, db.Insert(t.Context(), hook))
ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/wiki/_pages")
ctx.SetPathParam("id", strconv.FormatInt(hook.ID, 10))
contexttest.LoadRepo(t, ctx, 1)
contexttest.LoadRepoCommit(t, ctx)
contexttest.LoadUser(t, ctx, 2)
TestHook(ctx)
assert.Equal(t, http.StatusNoContent, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &webhook.HookTask{
HookID: hook.ID,
}, unittest.Cond("is_delivered=?", false))
}
File diff suppressed because it is too large Load Diff
+399
View File
@@ -0,0 +1,399 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
attachment_service "gitea.dev/services/attachment"
"gitea.dev/services/context"
"gitea.dev/services/context/upload"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
)
// GetIssueAttachment gets a single attachment of the issue
func GetIssueAttachment(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueGetIssueAttachment
// ---
// summary: Get an issue attachment
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: attachment_id
// in: path
// description: id of the attachment to get
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Attachment"
// "404":
// "$ref": "#/responses/error"
issue := getIssueFromContext(ctx)
if issue == nil {
return
}
attach := getIssueAttachmentSafeRead(ctx, issue)
if attach == nil {
return
}
ctx.JSON(http.StatusOK, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
}
// ListIssueAttachments lists all attachments of the issue
func ListIssueAttachments(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assets issue issueListIssueAttachments
// ---
// summary: List issue's attachments
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/AttachmentList"
// "404":
// "$ref": "#/responses/error"
issue := getIssueFromContext(ctx)
if issue == nil {
return
}
if err := issue.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue).Attachments)
}
// CreateIssueAttachment creates an attachment and saves the given file
func CreateIssueAttachment(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/assets issue issueCreateIssueAttachment
// ---
// summary: Create an issue attachment
// produces:
// - application/json
// consumes:
// - multipart/form-data
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: name
// in: query
// description: name of the attachment
// type: string
// required: false
// - name: attachment
// in: formData
// description: attachment to upload
// type: file
// required: true
// responses:
// "201":
// "$ref": "#/responses/Attachment"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/error"
// "413":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
issue := getIssueFromContext(ctx)
if issue == nil {
return
}
if !canUserWriteIssueAttachment(ctx, issue) {
return
}
// Get uploaded file from request
file, header, err := ctx.Req.FormFile("attachment")
if err != nil {
ctx.APIErrorInternal(err)
return
}
defer file.Close()
filename := header.Filename
if query := ctx.FormString("name"); query != "" {
filename = query
}
uploaderFile := attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
attachment, err := attachment_service.UploadAttachmentForIssue(ctx, uploaderFile, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
IssueID: issue.ID,
})
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else if errors.Is(err, util.ErrContentTooLarge) {
ctx.APIError(http.StatusRequestEntityTooLarge, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
issue.Attachments = append(issue.Attachments, attachment)
if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, issue.Content, issue.ContentVersion); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attachment))
}
// EditIssueAttachment updates the given attachment
func EditIssueAttachment(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueEditIssueAttachment
// ---
// summary: Edit an issue attachment
// produces:
// - application/json
// consumes:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: attachment_id
// in: path
// description: id of the attachment to edit
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditAttachmentOptions"
// responses:
// "201":
// "$ref": "#/responses/Attachment"
// "404":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
attachment := getIssueAttachmentSafeWrite(ctx)
if attachment == nil {
return
}
// do changes to attachment. only meaningful change is name.
form := web.GetForm(ctx).(*api.EditAttachmentOptions)
if form.Name != "" {
attachment.Name = form.Name
}
if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attachment); err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attachment))
}
// DeleteIssueAttachment delete a given attachment
func DeleteIssueAttachment(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueDeleteIssueAttachment
// ---
// summary: Delete an issue attachment
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: attachment_id
// in: path
// description: id of the attachment to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/error"
// "423":
// "$ref": "#/responses/repoArchivedError"
attachment := getIssueAttachmentSafeWrite(ctx)
if attachment == nil {
return
}
if err := repo_model.DeleteAttachment(ctx, attachment, true); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
func getIssueFromContext(ctx *context.APIContext) *issues_model.Issue {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.NotFoundOrServerError(err)
return nil
}
issue.Repo = ctx.Repo.Repository
return issue
}
func getIssueAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment {
issue := getIssueFromContext(ctx)
if issue == nil {
return nil
}
if !canUserWriteIssueAttachment(ctx, issue) {
return nil
}
return getIssueAttachmentSafeRead(ctx, issue)
}
func getIssueAttachmentSafeRead(ctx *context.APIContext, issue *issues_model.Issue) *repo_model.Attachment {
attachment, err := repo_model.GetAttachmentByID(ctx, ctx.PathParamInt64("attachment_id"))
if err != nil {
ctx.NotFoundOrServerError(err)
return nil
}
if !attachmentBelongsToRepoOrIssue(ctx, attachment, issue) {
return nil
}
return attachment
}
func canUserWriteIssueAttachment(ctx *context.APIContext, issue *issues_model.Issue) bool {
canEditIssue := ctx.IsSigned && (ctx.Doer.ID == issue.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin() || ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull))
if !canEditIssue {
ctx.APIError(http.StatusForbidden, "user should have permission to write issue")
return false
}
return true
}
func attachmentBelongsToRepoOrIssue(ctx *context.APIContext, attachment *repo_model.Attachment, issue *issues_model.Issue) bool {
if attachment.RepoID != ctx.Repo.Repository.ID {
log.Debug("Requested attachment[%d] does not belong to repo[%-v].", attachment.ID, ctx.Repo.Repository)
ctx.APIErrorNotFound("no such attachment in repo")
return false
}
if attachment.IssueID == 0 {
log.Debug("Requested attachment[%d] is not in an issue.", attachment.ID)
ctx.APIErrorNotFound("no such attachment in issue")
return false
} else if issue != nil && attachment.IssueID != issue.ID {
log.Debug("Requested attachment[%d] does not belong to issue[%d, #%d].", attachment.ID, issue.ID, issue.Index)
ctx.APIErrorNotFound("no such attachment in issue")
return false
}
return true
}
+706
View File
@@ -0,0 +1,706 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
// SPDX-License-Identifier: MIT
package repo
import (
stdCtx "context"
"errors"
"net/http"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/optional"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
)
// ListIssueComments list all the comments of an issue
func ListIssueComments(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/comments issue issueGetComments
// ---
// summary: List all comments on an issue
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: since
// in: query
// description: if provided, only comments updated since the specified time are returned.
// type: string
// format: date-time
// - name: before
// in: query
// description: if provided, only comments updated before the provided time are returned.
// type: string
// format: date-time
// responses:
// "200":
// "$ref": "#/responses/CommentList"
// "404":
// "$ref": "#/responses/notFound"
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return
}
issue.Repo = ctx.Repo.Repository
opts := &issues_model.FindCommentsOptions{
IssueID: issue.ID,
Since: since,
Before: before,
Type: issues_model.CommentTypeComment,
}
comments, err := issues_model.FindComments(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
totalCount, err := issues_model.CountComments(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err := comments.LoadPosters(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if err := comments.LoadAttachments(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
apiComments := make([]*api.Comment, len(comments))
for i, comment := range comments {
comment.Issue = issue
apiComments[i] = convert.ToAPIComment(ctx, ctx.Repo.Repository, comments[i])
}
ctx.SetTotalCountHeader(totalCount)
ctx.JSON(http.StatusOK, &apiComments)
}
// ListIssueCommentsAndTimeline list all the comments and events of an issue
func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/timeline issue issueGetCommentsAndTimeline
// ---
// summary: List all comments and events on an issue
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: since
// in: query
// description: if provided, only comments updated since the specified time are returned.
// type: string
// format: date-time
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// - name: before
// in: query
// description: if provided, only comments updated before the provided time are returned.
// type: string
// format: date-time
// responses:
// "200":
// "$ref": "#/responses/TimelineList"
// "404":
// "$ref": "#/responses/notFound"
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
issue.Repo = ctx.Repo.Repository
opts := &issues_model.FindCommentsOptions{
ListOptions: utils.GetListOptions(ctx),
IssueID: issue.ID,
Since: since,
Before: before,
Type: issues_model.CommentTypeUndefined,
}
comments, err := issues_model.FindComments(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err := comments.LoadPosters(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
var apiComments []*api.TimelineComment
for _, comment := range comments {
if comment.Type != issues_model.CommentTypeCode && isXRefCommentAccessible(ctx, ctx.Doer, comment, issue.RepoID) {
comment.Issue = issue
apiComments = append(apiComments, convert.ToTimelineComment(ctx, issue.Repo, comment, ctx.Doer))
}
}
ctx.SetTotalCountHeader(int64(len(apiComments)))
ctx.JSON(http.StatusOK, &apiComments)
}
func isXRefCommentAccessible(ctx stdCtx.Context, user *user_model.User, c *issues_model.Comment, issueRepoID int64) bool {
// Remove comments that the user has no permissions to see
if issues_model.CommentTypeIsRef(c.Type) && c.RefRepoID != issueRepoID && c.RefRepoID != 0 {
var err error
// Set RefRepo for description in template
c.RefRepo, err = repo_model.GetRepositoryByID(ctx, c.RefRepoID)
if err != nil {
return false
}
perm, err := access_model.GetDoerRepoPermission(ctx, c.RefRepo, user)
if err != nil {
return false
}
if !perm.CanReadIssuesOrPulls(c.RefIsPull) {
return false
}
}
return true
}
// ListRepoIssueComments returns all issue-comments for a repo
func ListRepoIssueComments(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/comments issue issueGetRepoComments
// ---
// summary: List all comments in a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: since
// in: query
// description: if provided, only comments updated since the provided time are returned.
// type: string
// format: date-time
// - name: before
// in: query
// description: if provided, only comments updated before the provided time are returned.
// type: string
// format: date-time
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/CommentList"
// "404":
// "$ref": "#/responses/notFound"
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
var isPull optional.Option[bool]
canReadIssue := ctx.Repo.Permission.CanRead(unit.TypeIssues)
canReadPull := ctx.Repo.Permission.CanRead(unit.TypePullRequests)
if canReadIssue && canReadPull {
isPull = optional.None[bool]()
} else if canReadIssue {
isPull = optional.Some(false)
} else if canReadPull {
isPull = optional.Some(true)
} else {
ctx.APIErrorNotFound()
return
}
opts := &issues_model.FindCommentsOptions{
ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID,
Type: issues_model.CommentTypeComment,
Since: since,
Before: before,
IsPull: isPull,
}
comments, err := issues_model.FindComments(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
totalCount, err := issues_model.CountComments(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err = comments.LoadPosters(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
apiComments := make([]*api.Comment, len(comments))
if err := comments.LoadIssues(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if err := comments.LoadAttachments(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if _, err := comments.Issues().LoadRepositories(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
for i := range comments {
apiComments[i] = convert.ToAPIComment(ctx, ctx.Repo.Repository, comments[i])
}
ctx.SetTotalCountHeader(totalCount)
ctx.JSON(http.StatusOK, &apiComments)
}
// CreateIssueComment create a comment for an issue
func CreateIssueComment(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/comments issue issueCreateComment
// ---
// summary: Add a comment to an issue
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateIssueCommentOption"
// responses:
// "201":
// "$ref": "#/responses/Comment"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.CreateIssueCommentOption)
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return
}
if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
ctx.APIError(http.StatusForbidden, errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked")))
return
}
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment))
}
// GetIssueComment Get a comment by ID
func GetIssueComment(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id} issue issueGetComment
// ---
// summary: Get a comment
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the comment
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Comment"
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIErrorNotFound()
return
}
if comment.Type != issues_model.CommentTypeComment {
ctx.Status(http.StatusNoContent)
return
}
if err := comment.LoadPoster(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment))
}
// EditIssueComment modify a comment of an issue
func EditIssueComment(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/issues/comments/{id} issue issueEditComment
// ---
// summary: Edit a comment
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the comment to edit
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditIssueCommentOption"
// responses:
// "200":
// "$ref": "#/responses/Comment"
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.EditIssueCommentOption)
editIssueComment(ctx, *form)
}
// EditIssueCommentDeprecated modify a comment of an issue
func EditIssueCommentDeprecated(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/issues/{index}/comments/{id} issue issueEditCommentDeprecated
// ---
// summary: Edit a comment
// deprecated: true
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: this parameter is ignored
// type: integer
// required: true
// - name: id
// in: path
// description: id of the comment to edit
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditIssueCommentOption"
// responses:
// "200":
// "$ref": "#/responses/Comment"
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditIssueCommentOption)
editIssueComment(ctx, *form)
}
func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) {
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Status(http.StatusForbidden)
return
}
if !comment.Type.HasContentSupport() {
ctx.Status(http.StatusNoContent)
return
}
if form.Body != comment.Content {
oldContent := comment.Content
comment.Content = form.Body
if err := issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, oldContent); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
}
ctx.JSON(http.StatusOK, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment))
}
// DeleteIssueComment delete a comment from an issue
func DeleteIssueComment(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id} issue issueDeleteComment
// ---
// summary: Delete a comment
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of comment to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
deleteIssueComment(ctx)
}
// DeleteIssueCommentDeprecated delete a comment from an issue
func DeleteIssueCommentDeprecated(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/comments/{id} issue issueDeleteCommentDeprecated
// ---
// summary: Delete a comment
// deprecated: true
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: this parameter is ignored
// type: integer
// required: true
// - name: id
// in: path
// description: id of comment to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
deleteIssueComment(ctx)
}
func deleteIssueComment(ctx *context.APIContext) {
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Status(http.StatusForbidden)
return
} else if !comment.Type.HasContentSupport() {
ctx.Status(http.StatusBadRequest)
return
}
if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
@@ -0,0 +1,420 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
attachment_service "gitea.dev/services/attachment"
"gitea.dev/services/context"
"gitea.dev/services/context/upload"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
)
// GetIssueCommentAttachment gets a single attachment of the comment
func GetIssueCommentAttachment(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueGetIssueCommentAttachment
// ---
// summary: Get a comment attachment
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the comment
// type: integer
// format: int64
// required: true
// - name: attachment_id
// in: path
// description: id of the attachment to get
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Attachment"
// "404":
// "$ref": "#/responses/error"
comment := getIssueCommentSafe(ctx)
if comment == nil {
return
}
attachment := getIssueCommentAttachmentSafeRead(ctx, comment)
if attachment == nil {
return
}
if attachment.CommentID != comment.ID {
log.Debug("User requested attachment[%d] is not in comment[%d].", attachment.ID, comment.ID)
ctx.APIErrorNotFound("attachment not in comment")
return
}
ctx.JSON(http.StatusOK, convert.ToAPIAttachment(ctx.Repo.Repository, attachment))
}
// ListIssueCommentAttachments lists all attachments of the comment
func ListIssueCommentAttachments(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/assets issue issueListIssueCommentAttachments
// ---
// summary: List comment's attachments
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the comment
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/AttachmentList"
// "404":
// "$ref": "#/responses/error"
comment := getIssueCommentSafe(ctx)
if comment == nil {
return
}
if err := comment.LoadAttachments(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToAPIAttachments(ctx.Repo.Repository, comment.Attachments))
}
// CreateIssueCommentAttachment creates an attachment and saves the given file
func CreateIssueCommentAttachment(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/comments/{id}/assets issue issueCreateIssueCommentAttachment
// ---
// summary: Create a comment attachment
// produces:
// - application/json
// consumes:
// - multipart/form-data
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the comment
// type: integer
// format: int64
// required: true
// - name: name
// in: query
// description: name of the attachment
// type: string
// required: false
// - name: attachment
// in: formData
// description: attachment to upload
// type: file
// required: true
// responses:
// "201":
// "$ref": "#/responses/Attachment"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/error"
// "413":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
// Check if comment exists and load comment
comment := getIssueCommentSafe(ctx)
if comment == nil {
return
}
if !canUserWriteIssueCommentAttachment(ctx, comment) {
return
}
// Get uploaded file from request
file, header, err := ctx.Req.FormFile("attachment")
if err != nil {
ctx.APIErrorInternal(err)
return
}
defer file.Close()
filename := header.Filename
if query := ctx.FormString("name"); query != "" {
filename = query
}
uploaderFile := attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
attachment, err := attachment_service.UploadAttachmentForIssue(ctx, uploaderFile, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
IssueID: comment.IssueID,
CommentID: comment.ID,
})
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else if errors.Is(err, util.ErrContentTooLarge) {
ctx.APIError(http.StatusRequestEntityTooLarge, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if err := comment.LoadAttachments(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if err = issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, comment.Content); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attachment))
}
// EditIssueCommentAttachment updates the given attachment
func EditIssueCommentAttachment(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueEditIssueCommentAttachment
// ---
// summary: Edit a comment attachment
// produces:
// - application/json
// consumes:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the comment
// type: integer
// format: int64
// required: true
// - name: attachment_id
// in: path
// description: id of the attachment to edit
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditAttachmentOptions"
// responses:
// "201":
// "$ref": "#/responses/Attachment"
// "404":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
attach := getIssueCommentAttachmentSafeWrite(ctx)
if attach == nil {
return
}
form := web.GetForm(ctx).(*api.EditAttachmentOptions)
if form.Name != "" {
attach.Name = form.Name
}
if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attach); err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
}
// DeleteIssueCommentAttachment delete a given attachment
func DeleteIssueCommentAttachment(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueDeleteIssueCommentAttachment
// ---
// summary: Delete a comment attachment
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the comment
// type: integer
// format: int64
// required: true
// - name: attachment_id
// in: path
// description: id of the attachment to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/error"
// "423":
// "$ref": "#/responses/repoArchivedError"
attach := getIssueCommentAttachmentSafeWrite(ctx)
if attach == nil {
return
}
if err := repo_model.DeleteAttachment(ctx, attach, true); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
ctx.NotFoundOrServerError(err)
return nil
}
if err := comment.LoadIssue(ctx); err != nil {
ctx.APIErrorInternal(err)
return nil
}
if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.APIError(http.StatusNotFound, "no matching issue comment found")
return nil
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
return nil
}
comment.Issue.Repo = ctx.Repo.Repository
return comment
}
func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment {
comment := getIssueCommentSafe(ctx)
if comment == nil {
return nil
}
if !canUserWriteIssueCommentAttachment(ctx, comment) {
return nil
}
return getIssueCommentAttachmentSafeRead(ctx, comment)
}
func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool {
canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)
if !canEditComment {
ctx.APIError(http.StatusForbidden, "user should have permission to edit comment")
return false
}
return true
}
func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_model.Comment) *repo_model.Attachment {
attachment, err := repo_model.GetAttachmentByID(ctx, ctx.PathParamInt64("attachment_id"))
if err != nil {
ctx.NotFoundOrServerError(err)
return nil
}
if !attachmentBelongsToRepoOrComment(ctx, attachment, comment) {
return nil
}
return attachment
}
func attachmentBelongsToRepoOrComment(ctx *context.APIContext, attachment *repo_model.Attachment, comment *issues_model.Comment) bool {
if attachment.RepoID != ctx.Repo.Repository.ID {
log.Debug("Requested attachment[%d] does not belong to repo[%-v].", attachment.ID, ctx.Repo.Repository)
ctx.APIErrorNotFound("no such attachment in repo")
return false
}
if attachment.IssueID == 0 || attachment.CommentID == 0 {
log.Debug("Requested attachment[%d] is not in a comment.", attachment.ID)
ctx.APIErrorNotFound("no such attachment in comment")
return false
}
if comment != nil && attachment.CommentID != comment.ID {
log.Debug("Requested attachment[%d] does not belong to comment[%d].", attachment.ID, comment.ID)
ctx.APIErrorNotFound("no such attachment in comment")
return false
}
return true
}
+599
View File
@@ -0,0 +1,599 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// GetIssueDependencies list an issue's dependencies
func GetIssueDependencies(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/dependencies issue issueListIssueDependencies
// ---
// summary: List an issue's dependencies, i.e all issues that block this issue.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/IssueList"
// "404":
// "$ref": "#/responses/notFound"
// If this issue's repository does not enable dependencies then there can be no dependencies by default
if !ctx.Repo.Repository.IsDependenciesEnabled(ctx) {
ctx.APIErrorNotFound()
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound("IsErrIssueNotExist", err)
} else {
ctx.APIErrorInternal(err)
}
return
}
// 1. We must be able to read this issue
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return
}
listOptions := utils.GetListOptions(ctx)
canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
blockerIssues := make([]*issues_model.Issue, 0, min(listOptions.PageSize, setting.API.MaxResponseItems))
// 2. Get the issues this issue depends on, i.e. the `<#b>`: `<issue> <- <#b>`
blockersInfo, total, err := issue.BlockedByDependencies(ctx, listOptions)
if err != nil {
ctx.APIErrorInternal(err)
return
}
repoPerms := make(map[int64]access_model.Permission)
repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
for _, blocker := range blockersInfo {
// Get the permissions for this repository
// If the repo ID exists in the map, return the exist permissions
// else get the permission and add it to the map
var perm access_model.Permission
existPerm, ok := repoPerms[blocker.RepoID]
if ok {
perm = existPerm
} else {
var err error
perm, err = access_model.GetDoerRepoPermission(ctx, &blocker.Repository, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
repoPerms[blocker.RepoID] = perm
}
// check permission
if !perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
if !canWrite {
hiddenBlocker := &issues_model.DependencyInfo{
Issue: issues_model.Issue{
Title: "HIDDEN",
},
}
blocker = hiddenBlocker
} else {
confidentialBlocker := &issues_model.DependencyInfo{
Issue: issues_model.Issue{
RepoID: blocker.Issue.RepoID,
Index: blocker.Index,
Title: blocker.Title,
IsClosed: blocker.IsClosed,
IsPull: blocker.IsPull,
},
Repository: repo_model.Repository{
ID: blocker.Issue.Repo.ID,
Name: blocker.Issue.Repo.Name,
OwnerName: blocker.Issue.Repo.OwnerName,
},
}
confidentialBlocker.Issue.Repo = &confidentialBlocker.Repository
blocker = confidentialBlocker
}
}
blockerIssues = append(blockerIssues, &blocker.Issue)
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, blockerIssues))
}
// CreateIssueDependency create a new issue dependencies
func CreateIssueDependency(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/dependencies issue issueCreateIssueDependencies
// ---
// summary: Make the issue in the url depend on the issue in the form.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/IssueMeta"
// responses:
// "201":
// "$ref": "#/responses/Issue"
// "404":
// description: the issue does not exist
// "423":
// "$ref": "#/responses/repoArchivedError"
// We want to make <:index> depend on <Form>, i.e. <:index> is the target
target := getParamsIssue(ctx)
if ctx.Written() {
return
}
// and <Form> represents the dependency
form := web.GetForm(ctx).(*api.IssueMeta)
dependency := getFormIssue(ctx, form)
if ctx.Written() {
return
}
dependencyPerm := getPermissionForRepo(ctx, dependency.Repo)
if ctx.Written() {
return
}
createIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm)
if ctx.Written() {
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target))
}
// RemoveIssueDependency remove an issue dependency
func RemoveIssueDependency(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/dependencies issue issueRemoveIssueDependencies
// ---
// summary: Remove an issue dependency
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/IssueMeta"
// responses:
// "200":
// "$ref": "#/responses/Issue"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
// We want to make <:index> depend on <Form>, i.e. <:index> is the target
target := getParamsIssue(ctx)
if ctx.Written() {
return
}
// and <Form> represents the dependency
form := web.GetForm(ctx).(*api.IssueMeta)
dependency := getFormIssue(ctx, form)
if ctx.Written() {
return
}
dependencyPerm := getPermissionForRepo(ctx, dependency.Repo)
if ctx.Written() {
return
}
removeIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm)
if ctx.Written() {
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target))
}
// GetIssueBlocks list issues that are blocked by this issue
func GetIssueBlocks(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/blocks issue issueListBlocks
// ---
// summary: List issues that are blocked by this issue
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/IssueList"
// "404":
// "$ref": "#/responses/notFound"
// We need to list the issues that DEPEND on this issue not the other way round
// Therefore whether dependencies are enabled or not in this repository is potentially irrelevant.
issue := getParamsIssue(ctx)
if ctx.Written() {
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return
}
page := max(ctx.FormInt("page"), 1)
limit := ctx.FormInt("limit")
if limit <= 1 {
limit = setting.API.DefaultPagingNum
}
skip := (page - 1) * limit
maxNum := page * limit
deps, err := issue.BlockingDependencies(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
}
var issues []*issues_model.Issue
repoPerms := make(map[int64]access_model.Permission)
repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
for i, depMeta := range deps {
if i < skip || i >= maxNum {
continue
}
// Get the permissions for this repository
// If the repo ID exists in the map, return the exist permissions
// else get the permission and add it to the map
var perm access_model.Permission
existPerm, ok := repoPerms[depMeta.RepoID]
if ok {
perm = existPerm
} else {
var err error
perm, err = access_model.GetDoerRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
repoPerms[depMeta.RepoID] = perm
}
if !perm.CanReadIssuesOrPulls(depMeta.Issue.IsPull) {
continue
}
depMeta.Issue.Repo = &depMeta.Repository
issues = append(issues, &depMeta.Issue)
}
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
}
// CreateIssueBlocking block the issue given in the body by the issue in path
func CreateIssueBlocking(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/blocks issue issueCreateIssueBlocking
// ---
// summary: Block the issue given in the body by the issue in path
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/IssueMeta"
// responses:
// "201":
// "$ref": "#/responses/Issue"
// "404":
// description: the issue does not exist
dependency := getParamsIssue(ctx)
if ctx.Written() {
return
}
form := web.GetForm(ctx).(*api.IssueMeta)
target := getFormIssue(ctx, form)
if ctx.Written() {
return
}
targetPerm := getPermissionForRepo(ctx, target.Repo)
if ctx.Written() {
return
}
createIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission)
if ctx.Written() {
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
}
// RemoveIssueBlocking unblock the issue given in the body by the issue in path
func RemoveIssueBlocking(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/blocks issue issueRemoveIssueBlocking
// ---
// summary: Unblock the issue given in the body by the issue in path
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/IssueMeta"
// responses:
// "200":
// "$ref": "#/responses/Issue"
// "404":
// "$ref": "#/responses/notFound"
dependency := getParamsIssue(ctx)
if ctx.Written() {
return
}
form := web.GetForm(ctx).(*api.IssueMeta)
target := getFormIssue(ctx, form)
if ctx.Written() {
return
}
targetPerm := getPermissionForRepo(ctx, target.Repo)
if ctx.Written() {
return
}
removeIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission)
if ctx.Written() {
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
}
func getParamsIssue(ctx *context.APIContext) *issues_model.Issue {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound("IsErrIssueNotExist", err)
} else {
ctx.APIErrorInternal(err)
}
return nil
}
issue.Repo = ctx.Repo.Repository
return issue
}
func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Issue {
var repo *repo_model.Repository
if form.Owner != ctx.Repo.Repository.OwnerName || form.Name != ctx.Repo.Repository.Name {
if !setting.Service.AllowCrossRepositoryDependencies {
ctx.JSON(http.StatusBadRequest, "CrossRepositoryDependencies not enabled")
return nil
}
var err error
repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, form.Owner, form.Name)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIErrorNotFound("IsErrRepoNotExist", err)
} else {
ctx.APIErrorInternal(err)
}
return nil
}
} else {
repo = ctx.Repo.Repository
}
issue, err := issues_model.GetIssueByIndex(ctx, repo.ID, form.Index)
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound("IsErrIssueNotExist", err)
} else {
ctx.APIErrorInternal(err)
}
return nil
}
issue.Repo = repo
return issue
}
func getPermissionForRepo(ctx *context.APIContext, repo *repo_model.Repository) *access_model.Permission {
if repo.ID == ctx.Repo.Repository.ID {
return &ctx.Repo.Permission
}
perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return nil
}
return &perm
}
func createIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) {
if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) {
// The target's repository doesn't have dependencies enabled
ctx.APIErrorNotFound()
return
}
if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) {
// We can't write to the target
ctx.APIErrorNotFound()
return
}
if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) {
// We can't read the dependency
ctx.APIErrorNotFound()
return
}
err := issues_model.CreateIssueDependency(ctx, ctx.Doer, target, dependency)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
func removeIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) {
if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) {
// The target's repository doesn't have dependencies enabled
ctx.APIErrorNotFound()
return
}
if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) {
// We can't write to the target
ctx.APIErrorNotFound()
return
}
if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) {
// We can't read the dependency
ctx.APIErrorNotFound()
return
}
err := issues_model.RemoveIssueDependency(ctx, ctx.Doer, target, dependency, issues_model.DependencyTypeBlockedBy)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
+371
View File
@@ -0,0 +1,371 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
"reflect"
issues_model "gitea.dev/models/issues"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
)
// ListIssueLabels list all the labels of an issue
func ListIssueLabels(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/labels issue issueGetLabels
// ---
// summary: Get an issue's labels
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/LabelList"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
if err := issue.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToLabelList(issue.Labels, ctx.Repo.Repository, ctx.Repo.Owner))
}
// AddIssueLabels add labels for an issue
func AddIssueLabels(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/labels issue issueAddLabel
// ---
// summary: Add a label to an issue
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/IssueLabelsOption"
// responses:
// "200":
// "$ref": "#/responses/LabelList"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.IssueLabelsOption)
issue, labels, err := prepareForReplaceOrAdd(ctx, *form)
if err != nil {
return
}
if err = issue_service.AddLabels(ctx, issue, ctx.Doer, labels); err != nil {
ctx.APIErrorInternal(err)
return
}
labels, err = issues_model.GetLabelsByIssueID(ctx, issue.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToLabelList(labels, ctx.Repo.Repository, ctx.Repo.Owner))
}
// DeleteIssueLabel delete a label for an issue
func DeleteIssueLabel(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/labels/{id} issue issueRemoveLabel
// ---
// summary: Remove a label from an issue
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: id
// in: path
// description: id of the label to remove
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return
}
label, err := issues_model.GetLabelByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrLabelNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if err := issue_service.RemoveLabel(ctx, issue, ctx.Doer, label); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// ReplaceIssueLabels replace labels for an issue
func ReplaceIssueLabels(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/issues/{index}/labels issue issueReplaceLabels
// ---
// summary: Replace an issue's labels
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/IssueLabelsOption"
// responses:
// "200":
// "$ref": "#/responses/LabelList"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.IssueLabelsOption)
issue, labels, err := prepareForReplaceOrAdd(ctx, *form)
if err != nil {
return
}
if err := issue_service.ReplaceLabels(ctx, issue, ctx.Doer, labels); err != nil {
ctx.APIErrorInternal(err)
return
}
labels, err = issues_model.GetLabelsByIssueID(ctx, issue.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToLabelList(labels, ctx.Repo.Repository, ctx.Repo.Owner))
}
// ClearIssueLabels delete all the labels for an issue
func ClearIssueLabels(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/labels issue issueClearLabels
// ---
// summary: Remove all labels from an issue
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return
}
if err := issue_service.ClearLabels(ctx, issue, ctx.Doer); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (*issues_model.Issue, []*issues_model.Label, error) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return nil, nil, err
}
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, "write permission is required")
return nil, nil, errors.New("permission denied")
}
var (
labelIDs []int64
labelNames []string
)
for _, label := range form.Labels {
rv := reflect.ValueOf(label)
switch rv.Kind() {
case reflect.Float64:
labelIDs = append(labelIDs, int64(rv.Float()))
case reflect.String:
labelNames = append(labelNames, rv.String())
default:
ctx.APIError(http.StatusBadRequest, "a label must be an integer or a string")
return nil, nil, errors.New("invalid label")
}
}
if len(labelIDs) > 0 && len(labelNames) > 0 {
ctx.APIError(http.StatusBadRequest, "labels should be an array of strings or integers")
return nil, nil, errors.New("invalid labels")
}
if len(labelNames) > 0 {
repoLabelIDs, err := issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil, err
}
labelIDs = append(labelIDs, repoLabelIDs...)
if ctx.Repo.Owner.IsOrganization() {
orgLabelIDs, err := issues_model.GetLabelIDsInOrgByNames(ctx, ctx.Repo.Owner.ID, labelNames)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil, err
}
labelIDs = append(labelIDs, orgLabelIDs...)
}
}
labels, err := issues_model.GetLabelsByIDs(ctx, labelIDs, "id", "repo_id", "org_id", "name", "exclusive")
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil, err
}
return issue, labels, err
}
+152
View File
@@ -0,0 +1,152 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
issues_model "gitea.dev/models/issues"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
)
// LockIssue lock an issue
func LockIssue(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/issues/{index}/lock issue issueLockIssue
// ---
// summary: Lock an issue
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/LockIssueOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
reason := web.GetForm(ctx).(*api.LockIssueOption).Reason
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to lock this issue"))
return
}
if !issue.IsLocked {
opt := &issues_model.IssueLockOptions{
Doer: ctx.ContextUser,
Issue: issue,
Reason: reason,
}
issue.Repo = ctx.Repo.Repository
err = issues_model.LockIssue(ctx, opt)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
ctx.Status(http.StatusNoContent)
}
// UnlockIssue unlock an issue
func UnlockIssue(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/lock issue issueUnlockIssue
// ---
// summary: Unlock an issue
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to unlock this issue"))
return
}
if issue.IsLocked {
opt := &issues_model.IssueLockOptions{
Doer: ctx.ContextUser,
Issue: issue,
}
issue.Repo = ctx.Repo.Repository
err = issues_model.UnlockIssue(ctx, opt)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
ctx.Status(http.StatusNoContent)
}
+309
View File
@@ -0,0 +1,309 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
issues_model "gitea.dev/models/issues"
api "gitea.dev/modules/structs"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// PinIssue pins a issue
func PinIssue(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/pin issue pinIssue
// ---
// summary: Pin an Issue
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of issue to pin
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else if issues_model.IsErrIssueMaxPinReached(err) {
ctx.APIError(http.StatusBadRequest, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
// If we don't do this, it will crash when trying to add the pin event to the comment history
err = issue.LoadRepo(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
}
err = issues_model.PinIssue(ctx, issue, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// UnpinIssue unpins a Issue
func UnpinIssue(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/pin issue unpinIssue
// ---
// summary: Unpin an Issue
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of issue to unpin
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
// If we don't do this, it will crash when trying to add the unpin event to the comment history
err = issue.LoadRepo(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
}
err = issues_model.UnpinIssue(ctx, issue, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// MoveIssuePin moves a pinned Issue to a new Position
func MoveIssuePin(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/issues/{index}/pin/{position} issue moveIssuePin
// ---
// summary: Moves the Pin to the given Position
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of issue
// type: integer
// format: int64
// required: true
// - name: position
// in: path
// description: the new position
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
err = issues_model.MovePin(ctx, issue, ctx.PathParamInt("position"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// ListPinnedIssues returns a list of all pinned Issues
func ListPinnedIssues(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/pinned repository repoListPinnedIssues
// ---
// summary: List a repo's pinned issues
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/IssueList"
// "404":
// "$ref": "#/responses/notFound"
issues, err := issues_model.GetPinnedIssues(ctx, ctx.Repo.Repository.ID, false)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
}
// ListPinnedPullRequests returns a list of all pinned PRs
func ListPinnedPullRequests(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/pulls/pinned repository repoListPinnedPullRequests
// ---
// summary: List a repo's pinned pull requests
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/PullRequestList"
// "404":
// "$ref": "#/responses/notFound"
issues, err := issues_model.GetPinnedIssues(ctx, ctx.Repo.Repository.ID, true)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiPrs := make([]*api.PullRequest, len(issues))
if err := issues.LoadPullRequests(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
for i, currentIssue := range issues {
pr := currentIssue.PullRequest
if err = pr.LoadAttributes(ctx); err != nil {
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
}
apiPrs[i] = convert.ToAPIPullRequest(ctx, pr, ctx.Doer)
}
ctx.JSON(http.StatusOK, &apiPrs)
}
// AreNewIssuePinsAllowed returns if new issues pins are allowed
func AreNewIssuePinsAllowed(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/new_pin_allowed repository repoNewPinAllowed
// ---
// summary: Returns if new Issue Pins are allowed
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RepoNewIssuePinsAllowed"
// "404":
// "$ref": "#/responses/notFound"
pinsAllowed := api.NewIssuePinsAllowed{}
var err error
pinsAllowed.Issues, err = issues_model.IsNewPinAllowed(ctx, ctx.Repo.Repository.ID, false)
if err != nil {
ctx.APIErrorInternal(err)
return
}
pinsAllowed.PullRequests, err = issues_model.IsNewPinAllowed(ctx, ctx.Repo.Repository.ID, true)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, pinsAllowed)
}
+468
View File
@@ -0,0 +1,468 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
issues_model "gitea.dev/models/issues"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
)
// GetIssueCommentReactions list reactions of a comment from an issue
func GetIssueCommentReactions(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issueGetCommentReactions
// ---
// summary: Get a list of reactions from a comment of an issue
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the comment to edit
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/ReactionList"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if err := comment.LoadIssue(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions"))
return
}
reactions, _, err := issues_model.FindCommentReactions(ctx, comment.IssueID, comment.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
_, err = reactions.LoadUsers(ctx, ctx.Repo.Repository)
if err != nil {
ctx.APIErrorInternal(err)
return
}
var result []api.Reaction
for _, r := range reactions {
result = append(result, api.Reaction{
User: convert.ToUser(ctx, r.User, ctx.Doer),
Reaction: r.Type,
Created: r.CreatedUnix.AsTime(),
})
}
ctx.JSON(http.StatusOK, result)
}
// PostIssueCommentReaction add a reaction to a comment of an issue
func PostIssueCommentReaction(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issuePostCommentReaction
// ---
// summary: Add a reaction to a comment of an issue
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the comment to edit
// type: integer
// format: int64
// required: true
// - name: content
// in: body
// schema:
// "$ref": "#/definitions/EditReactionOption"
// responses:
// "200":
// "$ref": "#/responses/Reaction"
// "201":
// "$ref": "#/responses/Reaction"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditReactionOption)
changeIssueCommentReaction(ctx, *form, true)
}
// DeleteIssueCommentReaction remove a reaction from a comment of an issue
func DeleteIssueCommentReaction(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issueDeleteCommentReaction
// ---
// summary: Remove a reaction from a comment of an issue
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the comment to edit
// type: integer
// format: int64
// required: true
// - name: content
// in: body
// schema:
// "$ref": "#/definitions/EditReactionOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditReactionOption)
changeIssueCommentReaction(ctx, *form, false)
}
func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if err = comment.LoadIssue(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIErrorNotFound()
return
}
if comment.Issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction"))
return
}
if isCreateType {
// PostIssueCommentReaction part
reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction)
if err != nil {
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
} else if issues_model.IsErrReactionAlreadyExist(err) {
ctx.JSON(http.StatusOK, api.Reaction{
User: convert.ToUser(ctx, ctx.Doer, ctx.Doer),
Reaction: reaction.Type,
Created: reaction.CreatedUnix.AsTime(),
})
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.JSON(http.StatusCreated, api.Reaction{
User: convert.ToUser(ctx, ctx.Doer, ctx.Doer),
Reaction: reaction.Type,
Created: reaction.CreatedUnix.AsTime(),
})
} else {
// DeleteIssueCommentReaction part
err = issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
}
// GetIssueReactions list reactions of an issue
func GetIssueReactions(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/reactions issue issueGetIssueReactions
// ---
// summary: Get a list reactions of an issue
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ReactionList"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions"))
return
}
reactions, count, err := issues_model.FindIssueReactions(ctx, issue.ID, utils.GetListOptions(ctx))
if err != nil {
ctx.APIErrorInternal(err)
return
}
_, err = reactions.LoadUsers(ctx, ctx.Repo.Repository)
if err != nil {
ctx.APIErrorInternal(err)
return
}
var result []api.Reaction
for _, r := range reactions {
result = append(result, api.Reaction{
User: convert.ToUser(ctx, r.User, ctx.Doer),
Reaction: r.Type,
Created: r.CreatedUnix.AsTime(),
})
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, result)
}
// PostIssueReaction add a reaction to an issue
func PostIssueReaction(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/reactions issue issuePostIssueReaction
// ---
// summary: Add a reaction to an issue
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: content
// in: body
// schema:
// "$ref": "#/definitions/EditReactionOption"
// responses:
// "200":
// "$ref": "#/responses/Reaction"
// "201":
// "$ref": "#/responses/Reaction"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditReactionOption)
changeIssueReaction(ctx, *form, true)
}
// DeleteIssueReaction remove a reaction from an issue
func DeleteIssueReaction(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/reactions issue issueDeleteIssueReaction
// ---
// summary: Remove a reaction from an issue
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: content
// in: body
// schema:
// "$ref": "#/definitions/EditReactionOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditReactionOption)
changeIssueReaction(ctx, *form, false)
}
func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction"))
return
}
if isCreateType {
// PostIssueReaction part
reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction)
if err != nil {
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
} else if issues_model.IsErrReactionAlreadyExist(err) {
ctx.JSON(http.StatusOK, api.Reaction{
User: convert.ToUser(ctx, ctx.Doer, ctx.Doer),
Reaction: reaction.Type,
Created: reaction.CreatedUnix.AsTime(),
})
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.JSON(http.StatusCreated, api.Reaction{
User: convert.ToUser(ctx, ctx.Doer, ctx.Doer),
Reaction: reaction.Type,
Created: reaction.CreatedUnix.AsTime(),
})
} else {
// DeleteIssueReaction part
err = issues_model.DeleteIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Reaction)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
}
+235
View File
@@ -0,0 +1,235 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
issues_model "gitea.dev/models/issues"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// StartIssueStopwatch creates a stopwatch for the given issue.
func StartIssueStopwatch(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/stopwatch/start issue issueStartStopWatch
// ---
// summary: Start stopwatch on an issue.
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue to create the stopwatch on
// type: integer
// format: int64
// required: true
// responses:
// "201":
// "$ref": "#/responses/empty"
// "403":
// description: Not repo writer, user does not have rights to toggle stopwatch
// "404":
// "$ref": "#/responses/notFound"
// "409":
// description: Cannot start a stopwatch again if it already exists
issue := prepareIssueForStopwatch(ctx)
if ctx.Written() {
return
}
if ok, err := issues_model.CreateIssueStopwatch(ctx, ctx.Doer, issue); err != nil {
ctx.APIErrorInternal(err)
return
} else if !ok {
ctx.APIError(http.StatusConflict, "cannot start a stopwatch again if it already exists")
return
}
ctx.Status(http.StatusCreated)
}
// StopIssueStopwatch stops a stopwatch for the given issue.
func StopIssueStopwatch(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/stopwatch/stop issue issueStopStopWatch
// ---
// summary: Stop an issue's existing stopwatch.
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue to stop the stopwatch on
// type: integer
// format: int64
// required: true
// responses:
// "201":
// "$ref": "#/responses/empty"
// "403":
// description: Not repo writer, user does not have rights to toggle stopwatch
// "404":
// "$ref": "#/responses/notFound"
// "409":
// description: Cannot stop a non-existent stopwatch
issue := prepareIssueForStopwatch(ctx)
if ctx.Written() {
return
}
if ok, err := issues_model.FinishIssueStopwatch(ctx, ctx.Doer, issue); err != nil {
ctx.APIErrorInternal(err)
return
} else if !ok {
ctx.APIError(http.StatusConflict, "cannot stop a non-existent stopwatch")
return
}
ctx.Status(http.StatusCreated)
}
// DeleteIssueStopwatch delete a specific stopwatch
func DeleteIssueStopwatch(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/stopwatch/delete issue issueDeleteStopWatch
// ---
// summary: Delete an issue's existing stopwatch.
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue to stop the stopwatch on
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// description: Not repo writer, user does not have rights to toggle stopwatch
// "404":
// "$ref": "#/responses/notFound"
// "409":
// description: Cannot cancel a non-existent stopwatch
issue := prepareIssueForStopwatch(ctx)
if ctx.Written() {
return
}
if ok, err := issues_model.CancelStopwatch(ctx, ctx.Doer, issue); err != nil {
ctx.APIErrorInternal(err)
return
} else if !ok {
ctx.APIError(http.StatusConflict, "cannot cancel a non-existent stopwatch")
return
}
ctx.Status(http.StatusNoContent)
}
func prepareIssueForStopwatch(ctx *context.APIContext) *issues_model.Issue {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return nil
}
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return nil
}
if !ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) {
ctx.Status(http.StatusForbidden)
return nil
}
return issue
}
// GetStopwatches get all stopwatches
func GetStopwatches(ctx *context.APIContext) {
// swagger:operation GET /user/stopwatches user userGetStopWatches
// ---
// summary: Get list of all existing stopwatches
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// consumes:
// - application/json
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/StopWatchList"
sws, err := issues_model.GetUserStopwatches(ctx, ctx.Doer.ID, utils.GetListOptions(ctx))
if err != nil {
ctx.APIErrorInternal(err)
return
}
count, err := issues_model.CountUserStopwatches(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiSWs, err := convert.ToStopWatches(ctx, ctx.Doer, sws)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiSWs)
}
+294
View File
@@ -0,0 +1,294 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"fmt"
"net/http"
issues_model "gitea.dev/models/issues"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// AddIssueSubscription Subscribe user to issue
func AddIssueSubscription(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/issues/{index}/subscriptions/{user} issue issueAddSubscription
// ---
// summary: Subscribe user to issue
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: user
// in: path
// description: username of the user to subscribe the issue to
// type: string
// required: true
// responses:
// "200":
// description: Already subscribed
// "201":
// description: Successfully Subscribed
// "304":
// description: User can only subscribe itself if he is no admin
// "404":
// "$ref": "#/responses/notFound"
setIssueSubscription(ctx, true)
}
// DelIssueSubscription Unsubscribe user from issue
func DelIssueSubscription(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/subscriptions/{user} issue issueDeleteSubscription
// ---
// summary: Unsubscribe user from issue
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: user
// in: path
// description: username of the user to unsubscribe from an issue
// type: string
// required: true
// responses:
// "200":
// description: Already unsubscribed
// "201":
// description: Successfully Unsubscribed
// "304":
// description: User can only subscribe itself if he is no admin
// "404":
// "$ref": "#/responses/notFound"
setIssueSubscription(ctx, false)
}
func setIssueSubscription(ctx *context.APIContext, watch bool) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
user, err := user_model.GetUserByName(ctx, ctx.PathParam("user"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
// only admin and user for itself can change subscription
if user.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
ctx.APIError(http.StatusForbidden, fmt.Errorf("%s is not permitted to change subscriptions for %s", ctx.Doer.Name, user.Name))
return
}
current, err := issues_model.CheckIssueWatch(ctx, user, issue)
if err != nil {
ctx.APIErrorInternal(err)
return
}
// If watch state won't change
if current == watch {
ctx.Status(http.StatusOK)
return
}
// Update watch state
if err := issues_model.CreateOrUpdateIssueWatch(ctx, user.ID, issue.ID, watch); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusCreated)
}
// CheckIssueSubscription check if user is subscribed to an issue
func CheckIssueSubscription(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/subscriptions/check issue issueCheckSubscription
// ---
// summary: Check if user is subscribed to an issue
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/WatchInfo"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
watching, err := issues_model.CheckIssueWatch(ctx, ctx.Doer, issue)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, api.WatchInfo{
Subscribed: watching,
Ignored: !watching,
Reason: nil,
CreatedAt: issue.CreatedUnix.AsTime(),
URL: issue.APIURL(ctx) + "/subscriptions",
RepositoryURL: ctx.Repo.Repository.APIURL(),
})
}
// GetIssueSubscribers return subscribers of an issue
func GetIssueSubscribers(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/subscriptions issue issueSubscriptions
// ---
// summary: Get users who subscribed on an issue.
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/UserList"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
iwl, err := issues_model.GetIssueWatchers(ctx, issue.ID, utils.GetListOptions(ctx))
if err != nil {
ctx.APIErrorInternal(err)
return
}
userIDs := make([]int64, 0, len(iwl))
for _, iw := range iwl {
userIDs = append(userIDs, iw.UserID)
}
users, err := user_model.GetUsersByIDs(ctx, userIDs)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiUsers := make([]*api.User, 0, len(users))
for _, v := range users {
apiUsers = append(apiUsers, convert.ToUser(ctx, v, ctx.Doer))
}
count, err := issues_model.CountIssueWatchers(ctx, issue.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiUsers)
}
+633
View File
@@ -0,0 +1,633 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
"time"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListTrackedTimes list all the tracked times of an issue
func ListTrackedTimes(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/times issue issueTrackedTimes
// ---
// summary: List an issue's tracked times
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: user
// in: query
// description: optional filter by user (available for issue managers)
// type: string
// - name: since
// in: query
// description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: before
// in: query
// description: Only show times updated before the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/TrackedTimeList"
// "404":
// "$ref": "#/responses/notFound"
if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
ctx.APIErrorNotFound("Timetracker is disabled")
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
opts := &issues_model.FindTrackedTimesOptions{
ListOptions: utils.GetListOptions(ctx),
RepositoryID: ctx.Repo.Repository.ID,
IssueID: issue.ID,
}
qUser := ctx.FormTrim("user")
if qUser != "" {
user, err := user_model.GetUserByName(ctx, qUser)
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
} else if err != nil {
ctx.APIErrorInternal(err)
return
}
opts.UserID = user.ID
}
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
cantSetUser := !ctx.Doer.IsAdmin &&
opts.UserID != ctx.Doer.ID &&
!ctx.IsUserRepoWriter([]unit.Type{unit.TypeIssues})
if cantSetUser {
if opts.UserID == 0 {
opts.UserID = ctx.Doer.ID
} else {
ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights"))
return
}
}
count, err := issues_model.CountTrackedTimes(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err = trackedTimes.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
}
// AddTime add time manual to the given issue
func AddTime(ctx *context.APIContext) {
// swagger:operation Post /repos/{owner}/{repo}/issues/{index}/times issue issueAddTime
// ---
// summary: Add tracked time to a issue
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/AddTimeOption"
// responses:
// "200":
// "$ref": "#/responses/TrackedTime"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.AddTimeOption)
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if !ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) {
if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
ctx.APIError(http.StatusBadRequest, "time tracking disabled")
return
}
ctx.Status(http.StatusForbidden)
return
}
user := ctx.Doer
if form.User != "" {
if (ctx.IsUserRepoAdmin() && ctx.Doer.Name != form.User) || ctx.Doer.IsAdmin {
// allow only RepoAdmin, Admin and User to add time
user, err = user_model.GetUserByName(ctx, form.User)
if err != nil {
ctx.APIErrorInternal(err)
}
}
}
created := time.Time{}
if !form.Created.IsZero() {
created = form.Created
}
trackedTime, err := issues_model.AddTime(ctx, user, issue, form.Time, created)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err = trackedTime.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToTrackedTime(ctx, user, trackedTime))
}
// ResetIssueTime reset time manual to the given issue
func ResetIssueTime(ctx *context.APIContext) {
// swagger:operation Delete /repos/{owner}/{repo}/issues/{index}/times issue issueResetTime
// ---
// summary: Reset a tracked time of an issue
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue to add tracked time to
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if !ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) {
if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"})
return
}
ctx.Status(http.StatusForbidden)
return
}
err = issues_model.DeleteIssueUserTimes(ctx, issue, ctx.Doer)
if err != nil {
if db.IsErrNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// DeleteTime delete a specific time by id
func DeleteTime(ctx *context.APIContext) {
// swagger:operation Delete /repos/{owner}/{repo}/issues/{index}/times/{id} issue issueDeleteTime
// ---
// summary: Delete specific tracked time
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: id
// in: path
// description: id of time to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if !ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) {
if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"})
return
}
ctx.Status(http.StatusForbidden)
return
}
time, err := issues_model.GetTrackedTimeByID(ctx, issue.ID, ctx.PathParamInt64("id"))
if err != nil {
if db.IsErrNotExist(err) {
ctx.APIErrorNotFound(err)
return
}
ctx.APIErrorInternal(err)
return
}
if time.Deleted {
ctx.APIErrorNotFound("tracked time was already deleted")
return
}
if !ctx.Doer.IsAdmin && time.UserID != ctx.Doer.ID {
// Only Admin and User itself can delete their time
ctx.Status(http.StatusForbidden)
return
}
err = issues_model.DeleteTime(ctx, time)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// ListTrackedTimesByUser lists all tracked times of the user
func ListTrackedTimesByUser(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/times/{user} repository userTrackedTimes
// ---
// summary: List a user's tracked times in a repo
// deprecated: true
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: user
// in: path
// description: username of the user whose tracked times are to be listed
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/TrackedTimeList"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
ctx.APIError(http.StatusBadRequest, "time tracking disabled")
return
}
user, err := user_model.GetUserByName(ctx, ctx.PathParam("timetrackingusername"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if user == nil {
ctx.APIErrorNotFound()
return
}
if !ctx.IsUserRepoAdmin() && !ctx.Doer.IsAdmin && ctx.Doer.ID != user.ID {
ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights"))
return
}
opts := &issues_model.FindTrackedTimesOptions{
UserID: user.ID,
RepositoryID: ctx.Repo.Repository.ID,
}
trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err = trackedTimes.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
}
// ListTrackedTimesByRepository lists all tracked times of the repository
func ListTrackedTimesByRepository(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/times repository repoTrackedTimes
// ---
// summary: List a repo's tracked times
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: user
// in: query
// description: optional filter by user (available for issue managers)
// type: string
// - name: since
// in: query
// description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: before
// in: query
// description: Only show times updated before the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/TrackedTimeList"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
ctx.APIError(http.StatusBadRequest, "time tracking disabled")
return
}
opts := &issues_model.FindTrackedTimesOptions{
ListOptions: utils.GetListOptions(ctx),
RepositoryID: ctx.Repo.Repository.ID,
}
// Filters
qUser := ctx.FormTrim("user")
if qUser != "" {
user, err := user_model.GetUserByName(ctx, qUser)
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
} else if err != nil {
ctx.APIErrorInternal(err)
return
}
opts.UserID = user.ID
}
var err error
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
cantSetUser := !ctx.Doer.IsAdmin &&
opts.UserID != ctx.Doer.ID &&
!ctx.IsUserRepoWriter([]unit.Type{unit.TypeIssues})
if cantSetUser {
if opts.UserID == 0 {
opts.UserID = ctx.Doer.ID
} else {
ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights"))
return
}
}
count, err := issues_model.CountTrackedTimes(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err = trackedTimes.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
}
// ListMyTrackedTimes lists all tracked times of the current user
func ListMyTrackedTimes(ctx *context.APIContext) {
// swagger:operation GET /user/times user userCurrentTrackedTimes
// ---
// summary: List the current user's tracked times
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// - name: since
// in: query
// description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: before
// in: query
// description: Only show times updated before the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// responses:
// "200":
// "$ref": "#/responses/TrackedTimeList"
opts := &issues_model.FindTrackedTimesOptions{
ListOptions: utils.GetListOptions(ctx),
UserID: ctx.Doer.ID,
}
var err error
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
count, err := issues_model.CountTrackedTimes(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err = trackedTimes.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
}
+292
View File
@@ -0,0 +1,292 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
// SPDX-License-Identifier: MIT
package repo
import (
stdCtx "context"
"fmt"
"net/http"
"net/url"
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/db"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// appendPrivateInformation appends the owner and key type information to api.PublicKey
func appendPrivateInformation(ctx stdCtx.Context, apiKey *api.DeployKey, key *asymkey_model.DeployKey, repository *repo_model.Repository) (*api.DeployKey, error) {
apiKey.ReadOnly = key.Mode == perm.AccessModeRead
if repository.ID == key.RepoID {
apiKey.Repository = convert.ToRepo(ctx, repository, access_model.Permission{AccessMode: key.Mode})
} else {
repo, err := repo_model.GetRepositoryByID(ctx, key.RepoID)
if err != nil {
return apiKey, err
}
apiKey.Repository = convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: key.Mode})
}
return apiKey, nil
}
func composeDeployKeysAPILink(owner, name string) string {
return setting.AppURL + "api/v1/repos/" + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/keys/"
}
// ListDeployKeys list all the deploy keys of a repository
func ListDeployKeys(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/keys repository repoListKeys
// ---
// summary: List a repository's keys
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: key_id
// in: query
// description: the key_id to search for
// type: integer
// - name: fingerprint
// in: query
// description: fingerprint of the key
// type: string
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/DeployKeyList"
// "404":
// "$ref": "#/responses/notFound"
opts := asymkey_model.ListDeployKeysOptions{
ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID,
KeyID: ctx.FormInt64("key_id"),
Fingerprint: ctx.FormString("fingerprint"),
}
keys, count, err := db.FindAndCount[asymkey_model.DeployKey](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
apiKeys := make([]*api.DeployKey, len(keys))
for i := range keys {
if err := keys[i].GetContent(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
apiKeys[i] = convert.ToDeployKey(apiLink, keys[i])
if ctx.Doer.IsAdmin || ((ctx.Repo.Repository.ID == keys[i].RepoID) && (ctx.Doer.ID == ctx.Repo.Owner.ID)) {
apiKeys[i], _ = appendPrivateInformation(ctx, apiKeys[i], keys[i], ctx.Repo.Repository)
}
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, &apiKeys)
}
// GetDeployKey get a deploy key by id
func GetDeployKey(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/keys/{id} repository repoGetKey
// ---
// summary: Get a repository's key by id
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the key to get
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/DeployKey"
// "404":
// "$ref": "#/responses/notFound"
key, err := asymkey_model.GetDeployKeyByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if asymkey_model.IsErrDeployKeyNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
// this check make it more consistent
if key.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return
}
if err = key.GetContent(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
apiKey := convert.ToDeployKey(apiLink, key)
if ctx.Doer.IsAdmin || ((ctx.Repo.Repository.ID == key.RepoID) && (ctx.Doer.ID == ctx.Repo.Owner.ID)) {
apiKey, _ = appendPrivateInformation(ctx, apiKey, key, ctx.Repo.Repository)
}
ctx.JSON(http.StatusOK, apiKey)
}
// HandleCheckKeyStringError handle check key error
func HandleCheckKeyStringError(ctx *context.APIContext, err error) {
if db.IsErrSSHDisabled(err) {
ctx.APIError(http.StatusUnprocessableEntity, "SSH is disabled")
} else if asymkey_model.IsErrKeyUnableVerify(err) {
ctx.APIError(http.StatusUnprocessableEntity, "Unable to verify key content")
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid key content: %w", err))
}
}
// HandleAddKeyError handle add key error
func HandleAddKeyError(ctx *context.APIContext, err error) {
switch {
case asymkey_model.IsErrDeployKeyAlreadyExist(err):
ctx.APIError(http.StatusUnprocessableEntity, "This key has already been added to this repository")
case asymkey_model.IsErrKeyAlreadyExist(err):
ctx.APIError(http.StatusUnprocessableEntity, "Key content has been used as non-deploy key")
case asymkey_model.IsErrKeyNameAlreadyUsed(err):
ctx.APIError(http.StatusUnprocessableEntity, "Key title has been used")
case asymkey_model.IsErrDeployKeyNameAlreadyUsed(err):
ctx.APIError(http.StatusUnprocessableEntity, "A key with the same name already exists")
default:
ctx.APIErrorInternal(err)
}
}
// CreateDeployKey create deploy key for a repository
func CreateDeployKey(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/keys repository repoCreateKey
// ---
// summary: Add a key to a repository
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateKeyOption"
// responses:
// "201":
// "$ref": "#/responses/DeployKey"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateKeyOption)
content, err := asymkey_model.CheckPublicKeyString(form.Key)
if err != nil {
HandleCheckKeyStringError(ctx, err)
return
}
key, err := asymkey_model.AddDeployKey(ctx, ctx.Repo.Repository.ID, form.Title, content, form.ReadOnly)
if err != nil {
HandleAddKeyError(ctx, err)
return
}
key.Content = content
apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
ctx.JSON(http.StatusCreated, convert.ToDeployKey(apiLink, key))
}
// DeleteDeploykey delete deploy key for a repository
func DeleteDeploykey(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/keys/{id} repository repoDeleteKey
// ---
// summary: Delete a key from a repository
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the key to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
if err := asymkey_service.DeleteDeployKey(ctx, ctx.Repo.Repository, ctx.PathParamInt64("id")); err != nil {
if asymkey_model.IsErrKeyAccessDenied(err) {
ctx.APIError(http.StatusForbidden, "You do not have access to this key")
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
+285
View File
@@ -0,0 +1,285 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"strconv"
issues_model "gitea.dev/models/issues"
"gitea.dev/modules/label"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListLabels list all the labels of a repository
func ListLabels(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/labels issue issueListLabels
// ---
// summary: Get all of a repository's labels
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/LabelList"
// "404":
// "$ref": "#/responses/notFound"
labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), utils.GetListOptions(ctx))
if err != nil {
ctx.APIErrorInternal(err)
return
}
count, err := issues_model.CountLabelsByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToLabelList(labels, ctx.Repo.Repository, nil))
}
// GetLabel get label by repository and label id
func GetLabel(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/labels/{id} issue issueGetLabel
// ---
// summary: Get a single label
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the label to get
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Label"
// "404":
// "$ref": "#/responses/notFound"
var (
l *issues_model.Label
err error
)
strID := ctx.PathParam("id")
if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil {
l, err = issues_model.GetLabelInRepoByName(ctx, ctx.Repo.Repository.ID, strID)
} else {
l, err = issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, intID)
}
if err != nil {
if issues_model.IsErrRepoLabelNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.JSON(http.StatusOK, convert.ToLabel(l, ctx.Repo.Repository, nil))
}
// CreateLabel create a label for a repository
func CreateLabel(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/labels issue issueCreateLabel
// ---
// summary: Create a label
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateLabelOption"
// responses:
// "201":
// "$ref": "#/responses/Label"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateLabelOption)
color, err := label.NormalizeColor(form.Color)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
form.Color = color
l := &issues_model.Label{
Name: form.Name,
Exclusive: form.Exclusive,
Color: form.Color,
RepoID: ctx.Repo.Repository.ID,
Description: form.Description,
}
l.SetArchived(form.IsArchived)
if err := issues_model.NewLabel(ctx, l); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convert.ToLabel(l, ctx.Repo.Repository, nil))
}
// EditLabel modify a label for a repository
func EditLabel(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/labels/{id} issue issueEditLabel
// ---
// summary: Update a label
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the label to edit
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditLabelOption"
// responses:
// "200":
// "$ref": "#/responses/Label"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.EditLabelOption)
l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrRepoLabelNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
if form.Name != nil {
l.Name = *form.Name
}
if form.Exclusive != nil {
l.Exclusive = *form.Exclusive
}
if form.Color != nil {
color, err := label.NormalizeColor(*form.Color)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
l.Color = color
}
if form.Description != nil {
l.Description = *form.Description
}
l.SetArchived(form.IsArchived != nil && *form.IsArchived)
if err := issues_model.UpdateLabel(ctx, l); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToLabel(l, ctx.Repo.Repository, nil))
}
// DeleteLabel delete a label for a repository
func DeleteLabel(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/labels/{id} issue issueDeleteLabel
// ---
// summary: Delete a label
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the label to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
if err := issues_model.DeleteLabel(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+81
View File
@@ -0,0 +1,81 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"bytes"
"net/http"
"strconv"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/log"
"gitea.dev/services/context"
)
type languageResponse []*repo_model.LanguageStat
func (l languageResponse) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
if _, err := buf.WriteString("{"); err != nil {
return nil, err
}
for i, lang := range l {
if i > 0 {
if _, err := buf.WriteString(","); err != nil {
return nil, err
}
}
if _, err := buf.WriteString(strconv.Quote(lang.Language)); err != nil {
return nil, err
}
if _, err := buf.WriteString(":"); err != nil {
return nil, err
}
if _, err := buf.WriteString(strconv.FormatInt(lang.Size, 10)); err != nil {
return nil, err
}
}
if _, err := buf.WriteString("}"); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// GetLanguages returns languages and number of bytes of code written
func GetLanguages(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/languages repository repoGetLanguages
// ---
// summary: Get languages and number of bytes of code written
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "404":
// "$ref": "#/responses/notFound"
// "200":
// "$ref": "#/responses/LanguageStatistics"
langs, err := repo_model.GetLanguageStats(ctx, ctx.Repo.Repository)
if err != nil {
log.Error("GetLanguageStats failed: %v", err)
ctx.APIErrorInternal(err)
return
}
resp := make(languageResponse, len(langs))
copy(resp, langs)
ctx.JSON(http.StatusOK, resp)
}
+51
View File
@@ -0,0 +1,51 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/log"
"gitea.dev/services/context"
)
// GetLicenses returns licenses
func GetLicenses(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/licenses repository repoGetLicenses
// ---
// summary: Get repo licenses
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "404":
// "$ref": "#/responses/notFound"
// "200":
// "$ref": "#/responses/LicensesList"
licenses, err := repo_model.GetRepoLicenses(ctx, ctx.Repo.Repository)
if err != nil {
log.Error("GetRepoLicenses failed: %v", err)
ctx.APIErrorInternal(err)
return
}
resp := make([]string, len(licenses))
for i := range licenses {
resp[i] = licenses[i].License
}
ctx.JSON(http.StatusOK, resp)
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"testing"
"gitea.dev/models/unittest"
"gitea.dev/modules/setting"
webhook_service "gitea.dev/services/webhook"
)
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
SetUp: func() error {
setting.LoadQueueSettings()
return webhook_service.Init()
},
})
}
+275
View File
@@ -0,0 +1,275 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
gocontext "context"
"errors"
"fmt"
"net/http"
"strings"
"gitea.dev/models/db"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/graceful"
"gitea.dev/modules/lfs"
"gitea.dev/modules/log"
base "gitea.dev/modules/migration"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/convert"
"gitea.dev/services/migrations"
notify_service "gitea.dev/services/notify"
repo_service "gitea.dev/services/repository"
)
// Migrate migrate remote git repository to gitea
func Migrate(ctx *context.APIContext) {
// swagger:operation POST /repos/migrate repository repoMigrate
// ---
// summary: Migrate a remote git repository
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/MigrateRepoOptions"
// responses:
// "201":
// "$ref": "#/responses/Repository"
// "403":
// "$ref": "#/responses/forbidden"
// "409":
// description: The repository with the same name already exists.
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.MigrateRepoOptions)
// get repoOwner
var (
repoOwner *user_model.User
err error
)
if len(form.RepoOwner) != 0 {
repoOwner, err = user_model.GetUserByName(ctx, form.RepoOwner)
} else if form.RepoOwnerID != 0 {
repoOwner, err = user_model.GetUserByID(ctx, form.RepoOwnerID)
} else {
repoOwner = ctx.Doer
}
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if !ctx.Doer.IsAdmin {
if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID {
ctx.APIError(http.StatusForbidden, "Given user is not an organization.")
return
}
if repoOwner.IsOrganization() {
// Check ownership of organization.
isOwner, err := organization.OrgFromUser(repoOwner).IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
} else if !isOwner {
ctx.APIError(http.StatusForbidden, "Given user is not owner of organization.")
return
}
}
}
remoteAddr, err := git.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword)
if err == nil {
err = migrations.IsMigrateURLAllowed(remoteAddr, ctx.Doer)
}
if err != nil {
handleRemoteAddrError(ctx, err)
return
}
gitServiceType := convert.ToGitServiceType(form.Service)
if form.Mirror && setting.Mirror.DisableNewPull {
ctx.APIError(http.StatusForbidden, errors.New("the site administrator has disabled the creation of new pull mirrors"))
return
}
if setting.Repository.DisableMigrations {
ctx.APIError(http.StatusForbidden, errors.New("the site administrator has disabled migrations"))
return
}
form.LFS = form.LFS && setting.LFS.StartServer
if form.LFS && len(form.LFSEndpoint) > 0 {
ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
if ep == nil {
ctx.APIErrorInternal(errors.New("the LFS endpoint is not valid"))
return
}
err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer)
if err != nil {
handleRemoteAddrError(ctx, err)
return
}
}
opts := migrations.MigrateOptions{
OriginalURL: form.CloneAddr,
CloneAddr: remoteAddr,
RepoName: form.RepoName,
Description: form.Description,
Private: form.Private || setting.Repository.ForcePrivate,
Mirror: form.Mirror,
LFS: form.LFS,
LFSEndpoint: form.LFSEndpoint,
AuthUsername: form.AuthUsername,
AuthPassword: form.AuthPassword,
AuthToken: form.AuthToken,
Wiki: form.Wiki,
Issues: form.Issues,
Milestones: form.Milestones,
Labels: form.Labels,
Comments: form.Issues || form.PullRequests,
PullRequests: form.PullRequests,
Releases: form.Releases,
GitServiceType: gitServiceType,
MirrorInterval: form.MirrorInterval,
}
if opts.Mirror {
opts.Issues = false
opts.Milestones = false
opts.Labels = false
opts.Comments = false
opts.PullRequests = false
opts.Releases = false
}
if gitServiceType == api.CodeCommitService {
opts.AWSAccessKeyID = form.AWSAccessKeyID
opts.AWSSecretAccessKey = form.AWSSecretAccessKey
}
createdRepo, err := repo_service.CreateRepositoryDirectly(ctx, ctx.Doer, repoOwner, repo_service.CreateRepoOptions{
Name: opts.RepoName,
Description: opts.Description,
OriginalURL: form.CloneAddr,
GitServiceType: gitServiceType,
IsPrivate: opts.Private || setting.Repository.ForcePrivate,
IsMirror: opts.Mirror,
Status: repo_model.RepositoryBeingMigrated,
}, false)
if err != nil {
handleMigrateError(ctx, repoOwner, err)
return
}
opts.MigrateToRepoID = createdRepo.ID
doLongTimeMigrate := func(ctx gocontext.Context, doer *user_model.User) (migratedRepo *repo_model.Repository, retErr error) {
defer func() {
if e := recover(); e != nil {
log.Error("MigrateRepository panic: %v\n%s", e, log.Stack(2))
if errDelete := repo_service.DeleteRepositoryDirectly(ctx, createdRepo.ID); errDelete != nil {
log.Error("Unable to delete repo after MigrateRepository panic: %v", errDelete)
}
retErr = errors.New("MigrateRepository panic") // no idea why it would happen, just legacy code
}
}()
migratedRepo, err := migrations.MigrateRepository(ctx, doer, repoOwner.Name, opts, nil)
if err != nil {
return nil, err
}
notify_service.MigrateRepository(ctx, doer, repoOwner, migratedRepo)
return migratedRepo, nil
}
// use a background context, don't cancel the migration even if the client goes away
// HammerContext doesn't seem right (from https://github.com/go-gitea/gitea/pull/9335/files)
// There are other abuses, maybe most HammerContext abuses should be fixed together in the future.
migratedRepo, err := doLongTimeMigrate(graceful.GetManager().HammerContext(), ctx.Doer)
if err != nil {
handleMigrateError(ctx, repoOwner, err)
return
}
ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, migratedRepo, access_model.Permission{AccessMode: perm.AccessModeAdmin}))
}
func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, err error) {
switch {
case repo_model.IsErrRepoAlreadyExist(err):
ctx.APIError(http.StatusConflict, "The repository with the same name already exists.")
case repo_model.IsErrRepoFilesAlreadyExist(err):
ctx.APIError(http.StatusConflict, "Files already exist for this repository. Adopt them or delete them.")
case migrations.IsRateLimitError(err):
ctx.APIError(http.StatusUnprocessableEntity, "Remote visit addressed rate limitation.")
case migrations.IsTwoFactorAuthError(err):
ctx.APIError(http.StatusUnprocessableEntity, "Remote visit required two factors authentication.")
case repo_model.IsErrReachLimitOfRepo(err):
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("You have already reached your limit of %d repositories.", repoOwner.MaxCreationLimit()))
case db.IsErrNameReserved(err):
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("The username '%s' is reserved.", err.(db.ErrNameReserved).Name))
case db.IsErrNameCharsNotAllowed(err):
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("The username '%s' contains invalid characters.", err.(db.ErrNameCharsNotAllowed).Name))
case db.IsErrNamePatternNotAllowed(err):
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(db.ErrNamePatternNotAllowed).Pattern))
case git.IsErrInvalidCloneAddr(err):
ctx.APIError(http.StatusUnprocessableEntity, err)
case base.IsErrNotSupported(err):
ctx.APIError(http.StatusUnprocessableEntity, err)
default:
err = util.SanitizeErrorCredentialURLs(err)
if strings.Contains(err.Error(), "Authentication failed") ||
strings.Contains(err.Error(), "Bad credentials") ||
strings.Contains(err.Error(), "could not read Username") {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Authentication failed: %v.", err))
} else if strings.Contains(err.Error(), "fatal:") {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Migration failed: %v.", err))
} else {
ctx.APIErrorInternal(err)
}
}
}
func handleRemoteAddrError(ctx *context.APIContext, err error) {
if git.IsErrInvalidCloneAddr(err) {
addrErr := err.(*git.ErrInvalidCloneAddr)
switch {
case addrErr.IsURLError:
ctx.APIError(http.StatusUnprocessableEntity, "The provided URL is invalid.")
case addrErr.IsPermissionDenied:
if addrErr.LocalPath {
ctx.APIError(http.StatusUnprocessableEntity, "You are not allowed to import local repositories.")
} else {
ctx.APIError(http.StatusUnprocessableEntity, "You can not import from disallowed hosts.")
}
case addrErr.IsInvalidPath:
ctx.APIError(http.StatusUnprocessableEntity, "Invalid local path, it does not exist or not a directory.")
default:
ctx.APIErrorInternal(fmt.Errorf("unknown error type (ErrInvalidCloneAddr): %w", err))
}
} else {
ctx.APIErrorInternal(err)
}
}
+301
View File
@@ -0,0 +1,301 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"strconv"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
api "gitea.dev/modules/structs"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/routers/common"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListMilestones list milestones for a repository
func ListMilestones(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/milestones issue issueGetMilestonesList
// ---
// summary: Get all of a repository's opened milestones
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: state
// in: query
// description: Milestone state, Recognized values are open, closed and all. Defaults to "open"
// type: string
// - name: name
// in: query
// description: filter by milestone name
// type: string
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/MilestoneList"
// "404":
// "$ref": "#/responses/notFound"
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
milestones, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID,
IsClosed: isClosed,
Name: ctx.FormString("name"),
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiMilestones := make([]*api.Milestone, len(milestones))
for i := range milestones {
apiMilestones[i] = convert.ToAPIMilestone(milestones[i])
}
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &apiMilestones)
}
// GetMilestone get a milestone for a repository by ID and if not available by name
func GetMilestone(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/milestones/{id} issue issueGetMilestone
// ---
// summary: Get a milestone
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: the milestone to get, identified by ID and if not available by name
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Milestone"
// "404":
// "$ref": "#/responses/notFound"
milestone := getMilestoneByIDOrName(ctx)
if ctx.Written() {
return
}
ctx.JSON(http.StatusOK, convert.ToAPIMilestone(milestone))
}
// CreateMilestone create a milestone for a repository
func CreateMilestone(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/milestones issue issueCreateMilestone
// ---
// summary: Create a milestone
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateMilestoneOption"
// responses:
// "201":
// "$ref": "#/responses/Milestone"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.CreateMilestoneOption)
var deadlineUnix int64
if form.Deadline != nil {
deadlineUnix = form.Deadline.Unix()
}
milestone := &issues_model.Milestone{
RepoID: ctx.Repo.Repository.ID,
Name: form.Title,
Content: form.Description,
DeadlineUnix: timeutil.TimeStamp(deadlineUnix),
}
if form.State == "closed" {
milestone.IsClosed = true
milestone.ClosedDateUnix = timeutil.TimeStampNow()
}
if err := issues_model.NewMilestone(ctx, milestone); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIMilestone(milestone))
}
// EditMilestone modify a milestone for a repository by ID and if not available by name
func EditMilestone(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/milestones/{id} issue issueEditMilestone
// ---
// summary: Update a milestone
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: the milestone to edit, identified by ID and if not available by name
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditMilestoneOption"
// responses:
// "200":
// "$ref": "#/responses/Milestone"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditMilestoneOption)
milestone := getMilestoneByIDOrName(ctx)
if ctx.Written() {
return
}
if len(form.Title) > 0 {
milestone.Name = form.Title
}
if form.Description != nil {
milestone.Content = *form.Description
}
milestone.DeadlineUnix, _ = common.ParseAPIDeadlineToEndOfDay(form.Deadline)
oldIsClosed := milestone.IsClosed
if form.State != nil {
milestone.IsClosed = *form.State == string(api.StateClosed)
}
if err := issues_model.UpdateMilestone(ctx, milestone, oldIsClosed); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToAPIMilestone(milestone))
}
// DeleteMilestone delete a milestone for a repository by ID and if not available by name
func DeleteMilestone(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/milestones/{id} issue issueDeleteMilestone
// ---
// summary: Delete a milestone
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: the milestone to delete, identified by ID and if not available by name
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
m := getMilestoneByIDOrName(ctx)
if ctx.Written() {
return
}
if err := issues_model.DeleteMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, m.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// getMilestoneByIDOrName get milestone by ID and if not available by name
func getMilestoneByIDOrName(ctx *context.APIContext) *issues_model.Milestone {
mile := ctx.PathParam("id")
mileID, _ := strconv.ParseInt(mile, 0, 64)
if mileID != 0 {
milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, mileID)
if err == nil {
return milestone
} else if !issues_model.IsErrMilestoneNotExist(err) {
ctx.APIErrorInternal(err)
return nil
}
}
milestone, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, mile)
if err != nil {
if issues_model.IsErrMilestoneNotExist(err) {
ctx.APIErrorNotFound()
return nil
}
ctx.APIErrorInternal(err)
return nil
}
return milestone
}
+415
View File
@@ -0,0 +1,415 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
"strings"
"time"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/modules/git"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
"gitea.dev/services/migrations"
mirror_service "gitea.dev/services/mirror"
)
// MirrorSync adds a mirrored repository to the sync queue
func MirrorSync(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/mirror-sync repository repoMirrorSync
// ---
// summary: Sync a mirrored repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo to sync
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo to sync
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
if !ctx.Repo.Permission.CanWrite(unit.TypeCode) {
ctx.APIError(http.StatusForbidden, "Must have write access")
}
if !setting.Mirror.Enabled {
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
return
}
if _, err := repo_model.GetMirrorByRepoID(ctx, repo.ID); err != nil {
if errors.Is(err, repo_model.ErrMirrorNotExist) {
ctx.APIError(http.StatusBadRequest, "Repository is not a mirror")
return
}
ctx.APIErrorInternal(err)
return
}
mirror_service.AddPullMirrorToQueue(repo.ID)
ctx.Status(http.StatusOK)
}
// PushMirrorSync adds all push mirrored repositories to the sync queue
func PushMirrorSync(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/push_mirrors-sync repository repoPushMirrorSync
// ---
// summary: Sync all push mirrored repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo to sync
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo to sync
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/empty"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
if !setting.Mirror.Enabled {
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
return
}
// Get All push mirrors of a specific repo
pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
if err != nil {
ctx.APIError(http.StatusNotFound, err)
return
}
failedPushMirrors := make([]string, 0)
for _, mirror := range pushMirrors {
ok := mirror_service.SyncPushMirror(ctx, mirror.ID)
if !ok {
failedPushMirrors = append(failedPushMirrors, mirror.RemoteName)
}
}
if len(failedPushMirrors) != 0 {
ctx.APIError(http.StatusUnprocessableEntity, "error occurred when syncing push mirrors: "+strings.Join(failedPushMirrors, ", "))
return
}
ctx.Status(http.StatusOK)
}
// ListPushMirrors get list of push mirrors of a repository
func ListPushMirrors(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/push_mirrors repository repoListPushMirrors
// ---
// summary: Get all push mirrors of the repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/PushMirrorList"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
if !setting.Mirror.Enabled {
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
return
}
repo := ctx.Repo.Repository
// Get all push mirrors for the specified repository.
pushMirrors, count, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, utils.GetListOptions(ctx))
if err != nil {
ctx.APIError(http.StatusNotFound, err)
return
}
responsePushMirrors := make([]*api.PushMirror, 0, len(pushMirrors))
for _, mirror := range pushMirrors {
m, err := convert.ToPushMirror(ctx, mirror)
if err == nil {
responsePushMirrors = append(responsePushMirrors, m)
}
}
ctx.SetLinkHeader(int64(len(responsePushMirrors)), utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, responsePushMirrors)
}
// GetPushMirrorByName get push mirror of a repository by name
func GetPushMirrorByName(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/push_mirrors/{name} repository repoGetPushMirrorByRemoteName
// ---
// summary: Get push mirror of the repository by remoteName
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: name
// in: path
// description: remote name of push mirror
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/PushMirror"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
if !setting.Mirror.Enabled {
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
return
}
mirrorName := ctx.PathParam("name")
// Get push mirror of a specific repo by remoteName
pushMirror, exist, err := db.Get[repo_model.PushMirror](ctx, repo_model.PushMirrorOptions{
RepoID: ctx.Repo.Repository.ID,
RemoteName: mirrorName,
}.ToConds())
if err != nil {
ctx.APIErrorInternal(err)
return
} else if !exist {
ctx.APIError(http.StatusNotFound, nil)
return
}
m, err := convert.ToPushMirror(ctx, pushMirror)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, m)
}
// AddPushMirror adds a push mirror to a repository
func AddPushMirror(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/push_mirrors repository repoAddPushMirror
// ---
// summary: add a push mirror to the repository
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreatePushMirrorOption"
// responses:
// "200":
// "$ref": "#/responses/PushMirror"
// "403":
// "$ref": "#/responses/forbidden"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
if !setting.Mirror.Enabled {
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
return
}
pushMirror := web.GetForm(ctx).(*api.CreatePushMirrorOption)
CreatePushMirror(ctx, pushMirror)
}
// DeletePushMirrorByRemoteName deletes a push mirror from a repository by remoteName
func DeletePushMirrorByRemoteName(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/push_mirrors/{name} repository repoDeletePushMirror
// ---
// summary: deletes a push mirror from a repository by remoteName
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: name
// in: path
// description: remote name of the pushMirror
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "400":
// "$ref": "#/responses/error"
if !setting.Mirror.Enabled {
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
return
}
remoteName := ctx.PathParam("name")
// Delete push mirror on repo by name.
err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: remoteName})
if err != nil {
ctx.APIError(http.StatusNotFound, err)
return
}
ctx.Status(http.StatusNoContent)
}
func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirrorOption) {
repo := ctx.Repo.Repository
interval, err := time.ParseDuration(mirrorOption.Interval)
if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
ctx.APIError(http.StatusBadRequest, err)
return
}
address, err := git.ParseRemoteAddr(mirrorOption.RemoteAddress, mirrorOption.RemoteUsername, mirrorOption.RemotePassword)
if err == nil {
err = migrations.IsMigrateURLAllowed(address, ctx.ContextUser)
}
if err != nil {
HandleRemoteAddressError(ctx, err)
return
}
remoteSuffix := util.CryptoRandomString(10)
remoteAddress, err := util.SanitizeURL(mirrorOption.RemoteAddress)
if err != nil {
ctx.APIErrorInternal(err)
return
}
pushMirror := &repo_model.PushMirror{
RepoID: repo.ID,
Repo: repo,
RemoteName: "remote_mirror_" + remoteSuffix,
Interval: interval,
SyncOnCommit: mirrorOption.SyncOnCommit,
RemoteAddress: remoteAddress,
}
if err = db.Insert(ctx, pushMirror); err != nil {
ctx.APIErrorInternal(err)
return
}
// if the registration of the push mirrorOption fails remove it from the database
if err = mirror_service.AddPushMirrorRemote(ctx, pushMirror, address); err != nil {
if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: pushMirror.ID, RepoID: pushMirror.RepoID}); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.APIErrorInternal(err)
return
}
m, err := convert.ToPushMirror(ctx, pushMirror)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, m)
}
func HandleRemoteAddressError(ctx *context.APIContext, err error) {
if git.IsErrInvalidCloneAddr(err) {
addrErr := err.(*git.ErrInvalidCloneAddr)
switch {
case addrErr.IsProtocolInvalid:
ctx.APIError(http.StatusBadRequest, "Invalid mirror protocol")
case addrErr.IsURLError:
ctx.APIError(http.StatusBadRequest, "Invalid Url ")
case addrErr.IsPermissionDenied:
ctx.APIError(http.StatusUnauthorized, "Permission denied")
default:
ctx.APIError(http.StatusBadRequest, "Unknown error")
}
return
}
}
+40
View File
@@ -0,0 +1,40 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"testing"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
// TestPushMirrorSync verifies the endpoint attempts every push mirror instead
// of aborting on the first failure, reporting all failed remotes with a 422.
// Each remote name is not a configured git remote, so SyncPushMirror fails fast
// without any network access.
func TestPushMirrorSync(t *testing.T) {
unittest.PrepareTestEnv(t)
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
for _, remoteName := range []string{"broken_remote_1", "broken_remote_2"} {
assert.NoError(t, db.Insert(t.Context(), &repo_model.PushMirror{RepoID: 1, RemoteName: remoteName}))
}
ctx, resp := contexttest.MockAPIContext(t, "user2/repo1")
contexttest.LoadRepo(t, ctx, 1)
PushMirrorSync(ctx)
assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus())
assert.Contains(t, resp.Body.String(), "broken_remote_1")
assert.Contains(t, resp.Body.String(), "broken_remote_2")
}
+104
View File
@@ -0,0 +1,104 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
"gitea.dev/modules/git"
api "gitea.dev/modules/structs"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// GetNote Get a note corresponding to a single commit from a repository
func GetNote(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/git/notes/{sha} repository repoGetNote
// ---
// summary: Get a note corresponding to a single commit from a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: sha
// in: path
// description: a git ref or commit sha
// type: string
// required: true
// - name: verification
// in: query
// description: include verification for every commit (disable for speedup, default 'true')
// type: boolean
// - name: files
// in: query
// description: include a list of affected files for every commit (disable for speedup, default 'true')
// type: boolean
// responses:
// "200":
// "$ref": "#/responses/Note"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
sha := ctx.PathParam("sha")
if !git.IsValidRefPattern(sha) {
ctx.APIError(http.StatusUnprocessableEntity, "no valid ref or sha: "+sha)
return
}
getNote(ctx, sha)
}
func getNote(ctx *context.APIContext, identifier string) {
if ctx.Repo.GitRepo == nil {
ctx.APIErrorInternal(errors.New("no open git repo"))
return
}
commitID, err := ctx.Repo.GitRepo.ConvertToGitID(identifier)
if err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
var note git.Note
if err := git.GetNote(ctx, ctx.Repo.GitRepo, commitID.String(), &note); err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorNotFound("commit doesn't exist: " + identifier)
return
}
ctx.APIErrorInternal(err)
return
}
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
files := ctx.FormString("files") == "" || ctx.FormBool("files")
cmt, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, note.Commit, nil,
convert.ToCommitOptions{
Stat: true,
Verification: verification,
Files: files,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiNote := api.Note{Message: string(note.Message), Commit: cmt}
ctx.JSON(http.StatusOK, apiNote)
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/services/context"
"gitea.dev/services/repository/files"
)
// ApplyDiffPatch handles API call for applying a patch
func ApplyDiffPatch(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/diffpatch repository repoApplyDiffPatch
// ---
// summary: Apply diff patch to repository
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/ApplyDiffPatchFileOptions"
// responses:
// "200":
// "$ref": "#/responses/FileResponse"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
apiOpts, changeRepoFileOpts := getAPIChangeRepoFileOptions[*api.ApplyDiffPatchFileOptions](ctx)
opts := &files.ApplyDiffPatchOptions{
Content: apiOpts.Content,
Message: util.IfZero(apiOpts.Message, "apply-patch"),
OldBranch: changeRepoFileOpts.OldBranch,
NewBranch: changeRepoFileOpts.NewBranch,
Committer: changeRepoFileOpts.Committer,
Author: changeRepoFileOpts.Author,
Dates: changeRepoFileOpts.Dates,
Signoff: changeRepoFileOpts.Signoff,
}
fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts)
if err != nil {
handleChangeRepoFilesError(ctx, err)
} else {
ctx.JSON(http.StatusCreated, fileResponse)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+443
View File
@@ -0,0 +1,443 @@
// Copyright 2016 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"fmt"
"net/http"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/modules/git"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
release_service "gitea.dev/services/release"
)
func canAccessReleaseDraft(ctx *context.APIContext) bool {
if !ctx.IsSigned || !ctx.Repo.Permission.CanWrite(unit.TypeReleases) {
return false
}
if ctx.Data["IsApiToken"] != true {
// not API token request, the request is from a user session with write access
return true
}
// the request is from an access token with scope
scope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
requiredScopes := auth_model.GetRequiredScopes(auth_model.Write, auth_model.AccessTokenScopeCategoryRepository)
allow, _ := scope.HasScope(requiredScopes...) // err (invalid token) can be safely ignored
return allow
}
// GetRelease get a single release of a repository
func GetRelease(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/releases/{id} repository repoGetRelease
// ---
// summary: Get a release
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the release to get
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Release"
// "404":
// "$ref": "#/responses/notFound"
id := ctx.PathParamInt64("id")
release, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id)
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
ctx.APIErrorInternal(err)
return
}
if err != nil && repo_model.IsErrReleaseNotExist(err) || release.IsTag {
ctx.APIErrorNotFound()
return
}
if release.IsDraft && !canAccessReleaseDraft(ctx) { // only the users with write access can see draft releases
ctx.APIErrorNotFound()
return
}
if err := release.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, release))
}
// GetLatestRelease gets the most recent non-prerelease, non-draft release of a repository, sorted by created_at
func GetLatestRelease(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/releases/latest repository repoGetLatestRelease
// ---
// summary: Gets the most recent non-prerelease, non-draft release of a repository, sorted by created_at
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Release"
// "404":
// "$ref": "#/responses/notFound"
release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
ctx.APIErrorInternal(err)
return
}
if err != nil && repo_model.IsErrReleaseNotExist(err) ||
release.IsTag || release.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return
}
if err := release.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, release))
}
// ListReleases list a repository's releases
func ListReleases(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/releases repository repoListReleases
// ---
// summary: List a repo's releases
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: draft
// in: query
// description: filter (exclude / include) drafts, if you don't have repo write access none will show
// type: boolean
// - name: pre-release
// in: query
// description: filter (exclude / include) pre-releases
// type: boolean
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/ReleaseList"
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
if ctx.Written() {
return
}
opts := repo_model.FindReleasesOptions{
ListOptions: listOptions,
IncludeDrafts: canAccessReleaseDraft(ctx),
IncludeTags: false,
IsDraft: ctx.FormOptionalBool("draft"),
IsPreRelease: ctx.FormOptionalBool("pre-release"),
RepoID: ctx.Repo.Repository.ID,
}
releases, err := db.Find[repo_model.Release](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
rels := make([]*api.Release, len(releases))
for i, release := range releases {
if err := release.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
rels[i] = convert.ToAPIRelease(ctx, ctx.Repo.Repository, release)
}
filteredCount, err := db.Count[repo_model.Release](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(filteredCount, listOptions.PageSize)
ctx.SetTotalCountHeader(filteredCount)
ctx.JSON(http.StatusOK, rels)
}
// CreateRelease create a release
func CreateRelease(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/releases repository repoCreateRelease
// ---
// summary: Create a release
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateReleaseOption"
// responses:
// "201":
// "$ref": "#/responses/Release"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateReleaseOption)
if ctx.Repo.Repository.IsEmpty {
ctx.APIError(http.StatusUnprocessableEntity, errors.New("repo is empty"))
return
}
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName)
if err != nil {
if !repo_model.IsErrReleaseNotExist(err) {
ctx.APIErrorInternal(err)
return
}
// If target is not provided use default branch
if len(form.Target) == 0 {
form.Target = ctx.Repo.Repository.DefaultBranch
}
rel = &repo_model.Release{
RepoID: ctx.Repo.Repository.ID,
PublisherID: ctx.Doer.ID,
Publisher: ctx.Doer,
TagName: form.TagName,
Target: form.Target,
Title: form.Title,
Note: form.Note,
IsDraft: form.IsDraft,
IsPrerelease: form.IsPrerelease,
IsTag: false,
Repo: ctx.Repo.Repository,
}
// GitHub doesn't have "tag_message", GitLab has: https://docs.gitlab.com/api/releases/#create-a-release
// It doesn't need to be the same as the "release note"
if err := release_service.CreateRelease(ctx.Repo.GitRepo, rel, nil, form.TagMessage); err != nil {
if repo_model.IsErrReleaseAlreadyExist(err) {
ctx.APIError(http.StatusConflict, err)
} else if release_service.IsErrProtectedTagName(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else if git.IsErrNotExist(err) {
ctx.APIError(http.StatusNotFound, fmt.Errorf("target \"%v\" not found: %w", rel.Target, err))
} else {
ctx.APIErrorInternal(err)
}
return
}
} else {
if !rel.IsTag {
ctx.APIError(http.StatusConflict, "Release is has no Tag")
return
}
rel.Title = form.Title
rel.Note = form.Note
rel.IsDraft = form.IsDraft
rel.IsPrerelease = form.IsPrerelease
rel.PublisherID = ctx.Doer.ID
rel.IsTag = false
rel.Repo = ctx.Repo.Repository
rel.Publisher = ctx.Doer
rel.Target = form.Target
if err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil {
ctx.APIErrorInternal(err)
return
}
}
ctx.JSON(http.StatusCreated, convert.ToAPIRelease(ctx, ctx.Repo.Repository, rel))
}
// EditRelease edit a release
func EditRelease(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/releases/{id} repository repoEditRelease
// ---
// summary: Update a release
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the release to edit
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditReleaseOption"
// responses:
// "200":
// "$ref": "#/responses/Release"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditReleaseOption)
id := ctx.PathParamInt64("id")
rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id)
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
ctx.APIErrorInternal(err)
return
}
if err != nil && repo_model.IsErrReleaseNotExist(err) || rel.IsTag {
ctx.APIErrorNotFound()
return
}
if len(form.TagName) > 0 {
rel.TagName = form.TagName
}
if len(form.Target) > 0 {
rel.Target = form.Target
}
if len(form.Title) > 0 {
rel.Title = form.Title
}
if len(form.Note) > 0 {
rel.Note = form.Note
}
if form.IsDraft != nil {
rel.IsDraft = *form.IsDraft
}
if form.IsPrerelease != nil {
rel.IsPrerelease = *form.IsPrerelease
}
if err := release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil {
ctx.APIErrorInternal(err)
return
}
// reload data from database
rel, err = repo_model.GetReleaseByID(ctx, id)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err := rel.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, rel))
}
// DeleteRelease delete a release from a repository
func DeleteRelease(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/releases/{id} repository repoDeleteRelease
// ---
// summary: Delete a release
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the release to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
id := ctx.PathParamInt64("id")
rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id)
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
ctx.APIErrorInternal(err)
return
}
if err != nil && repo_model.IsErrReleaseNotExist(err) || rel.IsTag {
ctx.APIErrorNotFound()
return
}
if err := release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, rel, ctx.Doer, false); err != nil {
if release_service.IsErrProtectedTagName(err) {
ctx.APIError(http.StatusUnprocessableEntity, "user not allowed to delete protected tag")
return
}
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+415
View File
@@ -0,0 +1,415 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"net/http"
"strings"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
attachment_service "gitea.dev/services/attachment"
"gitea.dev/services/context"
"gitea.dev/services/context/upload"
"gitea.dev/services/convert"
)
func checkReleaseMatchRepo(ctx *context.APIContext, releaseID int64) bool {
release, err := repo_model.GetReleaseByID(ctx, releaseID)
if err != nil {
if repo_model.IsErrReleaseNotExist(err) {
ctx.APIErrorNotFound()
return false
}
ctx.APIErrorInternal(err)
return false
}
if release.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return false
}
if release.IsDraft && !canAccessReleaseDraft(ctx) {
ctx.APIErrorNotFound()
return false
}
return true
}
// GetReleaseAttachment gets a single attachment of the release
func GetReleaseAttachment(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/releases/{id}/assets/{attachment_id} repository repoGetReleaseAttachment
// ---
// summary: Get a release attachment
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the release
// type: integer
// format: int64
// required: true
// - name: attachment_id
// in: path
// description: id of the attachment to get
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/Attachment"
// "404":
// "$ref": "#/responses/notFound"
releaseID := ctx.PathParamInt64("id")
if !checkReleaseMatchRepo(ctx, releaseID) {
return
}
attachID := ctx.PathParamInt64("attachment_id")
attach, err := repo_model.GetAttachmentByID(ctx, attachID)
if err != nil {
if repo_model.IsErrAttachmentNotExist(err) {
ctx.APIErrorNotFound()
return
}
ctx.APIErrorInternal(err)
return
}
if attach.ReleaseID != releaseID {
log.Info("User requested attachment is not in release, release_id %v, attachment_id: %v", releaseID, attachID)
ctx.APIErrorNotFound()
return
}
// FIXME Should prove the existence of the given repo, but results in unnecessary database requests
ctx.JSON(http.StatusOK, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
}
// ListReleaseAttachments lists all attachments of the release
func ListReleaseAttachments(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/releases/{id}/assets repository repoListReleaseAttachments
// ---
// summary: List release's attachments
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the release
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/AttachmentList"
// "404":
// "$ref": "#/responses/notFound"
releaseID := ctx.PathParamInt64("id")
release, err := repo_model.GetReleaseByID(ctx, releaseID)
if err != nil {
if repo_model.IsErrReleaseNotExist(err) {
ctx.APIErrorNotFound()
return
}
ctx.APIErrorInternal(err)
return
}
if release.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return
}
if release.IsDraft && !canAccessReleaseDraft(ctx) {
ctx.APIErrorNotFound()
return
}
if err := release.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, release).Attachments)
}
// CreateReleaseAttachment creates an attachment and saves the given file
func CreateReleaseAttachment(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/releases/{id}/assets repository repoCreateReleaseAttachment
// ---
// summary: Create a release attachment
// produces:
// - application/json
// consumes:
// - multipart/form-data
// - application/octet-stream
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the release
// type: integer
// format: int64
// required: true
// - name: name
// in: query
// description: name of the attachment
// type: string
// required: false
// - name: attachment
// in: formData
// description: attachment to upload
// type: file
// required: false
// responses:
// "201":
// "$ref": "#/responses/Attachment"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/error"
// Check if attachments are enabled
if !setting.Attachment.Enabled {
ctx.APIErrorNotFound("Attachment is not enabled")
return
}
// Check if release exists an load release
releaseID := ctx.PathParamInt64("id")
if !checkReleaseMatchRepo(ctx, releaseID) {
return
}
// Get uploaded file from request
var filename string
var uploaderFile *attachment_service.UploaderFile
if strings.HasPrefix(strings.ToLower(ctx.Req.Header.Get("Content-Type")), "multipart/form-data") {
file, header, err := ctx.Req.FormFile("attachment")
if err != nil {
ctx.APIErrorInternal(err)
return
}
defer file.Close()
filename = header.Filename
if name := ctx.FormString("name"); name != "" {
filename = name
}
uploaderFile = attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
} else {
filename = ctx.FormString("name")
uploaderFile = attachment_service.NewLimitedUploaderMaxBytesReader(ctx.Req.Body, ctx.Resp)
}
if filename == "" {
ctx.APIError(http.StatusBadRequest, "Could not determine name of attachment.")
return
}
// Create a new attachment and save the file
attach, err := attachment_service.UploadAttachmentForRelease(ctx, uploaderFile, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
ReleaseID: releaseID,
})
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusBadRequest, err)
return
}
if errors.Is(err, util.ErrContentTooLarge) {
ctx.APIError(http.StatusRequestEntityTooLarge, err)
return
}
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
}
// EditReleaseAttachment updates the given attachment
func EditReleaseAttachment(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/releases/{id}/assets/{attachment_id} repository repoEditReleaseAttachment
// ---
// summary: Edit a release attachment
// produces:
// - application/json
// consumes:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the release
// type: integer
// format: int64
// required: true
// - name: attachment_id
// in: path
// description: id of the attachment to edit
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditAttachmentOptions"
// responses:
// "201":
// "$ref": "#/responses/Attachment"
// "422":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.EditAttachmentOptions)
// Check if release exists an load release
releaseID := ctx.PathParamInt64("id")
if !checkReleaseMatchRepo(ctx, releaseID) {
return
}
attachID := ctx.PathParamInt64("attachment_id")
attach, err := repo_model.GetAttachmentByID(ctx, attachID)
if err != nil {
if repo_model.IsErrAttachmentNotExist(err) {
ctx.APIErrorNotFound()
return
}
ctx.APIErrorInternal(err)
return
}
if attach.ReleaseID != releaseID {
log.Info("User requested attachment is not in release, release_id %v, attachment_id: %v", releaseID, attachID)
ctx.APIErrorNotFound()
return
}
// FIXME Should prove the existence of the given repo, but results in unnecessary database requests
if form.Name != "" {
attach.Name = form.Name
}
if err := attachment_service.UpdateAttachment(ctx, setting.Repository.Release.AllowedTypes, attach); err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
}
// DeleteReleaseAttachment delete a given attachment
func DeleteReleaseAttachment(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/releases/{id}/assets/{attachment_id} repository repoDeleteReleaseAttachment
// ---
// summary: Delete a release attachment
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the release
// type: integer
// format: int64
// required: true
// - name: attachment_id
// in: path
// description: id of the attachment to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// Check if release exists an load release
releaseID := ctx.PathParamInt64("id")
if !checkReleaseMatchRepo(ctx, releaseID) {
return
}
attachID := ctx.PathParamInt64("attachment_id")
attach, err := repo_model.GetAttachmentByID(ctx, attachID)
if err != nil {
if repo_model.IsErrAttachmentNotExist(err) {
ctx.APIErrorNotFound()
return
}
ctx.APIErrorInternal(err)
return
}
if attach.ReleaseID != releaseID {
log.Info("User requested attachment is not in release, release_id %v, attachment_id: %v", releaseID, attachID)
ctx.APIErrorNotFound()
return
}
if err := repo_model.DeleteAttachment(ctx, attach, true); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+132
View File
@@ -0,0 +1,132 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
repo_model "gitea.dev/models/repo"
unit_model "gitea.dev/models/unit"
"gitea.dev/services/context"
"gitea.dev/services/convert"
release_service "gitea.dev/services/release"
)
// GetReleaseByTag get a single release of a repository by tag name
func GetReleaseByTag(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/releases/tags/{tag} repository repoGetReleaseByTag
// ---
// summary: Get a release by tag name
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: tag
// in: path
// description: tag name of the release to get
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Release"
// "404":
// "$ref": "#/responses/notFound"
tag := ctx.PathParam("tag")
release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tag)
if err != nil {
if repo_model.IsErrReleaseNotExist(err) {
ctx.APIErrorNotFound()
return
}
ctx.APIErrorInternal(err)
return
}
if release.IsTag {
ctx.APIErrorNotFound()
return
}
if release.IsDraft { // only the users with write access can see draft releases
if !ctx.IsSigned || !ctx.Repo.Permission.CanWrite(unit_model.TypeReleases) {
ctx.APIErrorNotFound()
return
}
}
if err = release.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, release))
}
// DeleteReleaseByTag delete a release from a repository by tag name
func DeleteReleaseByTag(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/releases/tags/{tag} repository repoDeleteReleaseByTag
// ---
// summary: Delete a release by tag name
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: tag
// in: path
// description: tag name of the release to delete
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
tag := ctx.PathParam("tag")
release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tag)
if err != nil {
if repo_model.IsErrReleaseNotExist(err) {
ctx.APIErrorNotFound()
return
}
ctx.APIErrorInternal(err)
return
}
if release.IsTag {
ctx.APIErrorNotFound()
return
}
if err = release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, release, ctx.Doer, false); err != nil {
if release_service.IsErrProtectedTagName(err) {
ctx.APIError(http.StatusUnprocessableEntity, "user not allowed to delete protected tag")
return
}
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"testing"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
func TestRepoEdit(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockAPIContext(t, "user2/repo1")
contexttest.LoadRepo(t, ctx, 1)
contexttest.LoadUser(t, ctx, 2)
ctx.Repo.Owner = ctx.Doer
description := "new description"
website := "http://wwww.newwebsite.com"
private := true
hasIssues := false
hasWiki := false
defaultBranch := "master"
hasPullRequests := true
ignoreWhitespaceConflicts := true
allowMerge := false
allowRebase := false
allowRebaseMerge := false
allowSquashMerge := false
allowFastForwardOnlyMerge := false
archived := true
opts := api.EditRepoOption{
Name: &ctx.Repo.Repository.Name,
Description: &description,
Website: &website,
Private: &private,
HasIssues: &hasIssues,
HasWiki: &hasWiki,
DefaultBranch: &defaultBranch,
HasPullRequests: &hasPullRequests,
IgnoreWhitespaceConflicts: &ignoreWhitespaceConflicts,
AllowMerge: &allowMerge,
AllowRebase: &allowRebase,
AllowRebaseMerge: &allowRebaseMerge,
AllowSquash: &allowSquashMerge,
AllowFastForwardOnly: &allowFastForwardOnlyMerge,
Archived: &archived,
}
web.SetForm(ctx, &opts)
Edit(ctx)
assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
ID: 1,
}, unittest.Cond("name = ? AND is_archived = 1", *opts.Name))
}
func TestRepoEditNameChange(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockAPIContext(t, "user2/repo1")
contexttest.LoadRepo(t, ctx, 1)
contexttest.LoadUser(t, ctx, 2)
ctx.Repo.Owner = ctx.Doer
name := "newname"
opts := api.EditRepoOption{
Name: &name,
}
web.SetForm(ctx, &opts)
Edit(ctx)
assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
ID: 1,
}, unittest.Cond("name = ?", opts.Name))
}
+62
View File
@@ -0,0 +1,62 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
repo_model "gitea.dev/models/repo"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListStargazers list a repository's stargazers
func ListStargazers(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/stargazers repository repoListStargazers
// ---
// summary: List a repo's stargazers
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/UserList"
// "404":
// "$ref": "#/responses/notFound"
// "403":
// "$ref": "#/responses/forbidden"
stargazers, err := repo_model.GetStargazers(ctx, ctx.Repo.Repository, utils.GetListOptions(ctx))
if err != nil {
ctx.APIErrorInternal(err)
return
}
users := make([]*api.User, len(stargazers))
for i, stargazer := range stargazers {
users[i] = convert.ToUser(ctx, stargazer, ctx.Doer)
}
ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumStars))
ctx.JSON(http.StatusOK, users)
}
+278
View File
@@ -0,0 +1,278 @@
// Copyright 2017 Gitea. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"fmt"
"net/http"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
commitstatus_service "gitea.dev/services/repository/commitstatus"
)
// NewCommitStatus creates a new CommitStatus
func NewCommitStatus(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/statuses/{sha} repository repoCreateStatus
// ---
// summary: Create a commit status
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: sha
// in: path
// description: sha of the commit
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateStatusOption"
// responses:
// "201":
// "$ref": "#/responses/CommitStatus"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.CreateStatusOption)
sha := ctx.PathParam("sha")
if len(sha) == 0 {
ctx.APIError(http.StatusBadRequest, nil)
return
}
status := &git_model.CommitStatus{
State: form.State,
TargetURL: form.TargetURL,
Description: form.Description,
Context: form.Context,
}
if err := commitstatus_service.CreateCommitStatus(ctx, ctx.Repo.Repository, ctx.Doer, sha, status); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convert.ToCommitStatus(ctx, status))
}
// GetCommitStatuses returns all statuses for any given commit hash
func GetCommitStatuses(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/statuses/{sha} repository repoListStatuses
// ---
// summary: Get a commit's statuses
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: sha
// in: path
// description: sha of the commit
// type: string
// required: true
// - name: sort
// in: query
// description: type of sort
// type: string
// enum: [oldest, recentupdate, leastupdate, leastindex, highestindex]
// required: false
// - name: state
// in: query
// description: type of state
// type: string
// enum: [pending, success, error, failure, warning]
// required: false
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/CommitStatusList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
getCommitStatuses(ctx, ctx.PathParam("sha"))
}
// GetCommitStatusesByRef returns all statuses for any given commit ref
func GetCommitStatusesByRef(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/commits/{ref}/statuses repository repoListStatusesByRef
// ---
// summary: Get a commit's statuses, by branch/tag/commit reference
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: ref
// in: path
// description: name of branch/tag/commit
// type: string
// required: true
// - name: sort
// in: query
// description: type of sort
// type: string
// enum: [oldest, recentupdate, leastupdate, leastindex, highestindex]
// required: false
// - name: state
// in: query
// description: type of state
// type: string
// enum: [pending, success, error, failure, warning]
// required: false
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/CommitStatusList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
refCommit := resolveRefCommit(ctx, ctx.PathParam("ref"), 7)
if ctx.Written() {
return
}
getCommitStatuses(ctx, refCommit.CommitID)
}
func getCommitStatuses(ctx *context.APIContext, commitID string) {
repo := ctx.Repo.Repository
listOptions := utils.GetListOptions(ctx)
statuses, maxResults, err := db.FindAndCount[git_model.CommitStatus](ctx, &git_model.CommitStatusOptions{
ListOptions: listOptions,
RepoID: repo.ID,
SHA: commitID,
SortType: ctx.FormTrim("sort"),
State: ctx.FormTrim("state"),
})
if err != nil {
ctx.APIErrorInternal(fmt.Errorf("GetCommitStatuses[%s, %s, %d]: %w", repo.FullName(), commitID, ctx.FormInt("page"), err))
return
}
apiStatuses := make([]*api.CommitStatus, 0, len(statuses))
for _, status := range statuses {
apiStatuses = append(apiStatuses, convert.ToCommitStatus(ctx, status))
}
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, apiStatuses)
}
// GetCombinedCommitStatusByRef returns the combined status for any given commit hash
func GetCombinedCommitStatusByRef(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/commits/{ref}/status repository repoGetCombinedStatusByRef
// ---
// summary: Get a commit's combined status, by branch/tag/commit reference
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: ref
// in: path
// description: name of branch/tag/commit
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/CombinedStatus"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
refCommit := resolveRefCommit(ctx, ctx.PathParam("ref"), 7)
if ctx.Written() {
return
}
repo := ctx.Repo.Repository
listOptions := utils.GetListOptions(ctx)
statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, refCommit.Commit.ID.String(), listOptions)
if err != nil {
ctx.APIErrorInternal(fmt.Errorf("GetLatestCommitStatus[%s, %s]: %w", repo.FullName(), refCommit.CommitID, err))
return
}
count, err := git_model.CountLatestCommitStatus(ctx, repo.ID, refCommit.Commit.ID.String())
if err != nil {
ctx.APIErrorInternal(fmt.Errorf("CountLatestCommitStatus[%s, %s]: %w", repo.FullName(), refCommit.CommitID, err))
return
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
combiStatus := convert.ToCombinedStatus(ctx, refCommit.Commit.ID.String(), statuses,
convert.ToRepo(ctx, repo, ctx.Repo.Permission))
ctx.JSON(http.StatusOK, combiStatus)
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
repo_model "gitea.dev/models/repo"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListSubscribers list a repo's subscribers (i.e. watchers)
func ListSubscribers(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/subscribers repository repoListSubscribers
// ---
// summary: List a repo's watchers
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/UserList"
// "404":
// "$ref": "#/responses/notFound"
subscribers, err := repo_model.GetRepoWatchers(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx))
if err != nil {
ctx.APIErrorInternal(err)
return
}
users := make([]*api.User, len(subscribers))
for i, subscriber := range subscribers {
users[i] = convert.ToUser(ctx, subscriber, ctx.Doer)
}
ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumWatches))
ctx.JSON(http.StatusOK, users)
}
+641
View File
@@ -0,0 +1,641 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"fmt"
"net/http"
"strings"
git_model "gitea.dev/models/git"
"gitea.dev/models/organization"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
release_service "gitea.dev/services/release"
)
// ListTags list all the tags of a repository
func ListTags(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/tags repository repoListTags
// ---
// summary: List a repository's tags
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results, default maximum page size is 50
// type: integer
// responses:
// "200":
// "$ref": "#/responses/TagList"
// "404":
// "$ref": "#/responses/notFound"
listOpts := utils.GetListOptions(ctx)
tags, total, err := ctx.Repo.GitRepo.GetTagInfos(listOpts.Page, listOpts.PageSize)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiTags := make([]*api.Tag, len(tags))
for i := range tags {
apiTags[i] = convert.ToTag(ctx.Repo.Repository, tags[i])
}
ctx.SetTotalCountHeader(int64(total))
ctx.JSON(http.StatusOK, &apiTags)
}
// GetAnnotatedTag get the tag of a repository.
func GetAnnotatedTag(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/git/tags/{sha} repository GetAnnotatedTag
// ---
// summary: Gets the tag object of an annotated tag (not lightweight tags)
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: sha
// in: path
// description: sha of the tag. The Git tags API only supports annotated tag objects, not lightweight tags.
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/AnnotatedTag"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
sha := ctx.PathParam("sha")
if len(sha) == 0 {
ctx.APIError(http.StatusBadRequest, "SHA not provided")
return
}
tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
commit, err := ctx.Repo.GitRepo.GetTagCommit(tag.Name)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit))
}
// GetTag get the tag of a repository
func GetTag(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/tags/{tag} repository repoGetTag
// ---
// summary: Get the tag of a repository by tag name
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: tag
// in: path
// description: name of tag
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Tag"
// "404":
// "$ref": "#/responses/notFound"
tagName := ctx.PathParam("*")
tag, err := ctx.Repo.GitRepo.GetTag(tagName)
if err != nil {
ctx.APIErrorNotFound("tag doesn't exist: " + tagName)
return
}
ctx.JSON(http.StatusOK, convert.ToTag(ctx.Repo.Repository, tag))
}
// CreateTag create a new git tag in a repository
func CreateTag(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/tags repository repoCreateTag
// ---
// summary: Create a new git tag in a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateTagOption"
// responses:
// "200":
// "$ref": "#/responses/Tag"
// "404":
// "$ref": "#/responses/notFound"
// "405":
// "$ref": "#/responses/empty"
// "409":
// "$ref": "#/responses/conflict"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.CreateTagOption)
// If target is not provided use default branch
if len(form.Target) == 0 {
form.Target = ctx.Repo.Repository.DefaultBranch
}
commit, err := ctx.Repo.GitRepo.GetCommit(form.Target)
if err != nil {
ctx.APIError(http.StatusNotFound, fmt.Errorf("target not found: %w", err))
return
}
if err := release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, commit.ID.String(), form.TagName, form.Message); err != nil {
if release_service.IsErrTagAlreadyExists(err) {
ctx.APIError(http.StatusConflict, err)
return
}
if release_service.IsErrProtectedTagName(err) {
ctx.APIError(http.StatusUnprocessableEntity, "user not allowed to create protected tag")
return
}
ctx.APIErrorInternal(err)
return
}
tag, err := ctx.Repo.GitRepo.GetTag(form.TagName)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convert.ToTag(ctx.Repo.Repository, tag))
}
// DeleteTag delete a specific tag of in a repository by name
func DeleteTag(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/tags/{tag} repository repoDeleteTag
// ---
// summary: Delete a repository's tag by name
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: tag
// in: path
// description: name of tag to delete
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "405":
// "$ref": "#/responses/empty"
// "409":
// "$ref": "#/responses/conflict"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
tagName := ctx.PathParam("*")
tag, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tagName)
if err != nil {
if repo_model.IsErrReleaseNotExist(err) {
ctx.APIErrorNotFound()
return
}
ctx.APIErrorInternal(err)
return
}
if !tag.IsTag {
ctx.APIError(http.StatusConflict, errors.New("a tag attached to a release cannot be deleted directly"))
return
}
if err = release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, tag, ctx.Doer, true); err != nil {
if release_service.IsErrProtectedTagName(err) {
ctx.APIError(http.StatusUnprocessableEntity, "user not allowed to delete protected tag")
return
}
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// ListTagProtection lists tag protections for a repo
func ListTagProtection(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/tag_protections repository repoListTagProtection
// ---
// summary: List tag protections for a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/TagProtectionList"
repo := ctx.Repo.Repository
pts, err := git_model.GetProtectedTags(ctx, repo.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiPts := make([]*api.TagProtection, len(pts))
for i := range pts {
apiPts[i] = convert.ToTagProtection(ctx, pts[i], repo)
}
ctx.JSON(http.StatusOK, apiPts)
}
// GetTagProtection gets a tag protection
func GetTagProtection(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/tag_protections/{id} repository repoGetTagProtection
// ---
// summary: Get a specific tag protection for the repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the tag protect to get
// type: integer
// required: true
// responses:
// "200":
// "$ref": "#/responses/TagProtection"
// "404":
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
id := ctx.PathParamInt64("id")
pt, err := git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if pt == nil || repo.ID != pt.RepoID {
ctx.APIErrorNotFound()
return
}
ctx.JSON(http.StatusOK, convert.ToTagProtection(ctx, pt, repo))
}
// CreateTagProtection creates a tag protection for a repo
func CreateTagProtection(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/tag_protections repository repoCreateTagProtection
// ---
// summary: Create a tag protections for a repository
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateTagProtectionOption"
// responses:
// "201":
// "$ref": "#/responses/TagProtection"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.CreateTagProtectionOption)
repo := ctx.Repo.Repository
namePattern := strings.TrimSpace(form.NamePattern)
if namePattern == "" {
ctx.APIError(http.StatusBadRequest, "name_pattern are empty")
return
}
if len(form.WhitelistUsernames) == 0 && len(form.WhitelistTeams) == 0 {
ctx.APIError(http.StatusBadRequest, "both whitelist_usernames and whitelist_teams are empty")
return
}
pt, err := git_model.GetProtectedTagByNamePattern(ctx, repo.ID, namePattern)
if err != nil {
ctx.APIErrorInternal(err)
return
} else if pt != nil {
ctx.APIError(http.StatusForbidden, "Tag protection already exist")
return
}
var whitelistUsers, whitelistTeams []int64
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
ctx.APIErrorInternal(err)
return
}
if repo.Owner.IsOrganization() {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
ctx.APIErrorInternal(err)
return
}
}
protectTag := &git_model.ProtectedTag{
RepoID: repo.ID,
NamePattern: strings.TrimSpace(namePattern),
AllowlistUserIDs: whitelistUsers,
AllowlistTeamIDs: whitelistTeams,
}
if err := git_model.InsertProtectedTag(ctx, protectTag); err != nil {
ctx.APIErrorInternal(err)
return
}
pt, err = git_model.GetProtectedTagByID(ctx, protectTag.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convert.ToTagProtection(ctx, pt, repo))
}
// EditTagProtection edits a tag protection for a repo
func EditTagProtection(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/tag_protections/{id} repository repoEditTagProtection
// ---
// summary: Edit a tag protections for a repository. Only fields that are set will be changed
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of protected tag
// type: integer
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditTagProtectionOption"
// responses:
// "200":
// "$ref": "#/responses/TagProtection"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
repo := ctx.Repo.Repository
form := web.GetForm(ctx).(*api.EditTagProtectionOption)
id := ctx.PathParamInt64("id")
pt, err := git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.APIErrorNotFound()
return
}
if form.NamePattern != nil {
pt.NamePattern = *form.NamePattern
}
var whitelistUsers, whitelistTeams []int64
if form.WhitelistTeams != nil {
if repo.Owner.IsOrganization() {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
ctx.APIErrorInternal(err)
return
}
}
pt.AllowlistTeamIDs = whitelistTeams
}
if form.WhitelistUsernames != nil {
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
ctx.APIErrorInternal(err)
return
}
pt.AllowlistUserIDs = whitelistUsers
}
err = git_model.UpdateProtectedTag(ctx, pt)
if err != nil {
ctx.APIErrorInternal(err)
return
}
pt, err = git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.APIErrorInternal(errors.New("new tag protection not found"))
return
}
ctx.JSON(http.StatusOK, convert.ToTagProtection(ctx, pt, repo))
}
// DeleteTagProtection
func DeleteTagProtection(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/tag_protections/{id} repository repoDeleteTagProtection
// ---
// summary: Delete a specific tag protection for the repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of protected tag
// type: integer
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
id := ctx.PathParamInt64("id")
pt, err := git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.APIErrorNotFound()
return
}
err = git_model.DeleteProtectedTag(ctx, pt)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+234
View File
@@ -0,0 +1,234 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"fmt"
"net/http"
"gitea.dev/models/organization"
"gitea.dev/services/context"
"gitea.dev/services/convert"
repo_service "gitea.dev/services/repository"
)
// ListTeams list a repository's teams
func ListTeams(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/teams repository repoListTeams
// ---
// summary: List a repository's teams
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/TeamList"
// "404":
// "$ref": "#/responses/notFound"
if !ctx.Repo.Owner.IsOrganization() {
ctx.APIError(http.StatusMethodNotAllowed, "repo is not owned by an organization")
return
}
teams, err := organization.GetRepoTeams(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiTeams, err := convert.ToTeams(ctx, teams, false)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiTeams)
}
// IsTeam check if a team is assigned to a repository
func IsTeam(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/teams/{team} repository repoCheckTeam
// ---
// summary: Check if a team is assigned to a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: team
// in: path
// description: team name
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Team"
// "404":
// "$ref": "#/responses/notFound"
// "405":
// "$ref": "#/responses/error"
if !ctx.Repo.Owner.IsOrganization() {
ctx.APIError(http.StatusMethodNotAllowed, "repo is not owned by an organization")
return
}
team := getTeamByParam(ctx)
if team == nil {
return
}
if repo_service.HasRepository(ctx, team, ctx.Repo.Repository.ID) {
apiTeam, err := convert.ToTeam(ctx, team)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiTeam)
return
}
ctx.APIErrorNotFound()
}
// AddTeam add a team to a repository
func AddTeam(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/teams/{team} repository repoAddTeam
// ---
// summary: Add a team to a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: team
// in: path
// description: team name
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "422":
// "$ref": "#/responses/validationError"
// "405":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
changeRepoTeam(ctx, true)
}
// DeleteTeam delete a team from a repository
func DeleteTeam(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/teams/{team} repository repoDeleteTeam
// ---
// summary: Delete a team from a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: team
// in: path
// description: team name
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "422":
// "$ref": "#/responses/validationError"
// "405":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
changeRepoTeam(ctx, false)
}
func changeRepoTeam(ctx *context.APIContext, add bool) {
if !ctx.Repo.Owner.IsOrganization() {
ctx.APIError(http.StatusMethodNotAllowed, "repo is not owned by an organization")
}
if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.Permission.IsOwner() {
ctx.APIError(http.StatusForbidden, "user is nor repo admin nor owner")
return
}
team := getTeamByParam(ctx)
if team == nil {
return
}
repoHasTeam := repo_service.HasRepository(ctx, team, ctx.Repo.Repository.ID)
var err error
if add {
if repoHasTeam {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team '%s' is already added to repo", team.Name))
return
}
err = repo_service.TeamAddRepository(ctx, team, ctx.Repo.Repository)
} else {
if !repoHasTeam {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team '%s' was not added to repo", team.Name))
return
}
err = repo_service.RemoveRepositoryFromTeam(ctx, team, ctx.Repo.Repository.ID)
}
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
func getTeamByParam(ctx *context.APIContext) *organization.Team {
team, err := organization.GetTeam(ctx, ctx.Repo.Owner.ID, ctx.PathParam("team"))
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
return nil
}
ctx.APIErrorInternal(err)
return nil
}
return team
}
+306
View File
@@ -0,0 +1,306 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"strings"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/log"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListTopics returns list of current topics for repo
func ListTopics(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/topics repository repoListTopics
// ---
// summary: Get list of topics that a repository has
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/TopicNames"
// "404":
// "$ref": "#/responses/notFound"
opts := &repo_model.FindTopicOptions{
ListOptions: utils.GetListOptions(ctx),
RepoID: ctx.Repo.Repository.ID,
}
topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
topicNames := make([]string, len(topics))
for i, topic := range topics {
topicNames[i] = topic.Name
}
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, map[string]any{
"topics": topicNames,
})
}
// UpdateTopics updates repo with a new set of topics
func UpdateTopics(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/topics repository repoUpdateTopics
// ---
// summary: Replace list of topics for a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/RepoTopicOptions"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/invalidTopicsError"
form := web.GetForm(ctx).(*api.RepoTopicOptions)
topicNames := form.Topics
validTopics, invalidTopics := repo_model.SanitizeAndValidateTopics(topicNames)
if len(validTopics) > 25 {
ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
"invalidTopics": nil,
"message": "Exceeding maximum number of topics per repo",
})
return
}
if len(invalidTopics) > 0 {
ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
"invalidTopics": invalidTopics,
"message": "Topic names are invalid",
})
return
}
err := repo_model.SaveTopics(ctx, ctx.Repo.Repository.ID, validTopics...)
if err != nil {
log.Error("SaveTopics failed: %v", err)
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// AddTopic adds a topic name to a repo
func AddTopic(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/topics/{topic} repository repoAddTopic
// ---
// summary: Add a topic to a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: topic
// in: path
// description: name of the topic to add
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/invalidTopicsError"
topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic")))
if !repo_model.ValidateTopic(topicName) {
ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
"invalidTopics": topicName,
"message": "Topic name is invalid",
})
return
}
// Prevent adding more topics than allowed to repo
count, err := db.Count[repo_model.Topic](ctx, &repo_model.FindTopicOptions{
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
log.Error("CountTopics failed: %v", err)
ctx.APIErrorInternal(err)
return
}
if count >= 25 {
ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
"message": "Exceeding maximum allowed topics per repo.",
})
return
}
_, err = repo_model.AddTopic(ctx, ctx.Repo.Repository.ID, topicName)
if err != nil {
log.Error("AddTopic failed: %v", err)
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// DeleteTopic removes topic name from repo
func DeleteTopic(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/topics/{topic} repository repoDeleteTopic
// ---
// summary: Delete a topic from a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: topic
// in: path
// description: name of the topic to delete
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/invalidTopicsError"
topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic")))
if !repo_model.ValidateTopic(topicName) {
ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
"invalidTopics": topicName,
"message": "Topic name is invalid",
})
return
}
topic, err := repo_model.DeleteTopic(ctx, ctx.Repo.Repository.ID, topicName)
if err != nil {
log.Error("DeleteTopic failed: %v", err)
ctx.APIErrorInternal(err)
return
}
if topic == nil {
ctx.APIErrorNotFound()
return
}
ctx.Status(http.StatusNoContent)
}
// TopicSearch search for creating topic
func TopicSearch(ctx *context.APIContext) {
// swagger:operation GET /topics/search repository topicSearch
// ---
// summary: search topics via keyword
// produces:
// - application/json
// parameters:
// - name: q
// in: query
// description: keywords to search
// required: true
// type: string
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/TopicListResponse"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
opts := &repo_model.FindTopicOptions{
Keyword: ctx.FormString("q"),
ListOptions: utils.GetListOptions(ctx),
}
topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
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]any{
"topics": topicResponses,
})
}
+220
View File
@@ -0,0 +1,220 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"errors"
"fmt"
"net/http"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/convert"
repo_service "gitea.dev/services/repository"
)
// Transfer transfers the ownership of a repository
func Transfer(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/transfer repository repoTransfer
// ---
// summary: Transfer a repo ownership
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo to transfer
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo to transfer
// type: string
// required: true
// - name: body
// in: body
// description: "Transfer Options"
// required: true
// schema:
// "$ref": "#/definitions/TransferRepoOption"
// responses:
// "202":
// "$ref": "#/responses/Repository"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
opts := web.GetForm(ctx).(*api.TransferRepoOption)
newOwner, err := user_model.GetUserByName(ctx, opts.NewOwner)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusNotFound, "The new owner does not exist or cannot be found")
return
}
ctx.APIErrorInternal(err)
return
}
if newOwner.Type == user_model.UserTypeOrganization {
if !ctx.Doer.IsAdmin && newOwner.Visibility == api.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) {
// The user shouldn't know about this organization
ctx.APIError(http.StatusNotFound, "The new owner does not exist or cannot be found")
return
}
}
var teams []*organization.Team
if opts.TeamIDs != nil {
if !newOwner.IsOrganization() {
ctx.APIError(http.StatusUnprocessableEntity, "Teams can only be added to organization-owned repositories")
return
}
org := convert.ToOrganization(ctx, organization.OrgFromUser(newOwner))
for _, tID := range *opts.TeamIDs {
team, err := organization.GetTeamByID(ctx, tID)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team %d not found", tID))
return
}
if team.OrgID != org.ID {
ctx.APIError(http.StatusForbidden, fmt.Errorf("team %d belongs not to org %d", tID, org.ID))
return
}
teams = append(teams, team)
}
}
if ctx.Repo.GitRepo != nil {
_ = ctx.Repo.GitRepo.Close()
ctx.Repo.GitRepo = nil
}
oldFullname := ctx.Repo.Repository.FullName()
if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil {
switch {
case repo_model.IsErrRepoTransferInProgress(err):
ctx.APIError(http.StatusConflict, err)
case repo_model.IsErrRepoAlreadyExist(err):
ctx.APIError(http.StatusUnprocessableEntity, err)
case repo_service.IsRepositoryLimitReached(err):
ctx.APIError(http.StatusForbidden, err)
case errors.Is(err, user_model.ErrBlockedUser):
ctx.APIError(http.StatusForbidden, err)
default:
ctx.APIErrorInternal(err)
}
return
}
if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
log.Trace("Repository transfer initiated: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeAdmin}))
return
}
log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeAdmin}))
}
// AcceptTransfer accept a repo transfer
func AcceptTransfer(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/transfer/accept repository acceptRepoTransfer
// ---
// summary: Accept a repo transfer
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo to transfer
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo to transfer
// type: string
// required: true
// responses:
// "202":
// "$ref": "#/responses/Repository"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
err := repo_service.AcceptTransferOwnership(ctx, ctx.Repo.Repository, ctx.Doer)
if err != nil {
switch {
case repo_model.IsErrNoPendingTransfer(err):
ctx.APIError(http.StatusNotFound, err)
case errors.Is(err, util.ErrPermissionDenied):
ctx.APIError(http.StatusForbidden, err)
case repo_service.IsRepositoryLimitReached(err):
ctx.APIError(http.StatusForbidden, err)
default:
ctx.APIErrorInternal(err)
}
return
}
ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission))
}
// RejectTransfer reject a repo transfer
func RejectTransfer(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/transfer/reject repository rejectRepoTransfer
// ---
// summary: Reject a repo transfer
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo to transfer
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo to transfer
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/Repository"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
err := repo_service.RejectRepositoryTransfer(ctx, ctx.Repo.Repository, ctx.Doer)
if err != nil {
switch {
case repo_model.IsErrNoPendingTransfer(err):
ctx.APIError(http.StatusNotFound, err)
case errors.Is(err, util.ErrPermissionDenied):
ctx.APIError(http.StatusForbidden, err)
default:
ctx.APIErrorInternal(err)
}
return
}
ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission))
}
+70
View File
@@ -0,0 +1,70 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"gitea.dev/services/context"
files_service "gitea.dev/services/repository/files"
)
// GetTree get the tree of a repository.
func GetTree(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/git/trees/{sha} repository GetTree
// ---
// summary: Gets the tree of a repository.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: sha
// in: path
// description: sha of the commit
// type: string
// required: true
// - name: recursive
// in: query
// description: show all directories and files
// required: false
// type: boolean
// - name: page
// in: query
// description: page number; the 'truncated' field in the response will be true if there are still more items after this page, false if the last page
// required: false
// type: integer
// - name: per_page
// in: query
// description: number of items per page
// required: false
// type: integer
// responses:
// "200":
// "$ref": "#/responses/GitTreeResponse"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
sha := ctx.PathParam("sha")
if len(sha) == 0 {
ctx.APIError(http.StatusBadRequest, "sha not provided")
return
}
if tree, err := files_service.GetTreeBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, sha, ctx.FormInt("page"), ctx.FormInt("per_page"), ctx.FormBool("recursive")); err != nil {
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.SetTotalCountHeader(int64(tree.TotalCount))
ctx.JSON(http.StatusOK, tree)
}
}
+528
View File
@@ -0,0 +1,528 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/convert"
notify_service "gitea.dev/services/notify"
wiki_service "gitea.dev/services/wiki"
)
// NewWikiPage response for wiki create request
func NewWikiPage(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/wiki/new repository repoCreateWikiPage
// ---
// summary: Create a wiki page
// consumes:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateWikiPageOptions"
// responses:
// "201":
// "$ref": "#/responses/WikiPage"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.CreateWikiPageOptions)
if util.IsEmptyString(form.Title) {
ctx.APIError(http.StatusBadRequest, nil)
return
}
wikiName := wiki_service.UserTitleToWebPath("", form.Title)
if len(form.Message) == 0 {
form.Message = fmt.Sprintf("Add %q", form.Title)
}
content, err := base64.StdEncoding.DecodeString(form.ContentBase64)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
form.ContentBase64 = string(content)
if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.ContentBase64, form.Message); err != nil {
if repo_model.IsErrWikiReservedName(err) {
ctx.APIError(http.StatusBadRequest, err)
} else if repo_model.IsErrWikiAlreadyExist(err) {
ctx.APIError(http.StatusBadRequest, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
wikiPage := getWikiPage(ctx, wikiName)
if !ctx.Written() {
notify_service.NewWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(wikiName), form.Message)
ctx.JSON(http.StatusCreated, wikiPage)
}
}
// EditWikiPage response for wiki modify request
func EditWikiPage(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/wiki/page/{pageName} repository repoEditWikiPage
// ---
// summary: Edit a wiki page
// consumes:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: pageName
// in: path
// description: name of the page
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateWikiPageOptions"
// responses:
// "200":
// "$ref": "#/responses/WikiPage"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.CreateWikiPageOptions)
oldWikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
newWikiName := wiki_service.UserTitleToWebPath("", form.Title)
if len(newWikiName) == 0 {
newWikiName = oldWikiName
}
if len(form.Message) == 0 {
form.Message = fmt.Sprintf("Update %q", newWikiName)
}
content, err := base64.StdEncoding.DecodeString(form.ContentBase64)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
form.ContentBase64 = string(content)
if err := wiki_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, oldWikiName, newWikiName, form.ContentBase64, form.Message); err != nil {
ctx.APIErrorInternal(err)
return
}
wikiPage := getWikiPage(ctx, newWikiName)
if !ctx.Written() {
notify_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(newWikiName), form.Message)
ctx.JSON(http.StatusOK, wikiPage)
}
}
func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.WikiPage {
wikiRepo, commit := findWikiRepoCommit(ctx)
if wikiRepo != nil {
defer wikiRepo.Close()
}
if ctx.Written() {
return nil
}
// lookup filename in wiki - get filecontent, real filename
content, pageFilename := wikiContentsByName(ctx, commit, wikiName, false)
if ctx.Written() {
return nil
}
sidebarContent, _ := wikiContentsByName(ctx, commit, "_Sidebar", true)
if ctx.Written() {
return nil
}
footerContent, _ := wikiContentsByName(ctx, commit, "_Footer", true)
if ctx.Written() {
return nil
}
// get commit count - wiki revisions
commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
// Get last change information.
lastCommit, err := wikiRepo.GetCommitByPath(pageFilename)
if err != nil {
ctx.APIErrorInternal(err)
return nil
}
return &api.WikiPage{
WikiPageMetaData: wiki_service.ToWikiPageMetaData(wikiName, lastCommit, ctx.Repo.Repository),
ContentBase64: content,
CommitCount: commitsCount,
Sidebar: sidebarContent,
Footer: footerContent,
}
}
// DeleteWikiPage delete wiki page
func DeleteWikiPage(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/wiki/page/{pageName} repository repoDeleteWikiPage
// ---
// summary: Delete a wiki page
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: pageName
// in: path
// description: name of the page
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "423":
// "$ref": "#/responses/repoArchivedError"
wikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil {
if err.Error() == "file does not exist" {
ctx.APIErrorNotFound(err)
return
}
ctx.APIErrorInternal(err)
return
}
notify_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(wikiName))
ctx.Status(http.StatusNoContent)
}
// ListWikiPages get wiki pages list
func ListWikiPages(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/wiki/pages repository repoGetWikiPages
// ---
// summary: Get all wiki pages
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/WikiPageList"
// "404":
// "$ref": "#/responses/notFound"
wikiRepo, commit := findWikiRepoCommit(ctx)
if wikiRepo != nil {
defer wikiRepo.Close()
}
if ctx.Written() {
return
}
page := max(ctx.FormInt("page"), 1)
limit := ctx.FormInt("limit")
if limit <= 1 {
limit = setting.API.DefaultPagingNum
}
skip := (page - 1) * limit
maxNum := page * limit
entries, err := commit.ListEntries()
if err != nil {
ctx.APIErrorInternal(err)
return
}
pages := make([]*api.WikiPageMetaData, 0, len(entries))
for i, entry := range entries {
if i < skip || i >= maxNum || !entry.IsRegular() {
continue
}
c, err := wikiRepo.GetCommitByPath(entry.Name())
if err != nil {
ctx.APIErrorInternal(err)
return
}
wikiName, err := wiki_service.GitPathToWebPath(entry.Name())
if err != nil {
if repo_model.IsErrWikiInvalidFileName(err) {
continue
}
ctx.APIErrorInternal(err)
return
}
pages = append(pages, wiki_service.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository))
}
ctx.SetLinkHeader(int64(len(entries)), limit)
ctx.SetTotalCountHeader(int64(len(entries)))
ctx.JSON(http.StatusOK, pages)
}
// GetWikiPage get single wiki page
func GetWikiPage(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/wiki/page/{pageName} repository repoGetWikiPage
// ---
// summary: Get a wiki page
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: pageName
// in: path
// description: name of the page
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/WikiPage"
// "404":
// "$ref": "#/responses/notFound"
// get requested pagename
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
wikiPage := getWikiPage(ctx, pageName)
if !ctx.Written() {
ctx.JSON(http.StatusOK, wikiPage)
}
}
// ListPageRevisions renders file revision list of wiki page
func ListPageRevisions(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/wiki/revisions/{pageName} repository repoGetWikiPageRevisions
// ---
// summary: Get revisions of a wiki page
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: pageName
// in: path
// description: name of the page
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// responses:
// "200":
// "$ref": "#/responses/WikiCommitList"
// "404":
// "$ref": "#/responses/notFound"
wikiRepo, commit := findWikiRepoCommit(ctx)
if wikiRepo != nil {
defer wikiRepo.Close()
}
if ctx.Written() {
return
}
// get requested pagename
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
if len(pageName) == 0 {
pageName = "Home"
}
// lookup filename in wiki - get filecontent, gitTree entry , real filename
_, pageFilename := wikiContentsByName(ctx, commit, pageName, false)
if ctx.Written() {
return
}
// get commit count - wiki revisions
commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
page := max(ctx.FormInt("page"), 1)
// get Commit Count
commitsHistory, err := wikiRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
Revision: ctx.Repo.Repository.DefaultWikiBranch,
File: pageFilename,
Page: page,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
// FIXME: SetLinkHeader missing
ctx.SetTotalCountHeader(commitsCount)
ctx.JSON(http.StatusOK, convert.ToWikiCommitList(commitsHistory, commitsCount))
}
// findEntryForFile finds the tree entry for a target filepath.
func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
entry, err := commit.GetTreeEntryByPath(target)
if err != nil {
return nil, err
}
if entry != nil {
return entry, nil
}
// Then the unescaped, shortest alternative
var unescapedTarget string
if unescapedTarget, err = url.QueryUnescape(target); err != nil {
return nil, err
}
return commit.GetTreeEntryByPath(unescapedTarget)
}
// findWikiRepoCommit opens the wiki repo and returns the latest commit, writing to context on error.
// The caller is responsible for closing the returned repo again
func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) {
wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
if err != nil {
if git.IsErrNotExist(err) || err.Error() == "no such file or directory" {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return nil, nil
}
commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
if err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return wikiRepo, nil
}
return wikiRepo, commit
}
// wikiContentsByEntry returns the contents of the wiki page referenced by the
// given tree entry, encoded with base64. Writes to ctx if an error occurs.
func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string {
blob := entry.Blob()
if blob.Size() > setting.API.DefaultMaxBlobSize {
return ""
}
content, err := blob.GetBlobContentBase64(nil)
if err != nil {
ctx.APIErrorInternal(err)
return ""
}
return content
}
// wikiContentsByName returns the contents of a wiki page, along with a boolean
// indicating whether the page exists. Writes to ctx if an error occurs.
func wikiContentsByName(ctx *context.APIContext, commit *git.Commit, wikiName wiki_service.WebPath, isSidebarOrFooter bool) (string, string) {
gitFilename := wiki_service.WebPathToGitPath(wikiName)
entry, err := findEntryForFile(commit, gitFilename)
if err != nil {
if git.IsErrNotExist(err) {
if !isSidebarOrFooter {
ctx.APIErrorNotFound()
}
} else {
ctx.APIErrorInternal(err)
}
return "", ""
}
return wikiContentsByEntry(ctx, entry), gitFilename
}
+86
View File
@@ -0,0 +1,86 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package settings
import (
"net/http"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/services/context"
)
// GetGeneralUISettings returns instance's global settings for ui
func GetGeneralUISettings(ctx *context.APIContext) {
// swagger:operation GET /settings/ui settings getGeneralUISettings
// ---
// summary: Get instance's global settings for ui
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/GeneralUISettings"
ctx.JSON(http.StatusOK, api.GeneralUISettings{
DefaultTheme: setting.UI.DefaultTheme,
AllowedReactions: setting.UI.Reactions,
CustomEmojis: setting.UI.CustomEmojis,
})
}
// GetGeneralAPISettings returns instance's global settings for api
func GetGeneralAPISettings(ctx *context.APIContext) {
// swagger:operation GET /settings/api settings getGeneralAPISettings
// ---
// summary: Get instance's global settings for api
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/GeneralAPISettings"
ctx.JSON(http.StatusOK, api.GeneralAPISettings{
MaxResponseItems: setting.API.MaxResponseItems,
DefaultPagingNum: setting.API.DefaultPagingNum,
DefaultGitTreesPerPage: setting.API.DefaultGitTreesPerPage,
DefaultMaxBlobSize: setting.API.DefaultMaxBlobSize,
DefaultMaxResponseSize: setting.API.DefaultMaxResponseSize,
})
}
// GetGeneralRepoSettings returns instance's global settings for repositories
func GetGeneralRepoSettings(ctx *context.APIContext) {
// swagger:operation GET /settings/repository settings getGeneralRepositorySettings
// ---
// summary: Get instance's global settings for repositories
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/GeneralRepoSettings"
ctx.JSON(http.StatusOK, api.GeneralRepoSettings{
MirrorsDisabled: !setting.Mirror.Enabled,
HTTPGitDisabled: setting.Repository.DisableHTTPGit,
MigrationsDisabled: setting.Repository.DisableMigrations,
StarsDisabled: setting.Repository.DisableStars,
TimeTrackingDisabled: !setting.Service.EnableTimetracking,
LFSDisabled: !setting.LFS.StartServer,
})
}
// GetGeneralAttachmentSettings returns instance's global settings for Attachment
func GetGeneralAttachmentSettings(ctx *context.APIContext) {
// swagger:operation GET /settings/attachment settings getGeneralAttachmentSettings
// ---
// summary: Get instance's global settings for Attachment
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/GeneralAttachmentSettings"
ctx.JSON(http.StatusOK, api.GeneralAttachmentSettings{
Enabled: setting.Attachment.Enabled,
AllowedTypes: setting.Attachment.AllowedTypes,
MaxFiles: setting.Attachment.MaxFiles,
MaxSize: setting.Attachment.MaxSize,
})
}
+215
View File
@@ -0,0 +1,215 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package shared
import (
"fmt"
"net/http"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/git"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/webhook"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListJobs lists jobs for api route validated ownerID and repoID
// ownerID == 0 and repoID == 0 means all jobs
// ownerID == 0 and repoID != 0 means all jobs for the given repo
// ownerID != 0 and repoID == 0 means all jobs for the given user/org
// ownerID != 0 and repoID != 0 undefined behavior
// runID == 0 means all jobs
// runID is used as an additional filter together with ownerID and repoID to only return jobs for the given run
// runAttemptID, when set, additionally limits the result to jobs of the specified run attempt. Only takes effect when runID > 0.
// Access rights are checked at the API route level
func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64, runAttemptID optional.Option[int64]) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
listOptions := utils.GetListOptions(ctx)
orderBy, ok := utils.ResolveSortOrder(ctx, actions_model.JobOrderByMap, actions_model.JobOrderByMap["asc"]["id"])
if !ok {
return
}
opts := actions_model.FindRunJobOptions{
OwnerID: ownerID,
RepoID: repoID,
RunID: runID,
ListOptions: listOptions,
OrderBy: orderBy,
}
if runID > 0 {
opts.RunAttemptID = runAttemptID
}
for _, status := range ctx.FormStrings("status") {
values, err := convertToInternal(status)
if err != nil {
ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status))
return
}
opts.Statuses = append(opts.Statuses, values...)
}
jobs, total, err := db.FindAndCount[actions_model.ActionRunJob](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
res := new(api.ActionWorkflowJobsResponse)
res.TotalCount = total
jobList := actions_model.ActionJobList(jobs)
if err := jobList.LoadAttributes(ctx, true); err != nil {
ctx.APIErrorInternal(err)
return
}
res.Entries = make([]*api.ActionWorkflowJob, len(jobs))
isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID
for i := range jobs {
var repository *repo_model.Repository
if isRepoLevel {
repository = ctx.Repo.Repository
} else {
if jobs[i].Run == nil || jobs[i].Run.Repo == nil {
ctx.APIErrorInternal(fmt.Errorf("job %d is missing its run or repository", jobs[i].ID))
return
}
repository = jobs[i].Run.Repo
}
convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, repository, nil, jobs[i])
if err != nil {
ctx.APIErrorInternal(err)
return
}
res.Entries[i] = convertedWorkflowJob
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &res)
}
func convertToInternal(s string) ([]actions_model.Status, error) {
switch s {
case "pending", "waiting", "requested", "action_required":
return []actions_model.Status{actions_model.StatusBlocked}, nil
case "queued":
return []actions_model.Status{actions_model.StatusWaiting}, nil
case "in_progress":
return []actions_model.Status{actions_model.StatusRunning}, nil
case "completed":
return []actions_model.Status{
actions_model.StatusSuccess,
actions_model.StatusFailure,
actions_model.StatusSkipped,
actions_model.StatusCancelled,
}, nil
case "failure":
return []actions_model.Status{actions_model.StatusFailure}, nil
case "success":
return []actions_model.Status{actions_model.StatusSuccess}, nil
case "skipped", "neutral":
return []actions_model.Status{actions_model.StatusSkipped}, nil
case "cancelled", "timed_out":
return []actions_model.Status{actions_model.StatusCancelled}, nil
default:
return nil, fmt.Errorf("invalid status %s", s)
}
}
// ListRuns lists jobs for api route validated ownerID and repoID
// ownerID == 0 and repoID == 0 means all runs
// ownerID == 0 and repoID != 0 means all runs for the given repo
// ownerID != 0 and repoID == 0 means all runs for the given user/org
// ownerID != 0 and repoID != 0 undefined behavior
// Access rights are checked at the API route level
func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
listOptions := utils.GetListOptions(ctx)
opts := actions_model.FindRunOptions{
OwnerID: ownerID,
RepoID: repoID,
ListOptions: listOptions,
}
if event := ctx.FormString("event"); event != "" {
opts.TriggerEvent = webhook.HookEventType(event)
}
if branch := ctx.FormString("branch"); branch != "" {
opts.Ref = string(git.RefNameFromBranch(branch))
}
for _, status := range ctx.FormStrings("status") {
values, err := convertToInternal(status)
if err != nil {
ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status))
return
}
opts.Status = append(opts.Status, values...)
}
if actor := ctx.FormString("actor"); actor != "" {
user, err := user_model.GetUserByName(ctx, actor)
if err != nil {
ctx.APIErrorInternal(err)
return
}
opts.TriggerUserID = user.ID
}
if headSHA := ctx.FormString("head_sha"); headSHA != "" {
opts.CommitSHA = headSHA
}
runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
res := new(api.ActionWorkflowRunsResponse)
res.TotalCount = total
runList := actions_model.RunList(runs)
if err := runList.LoadTriggerUser(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if err := runList.LoadRepos(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
repos := repo_model.RepositoryList(container.FilterSlice(runs, func(r *actions_model.ActionRun) (*repo_model.Repository, bool) {
return r.Repo, r.Repo != nil
}))
if err := repos.LoadOwners(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
res.Entries = make([]*api.ActionWorkflowRun, len(runs))
for i := range runs {
// TODO: load run attempts in batch
convertedRun, err := convert.ToActionWorkflowRun(ctx, runs[i], nil)
if err != nil {
ctx.APIErrorInternal(err)
return
}
res.Entries[i] = convertedRun
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &res)
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2024 The Gitea Authors.
// SPDX-License-Identifier: MIT
package shared
import (
"errors"
"net/http"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
user_service "gitea.dev/services/user"
)
func ListBlocks(ctx *context.APIContext, blocker *user_model.User) {
listOptions := utils.GetListOptions(ctx)
blocks, total, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
ListOptions: listOptions,
BlockerID: blocker.ID,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
users := make([]*api.User, 0, len(blocks))
for _, b := range blocks {
users = append(users, convert.ToUser(ctx, b.Blockee, blocker))
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &users)
}
func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) {
blockee, err := user_model.GetUserByName(ctx, ctx.PathParam("username"))
if err != nil {
ctx.APIErrorNotFound("GetUserByName", err)
return
}
_, err = user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
if errors.Is(err, util.ErrNotExist) {
ctx.Status(http.StatusNotFound)
} else if err == nil {
ctx.Status(http.StatusNoContent)
} else {
ctx.APIErrorInternal(err)
}
}
func BlockUser(ctx *context.APIContext, blocker *user_model.User) {
blockee, err := user_model.GetUserByName(ctx, ctx.PathParam("username"))
if err != nil {
ctx.APIErrorNotFound("GetUserByName", err)
return
}
if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, ctx.FormString("note")); err != nil {
if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
ctx.APIError(http.StatusBadRequest, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
func UnblockUser(ctx *context.APIContext, doer, blocker *user_model.User) {
blockee, err := user_model.GetUserByName(ctx, ctx.PathParam("username"))
if err != nil {
ctx.APIErrorNotFound("GetUserByName", err)
return
}
if err := user_service.UnblockUser(ctx, doer, blocker, blockee); err != nil {
if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
ctx.APIError(http.StatusBadRequest, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
+150
View File
@@ -0,0 +1,150 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package shared
import (
"errors"
"net/http"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// RegistrationToken is response related to registration token
// swagger:response RegistrationToken
type RegistrationToken struct {
Token string `json:"token"`
}
func GetRegistrationToken(ctx *context.APIContext, ownerID, repoID int64) {
token, err := actions_model.GetLatestRunnerToken(ctx, ownerID, repoID)
if errors.Is(err, util.ErrNotExist) || (token != nil && !token.IsActive) {
token, err = actions_model.NewRunnerToken(ctx, ownerID, repoID)
}
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, RegistrationToken{Token: token.Token})
}
// ListRunners lists runners for api route validated ownerID and repoID
// ownerID == 0 and repoID == 0 means all runners including global runners, does not appear in sql where clause
// ownerID == 0 and repoID != 0 means all runners for the given repo
// ownerID != 0 and repoID == 0 means all runners for the given user/org
// ownerID != 0 and repoID != 0 undefined behavior
// Access rights are checked at the API route level
func ListRunners(ctx *context.APIContext, ownerID, repoID int64) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
opts := &actions_model.FindRunnerOptions{
OwnerID: ownerID,
RepoID: repoID,
ListOptions: utils.GetListOptions(ctx),
}
opts.IsDisabled = ctx.FormOptionalBool("disabled")
runners, total, err := db.FindAndCount[actions_model.ActionRunner](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
res := new(api.ActionRunnersResponse)
res.TotalCount = total
res.Entries = make([]*api.ActionRunner, len(runners))
for i, runner := range runners {
res.Entries[i] = convert.ToActionRunner(ctx, runner)
}
ctx.JSON(http.StatusOK, &res)
}
func getRunnerByID(ctx *context.APIContext, ownerID, repoID, runnerID int64) (*actions_model.ActionRunner, bool) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound("Runner not found")
} else {
ctx.APIErrorInternal(err)
}
return nil, false
}
if !runner.EditableInContext(ownerID, repoID) {
ctx.APIErrorNotFound("No permission to access this runner")
return nil, false
}
return runner, true
}
// GetRunner get the runner for api route validated ownerID and repoID
// ownerID == 0 and repoID == 0 means any runner including global runners
// ownerID == 0 and repoID != 0 means any runner for the given repo
// ownerID != 0 and repoID == 0 means any runner for the given user/org
// ownerID != 0 and repoID != 0 undefined behavior
// Access rights are checked at the API route level
func GetRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID)
if !ok {
return
}
ctx.JSON(http.StatusOK, convert.ToActionRunner(ctx, runner))
}
// DeleteRunner deletes the runner for api route validated ownerID and repoID
// ownerID == 0 and repoID == 0 means any runner including global runners
// ownerID == 0 and repoID != 0 means any runner for the given repo
// ownerID != 0 and repoID == 0 means any runner for the given user/org
// ownerID != 0 and repoID != 0 undefined behavior
// Access rights are checked at the API route level
func DeleteRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID)
if !ok {
return
}
err := actions_model.DeleteRunner(ctx, runner.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
func UpdateRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID)
if !ok {
return
}
form := web.GetForm(ctx).(*api.EditActionRunnerOption)
if form.Disabled == nil {
ctx.APIError(http.StatusUnprocessableEntity, "[Disabled]: Required")
return
}
if err := actions_model.SetRunnerDisabled(ctx, runner, *form.Disabled); err != nil {
ctx.APIErrorInternal(err)
return
}
GetRunner(ctx, ownerID, repoID, runnerID)
}
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import api "gitea.dev/modules/structs"
// SecretList
// swagger:response SecretList
type swaggerResponseSecretList struct {
// in:body
Body []api.Secret `json:"body"`
}
// Secret
// swagger:response Secret
type swaggerResponseSecret struct {
// in:body
Body api.Secret `json:"body"`
}
// ActionVariable
// swagger:response ActionVariable
type swaggerResponseActionVariable struct {
// in:body
Body api.ActionVariable `json:"body"`
}
// VariableList
// swagger:response VariableList
type swaggerResponseVariableList struct {
// in:body
Body []api.ActionVariable `json:"body"`
}
// ActionWorkflow
// swagger:response ActionWorkflow
type swaggerResponseActionWorkflow struct {
// in:body
Body api.ActionWorkflow `json:"body"`
}
// ActionWorkflowList
// swagger:response ActionWorkflowList
type swaggerResponseActionWorkflowList struct {
// in:body
Body api.ActionWorkflowResponse `json:"body"`
}
// RunDetails
// swagger:response RunDetails
type swaggerResponseRunDetails struct {
// in:body
Body api.RunDetails `json:"body"`
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import (
api "gitea.dev/modules/structs"
)
// ActivityFeedsList
// swagger:response ActivityFeedsList
type swaggerActivityFeedsList struct {
// in:body
Body []api.Activity `json:"body"`
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import (
api "gitea.dev/modules/structs"
)
// OAuth2Application
// swagger:response OAuth2Application
type swaggerResponseOAuth2Application struct {
// in:body
Body api.OAuth2Application `json:"body"`
}
// AccessToken represents an API access token.
// swagger:response AccessToken
type swaggerResponseAccessToken struct {
// in:body
Body api.AccessToken `json:"body"`
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import (
api "gitea.dev/modules/structs"
)
// CronList
// swagger:response CronList
type swaggerResponseCronList struct {
// in:body
Body []api.Cron `json:"body"`
}
+127
View File
@@ -0,0 +1,127 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import (
api "gitea.dev/modules/structs"
)
// Issue
// swagger:response Issue
type swaggerResponseIssue struct {
// in:body
Body api.Issue `json:"body"`
}
// IssueList
// swagger:response IssueList
type swaggerResponseIssueList struct {
// in:body
Body []api.Issue `json:"body"`
}
// Comment
// swagger:response Comment
type swaggerResponseComment struct {
// in:body
Body api.Comment `json:"body"`
}
// CommentList
// swagger:response CommentList
type swaggerResponseCommentList struct {
// in:body
Body []api.Comment `json:"body"`
}
// TimelineList
// swagger:response TimelineList
type swaggerResponseTimelineList struct {
// in:body
Body []api.TimelineComment `json:"body"`
}
// Label
// swagger:response Label
type swaggerResponseLabel struct {
// in:body
Body api.Label `json:"body"`
}
// LabelList
// swagger:response LabelList
type swaggerResponseLabelList struct {
// in:body
Body []api.Label `json:"body"`
}
// Milestone
// swagger:response Milestone
type swaggerResponseMilestone struct {
// in:body
Body api.Milestone `json:"body"`
}
// MilestoneList
// swagger:response MilestoneList
type swaggerResponseMilestoneList struct {
// in:body
Body []api.Milestone `json:"body"`
}
// TrackedTime
// swagger:response TrackedTime
type swaggerResponseTrackedTime struct {
// in:body
Body api.TrackedTime `json:"body"`
}
// TrackedTimeList
// swagger:response TrackedTimeList
type swaggerResponseTrackedTimeList struct {
// in:body
Body []api.TrackedTime `json:"body"`
}
// IssueDeadline
// swagger:response IssueDeadline
type swaggerIssueDeadline struct {
// in:body
Body api.IssueDeadline `json:"body"`
}
// IssueTemplates
// swagger:response IssueTemplates
type swaggerIssueTemplates struct {
// in:body
Body []api.IssueTemplate `json:"body"`
}
// StopWatch
// swagger:response StopWatch
type swaggerResponseStopWatch struct {
// in:body
Body api.StopWatch `json:"body"`
}
// StopWatchList
// swagger:response StopWatchList
type swaggerResponseStopWatchList struct {
// in:body
Body []api.StopWatch `json:"body"`
}
// Reaction
// swagger:response Reaction
type swaggerReaction struct {
// in:body
Body api.Reaction `json:"body"`
}
// ReactionList
// swagger:response ReactionList
type swaggerReactionList struct {
// in:body
Body []api.Reaction `json:"body"`
}
+50
View File
@@ -0,0 +1,50 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import (
api "gitea.dev/modules/structs"
)
// PublicKey
// swagger:response PublicKey
type swaggerResponsePublicKey struct {
// in:body
Body api.PublicKey `json:"body"`
}
// PublicKeyList
// swagger:response PublicKeyList
type swaggerResponsePublicKeyList struct {
// in:body
Body []api.PublicKey `json:"body"`
}
// GPGKey
// swagger:response GPGKey
type swaggerResponseGPGKey struct {
// in:body
Body api.GPGKey `json:"body"`
}
// GPGKeyList
// swagger:response GPGKeyList
type swaggerResponseGPGKeyList struct {
// in:body
Body []api.GPGKey `json:"body"`
}
// DeployKey
// swagger:response DeployKey
type swaggerResponseDeployKey struct {
// in:body
Body api.DeployKey `json:"body"`
}
// DeployKeyList
// swagger:response DeployKeyList
type swaggerResponseDeployKeyList struct {
// in:body
Body []api.DeployKey `json:"body"`
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import (
api "gitea.dev/modules/structs"
)
// ServerVersion
// swagger:response ServerVersion
type swaggerResponseServerVersion struct {
// in:body
Body api.ServerVersion `json:"body"`
}
// GitignoreTemplateList
// swagger:response GitignoreTemplateList
type swaggerResponseGitignoreTemplateList struct {
// in:body
Body []string `json:"body"`
}
// GitignoreTemplateInfo
// swagger:response GitignoreTemplateInfo
type swaggerResponseGitignoreTemplateInfo struct {
// in:body
Body api.GitignoreTemplateInfo `json:"body"`
}
// LicenseTemplateList
// swagger:response LicenseTemplateList
type swaggerResponseLicensesTemplateList struct {
// in:body
Body []api.LicensesTemplateListEntry `json:"body"`
}
// LicenseTemplateInfo
// swagger:response LicenseTemplateInfo
type swaggerResponseLicenseTemplateInfo struct {
// in:body
Body api.LicenseTemplateInfo `json:"body"`
}
// StringSlice
// swagger:response StringSlice
type swaggerResponseStringSlice struct {
// in:body
Body []string `json:"body"`
}
// LabelTemplateList
// swagger:response LabelTemplateList
type swaggerResponseLabelTemplateList struct {
// in:body
Body []string `json:"body"`
}
// LabelTemplateInfo
// swagger:response LabelTemplateInfo
type swaggerResponseLabelTemplateInfo struct {
// in:body
Body []api.LabelTemplate `json:"body"`
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import (
api "gitea.dev/modules/structs"
)
// NodeInfo
// swagger:response NodeInfo
type swaggerResponseNodeInfo struct {
// in:body
Body api.NodeInfo `json:"body"`
}
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import (
api "gitea.dev/modules/structs"
)
// NotificationThread
// swagger:response NotificationThread
type swaggerNotificationThread struct {
// in:body
Body api.NotificationThread `json:"body"`
}
// NotificationThreadList
// swagger:response NotificationThreadList
type swaggerNotificationThreadList struct {
// in:body
Body []api.NotificationThread `json:"body"`
}
// Number of unread notifications
// swagger:response NotificationCount
type swaggerNotificationCount struct {
// in:body
Body api.NotificationCount `json:"body"`
}
+236
View File
@@ -0,0 +1,236 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import (
api "gitea.dev/modules/structs"
"gitea.dev/services/forms"
)
// not actually a response, just a hack to get go-swagger to include definitions
// of the various XYZOption structs
// parameterBodies
// swagger:response parameterBodies
type swaggerParameterBodies struct {
// in:body
AddCollaboratorOption api.AddCollaboratorOption
// in:body
CreateEmailOption api.CreateEmailOption
// in:body
DeleteEmailOption api.DeleteEmailOption
// in:body
CreateHookOption api.CreateHookOption
// in:body
EditHookOption api.EditHookOption
// in:body
EditGitHookOption api.EditGitHookOption
// in:body
CreateIssueOption api.CreateIssueOption
// in:body
EditIssueOption api.EditIssueOption
// in:body
EditDeadlineOption api.EditDeadlineOption
// in:body
CreateIssueCommentOption api.CreateIssueCommentOption
// in:body
EditIssueCommentOption api.EditIssueCommentOption
// in:body
IssueMeta api.IssueMeta
// in:body
IssueLabelsOption api.IssueLabelsOption
// in:body
CreateKeyOption api.CreateKeyOption
// in:body
RenameUserOption api.RenameUserOption
// in:body
CreateLabelOption api.CreateLabelOption
// in:body
EditLabelOption api.EditLabelOption
// in:body
MarkupOption api.MarkupOption
// in:body
MarkdownOption api.MarkdownOption
// in:body
CreateMilestoneOption api.CreateMilestoneOption
// in:body
EditMilestoneOption api.EditMilestoneOption
// in:body
CreateOrgOption api.CreateOrgOption
// in:body
EditOrgOption api.EditOrgOption
// in:body
CreatePullRequestOption api.CreatePullRequestOption
// in:body
EditPullRequestOption api.EditPullRequestOption
// in:body
MergePullRequestOption forms.MergePullRequestForm
// in:body
CreateReleaseOption api.CreateReleaseOption
// in:body
EditReleaseOption api.EditReleaseOption
// in:body
CreateRepoOption api.CreateRepoOption
// in:body
EditRepoOption api.EditRepoOption
// in:body
RenameBranchRepoOption api.RenameBranchRepoOption
// in:body
TransferRepoOption api.TransferRepoOption
// in:body
CreateForkOption api.CreateForkOption
// in:body
GenerateRepoOption api.GenerateRepoOption
// in:body
CreateStatusOption api.CreateStatusOption
// in:body
CreateTeamOption api.CreateTeamOption
// in:body
EditTeamOption api.EditTeamOption
// in:body
AddTimeOption api.AddTimeOption
// in:body
CreateUserOption api.CreateUserOption
// in:body
EditUserOption api.EditUserOption
// in:body
EditAttachmentOptions api.EditAttachmentOptions
// in:body
GetFilesOptions api.GetFilesOptions
// in:body
ApplyDiffPatchFileOptions api.ApplyDiffPatchFileOptions
// in:body
ChangeFilesOptions api.ChangeFilesOptions
// in:body
CreateFileOptions api.CreateFileOptions
// in:body
UpdateFileOptions api.UpdateFileOptions
// in:body
DeleteFileOptions api.DeleteFileOptions
// in:body
CommitDateOptions api.CommitDateOptions
// in:body
RepoTopicOptions api.RepoTopicOptions
// in:body
EditReactionOption api.EditReactionOption
// in:body
CreateBranchRepoOption api.CreateBranchRepoOption
// in:body
UpdateBranchRepoOption api.UpdateBranchRepoOption
// in:body
CreateBranchProtectionOption api.CreateBranchProtectionOption
// in:body
EditBranchProtectionOption api.EditBranchProtectionOption
// in:body
UpdateBranchProtectionPriories api.UpdateBranchProtectionPriories
// in:body
CreateOAuth2ApplicationOptions api.CreateOAuth2ApplicationOptions
// in:body
CreatePullReviewOptions api.CreatePullReviewOptions
// in:body
CreatePullReviewComment api.CreatePullReviewComment
// in:body
CreatePullReviewCommentReplyOptions api.CreatePullReviewCommentReplyOptions
// in:body
SubmitPullReviewOptions api.SubmitPullReviewOptions
// in:body
DismissPullReviewOptions api.DismissPullReviewOptions
// in:body
MigrateRepoOptions api.MigrateRepoOptions
// in:body
PullReviewRequestOptions api.PullReviewRequestOptions
// in:body
CreateTagOption api.CreateTagOption
// in:body
CreateTagProtectionOption api.CreateTagProtectionOption
// in:body
EditTagProtectionOption api.EditTagProtectionOption
// in:body
CreateAccessTokenOption api.CreateAccessTokenOption
// in:body
UserSettingsOptions api.UserSettingsOptions
// in:body
CreateWikiPageOptions api.CreateWikiPageOptions
// in:body
CreatePushMirrorOption api.CreatePushMirrorOption
// in:body
UpdateUserAvatarOptions api.UpdateUserAvatarOption
// in:body
UpdateRepoAvatarOptions api.UpdateRepoAvatarOption
// in:body
CreateOrUpdateSecretOption api.CreateOrUpdateSecretOption
// in:body
UserBadgeOption api.UserBadgeOption
// in:body
CreateVariableOption api.CreateVariableOption
// in:body
RenameOrgOption api.RenameOrgOption
// in:body
CreateActionWorkflowDispatch api.CreateActionWorkflowDispatch
// in:body
UpdateVariableOption api.UpdateVariableOption
// in:body
EditActionRunnerOption api.EditActionRunnerOption
// in:body
LockIssueOption api.LockIssueOption
}

Some files were not shown because too many files have changed in this diff Show More