feat: vendor gitea 1.16.2

This commit is contained in:
2026-06-02 08:36:01 +00:00
parent 247cd60f69
commit 54798b4615
2145 changed files with 162965 additions and 126447 deletions
@@ -4,103 +4,11 @@
package activitypub
import (
"fmt"
"net/http"
"strings"
"code.gitea.io/gitea/modules/activitypub"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"
ap "github.com/go-ap/activitypub"
"github.com/go-ap/jsonld"
)
// Person function returns the Person actor for a user
func Person(ctx *context.APIContext) {
// swagger:operation GET /activitypub/user-id/{user-id} activitypub activitypubPerson
// ---
// summary: Returns the Person actor for a user
// produces:
// - application/json
// parameters:
// - name: user-id
// in: path
// description: user ID of the user
// type: integer
// required: true
// responses:
// "200":
// "$ref": "#/responses/ActivityPub"
// TODO: the setting.AppURL during the test doesn't follow the definition: "It always has a '/' suffix"
link := fmt.Sprintf("%s/api/v1/activitypub/user-id/%d", strings.TrimSuffix(setting.AppURL, "/"), ctx.ContextUser.ID)
person := ap.PersonNew(ap.IRI(link))
person.Name = ap.NaturalLanguageValuesNew()
err := person.Name.Set("en", ap.Content(ctx.ContextUser.FullName))
if err != nil {
ctx.APIErrorInternal(err)
return
}
person.PreferredUsername = ap.NaturalLanguageValuesNew()
err = person.PreferredUsername.Set("en", ap.Content(ctx.ContextUser.Name))
if err != nil {
ctx.APIErrorInternal(err)
return
}
person.URL = ap.IRI(ctx.ContextUser.HTMLURL(ctx))
person.Icon = ap.Image{
Type: ap.ImageType,
MediaType: "image/png",
URL: ap.IRI(ctx.ContextUser.AvatarLink(ctx)),
}
person.Inbox = ap.IRI(link + "/inbox")
person.Outbox = ap.IRI(link + "/outbox")
person.PublicKey.ID = ap.IRI(link + "#main-key")
person.PublicKey.Owner = ap.IRI(link)
publicKeyPem, err := activitypub.GetPublicKey(ctx, ctx.ContextUser)
if err != nil {
ctx.APIErrorInternal(err)
return
}
person.PublicKey.PublicKeyPem = publicKeyPem
binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(person)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Resp.Header().Add("Content-Type", activitypub.ActivityStreamsContentType)
ctx.Resp.WriteHeader(http.StatusOK)
if _, err = ctx.Resp.Write(binary); err != nil {
log.Error("write to resp err: %v", err)
}
}
// PersonInbox function handles the incoming data for a user inbox
func PersonInbox(ctx *context.APIContext) {
// swagger:operation POST /activitypub/user-id/{user-id}/inbox activitypub activitypubPersonInbox
// ---
// summary: Send to the inbox
// produces:
// - application/json
// parameters:
// - name: user-id
// in: path
// description: user ID of the user
// type: integer
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
ctx.Status(http.StatusNoContent)
func NotImplemented(ctx *context.APIContext) {
http.Error(ctx.Resp, "Not implemented", http.StatusNotImplemented)
}
@@ -1,98 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package activitypub
import (
"crypto"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"code.gitea.io/gitea/modules/activitypub"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/setting"
gitea_context "code.gitea.io/gitea/services/context"
"github.com/42wim/httpsig"
ap "github.com/go-ap/activitypub"
)
func getPublicKeyFromResponse(b []byte, keyID *url.URL) (p crypto.PublicKey, err error) {
person := ap.PersonNew(ap.IRI(keyID.String()))
err = person.UnmarshalJSON(b)
if err != nil {
return nil, fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %w", err)
}
pubKey := person.PublicKey
if pubKey.ID.String() != keyID.String() {
return nil, fmt.Errorf("cannot find publicKey with id: %s in %s", keyID, string(b))
}
pubKeyPem := pubKey.PublicKeyPem
block, _ := pem.Decode([]byte(pubKeyPem))
if block == nil || block.Type != "PUBLIC KEY" {
return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
}
p, err = x509.ParsePKIXPublicKey(block.Bytes)
return p, err
}
func fetch(iri *url.URL) (b []byte, err error) {
req := httplib.NewRequest(iri.String(), http.MethodGet)
req.Header("Accept", activitypub.ActivityStreamsContentType)
req.Header("User-Agent", "Gitea/"+setting.AppVer)
resp, err := req.Response()
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("url IRI fetch [%s] failed with status (%d): %s", iri, resp.StatusCode, resp.Status)
}
b, err = io.ReadAll(io.LimitReader(resp.Body, setting.Federation.MaxSize))
return b, err
}
func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, err error) {
r := ctx.Req
// 1. Figure out what key we need to verify
v, err := httpsig.NewVerifier(r)
if err != nil {
return false, err
}
ID := v.KeyId()
idIRI, err := url.Parse(ID)
if err != nil {
return false, err
}
// 2. Fetch the public key of the other actor
b, err := fetch(idIRI)
if err != nil {
return false, err
}
pubKey, err := getPublicKeyFromResponse(b, idIRI)
if err != nil {
return false, err
}
// 3. Verify the other actor's key
algo := httpsig.Algorithm(setting.Federation.Algorithms[0])
authenticated = v.Verify(pubKey, algo) == nil
return authenticated, err
}
// ReqHTTPSignature function
func ReqHTTPSignature() func(ctx *gitea_context.APIContext) {
return func(ctx *gitea_context.APIContext) {
if authenticated, err := verifyHTTPSignatures(ctx); err != nil {
ctx.APIErrorInternal(err)
} else if !authenticated {
ctx.APIError(http.StatusForbidden, "request signature verification failed")
}
}
}
@@ -8,7 +8,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
@@ -50,7 +50,7 @@ func ListUnadoptedRepositories(ctx *context.APIContext) {
return
}
ctx.SetTotalCountHeader(int64(count))
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, repoNames)
}
@@ -99,12 +99,12 @@ func AdoptRepository(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
isDir, err := util.IsDir(repo_model.RepoPath(ctxUser.Name, repoName))
exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName)))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if has || !isDir {
if has || !exist {
ctx.APIErrorNotFound()
return
}
@@ -161,12 +161,12 @@ func DeleteUnadoptedRepository(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
isDir, err := util.IsDir(repo_model.RepoPath(ctxUser.Name, repoName))
exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName)))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if has || !isDir {
if has || !exist {
ctx.APIErrorNotFound()
return
}
@@ -51,7 +51,7 @@ func GetAllEmails(ctx *context.APIContext) {
results[i] = convert.ToEmailSearch(emails[i])
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &results)
}
@@ -57,8 +57,13 @@ func ListHooks(ctx *context.APIContext) {
case "all":
isSystemWebhook = optional.None[bool]()
}
listOptions := utils.GetListOptions(ctx)
opts := &webhook.ListSystemWebhookOptions{
ListOptions: listOptions,
IsSystem: isSystemWebhook,
}
sysHooks, err := webhook.GetSystemOrDefaultWebhooks(ctx, isSystemWebhook)
sysHooks, total, err := webhook.GetGlobalWebhooks(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -72,6 +77,8 @@ func ListHooks(ctx *context.APIContext) {
}
hooks[i] = h
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, hooks)
}
@@ -103,7 +103,7 @@ func GetAllOrgs(ctx *context.APIContext) {
users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer,
Type: user_model.UserTypeOrganization,
Types: []user_model.UserType{user_model.UserTypeOrganization},
OrderBy: db.SearchOrderByAlphabetically,
ListOptions: listOptions,
Visible: []api.VisibleType{api.VisibleTypePublic, api.VisibleTypeLimited, api.VisibleTypePrivate},
@@ -117,7 +117,7 @@ func GetAllOrgs(ctx *context.APIContext) {
orgs[i] = convert.ToOrganization(ctx, organization.OrgFromUser(users[i]))
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &orgs)
}
@@ -10,26 +10,11 @@ import (
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// GetRegistrationToken returns the token to register global runners
func GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /admin/runners/registration-token admin adminGetRunnerRegistrationToken
// ---
// summary: Get an global actions runner registration token
// produces:
// - application/json
// parameters:
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, 0, 0)
}
// 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 an global actions runner registration token
// summary: Get a global actions runner registration token
// produces:
// - application/json
// parameters:
@@ -47,9 +32,15 @@ func ListRunners(ctx *context.APIContext) {
// 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": "#/definitions/ActionRunnersResponse"
// "$ref": "#/responses/RunnerList"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -57,11 +48,11 @@ func ListRunners(ctx *context.APIContext) {
shared.ListRunners(ctx, 0, 0)
}
// GetRunner get an global runner
// GetRunner get a global runner
func GetRunner(ctx *context.APIContext) {
// swagger:operation GET /admin/actions/runners/{runner_id} admin getAdminRunner
// ---
// summary: Get an global runner
// summary: Get a global runner
// produces:
// - application/json
// parameters:
@@ -72,7 +63,7 @@ func GetRunner(ctx *context.APIContext) {
// required: true
// responses:
// "200":
// "$ref": "#/definitions/ActionRunner"
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -80,11 +71,11 @@ func GetRunner(ctx *context.APIContext) {
shared.GetRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id"))
}
// DeleteRunner delete an global runner
// DeleteRunner delete a global runner
func DeleteRunner(ctx *context.APIContext) {
// swagger:operation DELETE /admin/actions/runners/{runner_id} admin deleteAdminRunner
// ---
// summary: Delete an global runner
// summary: Delete a global runner
// produces:
// - application/json
// parameters:
@@ -102,3 +93,34 @@ func DeleteRunner(ctx *context.APIContext) {
// "$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"))
}
+104 -10
View File
@@ -414,22 +414,116 @@ func SearchUsers(ctx *context.APIContext) {
// 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)
users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer,
Type: user_model.UserTypeIndividual,
LoginName: ctx.FormTrim("login_name"),
SourceID: ctx.FormInt64("source_id"),
OrderBy: db.SearchOrderByAlphabetically,
ListOptions: listOptions,
})
orderBy := db.SearchOrderByAlphabetically
sortMode := ctx.FormString("sort")
if len(sortMode) > 0 {
sortOrder := ctx.FormString("order")
if len(sortOrder) == 0 {
sortOrder = "asc"
}
if searchModeMap, ok := user_model.AdminUserOrderByMap[sortOrder]; ok {
if order, ok := searchModeMap[sortMode]; ok {
orderBy = order
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
return
}
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
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
@@ -440,7 +534,7 @@ func SearchUsers(ctx *context.APIContext) {
results[i] = convert.ToUser(ctx, users[i], ctx.Doer)
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &results)
}
@@ -479,7 +573,7 @@ func RenameUser(ctx *context.APIContext) {
newName := web.GetForm(ctx).(*api.RenameUserOption).NewName
// Check if username has been changed
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
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 {
+130 -138
View File
@@ -11,11 +11,9 @@
//
// Consumes:
// - application/json
// - text/plain
//
// Produces:
// - application/json
// - text/html
//
// Security:
// - BasicAuth :
@@ -70,7 +68,6 @@ import (
"net/http"
"strings"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
@@ -78,7 +75,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
@@ -189,29 +185,12 @@ func repoAssignment() func(ctx *context.APIContext) {
repo.Owner = owner
ctx.Repo.Repository = repo
if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID {
taskID := ctx.Data["ActionsTaskID"].(int64)
task, err := actions_model.GetTaskByID(ctx, taskID)
if taskID, ok := user_model.GetActionsUserTaskID(ctx.Doer); ok {
ctx.Repo.Permission, err = access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if task.RepoID != repo.ID {
ctx.APIErrorNotFound()
return
}
if task.IsForkPullRequest {
ctx.Repo.Permission.AccessMode = perm.AccessModeRead
} else {
ctx.Repo.Permission.AccessMode = perm.AccessModeWrite
}
if err := ctx.Repo.Repository.LoadUnits(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode)
} else {
needTwoFactor, err := doerNeedTwoFactorAuth(ctx, ctx.Doer)
if err != nil {
@@ -221,7 +200,7 @@ func repoAssignment() func(ctx *context.APIContext) {
if needTwoFactor {
ctx.Repo.Permission = access_model.PermissionNoAccess()
} else {
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
ctx.Repo.Permission, err = access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -233,6 +212,11 @@ func repoAssignment() func(ctx *context.APIContext) {
ctx.APIErrorNotFound()
return
}
if !ctx.TokenCanAccessRepo(repo) {
ctx.APIErrorNotFound()
return
}
}
}
@@ -270,51 +254,66 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
return
}
// public Only permission check
switch {
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryIssue):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public issues")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization):
if ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
return
}
if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser):
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public users")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub):
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryNotification):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryPackage):
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
return
for _, category := range requiredScopeCategories {
switch category {
case auth_model.AccessTokenScopeCategoryRepository:
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
ctx.APIError(http.StatusForbidden, "token scope is limited to public repos")
return
}
case auth_model.AccessTokenScopeCategoryIssue:
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
ctx.APIError(http.StatusForbidden, "token scope is limited to public issues")
return
}
case auth_model.AccessTokenScopeCategoryOrganization:
orgPrivate := ctx.Org.Organization != nil && !ctx.Org.Organization.Visibility.IsPublic()
userOrgPrivate := ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && !ctx.ContextUser.Visibility.IsPublic()
if orgPrivate || userOrgPrivate {
ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs")
return
}
case auth_model.AccessTokenScopeCategoryUser:
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && !ctx.ContextUser.Visibility.IsPublic() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public users")
return
}
case auth_model.AccessTokenScopeCategoryActivityPub:
if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && !ctx.ContextUser.Visibility.IsPublic() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub")
return
}
case auth_model.AccessTokenScopeCategoryNotification:
if !ctx.TokenCanAccessRepo(ctx.Repo.Repository) {
ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications")
return
}
case auth_model.AccessTokenScopeCategoryPackage:
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
return
}
}
}
}
}
func rejectPublicOnly() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.PublicOnly {
return
}
ctx.APIError(http.StatusForbidden, "this endpoint is not available for public-only tokens")
}
}
func contextAuthenticatedUser() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
ctx.ContextUser = ctx.Doer
}
}
// if a token is being used for auth, we check that it contains the required scope
// if a token is not being used, reqToken will enforce other sign in methods
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) {
@@ -366,11 +365,7 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
// Contexter middleware already checks token for user sign in process.
func reqToken() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
// If actions token is present
if true == ctx.Data["IsActionsToken"] {
return
}
// if a real user is signed in, or the user is from a Actions task, we are good
if ctx.IsSigned {
return
}
@@ -778,13 +773,9 @@ func buildAuthGroup() *auth.Group {
&auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API
)
if setting.Service.EnableReverseProxyAuthAPI {
group.Add(&auth.ReverseProxy{})
group.Add(&auth.ReverseProxy{}) // TODO: does it still make sense to support reverse proxy auth in API?
}
if setting.IsWindows && auth_model.IsSSPIEnabled(graceful.GetManager().ShutdownContext()) {
group.Add(&auth.SSPI{}) // it MUST be the last, see the comment of SSPI
}
// others: API doesn't support SSPI auth because the caller should use token
return group
}
@@ -894,9 +885,9 @@ func checkDeprecatedAuthMethods(ctx *context.APIContext) {
func Routes() *web.Router {
m := web.NewRouter()
m.Use(securityHeaders())
m.BeforeRouting(securityHeaders())
if setting.CORSConfig.Enabled {
m.Use(cors.Handler(cors.Options{
m.BeforeRouting(cors.Handler(cors.Options{
AllowedOrigins: setting.CORSConfig.AllowDomain,
AllowedMethods: setting.CORSConfig.Methods,
AllowCredentials: setting.CORSConfig.AllowCredentials,
@@ -904,48 +895,49 @@ func Routes() *web.Router {
MaxAge: int(setting.CORSConfig.MaxAge.Seconds()),
}))
}
m.Use(context.APIContexter())
m.Use(checkDeprecatedAuthMethods)
m.AfterRouting(context.APIContexter())
m.AfterRouting(checkDeprecatedAuthMethods)
// Get user from session if logged in.
m.Use(apiAuth(buildAuthGroup()))
m.AfterRouting(apiAuth(buildAuthGroup()))
m.Use(verifyAuthWithOptions(&common.VerifyOptions{
m.AfterRouting(verifyAuthWithOptions(&common.VerifyOptions{
SignInRequired: setting.Service.RequireSignInViewStrict,
}))
addActionsRoutes := func(
m *web.Router,
reqChecker func(ctx *context.APIContext),
reqReaderCheck func(ctx *context.APIContext),
reqOwnerCheck func(ctx *context.APIContext),
act actions.API,
) {
m.Group("/actions", func() {
m.Group("/secrets", func() {
m.Get("", reqToken(), reqChecker, act.ListActionsSecrets)
m.Get("", reqToken(), reqOwnerCheck, act.ListActionsSecrets)
m.Combo("/{secretname}").
Put(reqToken(), reqChecker, bind(api.CreateOrUpdateSecretOption{}), act.CreateOrUpdateSecret).
Delete(reqToken(), reqChecker, act.DeleteSecret)
Put(reqToken(), reqOwnerCheck, bind(api.CreateOrUpdateSecretOption{}), act.CreateOrUpdateSecret).
Delete(reqToken(), reqOwnerCheck, act.DeleteSecret)
})
m.Group("/variables", func() {
m.Get("", reqToken(), reqChecker, act.ListVariables)
m.Get("", reqToken(), reqOwnerCheck, act.ListVariables)
m.Combo("/{variablename}").
Get(reqToken(), reqChecker, act.GetVariable).
Delete(reqToken(), reqChecker, act.DeleteVariable).
Post(reqToken(), reqChecker, bind(api.CreateVariableOption{}), act.CreateVariable).
Put(reqToken(), reqChecker, bind(api.UpdateVariableOption{}), act.UpdateVariable)
Get(reqToken(), reqOwnerCheck, act.GetVariable).
Delete(reqToken(), reqOwnerCheck, act.DeleteVariable).
Post(reqToken(), reqOwnerCheck, bind(api.CreateVariableOption{}), act.CreateVariable).
Put(reqToken(), reqOwnerCheck, bind(api.UpdateVariableOption{}), act.UpdateVariable)
})
m.Group("/runners", func() {
m.Get("", reqToken(), reqChecker, act.ListRunners)
m.Get("/registration-token", reqToken(), reqChecker, act.GetRegistrationToken)
m.Post("/registration-token", reqToken(), reqChecker, act.CreateRegistrationToken)
m.Get("/{runner_id}", reqToken(), reqChecker, act.GetRunner)
m.Delete("/{runner_id}", reqToken(), reqChecker, act.DeleteRunner)
m.Get("", reqToken(), reqOwnerCheck, act.ListRunners)
m.Post("/registration-token", reqToken(), reqOwnerCheck, act.CreateRegistrationToken)
m.Get("/{runner_id}", reqToken(), reqOwnerCheck, act.GetRunner)
m.Delete("/{runner_id}", reqToken(), reqOwnerCheck, act.DeleteRunner)
m.Patch("/{runner_id}", reqToken(), reqOwnerCheck, bind(api.EditActionRunnerOption{}), act.UpdateRunner)
})
m.Get("/runs", reqToken(), reqChecker, act.ListWorkflowRuns)
m.Get("/jobs", reqToken(), reqChecker, act.ListWorkflowJobs)
m.Get("/runs", reqToken(), reqReaderCheck, act.ListWorkflowRuns)
m.Get("/jobs", reqToken(), reqReaderCheck, act.ListWorkflowJobs)
})
}
@@ -958,18 +950,8 @@ func Routes() *web.Router {
}
if setting.Federation.Enabled {
m.Get("/nodeinfo", misc.NodeInfo)
m.Group("/activitypub", func() {
// deprecated, remove in 1.20, use /user-id/{user-id} instead
m.Group("/user/{username}", func() {
m.Get("", activitypub.Person)
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
m.Group("/user-id/{user-id}", func() {
m.Get("", activitypub.Person)
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
}, context.UserIDAssignmentAPI(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub))
m.Get("/nodeinfo", activitypub.NotImplemented)
m.Any("/activitypub/*", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub), activitypub.NotImplemented)
}
// Misc (public accessible)
@@ -996,6 +978,8 @@ func Routes() *web.Router {
})
// Notifications (requires 'notifications' scope)
// The notifications API is not available for public-only tokens because a user's notifications mix
// public and private repository events in the same mailbox.
m.Group("/notifications", func() {
m.Combo("").
Get(reqToken(), notify.ListNotifications).
@@ -1004,7 +988,7 @@ func Routes() *web.Router {
m.Combo("/threads/{id}").
Get(reqToken(), notify.GetThread).
Patch(reqToken(), notify.ReadThread)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification), rejectPublicOnly())
// Users (requires user scope)
m.Group("/users", func() {
@@ -1052,8 +1036,9 @@ func Routes() *web.Router {
m.Group("/settings", func() {
m.Get("", user.GetUserSettings)
m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
}, reqToken())
m.Combo("/emails").
}, rejectPublicOnly())
// Email addresses are always private account data.
m.Combo("/emails", rejectPublicOnly()).
Get(user.ListEmails).
Post(bind(api.CreateEmailOption{}), user.AddEmail).
Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail)
@@ -1077,15 +1062,15 @@ func Routes() *web.Router {
m.Group("/runners", func() {
m.Get("", reqToken(), user.ListRunners)
m.Get("/registration-token", reqToken(), user.GetRegistrationToken)
m.Post("/registration-token", reqToken(), user.CreateRegistrationToken)
m.Get("/{runner_id}", reqToken(), user.GetRunner)
m.Delete("/{runner_id}", reqToken(), user.DeleteRunner)
m.Patch("/{runner_id}", reqToken(), bind(api.EditActionRunnerOption{}), user.UpdateRunner)
})
m.Get("/runs", reqToken(), user.ListWorkflowRuns)
m.Get("/jobs", reqToken(), user.ListWorkflowJobs)
})
}, rejectPublicOnly())
m.Get("/followers", user.ListMyFollowers)
m.Group("/following", func() {
@@ -1103,7 +1088,7 @@ func Routes() *web.Router {
Post(bind(api.CreateKeyOption{}), user.CreatePublicKey)
m.Combo("/{id}").Get(user.GetPublicKey).
Delete(user.DeletePublicKey)
})
}, rejectPublicOnly())
// (admin:application scope)
m.Group("/applications", func() {
@@ -1114,7 +1099,7 @@ func Routes() *web.Router {
Delete(user.DeleteOauth2Application).
Patch(bind(api.CreateOAuth2ApplicationOptions{}), user.UpdateOauth2Application).
Get(user.GetOauth2Application)
})
}, rejectPublicOnly())
// (admin:gpg_key scope)
m.Group("/gpg_keys", func() {
@@ -1122,13 +1107,13 @@ func Routes() *web.Router {
Post(bind(api.CreateGPGKeyOption{}), user.CreateGPGKey)
m.Combo("/{id}").Get(user.GetGPGKey).
Delete(user.DeleteGPGKey)
})
m.Get("/gpg_key_token", user.GetVerificationToken)
m.Post("/gpg_key_verify", bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
}, rejectPublicOnly())
m.Get("/gpg_key_token", rejectPublicOnly(), user.GetVerificationToken)
m.Post("/gpg_key_verify", rejectPublicOnly(), bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
// (repo scope)
m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos).
Post(bind(api.CreateRepoOption{}), repo.Create)
Post(rejectPublicOnly(), bind(api.CreateRepoOption{}), repo.Create)
// (repo scope)
m.Group("/starred", func() {
@@ -1139,22 +1124,22 @@ func Routes() *web.Router {
m.Delete("", user.Unstar)
}, repoAssignment(), checkTokenPublicOnly())
}, reqStarsEnabled(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
m.Get("/times", repo.ListMyTrackedTimes)
m.Get("/stopwatches", repo.GetStopwatches)
m.Get("/times", rejectPublicOnly(), repo.ListMyTrackedTimes)
m.Get("/stopwatches", rejectPublicOnly(), repo.GetStopwatches)
m.Get("/subscriptions", user.GetMyWatchedRepos)
m.Get("/teams", org.ListUserTeams)
m.Get("/teams", rejectPublicOnly(), org.ListUserTeams)
m.Group("/hooks", func() {
m.Combo("").Get(user.ListHooks).
Post(bind(api.CreateHookOption{}), user.CreateHook)
m.Combo("/{id}").Get(user.GetHook).
Patch(bind(api.EditHookOption{}), user.EditHook).
Delete(user.DeleteHook)
}, reqWebhooksEnabled())
}, reqWebhooksEnabled(), rejectPublicOnly())
m.Group("/avatar", func() {
m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
m.Delete("", user.DeleteAvatar)
})
}, rejectPublicOnly())
m.Group("/blocks", func() {
m.Get("", user.ListBlocks)
@@ -1163,8 +1148,8 @@ func Routes() *web.Router {
m.Put("", user.BlockUser)
m.Delete("", user.UnblockUser)
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
}, rejectPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), contextAuthenticatedUser(), checkTokenPublicOnly())
// Repositories (requires repo scope, org scope)
m.Post("/org/{org}/repos",
@@ -1198,7 +1183,8 @@ func Routes() *web.Router {
m.Post("/reject", repo.RejectTransfer)
}, reqToken())
addActionsRoutes(m, reqOwner(), repo.NewAction()) // it adds the routes for secrets/variables and runner management
// Adds the routes for secrets/variables and runner management
addActionsRoutes(m, reqRepoReader(unit.TypeActions), reqOwner(), repo.NewAction())
m.Group("/actions/workflows", func() {
m.Get("", repo.ActionsListRepositoryWorkflows)
@@ -1259,15 +1245,16 @@ func Routes() *web.Router {
m.Get("/*", repo.GetBranch)
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch)
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
m.Put("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.UpdateBranchRepoOption{}), repo.UpdateBranch)
m.Patch("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.RenameBranchRepoOption{}), repo.RenameBranch)
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
m.Group("/branch_protections", func() {
m.Get("", repo.ListBranchProtections)
m.Post("", bind(api.CreateBranchProtectionOption{}), mustNotBeArchived, repo.CreateBranchProtection)
m.Group("/{name}", func() {
m.Group("/*", func() {
m.Get("", repo.GetBranchProtection)
m.Patch("", bind(api.EditBranchProtectionOption{}), mustNotBeArchived, repo.EditBranchProtection)
m.Delete("", repo.DeleteBranchProtection)
m.Delete("", mustNotBeArchived, repo.DeleteBranchProtection)
})
m.Post("/priority", bind(api.UpdateBranchProtectionPriories{}), mustNotBeArchived, repo.UpdateBranchProtectionPriories)
}, reqToken(), reqAdmin())
@@ -1292,7 +1279,10 @@ func Routes() *web.Router {
m.Group("/{run}", func() {
m.Get("", repo.GetWorkflowRun)
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
m.Post("/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowRun)
m.Post("/rerun-failed-jobs", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunFailedWorkflowRun)
m.Get("/jobs", repo.ListWorkflowRunJobs)
m.Post("/jobs/{job_id}/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowJob)
m.Get("/artifacts", repo.GetArtifactsOfRun)
})
})
@@ -1369,6 +1359,8 @@ func Routes() *web.Router {
m.Combo("").Get(repo.ListPullRequests).
Post(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
m.Get("/pinned", repo.ListPinnedPullRequests)
m.Post("/comments/{id}/resolve", reqToken(), mustNotBeArchived, repo.ResolvePullReviewComment)
m.Post("/comments/{id}/unresolve", reqToken(), mustNotBeArchived, repo.UnresolvePullReviewComment)
m.Group("/{index}", func() {
m.Combo("").Get(repo.GetPullRequest).
Patch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
@@ -1457,9 +1449,9 @@ func Routes() *web.Router {
Delete(reqToken(), repo.DeleteTopic)
}, reqAdmin())
}, reqAnyRepoReader())
m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates)
m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig)
m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig)
m.Get("/issue_templates", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueTemplates)
m.Get("/issue_config", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.GetIssueConfig)
m.Get("/issue_config/validate", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(), repo.ValidateIssueConfig)
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
m.Get("/licenses", reqRepoReader(unit.TypeCode), repo.GetLicenses)
m.Get("/activities/feeds", repo.ListRepoActivityFeeds)
@@ -1609,10 +1601,11 @@ func Routes() *web.Router {
m.Group("/packages/{username}", func() {
m.Group("/{type}/{name}", func() {
m.Get("/", packages.ListPackageVersions)
m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
m.Group("/{version}", func() {
m.Get("", packages.GetPackage)
m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackageVersion)
m.Get("/files", packages.ListPackageFiles)
})
@@ -1627,7 +1620,7 @@ func Routes() *web.Router {
}, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())
// Organizations
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), checkTokenPublicOnly(), org.ListMyOrgs)
m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
@@ -1648,6 +1641,7 @@ func Routes() *web.Router {
})
addActionsRoutes(
m,
reqOrgMembership(),
reqOrgOwnership(),
org.NewAction(),
)
@@ -1759,13 +1753,11 @@ func Routes() *web.Router {
m.Post("/registration-token", admin.CreateRegistrationToken)
m.Get("/{runner_id}", admin.GetRunner)
m.Delete("/{runner_id}", admin.DeleteRunner)
m.Patch("/{runner_id}", bind(api.EditActionRunnerOption{}), admin.UpdateRunner)
})
m.Get("/runs", admin.ListWorkflowRuns)
m.Get("/jobs", admin.ListWorkflowJobs)
})
m.Group("/runners", func() {
m.Get("/registration-token", admin.GetRegistrationToken)
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin())
m.Group("/topics", func() {
@@ -173,8 +173,8 @@ Here are some links to the most important topics. You can find the full list of
<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", "unsupported file to render: \"path/test.unknown\"\n", http.StatusUnprocessableEntity)
testRenderMarkup(t, "unknown", false, "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity)
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{
@@ -1,78 +0,0 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package misc
import (
"net/http"
"time"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
)
const cacheKeyNodeInfoUsage = "API_NodeInfoUsage"
// NodeInfo returns the NodeInfo for the Gitea instance to allow for federation
func NodeInfo(ctx *context.APIContext) {
// swagger:operation GET /nodeinfo miscellaneous getNodeInfo
// ---
// summary: Returns the nodeinfo of the Gitea application
// produces:
// - application/json
// responses:
// "200":
// "$ref": "#/responses/NodeInfo"
nodeInfoUsage := structs.NodeInfoUsage{}
if setting.Federation.ShareUserStatistics {
cached, _ := ctx.Cache.GetJSON(cacheKeyNodeInfoUsage, &nodeInfoUsage)
if !cached {
usersTotal := int(user_model.CountUsers(ctx, nil))
now := time.Now()
timeOneMonthAgo := now.AddDate(0, -1, 0).Unix()
timeHaveYearAgo := now.AddDate(0, -6, 0).Unix()
usersActiveMonth := int(user_model.CountUsers(ctx, &user_model.CountUserFilter{LastLoginSince: &timeOneMonthAgo}))
usersActiveHalfyear := int(user_model.CountUsers(ctx, &user_model.CountUserFilter{LastLoginSince: &timeHaveYearAgo}))
allIssues, _ := issues_model.CountIssues(ctx, &issues_model.IssuesOptions{})
allComments, _ := issues_model.CountComments(ctx, &issues_model.FindCommentsOptions{})
nodeInfoUsage = structs.NodeInfoUsage{
Users: structs.NodeInfoUsageUsers{
Total: usersTotal,
ActiveMonth: usersActiveMonth,
ActiveHalfyear: usersActiveHalfyear,
},
LocalPosts: int(allIssues),
LocalComments: int(allComments),
}
if err := ctx.Cache.PutJSON(cacheKeyNodeInfoUsage, nodeInfoUsage, 180); err != nil {
ctx.APIErrorInternal(err)
return
}
}
}
nodeInfo := &structs.NodeInfo{
Version: "2.1",
Software: structs.NodeInfoSoftware{
Name: "gitea",
Version: setting.AppVer,
Repository: "https://github.com/go-gitea/gitea.git",
Homepage: "https://gitea.io/",
},
Protocols: []string{"activitypub"},
Services: structs.NodeInfoServices{
Inbound: []string{},
Outbound: []string{"rss2.0"},
},
OpenRegistrations: setting.Service.ShowRegistrationButton,
Usage: nodeInfoUsage,
}
ctx.JSON(http.StatusOK, nodeInfo)
}
@@ -10,13 +10,8 @@ import (
)
func getSigningKey(ctx *context.APIContext, expectedFormat string) {
// if the handler is in the repo's route group, get the repo's signing key
// otherwise, get the global signing key
path := ""
if ctx.Repo != nil && ctx.Repo.Repository != nil {
path = ctx.Repo.Repository.RepoPath()
}
content, format, err := asymkey_service.PublicSigningKey(ctx, path)
// get the global signing key
content, format, err := asymkey_service.PublicSigningKey(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -125,8 +125,8 @@ func ListRepoNotifications(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(totalCount, opts.PageSize)
ctx.SetTotalCountHeader(totalCount)
ctx.JSON(http.StatusOK, convert.ToNotifications(ctx, nl))
}
@@ -86,6 +86,7 @@ func ListNotifications(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(totalCount, opts.PageSize)
ctx.SetTotalCountHeader(totalCount)
ctx.JSON(http.StatusOK, convert.ToNotifications(ctx, nl))
}
@@ -67,6 +67,7 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) {
}
}
ctx.SetLinkHeader(count, opts.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiSecrets)
}
@@ -169,27 +170,6 @@ func (Action) DeleteSecret(ctx *context.APIContext) {
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
// GetRegistrationToken returns the token to register org runners
func (Action) GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/runners/registration-token organization orgGetRunnerRegistrationToken
// ---
// 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)
}
// 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) {
@@ -240,9 +220,10 @@ func (Action) ListVariables(ctx *context.APIContext) {
// "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: utils.GetListOptions(ctx),
ListOptions: listOptions,
})
if err != nil {
ctx.APIErrorInternal(err)
@@ -259,7 +240,7 @@ func (Action) ListVariables(ctx *context.APIContext) {
Description: v.Description,
}
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables)
}
@@ -504,9 +485,14 @@ func (Action) ListRunners(ctx *context.APIContext) {
// 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": "#/definitions/ActionRunnersResponse"
// "$ref": "#/responses/RunnerList"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -534,7 +520,7 @@ func (Action) GetRunner(ctx *context.APIContext) {
// required: true
// responses:
// "200":
// "$ref": "#/definitions/ActionRunner"
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -570,6 +556,42 @@ func (Action) DeleteRunner(ctx *context.APIContext) {
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
// ---
@@ -13,7 +13,7 @@ import (
webhook_service "code.gitea.io/gitea/services/webhook"
)
// ListHooks list an organziation's webhooks
// ListHooks list an organization's webhooks
func ListHooks(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/hooks organization orgListHooks
// ---
@@ -20,11 +20,12 @@ import (
// 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: utils.GetListOptions(ctx),
ListOptions: listOptions,
}
count, err := organization.CountOrgMembers(ctx, opts)
@@ -44,6 +45,7 @@ func listMembers(ctx *context.APIContext, isMember bool) {
apiMembers[i] = convert.ToUser(ctx, member, ctx.Doer)
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiMembers)
}
+25 -16
View File
@@ -5,6 +5,7 @@
package org
import (
"errors"
"net/http"
activities_model "code.gitea.io/gitea/models/activities"
@@ -14,6 +15,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
@@ -31,6 +33,7 @@ func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
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)
@@ -42,7 +45,7 @@ func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
apiOrgs[i] = convert.ToOrganization(ctx, orgs[i])
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &apiOrgs)
}
@@ -135,7 +138,7 @@ func GetUserOrgsPermissions(ctx *context.APIContext) {
op := api.OrganizationPermissions{}
if !organization.HasOrgOrUserVisible(ctx, o, ctx.ContextUser) {
if !organization.HasOrgOrUserVisible(ctx, o, ctx.Doer) {
ctx.APIErrorNotFound("HasOrgOrUserVisible", nil)
return
}
@@ -190,7 +193,7 @@ func GetAll(ctx *context.APIContext) {
// "$ref": "#/responses/OrganizationList"
vMode := []api.VisibleType{api.VisibleTypePublic}
if ctx.IsSigned && !ctx.PublicOnly {
if ctx.IsSigned {
vMode = append(vMode, api.VisibleTypeLimited)
if ctx.Doer.IsAdmin {
vMode = append(vMode, api.VisibleTypePrivate)
@@ -199,13 +202,16 @@ func GetAll(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx)
publicOrgs, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
searchOpts := user_model.SearchUserOptions{
Actor: ctx.Doer,
ListOptions: listOptions,
Type: user_model.UserTypeOrganization,
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
@@ -215,7 +221,7 @@ func GetAll(ctx *context.APIContext) {
orgs[i] = convert.ToOrganization(ctx, organization.OrgFromUser(publicOrgs[i]))
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &orgs)
}
@@ -340,7 +346,7 @@ func Rename(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.RenameOrgOption)
orgUser := ctx.Org.Organization.AsUser()
if err := user_service.RenameUser(ctx, orgUser, form.NewName); err != nil {
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 {
@@ -379,19 +385,21 @@ func Edit(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditOrgOption)
if form.Email != "" {
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil {
ctx.APIErrorInternal(err)
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.Some(form.FullName),
Description: optional.Some(form.Description),
Website: optional.Some(form.Website),
Location: optional.Some(form.Location),
Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
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, optional.FromPtr(form.Visibility).Value()),
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
}
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
@@ -483,6 +491,7 @@ func ListOrgActivityFeeds(ctx *context.APIContext) {
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
feeds, count, err := feed_service.GetFeeds(ctx, opts)
if err != nil {
@@ -54,8 +54,9 @@ func ListTeams(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
teams, count, err := organization.SearchTeam(ctx, &organization.SearchTeamOptions{
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
OrgID: ctx.Org.Organization.ID,
})
if err != nil {
@@ -69,6 +70,7 @@ func ListTeams(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiTeams)
}
@@ -93,8 +95,9 @@ func ListUserTeams(ctx *context.APIContext) {
// "200":
// "$ref": "#/responses/TeamList"
listOptions := utils.GetListOptions(ctx)
teams, count, err := organization.SearchTeam(ctx, &organization.SearchTeamOptions{
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
UserID: ctx.Doer.ID,
})
if err != nil {
@@ -108,6 +111,7 @@ func ListUserTeams(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiTeams)
}
@@ -392,8 +396,9 @@ func GetTeamMembers(ctx *context.APIContext) {
return
}
listOptions := utils.GetListOptions(ctx)
teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
TeamID: ctx.Org.Team.ID,
})
if err != nil {
@@ -406,6 +411,7 @@ func GetTeamMembers(ctx *context.APIContext) {
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)
}
@@ -559,8 +565,9 @@ func GetTeamRepos(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
team := ctx.Org.Team
listOptions := utils.GetListOptions(ctx)
teamRepos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
TeamID: team.ID,
})
if err != nil {
@@ -569,13 +576,14 @@ func GetTeamRepos(ctx *context.APIContext) {
}
repos := make([]*api.Repository, len(teamRepos))
for i, repo := range teamRepos {
permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
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)
}
@@ -620,7 +628,7 @@ func GetTeamRepo(ctx *context.APIContext) {
return
}
permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -798,7 +806,7 @@ func SearchTeam(ctx *context.APIContext) {
ListOptions: listOptions,
}
// Only admin is allowd to search for all teams
// Only admin is allowed to search for all teams
if !ctx.Doer.IsAdmin {
opts.UserID = ctx.Doer.ID
}
@@ -819,7 +827,7 @@ func SearchTeam(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, map[string]any{
"ok": true,
@@ -874,7 +882,7 @@ func ListTeamActivityFeeds(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
}
@@ -43,7 +43,7 @@ func ListPackages(ctx *context.APIContext) {
// 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, vagrant]
// 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
@@ -68,7 +68,7 @@ func ListPackages(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiPackages)
}
@@ -118,7 +118,7 @@ func GetPackage(ctx *context.APIContext) {
// DeletePackage deletes a package
func DeletePackage(ctx *context.APIContext) {
// swagger:operation DELETE /packages/{owner}/{type}/{name}/{version} package deletePackage
// swagger:operation DELETE /packages/{owner}/{type}/{name} package deletePackage
// ---
// summary: Delete a package
// parameters:
@@ -137,6 +137,41 @@ func DeletePackage(ctx *context.APIContext) {
// 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
@@ -249,7 +284,7 @@ func ListPackageVersions(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(count), listOptions.PageSize)
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiPackages)
}
@@ -6,12 +6,12 @@ package repo
import (
go_context "context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/url"
"slices"
"strconv"
"strings"
"time"
@@ -22,7 +22,6 @@ import (
secret_model "code.gitea.io/gitea/models/secret"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -69,10 +68,11 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
listOptions := utils.GetListOptions(ctx)
opts := &secret_model.FindSecretsOptions{
RepoID: repo.ID,
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
}
secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts)
@@ -89,7 +89,7 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) {
Created: v.CreatedUnix.AsTime(),
}
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiSecrets)
}
@@ -482,9 +482,11 @@ func (Action) ListVariables(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
RepoID: ctx.Repo.Repository.ID,
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
})
if err != nil {
ctx.APIErrorInternal(err)
@@ -502,35 +504,11 @@ func (Action) ListVariables(ctx *context.APIContext) {
}
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables)
}
// GetRegistrationToken returns the token to register repo runners
func (Action) GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runners/registration-token repository repoGetRunnerRegistrationToken
// ---
// summary: Get a repository's actions runner registration token
// 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/RegistrationToken"
shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID)
}
// CreateRegistrationToken returns the token to register repo runners
func (Action) CreateRegistrationToken(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/runners/registration-token repository repoCreateRunnerRegistrationToken
@@ -574,9 +552,14 @@ func (Action) ListRunners(ctx *context.APIContext) {
// description: name of the repo
// type: string
// required: true
// - name: disabled
// in: query
// description: filter by disabled status (true or false)
// type: boolean
// required: false
// responses:
// "200":
// "$ref": "#/definitions/ActionRunnersResponse"
// "$ref": "#/responses/RunnerList"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -584,11 +567,11 @@ func (Action) ListRunners(ctx *context.APIContext) {
shared.ListRunners(ctx, 0, ctx.Repo.Repository.ID)
}
// GetRunner get an repo-level runner
// GetRunner get a repo-level runner
func (Action) GetRunner(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runners/{runner_id} repository getRepoRunner
// ---
// summary: Get an repo-level runner
// summary: Get a repo-level runner
// produces:
// - application/json
// parameters:
@@ -609,7 +592,7 @@ func (Action) GetRunner(ctx *context.APIContext) {
// required: true
// responses:
// "200":
// "$ref": "#/definitions/ActionRunner"
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -617,11 +600,11 @@ func (Action) GetRunner(ctx *context.APIContext) {
shared.GetRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
}
// DeleteRunner delete an repo-level runner
// DeleteRunner delete a repo-level runner
func (Action) DeleteRunner(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/actions/runners/{runner_id} repository deleteRepoRunner
// ---
// summary: Delete an repo-level runner
// summary: Delete a repo-level runner
// produces:
// - application/json
// parameters:
@@ -650,6 +633,47 @@ func (Action) DeleteRunner(ctx *context.APIContext) {
shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
}
// UpdateRunner update a repo-level runner
func (Action) UpdateRunner(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/actions/runners/{runner_id} repository updateRepoRunner
// ---
// summary: Update a repo-level runner
// 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: 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, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
}
// GetWorkflowRunJobs Lists all jobs for a workflow run.
func (Action) ListWorkflowJobs(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/jobs repository listWorkflowJobs
@@ -807,9 +831,10 @@ func ListActionTasks(ctx *context.APIContext) {
// "$ref": "#/responses/conflict"
// "422":
// "$ref": "#/responses/validationError"
listOptions := utils.GetListOptions(ctx)
tasks, total, err := db.FindAndCount[actions_model.ActionTask](ctx, &actions_model.FindTaskOptions{
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
RepoID: ctx.Repo.Repository.ID,
})
if err != nil {
@@ -830,6 +855,8 @@ func ListActionTasks(ctx *context.APIContext) {
res.Entries[i] = convertedTask
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total) // Duplicates api response field but it's better to set it for consistency
ctx.JSON(http.StatusOK, &res)
}
@@ -997,9 +1024,15 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
// in: body
// schema:
// "$ref": "#/definitions/CreateActionWorkflowDispatch"
// - name: return_run_details
// description: Whether the response should include the workflow run ID and URLs.
// in: query
// type: boolean
// responses:
// "200":
// "$ref": "#/responses/RunDetails"
// "204":
// description: No Content
// description: No Content, if return_run_details is missing or false
// "400":
// "$ref": "#/responses/error"
// "403":
@@ -1016,7 +1049,7 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
return
}
err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
runID, err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
if strings.Contains(ctx.Req.Header.Get("Content-Type"), "form-urlencoded") {
// The chi framework's "Binding" doesn't support to bind the form map values into a map[string]string
// So we have to manually read the `inputs[key]` from the form
@@ -1047,7 +1080,16 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
return
}
ctx.Status(http.StatusNoContent)
if !ctx.FormBool("return_run_details") {
ctx.Status(http.StatusNoContent)
return
}
ctx.JSON(http.StatusOK, &api.RunDetails{
WorkflowRunID: runID,
HTMLURL: fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.Repository.HTMLURL(ctx), runID),
RunURL: fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.Repository.APIURL(), runID),
})
}
func ActionsEnableWorkflow(ctx *context.APIContext) {
@@ -1100,6 +1142,33 @@ func ActionsEnableWorkflow(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
func getCurrentRepoActionRunByID(ctx *context.APIContext) *actions_model.ActionRun {
runID := ctx.PathParamInt64("run")
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound(err)
return nil
} else if err != nil {
ctx.APIErrorInternal(err)
return nil
}
return run
}
func getCurrentRepoActionRunJobsByID(ctx *context.APIContext) (*actions_model.ActionRun, actions_model.ActionJobList) {
run := getCurrentRepoActionRunByID(ctx)
if ctx.Written() {
return nil, nil
}
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
}
return run, jobs
}
// GetWorkflowRun Gets a specific workflow run.
func GetWorkflowRun(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun
@@ -1121,7 +1190,7 @@ func GetWorkflowRun(ctx *context.APIContext) {
// - name: run
// in: path
// description: id of the run
// type: string
// type: integer
// required: true
// responses:
// "200":
@@ -1131,19 +1200,12 @@ func GetWorkflowRun(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
runID := ctx.PathParamInt64("run")
job, has, err := db.GetByID[actions_model.ActionRun](ctx, runID)
if err != nil {
ctx.APIErrorInternal(err)
run := getCurrentRepoActionRunByID(ctx)
if ctx.Written() {
return
}
if !has || job.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound(util.ErrNotExist)
return
}
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, job)
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -1151,6 +1213,179 @@ func GetWorkflowRun(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, convertedRun)
}
// RerunWorkflowRun Reruns an entire workflow run.
func RerunWorkflowRun(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/rerun repository rerunWorkflowRun
// ---
// summary: Reruns an entire 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: run
// in: path
// description: id of the run
// type: integer
// required: true
// responses:
// "201":
// "$ref": "#/responses/WorkflowRun"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
run, jobs := getCurrentRepoActionRunJobsByID(ctx)
if ctx.Written() {
return
}
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs); err != nil {
handleWorkflowRerunError(ctx, err)
return
}
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convertedRun)
}
// RerunFailedWorkflowRun Reruns all failed jobs in a workflow run.
func RerunFailedWorkflowRun(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/rerun-failed-jobs repository rerunFailedWorkflowRun
// ---
// summary: Reruns all failed jobs in a workflow run
// 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: run
// in: path
// description: id of the run
// type: integer
// required: true
// responses:
// "201":
// "$ref": "#/responses/empty"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
run, jobs := getCurrentRepoActionRunJobsByID(ctx)
if ctx.Written() {
return
}
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetFailedRerunJobs(jobs)); err != nil {
handleWorkflowRerunError(ctx, err)
return
}
ctx.Status(http.StatusCreated)
}
// RerunWorkflowJob Reruns a specific workflow job in a run.
func RerunWorkflowJob(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/rerun repository rerunWorkflowJob
// ---
// summary: Reruns a specific workflow job in a 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: run
// in: path
// description: id of the run
// type: integer
// required: true
// - name: job_id
// in: path
// description: id of the job
// type: integer
// required: true
// responses:
// "201":
// "$ref": "#/responses/WorkflowJob"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
run, jobs := getCurrentRepoActionRunJobsByID(ctx)
if ctx.Written() {
return
}
jobID := ctx.PathParamInt64("job_id")
jobIdx := slices.IndexFunc(jobs, func(job *actions_model.ActionRunJob) bool { return job.ID == jobID })
if jobIdx == -1 {
ctx.APIErrorNotFound(util.NewNotExistErrorf("workflow job with id %d", jobID))
return
}
targetJob := jobs[jobIdx]
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetAllRerunJobs(targetJob, jobs)); err != nil {
handleWorkflowRerunError(ctx, err)
return
}
convertedJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, targetJob)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convertedJob)
}
func handleWorkflowRerunError(ctx *context.APIContext, err error) {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
return
}
ctx.APIErrorInternal(err)
}
// ListWorkflowRunJobs Lists all jobs for a workflow run.
func ListWorkflowRunJobs(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs repository listWorkflowRunJobs
@@ -1195,9 +1430,7 @@ func ListWorkflowRunJobs(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
repoID := ctx.Repo.Repository.ID
runID := ctx.PathParamInt64("run")
repoID, runID := ctx.Repo.Repository.ID, ctx.PathParamInt64("run")
// Avoid the list all jobs functionality for this api route to be used with a runID == 0.
if runID <= 0 {
@@ -1297,10 +1530,8 @@ func GetArtifactsOfRun(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
repoID := ctx.Repo.Repository.ID
artifactName := ctx.Req.URL.Query().Get("name")
runID := ctx.PathParamInt64("run")
repoID, runID := ctx.Repo.Repository.ID, ctx.PathParamInt64("run")
artifacts, total, err := db.FindAndCount[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
RepoID: repoID,
@@ -1361,15 +1592,11 @@ func DeleteActionRun(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
runID := ctx.PathParamInt64("run")
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound(err)
return
} else if err != nil {
ctx.APIErrorInternal(err)
run := getCurrentRepoActionRunByID(ctx)
if ctx.Written() {
return
}
if !run.Status.IsDone() {
ctx.APIError(http.StatusBadRequest, "this workflow run is not done")
return
@@ -1541,11 +1768,7 @@ func DeleteArtifact(ctx *context.APIContext) {
}
func buildSignature(endp string, expires, artifactID int64) []byte {
mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
mac.Write([]byte(endp))
fmt.Fprint(mac, expires)
fmt.Fprint(mac, artifactID)
return mac.Sum(nil)
return actions.BuildSignature("api", endp, strconv.FormatInt(expires, 10), strconv.FormatInt(artifactID, 10))
}
func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) string {
@@ -1555,7 +1778,7 @@ func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) str
func buildSigURL(ctx go_context.Context, endPoint string, artifactID int64) string {
// endPoint is a path like "api/v1/repos/owner/repo/actions/artifacts/1/zip/raw"
expires := time.Now().Add(60 * time.Minute).Unix()
uploadURL := httplib.GuessCurrentAppURL(ctx) + endPoint + "?sig=" + base64.URLEncoding.EncodeToString(buildSignature(endPoint, expires, artifactID)) + "&expires=" + strconv.FormatInt(expires, 10)
uploadURL := httplib.GuessCurrentAppURL(ctx) + endPoint + "?sig=" + base64.RawURLEncoding.EncodeToString(buildSignature(endPoint, expires, artifactID)) + "&expires=" + strconv.FormatInt(expires, 10)
return uploadURL
}
@@ -1600,18 +1823,16 @@ func DownloadArtifact(ctx *context.APIContext) {
ctx.APIError(http.StatusNotFound, "Artifact has expired")
return
}
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName))
if actions.IsArtifactV4(art) {
ok, err := actions.DownloadArtifactV4ServeDirectOnly(ctx.Base, art)
if ok {
return
}
if err != nil {
ctx.APIErrorInternal(err)
// @actions/toolkit asserts that downloaded artifacts of a different runid return 302
// https://github.com/actions/toolkit/blob/44d43b5490b02998bd09b0c4ff369a4cc67876c2/packages/artifact/src/internal/download/download-artifact.ts#L203-L210
if actions.DownloadArtifactV4ServeDirect(ctx.Base, art) {
return
}
// @actions/toolkit asserts a 302 for the artifact download, so we have to build a signed URL and redirect to it
// TODO: a perma link to the code for reference
redirectURL := buildSigURL(ctx, buildDownloadRawEndpoint(ctx.Repo.Repository, art.ID), art.ID)
ctx.Redirect(redirectURL, http.StatusFound)
return
@@ -1639,7 +1860,7 @@ func DownloadArtifactRaw(ctx *context.APIContext) {
sigStr := ctx.Req.URL.Query().Get("sig")
expiresStr := ctx.Req.URL.Query().Get("expires")
sigBytes, _ := base64.URLEncoding.DecodeString(sigStr)
sigBytes, _ := base64.RawURLEncoding.DecodeString(sigStr)
expires, _ := strconv.ParseInt(expiresStr, 10, 64)
expectedSig := buildSignature(buildDownloadRawEndpoint(repo, art.ID), expires, art.ID)
@@ -1658,8 +1879,6 @@ func DownloadArtifactRaw(ctx *context.APIContext) {
ctx.APIError(http.StatusNotFound, "Artifact has expired")
return
}
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName))
if actions.IsArtifactV4(art) {
err := actions.DownloadArtifactV4(ctx.Base, art)
if err != nil {
@@ -43,9 +43,13 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
jobID := ctx.PathParamInt64("job_id")
curJob, err := actions_model.GetRunJobByID(ctx, jobID)
curJob, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobID)
if err != nil {
ctx.APIErrorInternal(err)
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
return
}
if err = curJob.LoadRepo(ctx); err != nil {
@@ -150,12 +150,12 @@ func DeleteBranch(ctx *context.APIContext) {
}
}
if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName, nil); err != nil {
if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
switch {
case git.IsErrBranchNotExist(err):
ctx.APIErrorNotFound(err)
case errors.Is(err, repo_service.ErrBranchIsDefault):
ctx.APIError(http.StatusForbidden, errors.New("can not delete default branch"))
ctx.APIError(http.StatusForbidden, errors.New("can not delete default or pull request target branch"))
case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.APIError(http.StatusForbidden, errors.New("branch protected"))
default:
@@ -225,7 +225,7 @@ func CreateBranch(ctx *context.APIContext) {
return
}
} else if len(opt.OldBranchName) > 0 { //nolint:staticcheck // deprecated field
if gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, opt.OldBranchName) { //nolint:staticcheck // deprecated field
if exist, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, opt.OldBranchName); exist { //nolint:staticcheck // deprecated field
oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(opt.OldBranchName) //nolint:staticcheck // deprecated field
if err != nil {
ctx.APIErrorInternal(err)
@@ -243,7 +243,7 @@ func CreateBranch(ctx *context.APIContext) {
}
}
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, oldCommit.ID.String(), opt.BranchName)
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, oldCommit.ID.String(), opt.BranchName)
if err != nil {
if git_model.IsErrBranchNotExist(err) {
ctx.APIError(http.StatusNotFound, "The old branch does not exist")
@@ -375,11 +375,86 @@ func ListBranches(ctx *context.APIContext) {
}
}
ctx.SetLinkHeader(int(totalNumOfBranches), listOptions.PageSize)
ctx.SetLinkHeader(totalNumOfBranches, listOptions.PageSize)
ctx.SetTotalCountHeader(totalNumOfBranches)
ctx.JSON(http.StatusOK, apiBranches)
}
// UpdateBranch moves a branch reference to a new commit.
func UpdateBranch(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/branches/{branch} repository repoUpdateBranch
// ---
// summary: Update a branch reference to a new commit
// 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: branch
// in: path
// description: name of the branch
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateBranchRepoOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/conflict"
// "422":
// "$ref": "#/responses/validationError"
opt := web.GetForm(ctx).(*api.UpdateBranchRepoOption)
branchName := ctx.PathParam("*")
repo := ctx.Repo.Repository
if repo.IsEmpty {
ctx.APIError(http.StatusNotFound, "Git Repository is empty.")
return
}
if repo.IsMirror {
ctx.APIError(http.StatusForbidden, "Git Repository is a mirror.")
return
}
// permission check has been done in api.go
if err := repo_service.UpdateBranch(ctx, repo, ctx.Repo.GitRepo, ctx.Doer, branchName, opt.NewCommitID, opt.OldCommitID, opt.Force); err != nil {
switch {
case git_model.IsErrBranchNotExist(err):
ctx.APIErrorNotFound(err)
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIError(http.StatusUnprocessableEntity, err)
case git.IsErrPushRejected(err):
rej := err.(*git.ErrPushRejected)
ctx.APIError(http.StatusForbidden, rej.Message)
default:
ctx.APIErrorInternal(err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// RenameBranch renames a repository's branch.
func RenameBranch(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/branches/{branch} repository repoRenameBranch
@@ -434,13 +509,13 @@ func RenameBranch(ctx *context.APIContext) {
return
}
msg, err := repo_service.RenameBranch(ctx, repo, ctx.Doer, ctx.Repo.GitRepo, oldName, opt.Name)
msg, err := repo_service.RenameBranch(ctx, repo, ctx.Doer, oldName, opt.Name)
if err != nil {
switch {
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
ctx.APIError(http.StatusForbidden, "User must be a repo or site admin to rename default or protected branches.")
case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.APIError(http.StatusForbidden, "Branch is protected by glob-based protection rules.")
ctx.APIError(http.StatusForbidden, "Failed to rename branch due to branch protection rules.")
default:
ctx.APIErrorInternal(err)
}
@@ -488,7 +563,7 @@ func GetBranchProtection(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
bpName := ctx.PathParam("name")
bpName := ctx.PathParam("*")
bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
if err != nil {
ctx.APIErrorInternal(err)
@@ -770,7 +845,7 @@ func EditBranchProtection(ctx *context.APIContext) {
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.EditBranchProtectionOption)
repo := ctx.Repo.Repository
bpName := ctx.PathParam("name")
bpName := ctx.PathParam("*")
protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
if err != nil {
ctx.APIErrorInternal(err)
@@ -1011,7 +1086,11 @@ func EditBranchProtection(ctx *context.APIContext) {
isPlainRule := !git_model.IsRuleNameSpecial(bpName)
var isBranchExist bool
if isPlainRule {
isBranchExist = gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, bpName)
isBranchExist, err = git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, bpName)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
if isBranchExist {
@@ -1089,7 +1168,7 @@ func DeleteBranchProtection(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
bpName := ctx.PathParam("name")
bpName := ctx.PathParam("*")
bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
if err != nil {
ctx.APIErrorInternal(err)
@@ -291,7 +291,7 @@ func GetRepoPermissions(ctx *context.APIContext) {
return
}
permission, err := access_model.GetUserRepoPermission(ctx, ctx.Repo.Repository, collaborator)
permission, err := access_model.GetIndividualUserRepoPermission(ctx, ctx.Repo.Repository, collaborator)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -13,6 +13,7 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
@@ -222,8 +223,7 @@ func GetAllCommits(ctx *context.APIContext) {
}
// Total commit count
commitsCountTotal, err = git.CommitsCount(ctx.Repo.GitRepo.Ctx, git.CommitsCountOptions{
RepoPath: ctx.Repo.GitRepo.Path,
commitsCountTotal, err = gitrepo.CommitsCount(ctx, ctx.Repo.Repository, gitrepo.CommitsCountOptions{
Not: not,
Revision: []string{baseCommit.ID.String()},
Since: since,
@@ -245,9 +245,8 @@ func GetAllCommits(ctx *context.APIContext) {
sha = ctx.Repo.Repository.DefaultBranch
}
commitsCountTotal, err = git.CommitsCount(ctx,
git.CommitsCountOptions{
RepoPath: ctx.Repo.GitRepo.Path,
commitsCountTotal, err = gitrepo.CommitsCount(ctx, ctx.Repo.Repository,
gitrepo.CommitsCountOptions{
Not: not,
Revision: []string{sha},
RelPath: []string{path},
@@ -291,7 +290,7 @@ func GetAllCommits(ctx *context.APIContext) {
}
}
ctx.SetLinkHeader(int(commitsCountTotal), listOptions.PageSize)
ctx.SetLinkHeader(commitsCountTotal, listOptions.PageSize)
ctx.SetTotalCountHeader(commitsCountTotal)
// kept for backwards compatibility
@@ -5,7 +5,6 @@ package repo
import (
"net/http"
"strings"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
@@ -52,18 +51,7 @@ func CompareDiff(ctx *context.APIContext) {
}
}
infoPath := ctx.PathParam("*")
infos := []string{ctx.Repo.Repository.DefaultBranch, ctx.Repo.Repository.DefaultBranch}
if infoPath != "" {
infos = strings.SplitN(infoPath, "...", 2)
if len(infos) != 2 {
if infos = strings.SplitN(infoPath, "..", 2); len(infos) != 2 {
infos = []string{ctx.Repo.Repository.DefaultBranch, infoPath}
}
}
}
compareResult, closer := parseCompareInfo(ctx, api.CreatePullRequestOption{Base: infos[0], Head: infos[1]})
compareInfo, closer := parseCompareInfo(ctx, ctx.PathParam("*"))
if ctx.Written() {
return
}
@@ -72,15 +60,22 @@ func CompareDiff(ctx *context.APIContext) {
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
files := ctx.FormString("files") == "" || ctx.FormBool("files")
apiCommits := make([]*api.Commit, 0, len(compareResult.compareInfo.Commits))
apiCommits := make([]*api.Commit, 0, len(compareInfo.Commits))
userCache := make(map[string]*user_model.User)
for i := 0; i < len(compareResult.compareInfo.Commits); i++ {
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareResult.compareInfo.Commits[i], userCache,
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
@@ -89,7 +84,7 @@ func CompareDiff(ctx *context.APIContext) {
}
ctx.JSON(http.StatusOK, &api.Compare{
TotalCommits: len(compareResult.compareInfo.Commits),
TotalCommits: len(compareInfo.Commits),
Commits: apiCommits,
})
}
@@ -7,38 +7,48 @@ import (
"errors"
"net/http"
"code.gitea.io/gitea/modules/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
archiver_service "code.gitea.io/gitea/services/repository/archiver"
)
func serveRepoArchive(ctx *context.APIContext, reqFileName string) {
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, reqFileName)
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, archiver_service.ErrUnknownArchiveFormat{}) {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
} else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
archiver_service.ServeRepoArchive(ctx.Base, ctx.Repo.Repository, ctx.Repo.GitRepo, aReq)
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 git.ArchiveType
var tp repo_model.ArchiveType
switch ballType := ctx.PathParam("ball_type"); ballType {
case "tarball":
tp = git.ArchiveTarGz
tp = repo_model.ArchiveTarGz
case "zipball":
tp = git.ArchiveZip
tp = repo_model.ArchiveZip
case "bundle":
tp = git.ArchiveBundle
tp = repo_model.ArchiveBundle
default:
ctx.APIError(http.StatusBadRequest, "Unknown archive type: "+ballType)
return
}
serveRepoArchive(ctx, ctx.PathParam("*")+"."+tp.String())
serveRepoArchive(ctx, ctx.PathParam("*")+"."+tp.String(), ctx.FormStrings("path"))
}
@@ -17,9 +17,9 @@ import (
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
api "code.gitea.io/gitea/modules/structs"
@@ -138,7 +138,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
// 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.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
return
}
@@ -151,35 +151,18 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
// 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)
dataRc, err := blob.DataAsync()
lfsPointerBuf, err := blob.GetBlobBytes(lfs.MetaFileMaxSize)
if err != nil {
ctx.APIErrorInternal(err)
return
}
buf, err := io.ReadAll(dataRc)
if err != nil {
_ = dataRc.Close()
ctx.APIErrorInternal(err)
return
}
if err := dataRc.Close(); err != nil {
log.Error("Error whilst closing blob %s reader in %-v. Error: %v", blob.ID, ctx.Repo.Repository, err)
}
// Check if the blob represents a pointer
pointer, _ := lfs.ReadPointer(bytes.NewReader(buf))
pointer, _ := lfs.ReadPointerFromBuffer(lfsPointerBuf)
// if it's not a pointer, just serve the data directly
if !pointer.IsValid() {
// First handle caching for the blob
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
return
}
// If not cached - serve!
common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
_, _ = ctx.Resp.Write(lfsPointerBuf)
return
}
@@ -188,12 +171,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
// If there isn't one, just serve the data directly
if errors.Is(err, git_model.ErrLFSObjectNotExist) {
// Handle caching for the blob SHA (not the LFS object OID)
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
return
}
common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
_, _ = ctx.Resp.Write(lfsPointerBuf)
return
} else if err != nil {
ctx.APIErrorInternal(err)
@@ -201,27 +179,26 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
}
// Handle caching for the LFS object OID
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.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.URL(pointer.RelativePath(), blob.Name(), ctx.Req.Method, nil)
u, err := storage.LFS.ServeDirectURL(pointer.RelativePath(), blob.Name(), ctx.Req.Method, nil)
if u != nil && err == nil {
ctx.Redirect(u.String())
return
}
}
lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer)
lfsDataFile, err := lfs.ReadMetaObject(meta.Pointer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
defer lfsDataRc.Close()
common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc)
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) {
@@ -273,13 +250,19 @@ func GetArchive(ctx *context.APIContext) {
// 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("*"))
serveRepoArchive(ctx, ctx.PathParam("*"), ctx.FormStrings("path"))
}
// GetEditorconfig get editor config of a repository
@@ -355,6 +338,7 @@ func ReqChangeRepoFileOptionsAndCheck(ctx *context.APIContext) {
Message: commonOpts.Message,
OldBranch: commonOpts.BranchName,
NewBranch: commonOpts.NewBranchName,
ForcePush: commonOpts.ForcePush,
Committer: &files_service.IdentityOptions{
GitUserName: commonOpts.Committer.Name,
GitUserEmail: commonOpts.Committer.Email,
@@ -524,7 +508,7 @@ func CreateFile(ctx *context.APIContext) {
func UpdateFile(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
// ---
// summary: Update a file in a repository
// summary: Update a file in a repository if SHA is set, or create the file if SHA is not set
// consumes:
// - application/json
// produces:
@@ -553,6 +537,8 @@ func UpdateFile(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/FileResponse"
// "201":
// "$ref": "#/responses/FileResponse"
// "403":
// "$ref": "#/responses/error"
// "404":
@@ -571,8 +557,9 @@ func UpdateFile(ctx *context.APIContext) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
willCreate := apiOpts.SHA == ""
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
Operation: "update",
Operation: util.Iif(willCreate, "create", "update"),
ContentReader: contentReader,
SHA: apiOpts.SHA,
FromTreePath: apiOpts.FromPath,
@@ -586,11 +573,16 @@ func UpdateFile(ctx *context.APIContext) {
handleChangeRepoFilesError(ctx, err)
} else {
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
ctx.JSON(http.StatusOK, fileResponse)
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
@@ -601,10 +593,6 @@ func handleChangeRepoFilesError(ctx *context.APIContext, err error) {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
if git.IsErrBranchNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
return
}
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
return
@@ -6,7 +6,6 @@ package repo
import (
"errors"
"fmt"
"net/http"
"code.gitea.io/gitea/models/organization"
@@ -14,6 +13,7 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@@ -71,7 +71,7 @@ func ListForks(ctx *context.APIContext) {
apiForks := make([]*api.Repository, len(forks))
for i, fork := range forks {
permission, err := access_model.GetUserRepoPermission(ctx, fork, ctx.Doer)
permission, err := access_model.GetDoerRepoPermission(ctx, fork, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -83,6 +83,35 @@ func ListForks(ctx *context.APIContext) {
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
@@ -118,41 +147,18 @@ func CreateFork(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateForkOption)
repo := ctx.Repo.Repository
var forker *user_model.User // user/org that will own the fork
if form.Organization == nil {
forker = ctx.Doer
} else {
org, err := organization.GetOrgByName(ctx, *form.Organization)
if err != nil {
if organization.IsErrOrgNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
forkOwner := ctx.Doer // user/org that will own the fork
if form.Organization != nil {
org := prepareDoerCreateRepoInOrg(ctx, *form.Organization)
if ctx.Written() {
return
}
if !ctx.Doer.IsAdmin {
isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
} else if !isMember {
ctx.APIError(http.StatusForbidden, fmt.Sprintf("User is no Member of Organisation '%s'", org.Name))
return
}
}
forker = org.AsUser()
forkOwner = org.AsUser()
}
var name string
if form.Name == nil {
name = repo.Name
} else {
name = *form.Name
}
fork, err := repo_service.ForkRepository(ctx, ctx.Doer, forker, repo_service.ForkRepoOptions{
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,
@@ -34,7 +34,7 @@ func GetGitAllRefs(ctx *context.APIContext) {
// required: true
// responses:
// "200":
// # "$ref": "#/responses/Reference" TODO: swagger doesnt support different output formats by ref
// # "$ref": "#/responses/Reference" TODO: swagger doesn't support different output formats by ref
// "$ref": "#/responses/ReferenceList"
// "404":
// "$ref": "#/responses/notFound"
@@ -67,7 +67,7 @@ func GetGitRefs(ctx *context.APIContext) {
// required: true
// responses:
// "200":
// # "$ref": "#/responses/Reference" TODO: swagger doesnt support different output formats by ref
// # "$ref": "#/responses/Reference" TODO: swagger doesn't support different output formats by ref
// "$ref": "#/responses/ReferenceList"
// "404":
// "$ref": "#/responses/notFound"
+99 -113
View File
@@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
@@ -32,6 +33,61 @@ import (
issue_service "code.gitea.io/gitea/services/issue"
)
// buildSearchIssuesRepoIDs builds the list of repository IDs for issue search based on query parameters.
// It returns repoIDs, allPublic flag, and any error that occurred.
func buildSearchIssuesRepoIDs(ctx *context.APIContext) (repoIDs []int64, allPublic bool, err error) {
opts := repo_model.SearchRepoOptions{
Private: false,
AllPublic: true,
TopicOnly: false,
Collaborate: optional.None[bool](),
// This needs to be a column that is not nil in fixtures or
// MySQL will return different results when sorting by null in some cases
OrderBy: db.SearchOrderByAlphabetically,
Actor: ctx.Doer,
}
if ctx.IsSigned {
opts.Private = true
opts.AllLimited = true
}
opts.ApplyPublicOnly(ctx.PublicOnly)
if ctx.FormString("owner") != "" {
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
if err != nil {
return nil, false, err
}
opts.OwnerID = owner.ID
opts.AllLimited = false
opts.AllPublic = false
opts.Collaborate = optional.Some(false)
}
if ctx.FormString("team") != "" {
if ctx.FormString("owner") == "" {
return nil, false, util.NewInvalidArgumentErrorf("owner organisation is required for filtering on team")
}
team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
if err != nil {
return nil, false, err
}
opts.TeamID = team.ID
}
if opts.AllPublic {
allPublic = true
opts.AllPublic = false // set it false to avoid returning too many repos, we could filter by indexer
}
repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
if err != nil {
return nil, false, err
}
if len(repoIDs) == 0 {
// no repos found, don't let the indexer return all repos
repoIDs = []int64{0}
}
return repoIDs, allPublic, nil
}
// SearchIssues searches for issues across the repositories that the user has access to
func SearchIssues(ctx *context.APIContext) {
// swagger:operation GET /repos/issues/search issue issueSearchIssues
@@ -58,11 +114,6 @@ func SearchIssues(ctx *context.APIContext) {
// in: query
// description: Search string
// type: string
// - name: priority_repo_id
// in: query
// description: Repository ID to prioritize in the results
// type: integer
// format: int64
// - name: type
// in: query
// description: Filter by issue type
@@ -107,6 +158,10 @@ func SearchIssues(ctx *context.APIContext) {
// in: query
// description: Filter by repository owner
// type: string
// - name: created_by
// in: query
// description: Only show items which were created by the given user
// type: string
// - name: team
// in: query
// description: Filter by team (requires organization owner parameter)
@@ -136,81 +191,16 @@ func SearchIssues(ctx *context.APIContext) {
return
}
var isClosed optional.Option[bool]
switch ctx.FormString("state") {
case "closed":
isClosed = optional.Some(true)
case "all":
isClosed = optional.None[bool]()
default:
isClosed = optional.Some(false)
}
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
var (
repoIDs []int64
allPublic bool
)
{
// find repos user can access (for issue search)
opts := repo_model.SearchRepoOptions{
Private: false,
AllPublic: true,
TopicOnly: false,
Collaborate: optional.None[bool](),
// This needs to be a column that is not nil in fixtures or
// MySQL will return different results when sorting by null in some cases
OrderBy: db.SearchOrderByAlphabetically,
Actor: ctx.Doer,
}
if ctx.IsSigned {
opts.Private = !ctx.PublicOnly
opts.AllLimited = true
}
if ctx.FormString("owner") != "" {
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusBadRequest, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
opts.OwnerID = owner.ID
opts.AllLimited = false
opts.AllPublic = false
opts.Collaborate = optional.Some(false)
}
if ctx.FormString("team") != "" {
if ctx.FormString("owner") == "" {
ctx.APIError(http.StatusBadRequest, "Owner organisation is required for filtering on team")
return
}
team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusBadRequest, err)
} else {
ctx.APIErrorInternal(err)
}
return
}
opts.TeamID = team.ID
}
if opts.AllPublic {
allPublic = true
opts.AllPublic = false // set it false to avoid returning too many repos, we could filter by indexer
}
repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
if err != nil {
repoIDs, allPublic, err := buildSearchIssuesRepoIDs(ctx)
if err != nil {
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
} else {
ctx.APIErrorInternal(err)
return
}
if len(repoIDs) == 0 {
// no repos found, don't let the indexer return all repos
repoIDs = []int64{0}
}
return
}
keyword := ctx.FormTrim("q")
@@ -218,15 +208,7 @@ func SearchIssues(ctx *context.APIContext) {
keyword = ""
}
var isPull optional.Option[bool]
switch ctx.FormString("type") {
case "pulls":
isPull = optional.Some(true)
case "issues":
isPull = optional.Some(false)
default:
isPull = optional.None[bool]()
}
isPull := common.ParseIssueFilterTypeIsPull(ctx.FormString("type"))
var includedAnyLabels []int64
{
@@ -256,14 +238,7 @@ func SearchIssues(ctx *context.APIContext) {
}
}
// this api is also used in UI,
// so the default limit is set to fit UI needs
limit := ctx.FormInt("limit")
if limit == 0 {
limit = setting.UI.IssuePagingNum
} else if limit > setting.API.MaxResponseItems {
limit = setting.API.MaxResponseItems
}
limit := util.IfZero(ctx.FormInt("limit"), setting.API.DefaultPagingNum)
searchOpt := &issue_indexer.SearchOptions{
Paginator: &db.ListOptions{
@@ -287,6 +262,14 @@ func SearchIssues(ctx *context.APIContext) {
searchOpt.UpdatedBeforeUnix = optional.Some(before)
}
createdByID := getUserIDForFilter(ctx, "created_by")
if ctx.Written() {
return
}
if createdByID > 0 {
searchOpt.PosterID = strconv.FormatInt(createdByID, 10)
}
if ctx.IsSigned {
ctxUserID := ctx.Doer.ID
if ctx.FormBool("created") {
@@ -306,10 +289,6 @@ func SearchIssues(ctx *context.APIContext) {
}
}
// FIXME: It's unsupported to sort by priority repo when searching by indexer,
// it's indeed an regression, but I think it is worth to support filtering by indexer first.
_ = ctx.FormInt64("priority_repo_id")
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
if err != nil {
ctx.APIErrorInternal(err)
@@ -321,7 +300,7 @@ func SearchIssues(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(total), limit)
ctx.SetLinkHeader(total, limit)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
}
@@ -351,7 +330,7 @@ func ListIssues(ctx *context.APIContext) {
// enum: [closed, open, all]
// - name: labels
// in: query
// description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
// description: comma separated list of label names. Fetch only issues that have any of this label names. Non existent labels are discarded.
// type: string
// - name: q
// in: query
@@ -409,16 +388,7 @@ func ListIssues(ctx *context.APIContext) {
return
}
var isClosed optional.Option[bool]
switch ctx.FormString("state") {
case "closed":
isClosed = optional.Some(true)
case "all":
isClosed = optional.None[bool]()
default:
isClosed = optional.Some(false)
}
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
keyword := ctx.FormTrim("q")
if strings.IndexByte(keyword, 0) >= 0 {
keyword = ""
@@ -558,7 +528,7 @@ func ListIssues(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(total), listOptions.PageSize)
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
}
@@ -757,6 +727,9 @@ func EditIssue(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/issues/{index} issue issueEditIssue
// ---
// summary: Edit an issue. If using deadline only the date will be taken into account, and time of day ignored.
// description: |
// Pass `content_version` to enable optimistic locking on body edits.
// If the version doesn't match the current value, the request fails with 409 Conflict.
// consumes:
// - application/json
// produces:
@@ -816,6 +789,15 @@ func EditIssue(ctx *context.APIContext) {
return
}
// Fail fast: if content_version is provided and already stale, reject
// before any mutations. The DB-level check in ChangeContent still
// handles concurrent requests.
// TODO: wrap all mutations in a transaction to fully prevent partial writes.
if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion {
ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged)
return
}
if len(form.Title) > 0 {
err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
if err != nil {
@@ -824,10 +806,14 @@ func EditIssue(ctx *context.APIContext) {
}
}
if form.Body != nil {
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion)
contentVersion := issue.ContentVersion
if form.ContentVersion != nil {
contentVersion = *form.ContentVersion
}
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion)
if err != nil {
if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusConflict, err)
return
}
@@ -186,7 +186,7 @@ func CreateIssueAttachment(ctx *context.APIContext) {
}
uploaderFile := attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
attachment, err := attachment_service.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, setting.Attachment.AllowedTypes, &repo_model.Attachment{
attachment, err := attachment_service.UploadAttachmentForIssue(ctx, uploaderFile, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
@@ -219,7 +219,7 @@ func isXRefCommentAccessible(ctx stdCtx.Context, user *user_model.User, c *issue
if err != nil {
return false
}
perm, err := access_model.GetUserRepoPermission(ctx, c.RefRepo, user)
perm, err := access_model.GetDoerRepoPermission(ctx, c.RefRepo, user)
if err != nil {
return false
}
@@ -445,7 +445,7 @@ func GetIssueComment(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
@@ -455,15 +455,6 @@ func GetIssueComment(ctx *context.APIContext) {
return
}
if err = comment.LoadIssue(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.Status(http.StatusNotFound)
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIErrorNotFound()
return
@@ -579,7 +570,7 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) {
}
func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
@@ -589,16 +580,6 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
return
}
if err := comment.LoadIssue(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.Status(http.StatusNotFound)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Status(http.StatusForbidden)
return
@@ -698,7 +679,7 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) {
}
func deleteIssueComment(ctx *context.APIContext) {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
@@ -708,16 +689,6 @@ func deleteIssueComment(ctx *context.APIContext) {
return
}
if err := comment.LoadIssue(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.Status(http.StatusNotFound)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Status(http.StatusForbidden)
return
@@ -193,7 +193,7 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
}
uploaderFile := attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
attachment, err := attachment_service.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, setting.Attachment.AllowedTypes, &repo_model.Attachment{
attachment, err := attachment_service.UploadAttachmentForIssue(ctx, uploaderFile, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
@@ -7,13 +7,13 @@ package repo
import (
"net/http"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
)
@@ -77,23 +77,14 @@ func GetIssueDependencies(ctx *context.APIContext) {
return
}
page := max(ctx.FormInt("page"), 1)
limit := ctx.FormInt("limit")
if limit == 0 {
limit = setting.API.DefaultPagingNum
} else if limit > setting.API.MaxResponseItems {
limit = setting.API.MaxResponseItems
}
listOptions := utils.GetListOptions(ctx)
canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
blockerIssues := make([]*issues_model.Issue, 0, limit)
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, err := issue.BlockedByDependencies(ctx, db.ListOptions{
Page: page,
PageSize: limit,
})
blockersInfo, total, err := issue.BlockedByDependencies(ctx, listOptions)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -111,7 +102,7 @@ func GetIssueDependencies(ctx *context.APIContext) {
perm = existPerm
} else {
var err error
perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
perm, err = access_model.GetDoerRepoPermission(ctx, &blocker.Repository, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -149,7 +140,8 @@ func GetIssueDependencies(ctx *context.APIContext) {
}
blockerIssues = append(blockerIssues, &blocker.Issue)
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, blockerIssues))
}
@@ -359,7 +351,7 @@ func GetIssueBlocks(ctx *context.APIContext) {
perm = existPerm
} else {
var err error
perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
perm, err = access_model.GetDoerRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -545,7 +537,7 @@ func getPermissionForRepo(ctx *context.APIContext, repo *repo_model.Repository)
return &ctx.Repo.Permission
}
perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return nil
@@ -169,7 +169,7 @@ func MoveIssuePin(ctx *context.APIContext) {
return
}
err = issues_model.MovePin(ctx, issue, int(ctx.PathParamInt64("position")))
err = issues_model.MovePin(ctx, issue, ctx.PathParamInt("position"))
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -175,7 +175,7 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) {
// schema:
// "$ref": "#/definitions/EditReactionOption"
// responses:
// "200":
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
@@ -248,8 +248,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
ctx.APIErrorInternal(err)
return
}
// ToDo respond 204
ctx.Status(http.StatusOK)
ctx.Status(http.StatusNoContent)
}
}
@@ -408,7 +407,7 @@ func DeleteIssueReaction(ctx *context.APIContext) {
// schema:
// "$ref": "#/definitions/EditReactionOption"
// responses:
// "200":
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
@@ -464,7 +463,6 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
ctx.APIErrorInternal(err)
return
}
// ToDo respond 204
ctx.Status(http.StatusOK)
ctx.Status(http.StatusNoContent)
}
}
@@ -138,7 +138,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) {
return
}
// If watch state wont change
// If watch state won't change
if current == watch {
ctx.Status(http.StatusOK)
return
@@ -356,7 +356,7 @@ func DeleteTime(ctx *context.APIContext) {
return
}
time, err := issues_model.GetTrackedTimeByID(ctx, ctx.PathParamInt64("id"))
time, err := issues_model.GetTrackedTimeByID(ctx, issue.ID, ctx.PathParamInt64("id"))
if err != nil {
if db.IsErrNotExist(err) {
ctx.APIErrorNotFound(err)
@@ -140,6 +140,7 @@ func Migrate(ctx *context.APIContext) {
}
opts := migrations.MigrateOptions{
OriginalURL: form.CloneAddr,
CloneAddr: remoteAddr,
RepoName: form.RepoName,
Description: form.Description,
@@ -10,7 +10,6 @@ import (
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
@@ -60,12 +59,7 @@ func ListMilestones(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
state := api.StateType(ctx.FormString("state"))
var isClosed optional.Option[bool]
switch state {
case api.StateClosed, api.StateOpen:
isClosed = optional.Some(state == api.StateClosed)
}
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
milestones, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
ListOptions: utils.GetListOptions(ctx),
@@ -179,7 +179,7 @@ func ListPushMirrors(ctx *context.APIContext) {
responsePushMirrors = append(responsePushMirrors, m)
}
}
ctx.SetLinkHeader(len(responsePushMirrors), utils.GetListOptions(ctx).PageSize)
ctx.SetLinkHeader(int64(len(responsePushMirrors)), utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, responsePushMirrors)
}
@@ -13,6 +13,7 @@ import (
"time"
activities_model "code.gitea.io/gitea/models/activities"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
pull_model "code.gitea.io/gitea/models/pull"
@@ -21,12 +22,15 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
@@ -35,6 +39,7 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
git_service "code.gitea.io/gitea/services/git"
"code.gitea.io/gitea/services/gitdiff"
issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify"
@@ -150,7 +155,7 @@ func ListPullRequests(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &apiPrs)
}
@@ -413,20 +418,20 @@ func CreatePullRequest(ctx *context.APIContext) {
)
// Get repo/branch information
compareResult, closer := parseCompareInfo(ctx, form)
compareResult, closer := parseCompareInfo(ctx, form.Base+".."+form.Head)
if ctx.Written() {
return
}
defer closer()
if !compareResult.baseRef.IsBranch() || !compareResult.headRef.IsBranch() {
if !compareResult.BaseRef.IsBranch() || !compareResult.HeadRef.IsBranch() {
ctx.APIError(http.StatusUnprocessableEntity, "Invalid PullRequest: base and head must be branches")
return
}
// Check if another PR exists with the same targets
existingPr, err := issues_model.GetUnmergedPullRequest(ctx, compareResult.headRepo.ID, ctx.Repo.Repository.ID,
compareResult.headRef.ShortName(), compareResult.baseRef.ShortName(),
existingPr, err := issues_model.GetUnmergedPullRequest(ctx, compareResult.HeadRepo.ID, ctx.Repo.Repository.ID,
compareResult.HeadRef.ShortName(), compareResult.BaseRef.ShortName(),
issues_model.PullRequestFlowGithub,
)
if err != nil {
@@ -493,6 +498,12 @@ func CreatePullRequest(ctx *context.APIContext) {
deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
}
unitPullRequest, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypePullRequests)
if err != nil {
ctx.APIErrorInternal(err)
return
}
prIssue := &issues_model.Issue{
RepoID: repo.ID,
Title: form.Title,
@@ -504,16 +515,18 @@ func CreatePullRequest(ctx *context.APIContext) {
DeadlineUnix: deadlineUnix,
}
pr := &issues_model.PullRequest{
HeadRepoID: compareResult.headRepo.ID,
HeadRepoID: compareResult.HeadRepo.ID,
BaseRepoID: repo.ID,
HeadBranch: compareResult.headRef.ShortName(),
BaseBranch: compareResult.baseRef.ShortName(),
HeadRepo: compareResult.headRepo,
HeadBranch: compareResult.HeadRef.ShortName(),
BaseBranch: compareResult.BaseRef.ShortName(),
HeadRepo: compareResult.HeadRepo,
BaseRepo: repo,
MergeBase: compareResult.compareInfo.MergeBase,
MergeBase: compareResult.MergeBase,
Type: issues_model.PullRequestGitea,
}
pr.AllowMaintainerEdit = optional.FromPtr(form.AllowMaintainerEdit).ValueOrDefault(unitPullRequest.PullRequestsConfig().DefaultAllowMaintainerEdit)
// Get all assignee IDs
assigneeIDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees)
if err != nil {
@@ -645,6 +658,15 @@ func EditPullRequest(ctx *context.APIContext) {
return
}
// Fail fast: if content_version is provided and already stale, reject
// before any mutations. The DB-level check in ChangeContent still
// handles concurrent requests.
// TODO: wrap all mutations in a transaction to fully prevent partial writes.
if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion {
ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged)
return
}
if len(form.Title) > 0 {
err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
if err != nil {
@@ -653,10 +675,14 @@ func EditPullRequest(ctx *context.APIContext) {
}
}
if form.Body != nil {
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion)
contentVersion := issue.ContentVersion
if form.ContentVersion != nil {
contentVersion = *form.ContentVersion
}
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion)
if err != nil {
if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusConflict, err)
return
}
@@ -756,7 +782,12 @@ func EditPullRequest(ctx *context.APIContext) {
// change pull target branch
if !pr.HasMerged && len(form.Base) != 0 && form.Base != pr.BaseBranch {
if !gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, form.Base) {
branchExist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, form.Base)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !branchExist {
ctx.APIError(http.StatusNotFound, fmt.Errorf("new base '%s' not exist", form.Base))
return
}
@@ -881,6 +912,8 @@ func MergePullRequest(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "405":
@@ -961,7 +994,7 @@ func MergePullRequest(ctx *context.APIContext) {
return
}
if strings.Contains(err.Error(), "Wrong commit ID") {
ctx.JSON(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err)
return
}
ctx.APIErrorInternal(err)
@@ -1011,7 +1044,7 @@ func MergePullRequest(ctx *context.APIContext) {
}
}
if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
if err := pull_service.Merge(ctx, pr, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
if pull_service.IsErrInvalidMergeStyle(err) {
ctx.APIError(http.StatusMethodNotAllowed, fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
} else if pull_service.IsErrMergeConflicts(err) {
@@ -1051,63 +1084,32 @@ func MergePullRequest(ctx *context.APIContext) {
ctx.Status(http.StatusOK)
}
type parseCompareInfoResult struct {
headRepo *repo_model.Repository
headGitRepo *git.Repository
compareInfo *pull_service.CompareInfo
baseRef git.RefName
headRef git.RefName
}
// parseCompareInfo returns non-nil if it succeeds, it always writes to the context and returns nil if it fails
func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (result *parseCompareInfoResult, closer func()) {
var err error
// Get compared branches information
// format: <base branch>...[<head repo>:]<head branch>
// base<-head: master...head:feature
// same repo: master...feature
func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git_service.CompareInfo, closer func()) {
baseRepo := ctx.Repo.Repository
baseRefToGuess := form.Base
compareReq := common.ParseCompareRouterParam(compareParam)
headUser := ctx.Repo.Owner
headRefToGuess := form.Head
if headInfos := strings.Split(form.Head, ":"); len(headInfos) == 1 {
// If there is no head repository, it means pull request between same repository.
// Do nothing here because the head variables have been assigned above.
} else if len(headInfos) == 2 {
// There is a head repository (the head repository could also be the same base repo)
headRefToGuess = headInfos[1]
headUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound("GetUserByName")
} else {
ctx.APIErrorInternal(err)
}
return nil, nil
}
} else {
ctx.APIErrorNotFound()
// remove the check when we support compare with carets
if compareReq.BaseOriRefSuffix != "" {
ctx.APIError(http.StatusBadRequest, "Unsupported comparison syntax: ref with suffix")
return nil, nil
}
isSameRepo := ctx.Repo.Owner.ID == headUser.ID
var headRepo *repo_model.Repository
if isSameRepo {
headRepo = baseRepo
} else {
headRepo, err = common.FindHeadRepo(ctx, baseRepo, headUser.ID)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
}
if headRepo == nil {
ctx.APIErrorNotFound("head repository not found")
return nil, nil
}
_, headRepo, err := common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq)
switch {
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIError(http.StatusBadRequest, err.Error())
return nil, nil
case errors.Is(err, util.ErrNotExist):
ctx.APIErrorNotFound()
return nil, nil
case err != nil:
ctx.APIErrorInternal(err)
return nil, nil
}
isSameRepo := baseRepo.ID == headRepo.ID
var headGitRepo *git.Repository
if isSameRepo {
headGitRepo = ctx.Repo.GitRepo
@@ -1127,21 +1129,21 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
}()
// user should have permission to read baseRepo's codes and pulls, NOT headRepo's
permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
permBase, err := access_model.GetDoerRepoPermission(ctx, baseRepo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
}
if !permBase.CanRead(unit.TypeCode) {
log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase)
ctx.APIErrorNotFound("Can't read pulls or can't read UnitTypeCode")
log.Trace("Permission Denied: User %-v cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase)
ctx.APIErrorNotFound("can't read baseRepo UnitTypeCode")
return nil, nil
}
// user should have permission to read headRepo's codes
// TODO: could the logic be simplified if the headRepo is the same as the baseRepo? Need to think more about it.
permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer)
permHead, err := access_model.GetDoerRepoPermission(ctx, headRepo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
@@ -1152,10 +1154,10 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
return nil, nil
}
baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefToGuess)
headRef := headGitRepo.UnstableGuessRefByShortName(headRefToGuess)
baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx)))
headRef := headGitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch))
log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.GitRepo.Path, baseRefToGuess, baseRef, headRefToGuess, headRef)
log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.RelativePath(), compareReq.BaseOriRef, baseRef, compareReq.HeadOriRef, headRef)
baseRefValid := baseRef.IsBranch() || baseRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName), baseRef.ShortName())
headRefValid := headRef.IsBranch() || headRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(headRepo.ObjectFormatName), headRef.ShortName())
@@ -1165,14 +1167,13 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
return nil, nil
}
compareInfo, err := pull_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseRef.ShortName(), headRef.ShortName(), false, false)
compareInfo, err := git_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseRef, headRef, compareReq.DirectComparison(), false)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
}
result = &parseCompareInfoResult{headRepo: headRepo, headGitRepo: headGitRepo, compareInfo: compareInfo, baseRef: baseRef, headRef: headRef}
return result, closer
return compareInfo, closer
}
// UpdatePullRequest merge PR's baseBranch into headBranch
@@ -1416,7 +1417,7 @@ func GetPullRequestCommits(ctx *context.APIContext) {
return
}
var prInfo *pull_service.CompareInfo
var compareInfo *git_service.CompareInfo
baseGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
if err != nil {
ctx.APIErrorInternal(err)
@@ -1425,19 +1426,22 @@ func GetPullRequestCommits(ctx *context.APIContext) {
defer closer.Close()
if pr.HasMerged {
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.MergeBase, pr.GetGitHeadRefName(), false, false)
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefName(pr.MergeBase), git.RefName(pr.GetGitHeadRefName()), false, false)
} else {
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.BaseBranch, pr.GetGitHeadRefName(), false, false)
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), false, false)
}
if err != nil {
if gitcmd.StderrHasPrefix(err, "fatal: bad revision") {
ctx.APIError(http.StatusNotFound, "invalid base branch or revision")
return
} else if err != nil {
ctx.APIErrorInternal(err)
return
}
commits := prInfo.Commits
listOptions := utils.GetListOptions(ctx)
totalNumberOfCommits := len(commits)
totalNumberOfCommits := len(compareInfo.Commits)
totalNumberOfPages := int(math.Ceil(float64(totalNumberOfCommits) / float64(listOptions.PageSize)))
userCache := make(map[string]*user_model.User)
@@ -1452,7 +1456,7 @@ func GetPullRequestCommits(ctx *context.APIContext) {
apiCommits := make([]*api.Commit, 0, limit)
for i := start; i < start+limit; i++ {
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, baseGitRepo, commits[i], userCache,
apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, baseGitRepo, compareInfo.Commits[i], userCache,
convert.ToCommitOptions{
Stat: true,
Verification: verification,
@@ -1465,7 +1469,7 @@ func GetPullRequestCommits(ctx *context.APIContext) {
apiCommits = append(apiCommits, apiCommit)
}
ctx.SetLinkHeader(totalNumberOfCommits, listOptions.PageSize)
ctx.SetLinkHeader(int64(totalNumberOfCommits), listOptions.PageSize)
ctx.SetTotalCountHeader(int64(totalNumberOfCommits))
ctx.RespHeader().Set("X-Page", strconv.Itoa(listOptions.Page))
@@ -1546,11 +1550,11 @@ func GetPullRequestFiles(ctx *context.APIContext) {
baseGitRepo := ctx.Repo.GitRepo
var prInfo *pull_service.CompareInfo
var compareInfo *git_service.CompareInfo
if pr.HasMerged {
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.MergeBase, pr.GetGitHeadRefName(), false, false)
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefName(pr.MergeBase), git.RefName(pr.GetGitHeadRefName()), false, false)
} else {
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.BaseBranch, pr.GetGitHeadRefName(), false, false)
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), false, false)
}
if err != nil {
ctx.APIErrorInternal(err)
@@ -1563,7 +1567,7 @@ func GetPullRequestFiles(ctx *context.APIContext) {
return
}
startCommitID := prInfo.MergeBase
startCommitID := compareInfo.MergeBase
endCommitID := headCommitID
maxLines := setting.Git.MaxGitDiffLines
@@ -1584,7 +1588,7 @@ func GetPullRequestFiles(ctx *context.APIContext) {
return
}
diffShortStat, err := gitdiff.GetDiffShortStat(baseGitRepo, startCommitID, endCommitID)
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ctx.Repo.Repository, baseGitRepo, startCommitID, endCommitID)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -1607,7 +1611,7 @@ func GetPullRequestFiles(ctx *context.APIContext) {
apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.BaseRepo, endCommitID))
}
ctx.SetLinkHeader(totalNumberOfFiles, listOptions.PageSize)
ctx.SetLinkHeader(int64(totalNumberOfFiles), listOptions.PageSize)
ctx.SetTotalCountHeader(int64(totalNumberOfFiles))
ctx.RespHeader().Set("X-Page", strconv.Itoa(listOptions.Page))
@@ -208,6 +208,126 @@ func GetPullReviewComments(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiComments)
}
// ResolvePullReviewComment resolves a review comment in a pull request
func ResolvePullReviewComment(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/comments/{id}/resolve repository repoResolvePullReviewComment
// ---
// summary: Resolve a pull request review comment
// 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 review comment
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "400":
// "$ref": "#/responses/validationError"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
updatePullReviewCommentResolve(ctx, true)
}
// UnresolvePullReviewComment unresolves a review comment in a pull request
func UnresolvePullReviewComment(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/comments/{id}/unresolve repository repoUnresolvePullReviewComment
// ---
// summary: Unresolve a pull request review comment
// 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 review comment
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "400":
// "$ref": "#/responses/validationError"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
updatePullReviewCommentResolve(ctx, false)
}
func updatePullReviewCommentResolve(ctx *context.APIContext, isResolve bool) {
comment := getPullReviewCommentToResolve(ctx)
if comment == nil {
return
}
canMarkConv, err := issues_model.CanMarkConversation(ctx, comment.Issue, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !canMarkConv {
ctx.APIError(http.StatusForbidden, "user should have permission to resolve comment")
return
}
if err = issues_model.MarkConversation(ctx, comment, ctx.Doer, isResolve); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
func getPullReviewCommentToResolve(ctx *context.APIContext) *issues_model.Comment {
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound("GetCommentByID", err)
} else {
ctx.APIErrorInternal(err)
}
return nil
}
if !comment.Issue.IsPull {
ctx.APIError(http.StatusBadRequest, "comment does not belong to a pull request")
return nil
}
if comment.Type != issues_model.CommentTypeCode {
ctx.APIError(http.StatusBadRequest, "comment is not a review comment")
return nil
}
return comment
}
// DeletePullReview delete a specific review from a pull request
func DeletePullReview(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview
@@ -713,7 +833,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
return
}
permDoer, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, ctx.Doer)
permDoer, err := access_model.GetDoerRepoPermission(ctx, pr.Issue.Repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -8,8 +8,8 @@ import (
"fmt"
"net/http"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/git"
@@ -21,6 +21,21 @@ import (
release_service "code.gitea.io/gitea/services/release"
)
func canAccessReleaseDraft(ctx *context.APIContext) bool {
if !ctx.IsSigned || !ctx.Repo.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
@@ -62,6 +77,11 @@ func GetRelease(ctx *context.APIContext) {
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
@@ -130,7 +150,7 @@ func ListReleases(ctx *context.APIContext) {
// required: true
// - name: draft
// in: query
// description: filter (exclude / include) drafts, if you dont have repo write access none will show
// description: filter (exclude / include) drafts, if you don't have repo write access none will show
// type: boolean
// - name: pre-release
// in: query
@@ -150,10 +170,12 @@ func ListReleases(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
if ctx.Written() {
return
}
opts := repo_model.FindReleasesOptions{
ListOptions: listOptions,
IncludeDrafts: ctx.Repo.AccessMode >= perm.AccessModeWrite || ctx.Repo.UnitAccessMode(unit.TypeReleases) >= perm.AccessModeWrite,
IncludeDrafts: canAccessReleaseDraft(ctx),
IncludeTags: false,
IsDraft: ctx.FormOptionalBool("draft"),
IsPreRelease: ctx.FormOptionalBool("pre-release"),
@@ -180,7 +202,7 @@ func ListReleases(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int(filteredCount), listOptions.PageSize)
ctx.SetLinkHeader(filteredCount, listOptions.PageSize)
ctx.SetTotalCountHeader(filteredCount)
ctx.JSON(http.StatusOK, rels)
}
@@ -34,6 +34,10 @@ func checkReleaseMatchRepo(ctx *context.APIContext, releaseID int64) bool {
ctx.APIErrorNotFound()
return false
}
if release.IsDraft && !canAccessReleaseDraft(ctx) {
ctx.APIErrorNotFound()
return false
}
return true
}
@@ -141,6 +145,10 @@ func ListReleaseAttachments(ctx *context.APIContext) {
ctx.APIErrorNotFound()
return
}
if release.IsDraft && !canAccessReleaseDraft(ctx) {
ctx.APIErrorNotFound()
return
}
if err := release.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
@@ -234,7 +242,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
}
// Create a new attachment and save the file
attach, err := attachment_service.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, setting.Repository.Release.AllowedTypes, &repo_model.Attachment{
attach, err := attachment_service.UploadAttachmentForRelease(ctx, uploaderFile, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
@@ -28,6 +28,7 @@ import (
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
@@ -130,9 +131,6 @@ func Search(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
private := ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private"))
if ctx.PublicOnly {
private = false
}
opts := repo_model.SearchRepoOptions{
ListOptions: utils.GetListOptions(ctx),
@@ -148,6 +146,7 @@ func Search(ctx *context.APIContext) {
StarredByID: ctx.FormInt64("starredBy"),
IncludeDescription: ctx.FormBool("includeDesc"),
}
opts.ApplyPublicOnly(ctx.PublicOnly)
if ctx.FormString("template") != "" {
opts.Template = optional.Some(ctx.FormBool("template"))
@@ -220,7 +219,7 @@ func Search(ctx *context.APIContext) {
})
return
}
permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.JSON(http.StatusInternalServerError, api.SearchError{
OK: false,
@@ -229,7 +228,7 @@ func Search(ctx *context.APIContext) {
}
results[i] = convert.ToRepo(ctx, repo, permission)
}
ctx.SetLinkHeader(int(count), opts.PageSize)
ctx.SetLinkHeader(count, opts.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, api.SearchResults{
OK: true,
@@ -270,6 +269,8 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
db.IsErrNamePatternNotAllowed(err) ||
label.IsErrTemplateLoad(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else if errors.Is(err, util.ErrPermissionDenied) {
ctx.APIError(http.StatusForbidden, err)
} else {
ctx.APIErrorInternal(err)
}
@@ -495,31 +496,11 @@ func CreateOrgRepo(ctx *context.APIContext) {
// "403":
// "$ref": "#/responses/forbidden"
opt := web.GetForm(ctx).(*api.CreateRepoOption)
org, err := organization.GetOrgByName(ctx, ctx.PathParam("org"))
if err != nil {
if organization.IsErrOrgNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
orgName := ctx.PathParam("org")
org := prepareDoerCreateRepoInOrg(ctx, orgName)
if ctx.Written() {
return
}
if !organization.HasOrgOrUserVisible(ctx, org.AsUser(), ctx.Doer) {
ctx.APIErrorNotFound("HasOrgOrUserVisible", nil)
return
}
if !ctx.Doer.IsAdmin {
canCreate, err := org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
} else if !canCreate {
ctx.APIError(http.StatusForbidden, "Given user is not allowed to create repository in organization.")
return
}
}
CreateUserRepo(ctx, org.AsUser(), *opt)
}
@@ -584,8 +565,12 @@ func GetByID(ctx *context.APIContext) {
}
return
}
if !ctx.TokenCanAccessRepo(repo) {
ctx.APIErrorNotFound()
return
}
permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -881,77 +866,44 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
}
}
if opts.HasPullRequests != nil && !unit_model.TypePullRequests.UnitGlobalDisabled() {
if *opts.HasPullRequests {
// We do allow setting individual PR settings through the API, so
// we get the config settings and then set them
// if those settings were provided in the opts.
unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests)
var config *repo_model.PullRequestsConfig
if err != nil {
// Unit type doesn't exist so we make a new config file with default values
config = &repo_model.PullRequestsConfig{
IgnoreWhitespaceConflicts: false,
AllowMerge: true,
AllowRebase: true,
AllowRebaseMerge: true,
AllowSquash: true,
AllowFastForwardOnly: true,
AllowManualMerge: true,
AutodetectManualMerge: false,
AllowRebaseUpdate: true,
DefaultDeleteBranchAfterMerge: false,
DefaultMergeStyle: repo_model.MergeStyleMerge,
DefaultAllowMaintainerEdit: false,
}
} else {
config = unit.PullRequestsConfig()
}
if opts.IgnoreWhitespaceConflicts != nil {
config.IgnoreWhitespaceConflicts = *opts.IgnoreWhitespaceConflicts
}
if opts.AllowMerge != nil {
config.AllowMerge = *opts.AllowMerge
}
if opts.AllowRebase != nil {
config.AllowRebase = *opts.AllowRebase
}
if opts.AllowRebaseMerge != nil {
config.AllowRebaseMerge = *opts.AllowRebaseMerge
}
if opts.AllowSquash != nil {
config.AllowSquash = *opts.AllowSquash
}
if opts.AllowFastForwardOnly != nil {
config.AllowFastForwardOnly = *opts.AllowFastForwardOnly
}
if opts.AllowManualMerge != nil {
config.AllowManualMerge = *opts.AllowManualMerge
}
if opts.AutodetectManualMerge != nil {
config.AutodetectManualMerge = *opts.AutodetectManualMerge
}
if opts.AllowRebaseUpdate != nil {
config.AllowRebaseUpdate = *opts.AllowRebaseUpdate
}
if opts.DefaultDeleteBranchAfterMerge != nil {
config.DefaultDeleteBranchAfterMerge = *opts.DefaultDeleteBranchAfterMerge
}
if opts.DefaultMergeStyle != nil {
config.DefaultMergeStyle = repo_model.MergeStyle(*opts.DefaultMergeStyle)
}
if opts.DefaultAllowMaintainerEdit != nil {
config.DefaultAllowMaintainerEdit = *opts.DefaultAllowMaintainerEdit
}
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
Type: unit_model.TypePullRequests,
Config: config,
})
} else {
if !unit_model.TypePullRequests.UnitGlobalDisabled() {
mustDeletePullRequestUnit := opts.HasPullRequests != nil && !*opts.HasPullRequests
mustInsertPullRequestUnit := opts.HasPullRequests != nil && *opts.HasPullRequests
if mustDeletePullRequestUnit {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
} else {
// We do allow setting individual PR settings through the API,
// so we get the config settings and then set them if those settings were provided in the opts.
unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return err
}
if unit == nil {
// Unit doesn't exist yet but is being enabled, create with defaults
unit = new(repo_model.DefaultPullRequestsUnit(repo.ID))
}
changed := new(false)
config := unit.PullRequestsConfig()
optional.AssignPtrValue(changed, &config.IgnoreWhitespaceConflicts, opts.IgnoreWhitespaceConflicts)
optional.AssignPtrValue(changed, &config.AllowMerge, opts.AllowMerge)
optional.AssignPtrValue(changed, &config.AllowRebase, opts.AllowRebase)
optional.AssignPtrValue(changed, &config.AllowRebaseMerge, opts.AllowRebaseMerge)
optional.AssignPtrValue(changed, &config.AllowSquash, opts.AllowSquash)
optional.AssignPtrValue(changed, &config.AllowFastForwardOnly, opts.AllowFastForwardOnly)
optional.AssignPtrValue(changed, &config.AllowManualMerge, opts.AllowManualMerge)
optional.AssignPtrValue(changed, &config.AutodetectManualMerge, opts.AutodetectManualMerge)
optional.AssignPtrValue(changed, &config.AllowRebaseUpdate, opts.AllowRebaseUpdate)
optional.AssignPtrValue(changed, &config.DefaultDeleteBranchAfterMerge, opts.DefaultDeleteBranchAfterMerge)
optional.AssignPtrValue(changed, &config.DefaultAllowMaintainerEdit, opts.DefaultAllowMaintainerEdit)
optional.AssignPtrString(changed, &config.DefaultMergeStyle, opts.DefaultMergeStyle)
if *changed || mustInsertPullRequestUnit {
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
Type: unit_model.TypePullRequests,
Config: config,
})
}
}
}
@@ -1304,6 +1256,7 @@ func ListRepoActivityFeeds(ctx *context.APIContext) {
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
feeds, count, err := feed_service.GetFeeds(ctx, opts)
if err != nil {
@@ -206,7 +206,7 @@ func getCommitStatuses(ctx *context.APIContext, commitID string) {
apiStatuses = append(apiStatuses, convert.ToCommitStatus(ctx, status))
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, apiStatuses)
@@ -257,8 +257,8 @@ func GetCombinedCommitStatusByRef(ctx *context.APIContext) {
}
repo := ctx.Repo.Repository
statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, refCommit.Commit.ID.String(), utils.GetListOptions(ctx))
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
@@ -269,6 +269,7 @@ func GetCombinedCommitStatusByRef(ctx *context.APIContext) {
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,
@@ -107,15 +107,18 @@ func GetAnnotatedTag(ctx *context.APIContext) {
return
}
if tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha); err != nil {
tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
} else {
commit, err := ctx.Repo.GitRepo.GetTagCommit(tag.Name)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
}
ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit))
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
@@ -193,7 +193,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi
}
// get commit count - wiki revisions
commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
// Get last change information.
lastCommit, err := wikiRepo.GetCommitByPath(pageFilename)
@@ -333,6 +333,7 @@ func ListWikiPages(ctx *context.APIContext) {
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)
}
@@ -429,7 +430,7 @@ func ListPageRevisions(ctx *context.APIContext) {
}
// get commit count - wiki revisions
commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
page := max(ctx.FormInt("page"), 1)
@@ -445,6 +446,7 @@ func ListPageRevisions(ctx *context.APIContext) {
return
}
// FIXME: SetLinkHeader missing
ctx.SetTotalCountHeader(commitsCount)
ctx.JSON(http.StatusOK, convert.ToWikiCommitList(commitsHistory, commitsCount))
}
@@ -32,11 +32,12 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
listOptions := utils.GetListOptions(ctx)
opts := actions_model.FindRunJobOptions{
OwnerID: ownerID,
RepoID: repoID,
RunID: runID,
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
}
for _, status := range ctx.FormStrings("status") {
values, err := convertToInternal(status)
@@ -78,7 +79,8 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) {
}
res.Entries[i] = convertedWorkflowJob
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &res)
}
@@ -120,10 +122,11 @@ 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: utils.GetListOptions(ctx),
ListOptions: listOptions,
}
if event := ctx.FormString("event"); event != "" {
@@ -182,6 +185,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
}
res.Entries[i] = convertedRun
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &res)
}
@@ -16,8 +16,9 @@ import (
)
func ListBlocks(ctx *context.APIContext, blocker *user_model.User) {
listOptions := utils.GetListOptions(ctx)
blocks, total, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
BlockerID: blocker.ID,
})
if err != nil {
@@ -35,6 +36,7 @@ func ListBlocks(ctx *context.APIContext, blocker *user_model.User) {
users = append(users, convert.ToUser(ctx, b.Blockee, blocker))
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &users)
}
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
@@ -46,11 +47,13 @@ func ListRunners(ctx *context.APIContext, ownerID, repoID int64) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
runners, total, err := db.FindAndCount[actions_model.ActionRunner](ctx, &actions_model.FindRunnerOptions{
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
@@ -125,3 +128,23 @@ func DeleteRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
}
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)
}
@@ -46,3 +46,10 @@ type swaggerResponseActionWorkflowList struct {
// in:body
Body api.ActionWorkflowResponse `json:"body"`
}
// RunDetails
// swagger:response RunDetails
type swaggerResponseRunDetails struct {
// in:body
Body api.RunDetails `json:"body"`
}
@@ -1,15 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import (
api "code.gitea.io/gitea/modules/structs"
)
// ActivityPub
// swagger:response ActivityPub
type swaggerResponseActivityPub struct {
// in:body
Body api.ActivityPub `json:"body"`
}
@@ -147,6 +147,8 @@ type swaggerParameterBodies struct {
// in:body
CreateBranchRepoOption api.CreateBranchRepoOption
// in:body
UpdateBranchRepoOption api.UpdateBranchRepoOption
// in:body
CreateBranchProtectionOption api.CreateBranchProtectionOption
@@ -223,6 +225,9 @@ type swaggerParameterBodies struct {
// in:body
UpdateVariableOption api.UpdateVariableOption
// in:body
EditActionRunnerOption api.EditActionRunnerOption
// in:body
LockIssueOption api.LockIssueOption
}
@@ -333,10 +333,10 @@ func ListVariables(ctx *context.APIContext) {
// "$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.Doer.ID,
ListOptions: utils.GetListOptions(ctx),
ListOptions: listOptions,
})
if err != nil {
ctx.APIErrorInternal(err)
@@ -354,6 +354,7 @@ func ListVariables(ctx *context.APIContext) {
}
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables)
}
@@ -24,12 +24,14 @@ func responseAPIUsers(ctx *context.APIContext, users []*user_model.User) {
}
func listUserFollowers(ctx *context.APIContext, u *user_model.User) {
users, count, err := user_model.GetUserFollowers(ctx, u, ctx.Doer, utils.GetListOptions(ctx))
listOptions := utils.GetListOptions(ctx)
users, count, err := user_model.GetUserFollowers(ctx, u, ctx.Doer, listOptions)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
responseAPIUsers(ctx, users)
}
@@ -88,12 +90,14 @@ func ListFollowers(ctx *context.APIContext) {
}
func listUserFollowing(ctx *context.APIContext, u *user_model.User) {
users, count, err := user_model.GetUserFollowing(ctx, u, ctx.Doer, utils.GetListOptions(ctx))
listOptions := utils.GetListOptions(ctx)
users, count, err := user_model.GetUserFollowing(ctx, u, ctx.Doer, listOptions)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
responseAPIUsers(ctx, users)
}
+10 -10
View File
@@ -1,4 +1,5 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
@@ -53,11 +54,11 @@ func composePublicKeysAPILink() string {
func listPublicKeys(ctx *context.APIContext, user *user_model.User) {
var keys []*asymkey_model.PublicKey
var err error
var count int
var count int64
fingerprint := ctx.FormString("fingerprint")
username := ctx.PathParam("username")
listOptions := utils.GetListOptions(ctx)
if fingerprint != "" {
var userID int64 // Unrestricted
// Querying not just listing
@@ -65,20 +66,18 @@ func listPublicKeys(ctx *context.APIContext, user *user_model.User) {
// Restrict to provided uid
userID = user.ID
}
keys, err = db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
keys, count, err = db.FindAndCount[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
ListOptions: listOptions,
OwnerID: userID,
Fingerprint: fingerprint,
})
count = len(keys)
} else {
var total int64
// Use ListPublicKeys
keys, total, err = db.FindAndCount[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
ListOptions: utils.GetListOptions(ctx),
keys, count, err = db.FindAndCount[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
ListOptions: listOptions,
OwnerID: user.ID,
NotKeytype: asymkey_model.KeyTypePrincipal,
})
count = int(total)
}
if err != nil {
@@ -95,7 +94,8 @@ func listPublicKeys(ctx *context.APIContext, user *user_model.User) {
}
}
ctx.SetTotalCountHeader(int64(count))
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, &apiKeys)
}
@@ -211,7 +211,7 @@ func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid
return
}
key, err := asymkey_model.AddPublicKey(ctx, uid, form.Title, content, 0)
key, err := asymkey_model.AddPublicKey(ctx, uid, form.Title, content, 0, false)
if err != nil {
repo.HandleAddKeyError(ctx, err)
return
@@ -19,12 +19,15 @@ import (
func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
opts := utils.GetListOptions(ctx)
repos, count, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
searchOpts := repo_model.SearchRepoOptions{
Actor: u,
Private: private,
ListOptions: opts,
OrderBy: "id ASC",
})
}
searchOpts.ApplyPublicOnly(ctx.PublicOnly)
repos, count, err := repo_model.GetUserRepositories(ctx, searchOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -37,7 +40,7 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
apiRepos := make([]*api.Repository, 0, len(repos))
for i := range repos {
permission, err := access_model.GetUserRepoPermission(ctx, repos[i], ctx.Doer)
permission, err := access_model.GetDoerRepoPermission(ctx, repos[i], ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -47,7 +50,7 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
}
}
ctx.SetLinkHeader(int(count), opts.PageSize)
ctx.SetLinkHeader(count, opts.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, &apiRepos)
}
@@ -79,8 +82,7 @@ func ListUserRepos(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
private := ctx.IsSigned
listUserRepos(ctx, ctx.ContextUser, private)
listUserRepos(ctx, ctx.ContextUser, ctx.IsSigned)
}
// ListMyRepos - list the repositories you own or have access to.
@@ -110,6 +112,7 @@ func ListMyRepos(ctx *context.APIContext) {
Private: ctx.IsSigned,
IncludeDescription: true,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
repos, count, err := repo_model.SearchRepository(ctx, opts)
if err != nil {
@@ -123,14 +126,14 @@ func ListMyRepos(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
}
results[i] = convert.ToRepo(ctx, repo, permission)
}
ctx.SetLinkHeader(int(count), opts.ListOptions.PageSize)
ctx.SetLinkHeader(count, opts.ListOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, &results)
}
@@ -10,21 +10,6 @@ import (
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// GetRegistrationToken returns the token to register user runners
func GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /user/actions/runners/registration-token user userGetRunnerRegistrationToken
// ---
// summary: Get an user's actions runner registration token
// produces:
// - application/json
// parameters:
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0)
}
// CreateRegistrationToken returns the token to register user runners
func CreateRegistrationToken(ctx *context.APIContext) {
// swagger:operation POST /user/actions/runners/registration-token user userCreateRunnerRegistrationToken
@@ -47,9 +32,15 @@ func ListRunners(ctx *context.APIContext) {
// summary: Get user-level runners
// produces:
// - application/json
// parameters:
// - name: disabled
// in: query
// description: filter by disabled status (true or false)
// type: boolean
// required: false
// responses:
// "200":
// "$ref": "#/definitions/ActionRunnersResponse"
// "$ref": "#/responses/RunnerList"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -57,11 +48,11 @@ func ListRunners(ctx *context.APIContext) {
shared.ListRunners(ctx, ctx.Doer.ID, 0)
}
// GetRunner get an user-level runner
// GetRunner get a user-level runner
func GetRunner(ctx *context.APIContext) {
// swagger:operation GET /user/actions/runners/{runner_id} user getUserRunner
// ---
// summary: Get an user-level runner
// summary: Get a user-level runner
// produces:
// - application/json
// parameters:
@@ -72,7 +63,7 @@ func GetRunner(ctx *context.APIContext) {
// required: true
// responses:
// "200":
// "$ref": "#/definitions/ActionRunner"
// "$ref": "#/responses/Runner"
// "400":
// "$ref": "#/responses/error"
// "404":
@@ -80,11 +71,11 @@ func GetRunner(ctx *context.APIContext) {
shared.GetRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id"))
}
// DeleteRunner delete an user-level runner
// DeleteRunner delete a user-level runner
func DeleteRunner(ctx *context.APIContext) {
// swagger:operation DELETE /user/actions/runners/{runner_id} user deleteUserRunner
// ---
// summary: Delete an user-level runner
// summary: Delete a user-level runner
// produces:
// - application/json
// parameters:
@@ -102,3 +93,34 @@ func DeleteRunner(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
shared.DeleteRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id"))
}
// UpdateRunner update a user-level runner
func UpdateRunner(ctx *context.APIContext) {
// swagger:operation PATCH /user/actions/runners/{runner_id} user updateUserRunner
// ---
// summary: Update a user-level 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, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id"))
}
@@ -20,18 +20,21 @@ import (
// getStarredRepos returns the repos that the user with the specified userID has
// starred
func getStarredRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, error) {
starredRepos, err := repo_model.GetStarredRepos(ctx, &repo_model.StarredReposOptions{
opts := &repo_model.StarredReposOptions{
ListOptions: utils.GetListOptions(ctx),
StarrerID: user.ID,
IncludePrivate: private,
})
}
opts.ApplyPublicOnly(ctx.PublicOnly)
starredRepos, err := repo_model.GetStarredRepos(ctx, opts)
if err != nil {
return nil, err
}
repos := make([]*api.Repository, len(starredRepos))
for i, starred := range starredRepos {
permission, err := access_model.GetUserRepoPermission(ctx, starred, user)
permission, err := access_model.GetIndividualUserRepoPermission(ctx, starred, user)
if err != nil {
return nil, err
}
@@ -76,6 +79,7 @@ func GetStarredRepos(ctx *context.APIContext) {
return
}
ctx.SetLinkHeader(int64(ctx.ContextUser.NumStars), utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(int64(ctx.ContextUser.NumStars))
ctx.JSON(http.StatusOK, &repos)
}
@@ -107,6 +111,7 @@ func GetMyStarredRepos(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
}
ctx.SetLinkHeader(int64(ctx.Doer.NumStars), utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(int64(ctx.Doer.NumStars))
ctx.JSON(http.StatusOK, &repos)
}
@@ -9,7 +9,6 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
@@ -69,19 +68,16 @@ func Search(ctx *context.APIContext) {
maxResults = 1
users = []*user_model.User{user_model.NewActionsUser()}
default:
var visible []structs.VisibleType
if ctx.PublicOnly {
visible = []structs.VisibleType{structs.VisibleTypePublic}
}
users, maxResults, err = user_model.SearchUsers(ctx, user_model.SearchUserOptions{
opts := user_model.SearchUserOptions{
Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"),
UID: uid,
Type: user_model.UserTypeIndividual,
Types: []user_model.UserType{user_model.UserTypeIndividual},
SearchByEmail: true,
Visible: visible,
ListOptions: listOptions,
})
}
opts.ApplyPublicOnly(ctx.PublicOnly)
users, maxResults, err = user_model.SearchUsers(ctx, opts)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]any{
"ok": false,
@@ -91,7 +87,7 @@ func Search(ctx *context.APIContext) {
}
}
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, map[string]any{
@@ -214,6 +210,7 @@ func ListUserActivityFeeds(ctx *context.APIContext) {
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
feeds, count, err := feed_service.GetFeeds(ctx, opts)
if err != nil {
@@ -18,18 +18,21 @@ import (
// getWatchedRepos returns the repos that the user with the specified userID is watching
func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) {
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, &repo_model.WatchedReposOptions{
opts := &repo_model.WatchedReposOptions{
ListOptions: utils.GetListOptions(ctx),
WatcherID: user.ID,
IncludePrivate: private,
})
}
opts.ApplyPublicOnly(ctx.PublicOnly)
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, opts)
if err != nil {
return nil, 0, err
}
repos := make([]*api.Repository, len(watchedRepos))
for i, watched := range watchedRepos {
permission, err := access_model.GetUserRepoPermission(ctx, watched, user)
permission, err := access_model.GetIndividualUserRepoPermission(ctx, watched, user)
if err != nil {
return nil, 0, err
}
@@ -71,6 +74,7 @@ func GetWatchedRepos(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
}
ctx.SetLinkHeader(total, utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &repos)
}
@@ -99,7 +103,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) {
if err != nil {
ctx.APIErrorInternal(err)
}
ctx.SetLinkHeader(total, utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &repos)
}
@@ -6,6 +6,7 @@ package utils
import (
"errors"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
@@ -27,7 +28,7 @@ func ResolveRefCommit(ctx reqctx.RequestContext, repo *repo_model.Repository, in
return nil, err
}
refCommit := RefCommit{InputRef: inputRef}
if gitrepo.IsBranchExist(ctx, repo, inputRef) {
if exist, _ := git_model.IsBranchExist(ctx, repo.ID, inputRef); exist {
refCommit.RefName = git.RefNameFromBranch(inputRef)
} else if gitrepo.IsTagExist(ctx, repo, inputRef) {
refCommit.RefName = git.RefNameFromTag(inputRef)
@@ -23,8 +23,9 @@ import (
// ListOwnerHooks lists the webhooks of the provided owner
func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) {
listOptions := GetListOptions(ctx)
opts := &webhook.ListWebhookOptions{
ListOptions: GetListOptions(ctx),
ListOptions: listOptions,
OwnerID: owner.ID,
}
@@ -42,7 +43,7 @@ func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) {
return
}
}
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiHooks)
}
@@ -214,6 +215,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
w := &webhook.Webhook{
OwnerID: ownerID,
RepoID: repoID,
Name: strings.TrimSpace(form.Name),
URL: form.Config["url"],
ContentType: webhook.ToHookContentType(form.Config["content_type"]),
Secret: form.Config["secret"],
@@ -391,6 +393,10 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
w.IsActive = *form.Active
}
if form.Name != nil {
w.Name = strings.TrimSpace(*form.Name)
}
if err := webhook.UpdateWebhook(ctx, w); err != nil {
ctx.APIErrorInternal(err)
return false
@@ -12,7 +12,7 @@ import (
// GetListOptions returns list options using the page and limit parameters
func GetListOptions(ctx *context.APIContext) db.ListOptions {
return db.ListOptions{
Page: ctx.FormInt("page"),
Page: max(ctx.FormInt("page"), 1),
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
}
}