初始提交: Gitea 项目代码
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/routers/common"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func DownloadActionsRunJobLogs(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs repository downloadActionsRunJobLogs
|
||||
// ---
|
||||
// summary: Downloads the job logs for a workflow run
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: job_id
|
||||
// in: path
|
||||
// description: id of the job
|
||||
// type: integer
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: output blob content
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
jobID := ctx.PathParamInt64("job_id")
|
||||
curJob, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobID)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err = curJob.LoadRepo(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, curJob)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
// UpdateVatar updates the Avatar of an Repo
|
||||
func UpdateAvatar(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/avatar repository repoUpdateAvatar
|
||||
// ---
|
||||
// summary: Update avatar
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/UpdateRepoAvatarOption"
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
form := web.GetForm(ctx).(*api.UpdateRepoAvatarOption)
|
||||
|
||||
content, err := base64.StdEncoding.DecodeString(form.Image)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = repo_service.UploadAvatar(ctx, ctx.Repo.Repository, content)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// UpdateAvatar deletes the Avatar of an Repo
|
||||
func DeleteAvatar(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/avatar repository repoDeleteAvatar
|
||||
// ---
|
||||
// summary: Delete avatar
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
err := repo_service.DeleteAvatar(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/services/context"
|
||||
files_service "gitea.dev/services/repository/files"
|
||||
)
|
||||
|
||||
// GetBlob get the blob of a repository file.
|
||||
func GetBlob(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/git/blobs/{sha} repository GetBlob
|
||||
// ---
|
||||
// summary: Gets the blob of a repository.
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: sha
|
||||
// in: path
|
||||
// description: sha of the commit
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/GitBlobResponse"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
sha := ctx.PathParam("sha")
|
||||
if len(sha) == 0 {
|
||||
ctx.APIError(http.StatusBadRequest, "sha not provided")
|
||||
return
|
||||
}
|
||||
|
||||
if blob, err := files_service.GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, blob)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,371 @@
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/perm"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
issue_service "gitea.dev/services/issue"
|
||||
pull_service "gitea.dev/services/pull"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
// ListCollaborators list a repository's collaborators
|
||||
func ListCollaborators(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/collaborators repository repoListCollaborators
|
||||
// ---
|
||||
// summary: List a repository's collaborators
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
collaborators, total, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
users := make([]*api.User, len(collaborators))
|
||||
for i, collaborator := range collaborators {
|
||||
users[i] = convert.ToUser(ctx, collaborator.User, ctx.Doer)
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(total)
|
||||
ctx.JSON(http.StatusOK, users)
|
||||
}
|
||||
|
||||
// IsCollaborator check if a user is a collaborator of a repository
|
||||
func IsCollaborator(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/collaborators/{collaborator} repository repoCheckCollaborator
|
||||
// ---
|
||||
// summary: Check if a user is a collaborator of a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: collaborator
|
||||
// in: path
|
||||
// description: username of the user to check for being a collaborator
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
user, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator"))
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
isColab, err := repo_model.IsCollaborator(ctx, ctx.Repo.Repository.ID, user.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if isColab {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
} else {
|
||||
ctx.APIErrorNotFound()
|
||||
}
|
||||
}
|
||||
|
||||
// AddOrUpdateCollaborator add or update a collaborator to a repository
|
||||
func AddOrUpdateCollaborator(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /repos/{owner}/{repo}/collaborators/{collaborator} repository repoAddCollaborator
|
||||
// ---
|
||||
// summary: Add or Update a collaborator to a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: collaborator
|
||||
// in: path
|
||||
// description: username of the user to add or update as a collaborator
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/AddCollaboratorOption"
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.AddCollaboratorOption)
|
||||
|
||||
collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator"))
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !collaborator.IsActive {
|
||||
ctx.APIErrorInternal(errors.New("collaborator's account is inactive"))
|
||||
return
|
||||
}
|
||||
|
||||
p := perm.AccessModeWrite
|
||||
if form.Permission != nil {
|
||||
p = perm.ParseAccessMode(string(*form.Permission), perm.AccessModeRead, perm.AccessModeWrite, perm.AccessModeAdmin)
|
||||
}
|
||||
|
||||
if err := repo_service.AddOrUpdateCollaborator(ctx, ctx.Repo.Repository, collaborator, p); err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// DeleteCollaborator delete a collaborator from a repository
|
||||
func DeleteCollaborator(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/collaborators/{collaborator} repository repoDeleteCollaborator
|
||||
// ---
|
||||
// summary: Delete a collaborator from a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: collaborator
|
||||
// in: path
|
||||
// description: username of the collaborator to delete
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator"))
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetRepoPermissions gets repository permissions for a user
|
||||
func GetRepoPermissions(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/collaborators/{collaborator}/permission repository repoGetRepoPermissions
|
||||
// ---
|
||||
// summary: Get repository permissions for a user
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: collaborator
|
||||
// in: path
|
||||
// description: username of the collaborator whose permissions are to be obtained
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/RepoCollaboratorPermission"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
|
||||
collaboratorUsername := ctx.PathParam("collaborator")
|
||||
if !ctx.Doer.IsAdmin && !strings.EqualFold(ctx.Doer.LowerName, collaboratorUsername) && !ctx.IsUserRepoAdmin() {
|
||||
ctx.APIError(http.StatusForbidden, "Only admins can query all permissions, repo admins can query all repo permissions, collaborators can query only their own")
|
||||
return
|
||||
}
|
||||
|
||||
collaborator, err := user_model.GetUserByName(ctx, collaboratorUsername)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
permission, err := access_model.GetIndividualUserRepoPermission(ctx, ctx.Repo.Repository, collaborator)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToUserAndPermission(ctx, collaborator, ctx.ContextUser, permission.AccessMode))
|
||||
}
|
||||
|
||||
// GetReviewers return all users that can be requested to review in this repo
|
||||
func GetReviewers(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/reviewers repository repoGetReviewers
|
||||
// ---
|
||||
// summary: Return all users that can be requested to review in this repo
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
canChooseReviewer := issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, ctx.Repo.Repository, 0)
|
||||
if !canChooseReviewer {
|
||||
ctx.APIError(http.StatusForbidden, errors.New("doer has no permission to get reviewers"))
|
||||
return
|
||||
}
|
||||
|
||||
reviewers, err := pull_service.GetReviewers(ctx, ctx.Repo.Repository, ctx.Doer.ID, 0)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToUsers(ctx, ctx.Doer, reviewers))
|
||||
}
|
||||
|
||||
// GetAssignees return all users that have write access and can be assigned to issues
|
||||
func GetAssignees(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/assignees repository repoGetAssignees
|
||||
// ---
|
||||
// summary: Return all users that have write access and can be assigned to issues
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
assignees, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToUsers(ctx, ctx.Doer, assignees))
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
// Copyright 2018 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// GetSingleCommit get a commit via sha
|
||||
func GetSingleCommit(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/git/commits/{sha} repository repoGetSingleCommit
|
||||
// ---
|
||||
// summary: Get a single commit from a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: sha
|
||||
// in: path
|
||||
// description: a git ref or commit sha
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: stat
|
||||
// in: query
|
||||
// description: include diff stats for every commit (disable for speedup, default 'true')
|
||||
// type: boolean
|
||||
// - name: verification
|
||||
// in: query
|
||||
// description: include verification for every commit (disable for speedup, default 'true')
|
||||
// type: boolean
|
||||
// - name: files
|
||||
// in: query
|
||||
// description: include a list of affected files for every commit (disable for speedup, default 'true')
|
||||
// type: boolean
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Commit"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
sha := ctx.PathParam("sha")
|
||||
if !git.IsValidRefPattern(sha) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "no valid ref or sha: "+sha)
|
||||
return
|
||||
}
|
||||
|
||||
getCommit(ctx, sha, convert.ParseCommitOptions(ctx))
|
||||
}
|
||||
|
||||
func getCommit(ctx *context.APIContext, identifier string, toCommitOpts convert.ToCommitOptions) {
|
||||
commit, err := ctx.Repo.GitRepo.GetCommit(identifier)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.APIErrorNotFound("commit doesn't exist: " + identifier)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
json, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, commit, nil, toCommitOpts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, json)
|
||||
}
|
||||
|
||||
// GetAllCommits get all commits via
|
||||
func GetAllCommits(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/commits repository repoGetAllCommits
|
||||
// ---
|
||||
// summary: Get a list of all commits from a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: sha
|
||||
// in: query
|
||||
// description: SHA or branch to start listing commits from (usually 'master')
|
||||
// type: string
|
||||
// - name: path
|
||||
// in: query
|
||||
// description: filepath of a file/dir
|
||||
// type: string
|
||||
// - name: since
|
||||
// in: query
|
||||
// description: Only commits after this date will be returned (ISO 8601 format)
|
||||
// type: string
|
||||
// format: date-time
|
||||
// - name: until
|
||||
// in: query
|
||||
// description: Only commits before this date will be returned (ISO 8601 format)
|
||||
// type: string
|
||||
// format: date-time
|
||||
// - name: stat
|
||||
// in: query
|
||||
// description: include diff stats for every commit (disable for speedup, default 'true')
|
||||
// type: boolean
|
||||
// - name: verification
|
||||
// in: query
|
||||
// description: include verification for every commit (disable for speedup, default 'true')
|
||||
// type: boolean
|
||||
// - name: files
|
||||
// in: query
|
||||
// description: include a list of affected files for every commit (disable for speedup, default 'true')
|
||||
// type: boolean
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results (ignored if used with 'path')
|
||||
// type: integer
|
||||
// - name: not
|
||||
// in: query
|
||||
// description: commits that match the given specifier will not be listed.
|
||||
// type: string
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/CommitList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "409":
|
||||
// "$ref": "#/responses/EmptyRepository"
|
||||
|
||||
since := ctx.FormString("since")
|
||||
until := ctx.FormString("until")
|
||||
|
||||
// Validate since/until as ISO 8601 (RFC3339)
|
||||
if since != "" {
|
||||
if _, err := time.Parse(time.RFC3339, since); err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "invalid 'since' format, expected ISO 8601 (RFC3339)")
|
||||
return
|
||||
}
|
||||
}
|
||||
if until != "" {
|
||||
if _, err := time.Parse(time.RFC3339, until); err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "invalid 'until' format, expected ISO 8601 (RFC3339)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
ctx.JSON(http.StatusConflict, api.APIError{
|
||||
Message: "Git Repository is empty.",
|
||||
URL: setting.API.SwaggerURL,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
if listOptions.Page <= 0 {
|
||||
listOptions.Page = 1
|
||||
}
|
||||
|
||||
if listOptions.PageSize > setting.Git.CommitsRangeSize {
|
||||
listOptions.PageSize = setting.Git.CommitsRangeSize
|
||||
}
|
||||
|
||||
sha := ctx.FormString("sha")
|
||||
path := ctx.FormString("path")
|
||||
not := ctx.FormString("not")
|
||||
|
||||
var (
|
||||
commitsCountTotal int64
|
||||
commits []*git.Commit
|
||||
err error
|
||||
)
|
||||
|
||||
if len(path) == 0 {
|
||||
var baseCommit *git.Commit
|
||||
if len(sha) == 0 {
|
||||
// no sha supplied - use default branch
|
||||
baseCommit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// get commit specified by sha
|
||||
baseCommit, err = ctx.Repo.GitRepo.GetCommit(sha)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Total commit count
|
||||
commitsCountTotal, err = gitrepo.CommitsCount(ctx, ctx.Repo.Repository, gitrepo.CommitsCountOptions{
|
||||
Not: not,
|
||||
Revision: []string{baseCommit.ID.String()},
|
||||
Since: since,
|
||||
Until: until,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Query commits
|
||||
commits, err = baseCommit.CommitsByRange(listOptions.Page, listOptions.PageSize, not, since, until)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if len(sha) == 0 {
|
||||
sha = ctx.Repo.Repository.DefaultBranch
|
||||
}
|
||||
|
||||
commitsCountTotal, err = gitrepo.CommitsCount(ctx, ctx.Repo.Repository,
|
||||
gitrepo.CommitsCountOptions{
|
||||
Not: not,
|
||||
Revision: []string{sha},
|
||||
RelPath: []string{path},
|
||||
Since: since,
|
||||
Until: until,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
} else if commitsCountTotal == 0 {
|
||||
ctx.APIErrorNotFound("FileCommitsCount", nil)
|
||||
return
|
||||
}
|
||||
|
||||
commits, err = ctx.Repo.GitRepo.CommitsByFileAndRange(
|
||||
git.CommitsByFileAndRangeOptions{
|
||||
Revision: sha,
|
||||
File: path,
|
||||
Not: not,
|
||||
Page: listOptions.Page,
|
||||
Since: since,
|
||||
Until: until,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pageCount := int(math.Ceil(float64(commitsCountTotal) / float64(listOptions.PageSize)))
|
||||
userCache := make(map[string]*user_model.User)
|
||||
apiCommits := make([]*api.Commit, len(commits))
|
||||
|
||||
for i, commit := range commits {
|
||||
// Create json struct
|
||||
apiCommits[i], err = convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, commit, userCache, convert.ParseCommitOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(commitsCountTotal, listOptions.PageSize)
|
||||
ctx.SetTotalCountHeader(commitsCountTotal)
|
||||
|
||||
// kept for backwards compatibility
|
||||
ctx.RespHeader().Set("X-Page", strconv.Itoa(listOptions.Page))
|
||||
ctx.RespHeader().Set("X-PerPage", strconv.Itoa(listOptions.PageSize))
|
||||
ctx.RespHeader().Set("X-Total", strconv.FormatInt(commitsCountTotal, 10))
|
||||
ctx.RespHeader().Set("X-PageCount", strconv.Itoa(pageCount))
|
||||
ctx.RespHeader().Set("X-HasMore", strconv.FormatBool(listOptions.Page < pageCount))
|
||||
ctx.AppendAccessControlExposeHeaders("X-Page", "X-PerPage", "X-Total", "X-PageCount", "X-HasMore")
|
||||
|
||||
ctx.JSON(http.StatusOK, &apiCommits)
|
||||
}
|
||||
|
||||
// DownloadCommitDiffOrPatch render a commit's raw diff or patch
|
||||
func DownloadCommitDiffOrPatch(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/git/commits/{sha}.{diffType} repository repoDownloadCommitDiffOrPatch
|
||||
// ---
|
||||
// summary: Get a commit's diff or patch
|
||||
// produces:
|
||||
// - text/plain
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: sha
|
||||
// in: path
|
||||
// description: SHA of the commit to get
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: diffType
|
||||
// in: path
|
||||
// description: whether the output is diff or patch
|
||||
// type: string
|
||||
// enum: [diff, patch]
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/string"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
sha := ctx.PathParam("sha")
|
||||
diffType := git.RawDiffType(ctx.PathParam("diffType"))
|
||||
|
||||
if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, diffType, ctx.Resp); err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.APIErrorNotFound("commit doesn't exist: " + sha)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetCommitPullRequest returns the merged pull request of the commit
|
||||
func GetCommitPullRequest(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/commits/{sha}/pull repository repoGetCommitPullRequest
|
||||
// ---
|
||||
// summary: Get the merged pull request of the commit
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: sha
|
||||
// in: path
|
||||
// description: SHA of the commit to get
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/PullRequest"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
pr, err := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, ctx.PathParam("sha"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrPullRequestNotExist(err) {
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = pr.LoadBaseRepo(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if err = pr.LoadHeadRepo(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// CompareDiff compare two branches or commits
|
||||
func CompareDiff(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/compare/{basehead} repository repoCompareDiff
|
||||
// ---
|
||||
// summary: Get commit comparison information
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: basehead
|
||||
// in: path
|
||||
// description: compare two branches or commits
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Compare"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
var err error
|
||||
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
compareInfo, closer := parseCompareInfo(ctx, ctx.PathParam("*"))
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
defer closer()
|
||||
|
||||
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
|
||||
files := ctx.FormString("files") == "" || ctx.FormBool("files")
|
||||
|
||||
apiCommits := make([]*api.Commit, 0, len(compareInfo.Commits))
|
||||
userCache := make(map[string]*user_model.User)
|
||||
|
||||
for i := 0; i < len(compareInfo.Commits); i++ {
|
||||
apiCommit, err := convert.ToCommit(
|
||||
ctx,
|
||||
compareInfo.HeadRepo,
|
||||
compareInfo.HeadGitRepo,
|
||||
compareInfo.Commits[i],
|
||||
userCache,
|
||||
convert.ToCommitOptions{
|
||||
Stat: true,
|
||||
Verification: verification,
|
||||
Files: files,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
apiCommits = append(apiCommits, apiCommit)
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &api.Compare{
|
||||
TotalCommits: len(compareInfo.Commits),
|
||||
Commits: apiCommits,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
archiver_service "gitea.dev/services/repository/archiver"
|
||||
)
|
||||
|
||||
func serveRepoArchive(ctx *context.APIContext, reqFileName string, paths []string) {
|
||||
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, reqFileName, paths)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
} else if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
err = archiver_service.ServeRepoArchive(ctx.Base, aReq)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DownloadArchive is the GitHub-compatible endpoint to download repository archives
|
||||
// TODO: The API document is missing: Add github compatible tarball download API endpoints (#32572)
|
||||
func DownloadArchive(ctx *context.APIContext) {
|
||||
var tp repo_model.ArchiveType
|
||||
switch ballType := ctx.PathParam("ball_type"); ballType {
|
||||
case "tarball":
|
||||
tp = repo_model.ArchiveTarGz
|
||||
case "zipball":
|
||||
tp = repo_model.ArchiveZip
|
||||
case "bundle":
|
||||
tp = repo_model.ArchiveBundle
|
||||
default:
|
||||
ctx.APIError(http.StatusBadRequest, "Unknown archive type: "+ballType)
|
||||
return
|
||||
}
|
||||
serveRepoArchive(ctx, ctx.PathParam("*")+"."+tp.String(), ctx.FormStrings("path"))
|
||||
}
|
||||
@@ -0,0 +1,970 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
git_model "gitea.dev/models/git"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/httpcache"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/lfs"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/storage"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/routers/common"
|
||||
"gitea.dev/services/context"
|
||||
pull_service "gitea.dev/services/pull"
|
||||
files_service "gitea.dev/services/repository/files"
|
||||
)
|
||||
|
||||
const giteaObjectTypeHeader = "X-Gitea-Object-Type"
|
||||
|
||||
// GetRawFile get a file by path on a repository
|
||||
func GetRawFile(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/raw/{filepath} repository repoGetRawFile
|
||||
// ---
|
||||
// summary: Get a file from a repository
|
||||
// produces:
|
||||
// - application/octet-stream
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: filepath
|
||||
// in: path
|
||||
// description: path of the file to get, it should be "{ref}/{filepath}". If there is no ref could be inferred, it will be treated as the default branch
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: query
|
||||
// description: "The name of the commit/branch/tag. Default to the repository’s default branch"
|
||||
// type: string
|
||||
// required: false
|
||||
// responses:
|
||||
// 200:
|
||||
// description: Returns raw file content.
|
||||
// schema:
|
||||
// type: file
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
blob, entry, lastModified := getBlobForEntry(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
|
||||
|
||||
if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetRawFileOrLFS get a file by repo's path, redirecting to LFS if necessary.
|
||||
func GetRawFileOrLFS(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/media/{filepath} repository repoGetRawFileOrLFS
|
||||
// ---
|
||||
// summary: Get a file or it's LFS object from a repository
|
||||
// produces:
|
||||
// - application/octet-stream
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: filepath
|
||||
// in: path
|
||||
// description: path of the file to get, it should be "{ref}/{filepath}". If there is no ref could be inferred, it will be treated as the default branch
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: query
|
||||
// description: "The name of the commit/branch/tag. Default to the repository’s default branch"
|
||||
// type: string
|
||||
// required: false
|
||||
// responses:
|
||||
// 200:
|
||||
// description: Returns raw file content.
|
||||
// schema:
|
||||
// type: file
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
blob, entry, lastModified := getBlobForEntry(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
|
||||
|
||||
// LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file
|
||||
if blob.Size() > lfs.MetaFileMaxSize {
|
||||
// First handle caching for the blob
|
||||
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
|
||||
return
|
||||
}
|
||||
|
||||
// If not cached - serve!
|
||||
if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// OK, now the blob is known to have at most 1024 (lfs pointer max size) bytes,
|
||||
// we can simply read this in one go (This saves reading it twice)
|
||||
lfsPointerBuf, err := blob.GetBlobBytes(lfs.MetaFileMaxSize)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the blob represents a pointer
|
||||
pointer, _ := lfs.ReadPointerFromBuffer(lfsPointerBuf)
|
||||
|
||||
// if it's not a pointer, just serve the data directly
|
||||
if !pointer.IsValid() {
|
||||
_, _ = ctx.Resp.Write(lfsPointerBuf)
|
||||
return
|
||||
}
|
||||
|
||||
// Now check if there is a MetaObject for this pointer
|
||||
meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid)
|
||||
|
||||
// If there isn't one, just serve the data directly
|
||||
if errors.Is(err, git_model.ErrLFSObjectNotExist) {
|
||||
_, _ = ctx.Resp.Write(lfsPointerBuf)
|
||||
return
|
||||
} else if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle caching for the LFS object OID
|
||||
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`, meta.UpdatedUnix.AsTimePtr()) {
|
||||
return
|
||||
}
|
||||
|
||||
if setting.LFS.Storage.ServeDirect() {
|
||||
// If we have a signed url (S3, object storage), redirect to this directly.
|
||||
u, err := storage.LFS.ServeDirectURL(pointer.RelativePath(), blob.Name(), ctx.Req.Method, nil)
|
||||
if u != nil && err == nil {
|
||||
ctx.Redirect(u.String())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
lfsDataFile, err := lfs.ReadMetaObject(meta.Pointer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
defer lfsDataFile.Close()
|
||||
httplib.ServeUserContentByFile(ctx.Base.Req, ctx.Base.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: ctx.Repo.TreePath})
|
||||
}
|
||||
|
||||
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) {
|
||||
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
if entry.IsDir() || entry.IsSubModule() {
|
||||
ctx.APIErrorNotFound("getBlobForEntry", nil)
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, nil, nil
|
||||
}
|
||||
when := &latestCommit.Committer.When
|
||||
|
||||
return entry.Blob(), entry, when
|
||||
}
|
||||
|
||||
// GetArchive get archive of a repository
|
||||
func GetArchive(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/archive/{archive} repository repoGetArchive
|
||||
// ---
|
||||
// summary: Get an archive of a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: archive
|
||||
// in: path
|
||||
// description: the git reference for download with attached archive format (e.g. master.zip)
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: path
|
||||
// in: query
|
||||
// type: array
|
||||
// items:
|
||||
// type: string
|
||||
// description: subpath of the repository to download
|
||||
// collectionFormat: multi
|
||||
// responses:
|
||||
// 200:
|
||||
// description: success
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
serveRepoArchive(ctx, ctx.PathParam("*"), ctx.FormStrings("path"))
|
||||
}
|
||||
|
||||
// GetEditorconfig get editor config of a repository
|
||||
func GetEditorconfig(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/editorconfig/{filepath} repository repoGetEditorConfig
|
||||
// ---
|
||||
// summary: Get the EditorConfig definitions of a file in a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: filepath
|
||||
// in: path
|
||||
// description: filepath of file to get
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: query
|
||||
// description: "The name of the commit/branch/tag. Default to the repository’s default branch."
|
||||
// type: string
|
||||
// required: false
|
||||
// responses:
|
||||
// 200:
|
||||
// description: success
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
ec, _, err := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fileName := ctx.PathParam("filename")
|
||||
def, err := ec.GetDefinitionForFilename(fileName)
|
||||
if def == nil {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, def)
|
||||
}
|
||||
|
||||
func base64Reader(s string) (io.ReadSeeker, error) {
|
||||
b, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bytes.NewReader(b), nil
|
||||
}
|
||||
|
||||
func ReqChangeRepoFileOptionsAndCheck(ctx *context.APIContext) {
|
||||
commonOpts := web.GetForm(ctx).(api.FileOptionsInterface).GetFileOptions()
|
||||
commonOpts.BranchName = util.IfZero(commonOpts.BranchName, ctx.Repo.Repository.DefaultBranch)
|
||||
commonOpts.NewBranchName = util.IfZero(commonOpts.NewBranchName, commonOpts.BranchName)
|
||||
if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, commonOpts.NewBranchName) && !ctx.IsUserSiteAdmin() {
|
||||
ctx.APIError(http.StatusForbidden, "user should have a permission to write to the target branch")
|
||||
return
|
||||
}
|
||||
changeFileOpts := &files_service.ChangeRepoFilesOptions{
|
||||
Message: commonOpts.Message,
|
||||
OldBranch: commonOpts.BranchName,
|
||||
NewBranch: commonOpts.NewBranchName,
|
||||
ForcePush: commonOpts.ForcePush,
|
||||
Committer: &files_service.IdentityOptions{
|
||||
GitUserName: commonOpts.Committer.Name,
|
||||
GitUserEmail: commonOpts.Committer.Email,
|
||||
},
|
||||
Author: &files_service.IdentityOptions{
|
||||
GitUserName: commonOpts.Author.Name,
|
||||
GitUserEmail: commonOpts.Author.Email,
|
||||
},
|
||||
Dates: &files_service.CommitDateOptions{
|
||||
Author: commonOpts.Dates.Author,
|
||||
Committer: commonOpts.Dates.Committer,
|
||||
},
|
||||
Signoff: commonOpts.Signoff,
|
||||
}
|
||||
if changeFileOpts.Dates.Author.IsZero() {
|
||||
changeFileOpts.Dates.Author = time.Now()
|
||||
}
|
||||
if changeFileOpts.Dates.Committer.IsZero() {
|
||||
changeFileOpts.Dates.Committer = time.Now()
|
||||
}
|
||||
ctx.Data["__APIChangeRepoFilesOptions"] = changeFileOpts
|
||||
}
|
||||
|
||||
func getAPIChangeRepoFileOptions[T api.FileOptionsInterface](ctx *context.APIContext) (apiOpts T, opts *files_service.ChangeRepoFilesOptions) {
|
||||
return web.GetForm(ctx).(T), ctx.Data["__APIChangeRepoFilesOptions"].(*files_service.ChangeRepoFilesOptions)
|
||||
}
|
||||
|
||||
// ChangeFiles handles API call for modifying multiple files
|
||||
func ChangeFiles(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/contents repository repoChangeFiles
|
||||
// ---
|
||||
// summary: Modify multiple files in a repository
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ChangeFilesOptions"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/FilesResponse"
|
||||
// "403":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/error"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
apiOpts, opts := getAPIChangeRepoFileOptions[*api.ChangeFilesOptions](ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
for _, file := range apiOpts.Files {
|
||||
contentReader, err := base64Reader(file.ContentBase64)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
// FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options
|
||||
// But the LastCommitID is not provided in the API options, need to fully fix them in API
|
||||
changeRepoFile := &files_service.ChangeRepoFile{
|
||||
Operation: file.Operation,
|
||||
TreePath: file.Path,
|
||||
FromTreePath: file.FromPath,
|
||||
ContentReader: contentReader,
|
||||
SHA: file.SHA,
|
||||
}
|
||||
opts.Files = append(opts.Files, changeRepoFile)
|
||||
}
|
||||
|
||||
if opts.Message == "" {
|
||||
opts.Message = changeFilesCommitMessage(ctx, opts.Files)
|
||||
}
|
||||
|
||||
if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
|
||||
handleChangeRepoFilesError(ctx, err)
|
||||
} else {
|
||||
ctx.JSON(http.StatusCreated, filesResponse)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateFile handles API call for creating a file
|
||||
func CreateFile(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
|
||||
// ---
|
||||
// summary: Create a file in a repository
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: filepath
|
||||
// in: path
|
||||
// description: path of the file to create
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateFileOptions"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/FileResponse"
|
||||
// "403":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/error"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
apiOpts, opts := getAPIChangeRepoFileOptions[*api.CreateFileOptions](ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
contentReader, err := base64Reader(apiOpts.ContentBase64)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
|
||||
Operation: "create",
|
||||
TreePath: ctx.PathParam("*"),
|
||||
ContentReader: contentReader,
|
||||
})
|
||||
if opts.Message == "" {
|
||||
opts.Message = changeFilesCommitMessage(ctx, opts.Files)
|
||||
}
|
||||
|
||||
if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
|
||||
handleChangeRepoFilesError(ctx, err)
|
||||
} else {
|
||||
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
||||
ctx.JSON(http.StatusCreated, fileResponse)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateFile handles API call for updating a file
|
||||
func UpdateFile(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
|
||||
// ---
|
||||
// summary: Update a file in a repository if SHA is set, or create the file if SHA is not set
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: filepath
|
||||
// in: path
|
||||
// description: path of the file to update
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/UpdateFileOptions"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/FileResponse"
|
||||
// "201":
|
||||
// "$ref": "#/responses/FileResponse"
|
||||
// "403":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/error"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
apiOpts, opts := getAPIChangeRepoFileOptions[*api.UpdateFileOptions](ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
contentReader, err := base64Reader(apiOpts.ContentBase64)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
willCreate := apiOpts.SHA == ""
|
||||
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
|
||||
Operation: util.Iif(willCreate, "create", "update"),
|
||||
ContentReader: contentReader,
|
||||
SHA: apiOpts.SHA,
|
||||
FromTreePath: apiOpts.FromPath,
|
||||
TreePath: ctx.PathParam("*"),
|
||||
})
|
||||
if opts.Message == "" {
|
||||
opts.Message = changeFilesCommitMessage(ctx, opts.Files)
|
||||
}
|
||||
|
||||
if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
|
||||
handleChangeRepoFilesError(ctx, err)
|
||||
} else {
|
||||
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
||||
ctx.JSON(util.Iif(willCreate, http.StatusCreated, http.StatusOK), fileResponse)
|
||||
}
|
||||
}
|
||||
|
||||
func handleChangeRepoFilesError(ctx *context.APIContext, err error) {
|
||||
if git.IsErrPushRejected(err) {
|
||||
err := err.(*git.ErrPushRejected)
|
||||
ctx.APIError(http.StatusForbidden, err.Message)
|
||||
return
|
||||
}
|
||||
if files_service.IsErrUserCannotCommit(err) || pull_service.IsErrFilePathProtected(err) {
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
return
|
||||
}
|
||||
if git_model.IsErrBranchAlreadyExists(err) || files_service.IsErrFilenameInvalid(err) || pull_service.IsErrSHADoesNotMatch(err) ||
|
||||
files_service.IsErrFilePathInvalid(err) || files_service.IsErrRepoFileAlreadyExists(err) ||
|
||||
files_service.IsErrCommitIDDoesNotMatch(err) || files_service.IsErrSHAOrCommitIDNotProvided(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
|
||||
// format commit message if empty
|
||||
func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.ChangeRepoFile) string {
|
||||
var (
|
||||
createFiles []string
|
||||
updateFiles []string
|
||||
deleteFiles []string
|
||||
)
|
||||
for _, file := range files {
|
||||
switch file.Operation {
|
||||
case "create":
|
||||
createFiles = append(createFiles, file.TreePath)
|
||||
case "update", "upload", "rename": // upload and rename works like "update", there is no translation for them at the moment
|
||||
updateFiles = append(updateFiles, file.TreePath)
|
||||
case "delete":
|
||||
deleteFiles = append(deleteFiles, file.TreePath)
|
||||
}
|
||||
}
|
||||
message := ""
|
||||
if len(createFiles) != 0 {
|
||||
message += ctx.Locale.TrString("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
|
||||
}
|
||||
if len(updateFiles) != 0 {
|
||||
message += ctx.Locale.TrString("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
|
||||
}
|
||||
if len(deleteFiles) != 0 {
|
||||
message += ctx.Locale.TrString("repo.editor.delete", strings.Join(deleteFiles, ", "))
|
||||
}
|
||||
return strings.Trim(message, "\n")
|
||||
}
|
||||
|
||||
// DeleteFile Delete a file in a repository
|
||||
func DeleteFile(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/contents/{filepath} repository repoDeleteFile
|
||||
// ---
|
||||
// summary: Delete a file in a repository
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: filepath
|
||||
// in: path
|
||||
// description: path of the file to delete
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/DeleteFileOptions"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/FileDeleteResponse"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
// "422":
|
||||
// "$ref": "#/responses/error"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
apiOpts, opts := getAPIChangeRepoFileOptions[*api.DeleteFileOptions](ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
|
||||
Operation: "delete",
|
||||
SHA: apiOpts.SHA,
|
||||
TreePath: ctx.PathParam("*"),
|
||||
})
|
||||
if opts.Message == "" {
|
||||
opts.Message = changeFilesCommitMessage(ctx, opts.Files)
|
||||
}
|
||||
|
||||
if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
|
||||
handleChangeRepoFilesError(ctx, err)
|
||||
} else {
|
||||
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
||||
ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
|
||||
}
|
||||
}
|
||||
|
||||
func resolveRefCommit(ctx *context.APIContext, ref string, minCommitIDLen ...int) *utils.RefCommit {
|
||||
ref = util.IfZero(ref, ctx.Repo.Repository.DefaultBranch)
|
||||
refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ref, minCommitIDLen...)
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return refCommit
|
||||
}
|
||||
|
||||
func GetContentsExt(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/contents-ext/{filepath} repository repoGetContentsExt
|
||||
// ---
|
||||
// summary: The extended "contents" API, to get file metadata and/or content, or list a directory.
|
||||
// description: It guarantees that only one of the response fields is set if the request succeeds.
|
||||
// Users can pass "includes=file_content" or "includes=lfs_metadata" to retrieve more fields.
|
||||
// "includes=file_content" only works for single file, if you need to retrieve file contents in batch,
|
||||
// use "file-contents" API after listing the directory.
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: filepath
|
||||
// in: path
|
||||
// description: path of the dir, file, symlink or submodule in the repo. Swagger requires path parameter to be "required",
|
||||
// you can leave it empty or pass a single dot (".") to get the root directory.
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: query
|
||||
// description: the name of the commit/branch/tag, default to the repository’s default branch.
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: includes
|
||||
// in: query
|
||||
// description: By default this API's response only contains file's metadata. Use comma-separated "includes" options to retrieve more fields.
|
||||
// Option "file_content" will try to retrieve the file content, "lfs_metadata" will try to retrieve LFS metadata,
|
||||
// "commit_metadata" will try to retrieve commit metadata, and "commit_message" will try to retrieve commit message.
|
||||
// type: string
|
||||
// required: false
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ContentsExtResponse"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if treePath := ctx.PathParam("*"); treePath == "." || treePath == "/" {
|
||||
ctx.SetPathParam("*", "") // workaround for swagger, it requires path parameter to be "required", but we need to list root directory
|
||||
}
|
||||
opts := files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*")}
|
||||
for includeOpt := range strings.SplitSeq(ctx.FormString("includes"), ",") {
|
||||
if includeOpt == "" {
|
||||
continue
|
||||
}
|
||||
switch includeOpt {
|
||||
case "file_content":
|
||||
opts.IncludeSingleFileContent = true
|
||||
case "lfs_metadata":
|
||||
opts.IncludeLfsMetadata = true
|
||||
case "commit_metadata":
|
||||
opts.IncludeCommitMetadata = true
|
||||
case "commit_message":
|
||||
opts.IncludeCommitMessage = true
|
||||
default:
|
||||
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("unknown include option %q", includeOpt))
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.JSON(http.StatusOK, getRepoContents(ctx, opts))
|
||||
}
|
||||
|
||||
func GetContents(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents
|
||||
// ---
|
||||
// summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir.
|
||||
// description: This API follows GitHub's design, and it is not easy to use. Recommend users to use the "contents-ext" API instead.
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: filepath
|
||||
// in: path
|
||||
// description: path of the dir, file, symlink or submodule in the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: query
|
||||
// description: "The name of the commit/branch/tag. Default to the repository’s default branch."
|
||||
// type: string
|
||||
// required: false
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ContentsResponse"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
ret := getRepoContents(ctx, files_service.GetContentsOrListOptions{
|
||||
TreePath: ctx.PathParam("*"),
|
||||
IncludeSingleFileContent: true,
|
||||
IncludeCommitMetadata: true,
|
||||
})
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, util.Iif[any](ret.FileContents != nil, ret.FileContents, ret.DirContents))
|
||||
}
|
||||
|
||||
func getRepoContents(ctx *context.APIContext, opts files_service.GetContentsOrListOptions) *api.ContentsExtResponse {
|
||||
refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref"))
|
||||
if ctx.Written() {
|
||||
return nil
|
||||
}
|
||||
ret, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.APIErrorNotFound("GetContentsOrList", err)
|
||||
return nil
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return &ret
|
||||
}
|
||||
|
||||
func GetContentsList(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList
|
||||
// ---
|
||||
// summary: Gets the metadata of all the entries of the root dir.
|
||||
// description: This API follows GitHub's design, and it is not easy to use. Recommend users to use our "contents-ext" API instead.
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: query
|
||||
// description: "The name of the commit/branch/tag. Default to the repository’s default branch."
|
||||
// type: string
|
||||
// required: false
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ContentsListResponse"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
// same as GetContents(), this function is here because swagger fails if path is empty in GetContents() interface
|
||||
GetContents(ctx)
|
||||
}
|
||||
|
||||
func GetFileContentsGet(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/file-contents repository repoGetFileContents
|
||||
// ---
|
||||
// summary: Get the metadata and contents of requested files
|
||||
// description: See the POST method. This GET method supports using JSON encoded request body in query parameter.
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: query
|
||||
// description: "The name of the commit/branch/tag. Default to the repository’s default branch."
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: body
|
||||
// in: query
|
||||
// description: "The JSON encoded body (see the POST request): {\"files\": [\"filename1\", \"filename2\"]}"
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ContentsListResponse"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
// The POST method requires "write" permission, so we also support this "GET" method
|
||||
handleGetFileContents(ctx)
|
||||
}
|
||||
|
||||
func GetFileContentsPost(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/file-contents repository repoGetFileContentsPost
|
||||
// ---
|
||||
// summary: Get the metadata and contents of requested files
|
||||
// description: Uses automatic pagination based on default page size and
|
||||
// max response size and returns the maximum allowed number of files.
|
||||
// Files which could not be retrieved are null. Files which are too large
|
||||
// are being returned with `encoding == null`, `content == null` and `size > 0`,
|
||||
// they can be requested separately by using the `download_url`.
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: query
|
||||
// description: "The name of the commit/branch/tag. Default to the repository’s default branch."
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/GetFilesOptions"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ContentsListResponse"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
// This is actually a "read" request, but we need to accept a "files" list, then POST method seems easy to use.
|
||||
// But the permission system requires that the caller must have "write" permission to use POST method.
|
||||
// At the moment, there is no other way to get around the permission check, so there is a "GET" workaround method above.
|
||||
handleGetFileContents(ctx)
|
||||
}
|
||||
|
||||
func handleGetFileContents(ctx *context.APIContext) {
|
||||
opts, ok := web.GetForm(ctx).(*api.GetFilesOptions)
|
||||
if !ok {
|
||||
err := json.Unmarshal(util.UnsafeStringToBytes(ctx.FormString("body")), &opts)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusBadRequest, "invalid body parameter")
|
||||
return
|
||||
}
|
||||
}
|
||||
refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref"))
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
filesResponse := files_service.GetContentsListFromTreePaths(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts.Files)
|
||||
ctx.JSON(http.StatusOK, util.SliceNilAsEmpty(filesResponse))
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/optional"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
// ListForks list a repository's forks
|
||||
func ListForks(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/forks repository listForks
|
||||
// ---
|
||||
// summary: List a repository's forks
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/RepositoryList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if err := repo_model.RepositoryList(forks).LoadOwners(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if err := repo_model.RepositoryList(forks).LoadUnits(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiForks := make([]*api.Repository, len(forks))
|
||||
for i, fork := range forks {
|
||||
permission, err := access_model.GetDoerRepoPermission(ctx, fork, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
apiForks[i] = convert.ToRepo(ctx, fork, permission)
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(total)
|
||||
ctx.JSON(http.StatusOK, apiForks)
|
||||
}
|
||||
|
||||
func prepareDoerCreateRepoInOrg(ctx *context.APIContext, orgName string) *organization.Organization {
|
||||
org, err := organization.GetOrgByName(ctx, orgName)
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.APIErrorNotFound()
|
||||
return nil
|
||||
} else if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !organization.HasOrgOrUserVisible(ctx, org.AsUser(), ctx.Doer) {
|
||||
ctx.APIErrorNotFound()
|
||||
return nil
|
||||
}
|
||||
|
||||
if !ctx.Doer.IsAdmin {
|
||||
canCreate, err := org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil
|
||||
}
|
||||
if !canCreate {
|
||||
ctx.APIError(http.StatusForbidden, "User is not allowed to create repositories in this organization.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return org
|
||||
}
|
||||
|
||||
// CreateFork create a fork of a repo
|
||||
func CreateFork(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/forks repository createFork
|
||||
// ---
|
||||
// summary: Fork a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo to fork
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo to fork
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateForkOption"
|
||||
// responses:
|
||||
// "202":
|
||||
// "$ref": "#/responses/Repository"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "409":
|
||||
// description: The repository with the same name already exists.
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateForkOption)
|
||||
forkOwner := ctx.Doer // user/org that will own the fork
|
||||
if form.Organization != nil {
|
||||
org := prepareDoerCreateRepoInOrg(ctx, *form.Organization)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
forkOwner = org.AsUser()
|
||||
}
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
name := optional.FromPtr(form.Name).ValueOrDefault(repo.Name)
|
||||
fork, err := repo_service.ForkRepository(ctx, ctx.Doer, forkOwner, repo_service.ForkRepoOptions{
|
||||
BaseRepo: repo,
|
||||
Name: name,
|
||||
Description: repo.Description,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
|
||||
ctx.APIError(http.StatusConflict, err)
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TODO change back to 201
|
||||
ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, fork, access_model.Permission{AccessMode: perm.AccessModeOwner}))
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/modules/git"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// ListGitHooks list all Git hooks of a repository
|
||||
func ListGitHooks(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/hooks/git repository repoListGitHooks
|
||||
// ---
|
||||
// summary: List the Git hooks in a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/GitHookList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
hooks, err := ctx.Repo.GitRepo.Hooks()
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiHooks := make([]*api.GitHook, len(hooks))
|
||||
for i := range hooks {
|
||||
apiHooks[i] = convert.ToGitHook(hooks[i])
|
||||
}
|
||||
ctx.JSON(http.StatusOK, &apiHooks)
|
||||
}
|
||||
|
||||
// GetGitHook get a repo's Git hook by id
|
||||
func GetGitHook(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/hooks/git/{id} repository repoGetGitHook
|
||||
// ---
|
||||
// summary: Get a Git hook
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the hook to get
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/GitHook"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
hookID := ctx.PathParam("id")
|
||||
hook, err := ctx.Repo.GitRepo.GetHook(hookID)
|
||||
if err != nil {
|
||||
if errors.Is(err, git.ErrNotValidHook) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToGitHook(hook))
|
||||
}
|
||||
|
||||
// EditGitHook modify a Git hook of a repository
|
||||
func EditGitHook(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/hooks/git/{id} repository repoEditGitHook
|
||||
// ---
|
||||
// summary: Edit a Git hook in a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the hook to get
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditGitHookOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/GitHook"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditGitHookOption)
|
||||
hookID := ctx.PathParam("id")
|
||||
hook, err := ctx.Repo.GitRepo.GetHook(hookID)
|
||||
if err != nil {
|
||||
if errors.Is(err, git.ErrNotValidHook) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
hook.Content = form.Content
|
||||
if err = hook.Update(); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToGitHook(hook))
|
||||
}
|
||||
|
||||
// DeleteGitHook delete a Git hook of a repository
|
||||
func DeleteGitHook(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/hooks/git/{id} repository repoDeleteGitHook
|
||||
// ---
|
||||
// summary: Delete a Git hook in a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the hook to get
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
hookID := ctx.PathParam("id")
|
||||
hook, err := ctx.Repo.GitRepo.GetHook(hookID)
|
||||
if err != nil {
|
||||
if errors.Is(err, git.ErrNotValidHook) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
hook.Content = ""
|
||||
if err = hook.Update(); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// GetGitAllRefs get ref or an list all the refs of a repository
|
||||
func GetGitAllRefs(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/git/refs repository repoListAllGitRefs
|
||||
// ---
|
||||
// summary: Get specified ref or filtered repository's refs
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// # "$ref": "#/responses/Reference" TODO: swagger doesn't support different output formats by ref
|
||||
// "$ref": "#/responses/ReferenceList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
getGitRefsInternal(ctx, "")
|
||||
}
|
||||
|
||||
// GetGitRefs get ref or an filteresd list of refs of a repository
|
||||
func GetGitRefs(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/git/refs/{ref} repository repoListGitRefs
|
||||
// ---
|
||||
// summary: Get specified ref or filtered repository's refs
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: path
|
||||
// description: part or full name of the ref
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// # "$ref": "#/responses/Reference" TODO: swagger doesn't support different output formats by ref
|
||||
// "$ref": "#/responses/ReferenceList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
getGitRefsInternal(ctx, ctx.PathParam("*"))
|
||||
}
|
||||
|
||||
func getGitRefsInternal(ctx *context.APIContext, filter string) {
|
||||
refs, lastMethodName, err := utils.GetGitRefs(ctx, filter)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(fmt.Errorf("%s: %w", lastMethodName, err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(refs) == 0 {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
apiRefs := make([]*api.Reference, len(refs))
|
||||
for i := range refs {
|
||||
apiRefs[i] = &api.Reference{
|
||||
Ref: refs[i].Name,
|
||||
URL: ctx.Repo.Repository.APIURL() + "/git/" + util.PathEscapeSegments(refs[i].Name),
|
||||
Object: &api.GitObject{
|
||||
SHA: refs[i].Object.String(),
|
||||
Type: refs[i].Type,
|
||||
URL: ctx.Repo.Repository.APIURL() + "/git/" + url.PathEscape(refs[i].Type) + "s/" + url.PathEscape(refs[i].Object.String()),
|
||||
},
|
||||
}
|
||||
}
|
||||
// If single reference is found and it matches filter exactly return it as object
|
||||
if len(apiRefs) == 1 && apiRefs[0].Ref == filter {
|
||||
ctx.JSON(http.StatusOK, &apiRefs[0])
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, &apiRefs)
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
"gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
webhook_service "gitea.dev/services/webhook"
|
||||
)
|
||||
|
||||
// ListHooks list all hooks of a repository
|
||||
func ListHooks(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/hooks repository repoListHooks
|
||||
// ---
|
||||
// summary: List the hooks in a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/HookList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
opts := &webhook.ListWebhookOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
}
|
||||
|
||||
hooks, count, err := db.FindAndCount[webhook.Webhook](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiHooks := make([]*api.Hook, len(hooks))
|
||||
for i := range hooks {
|
||||
apiHooks[i], err = webhook_service.ToHook(ctx.Repo.RepoLink, hooks[i])
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, &apiHooks)
|
||||
}
|
||||
|
||||
// GetHook get a repo's hook by id
|
||||
func GetHook(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/hooks/{id} repository repoGetHook
|
||||
// ---
|
||||
// summary: Get a hook
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the hook to get
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Hook"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
repo := ctx.Repo
|
||||
hookID := ctx.PathParamInt64("id")
|
||||
hook, err := utils.GetRepoHook(ctx, repo.Repository.ID, hookID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
apiHook, err := webhook_service.ToHook(repo.RepoLink, hook)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, apiHook)
|
||||
}
|
||||
|
||||
// TestHook tests a hook
|
||||
func TestHook(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/hooks/{id}/tests repository repoTestHook
|
||||
// ---
|
||||
// summary: Test a push webhook
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the hook to test
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: query
|
||||
// description: "The name of the commit/branch/tag, indicates which commit will be loaded to the webhook payload."
|
||||
// type: string
|
||||
// required: false
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if ctx.Repo.Commit == nil {
|
||||
// if repo does not have any commits, then don't send a webhook
|
||||
ctx.Status(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
ref := git.BranchPrefix + ctx.Repo.Repository.DefaultBranch
|
||||
if r := ctx.FormTrim("ref"); r != "" {
|
||||
ref = r
|
||||
}
|
||||
|
||||
hookID := ctx.PathParamInt64("id")
|
||||
hook, err := utils.GetRepoHook(ctx, ctx.Repo.Repository.ID, hookID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
commit := convert.ToPayloadCommit(ctx, ctx.Repo.Repository, ctx.Repo.Commit)
|
||||
|
||||
commitID := ctx.Repo.Commit.ID.String()
|
||||
if err := webhook_service.PrepareWebhook(ctx, hook, webhook_module.HookEventPush, &api.PushPayload{
|
||||
Ref: ref,
|
||||
Before: commitID,
|
||||
After: commitID,
|
||||
CompareURL: setting.AppURL + ctx.Repo.Repository.ComposeCompareURL(commitID, commitID),
|
||||
Commits: []*api.PayloadCommit{commit},
|
||||
TotalCommits: 1,
|
||||
HeadCommit: commit,
|
||||
Repo: convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeNone}),
|
||||
Pusher: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone),
|
||||
Sender: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone),
|
||||
}); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// CreateHook create a hook for a repository
|
||||
func CreateHook(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/hooks repository repoCreateHook
|
||||
// ---
|
||||
// summary: Create a hook
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateHookOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Hook"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
utils.AddRepoHook(ctx, web.GetForm(ctx).(*api.CreateHookOption))
|
||||
}
|
||||
|
||||
// EditHook modify a hook of a repository
|
||||
func EditHook(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/hooks/{id} repository repoEditHook
|
||||
// ---
|
||||
// summary: Edit a hook in a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: index of the hook
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditHookOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Hook"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
form := web.GetForm(ctx).(*api.EditHookOption)
|
||||
hookID := ctx.PathParamInt64("id")
|
||||
utils.EditRepoHook(ctx, form, hookID)
|
||||
}
|
||||
|
||||
// DeleteHook delete a hook of a repository
|
||||
func DeleteHook(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/hooks/{id} repository repoDeleteHook
|
||||
// ---
|
||||
// summary: Delete a hook in a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the hook to delete
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
if err := webhook.DeleteWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")); err != nil {
|
||||
if webhook.IsErrWebhookNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/models/webhook"
|
||||
"gitea.dev/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTestHook(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
hook := &webhook.Webhook{
|
||||
RepoID: 1,
|
||||
URL: "https://www.example.com/test_hook",
|
||||
ContentType: webhook.ContentTypeJSON,
|
||||
Events: `{"push_only":true}`,
|
||||
IsActive: true,
|
||||
}
|
||||
assert.NoError(t, db.Insert(t.Context(), hook))
|
||||
|
||||
ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/wiki/_pages")
|
||||
ctx.SetPathParam("id", strconv.FormatInt(hook.ID, 10))
|
||||
contexttest.LoadRepo(t, ctx, 1)
|
||||
contexttest.LoadRepoCommit(t, ctx)
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
TestHook(ctx)
|
||||
assert.Equal(t, http.StatusNoContent, ctx.Resp.WrittenStatus())
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &webhook.HookTask{
|
||||
HookID: hook.ID,
|
||||
}, unittest.Cond("is_delivered=?", false))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,399 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
attachment_service "gitea.dev/services/attachment"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/context/upload"
|
||||
"gitea.dev/services/convert"
|
||||
issue_service "gitea.dev/services/issue"
|
||||
)
|
||||
|
||||
// GetIssueAttachment gets a single attachment of the issue
|
||||
func GetIssueAttachment(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueGetIssueAttachment
|
||||
// ---
|
||||
// summary: Get an issue attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: attachment_id
|
||||
// in: path
|
||||
// description: id of the attachment to get
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
issue := getIssueFromContext(ctx)
|
||||
if issue == nil {
|
||||
return
|
||||
}
|
||||
|
||||
attach := getIssueAttachmentSafeRead(ctx, issue)
|
||||
if attach == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
|
||||
}
|
||||
|
||||
// ListIssueAttachments lists all attachments of the issue
|
||||
func ListIssueAttachments(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assets issue issueListIssueAttachments
|
||||
// ---
|
||||
// summary: List issue's attachments
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/AttachmentList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
issue := getIssueFromContext(ctx)
|
||||
if issue == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := issue.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue).Attachments)
|
||||
}
|
||||
|
||||
// CreateIssueAttachment creates an attachment and saves the given file
|
||||
func CreateIssueAttachment(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/assets issue issueCreateIssueAttachment
|
||||
// ---
|
||||
// summary: Create an issue attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: name
|
||||
// in: query
|
||||
// description: name of the attachment
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: attachment
|
||||
// in: formData
|
||||
// description: attachment to upload
|
||||
// type: file
|
||||
// required: true
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
// "413":
|
||||
// "$ref": "#/responses/error"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
issue := getIssueFromContext(ctx)
|
||||
if issue == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !canUserWriteIssueAttachment(ctx, issue) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get uploaded file from request
|
||||
file, header, err := ctx.Req.FormFile("attachment")
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
filename := header.Filename
|
||||
if query := ctx.FormString("name"); query != "" {
|
||||
filename = query
|
||||
}
|
||||
|
||||
uploaderFile := attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
|
||||
attachment, err := attachment_service.UploadAttachmentForIssue(ctx, uploaderFile, &repo_model.Attachment{
|
||||
Name: filename,
|
||||
UploaderID: ctx.Doer.ID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IssueID: issue.ID,
|
||||
})
|
||||
if err != nil {
|
||||
if upload.IsErrFileTypeForbidden(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
} else if errors.Is(err, util.ErrContentTooLarge) {
|
||||
ctx.APIError(http.StatusRequestEntityTooLarge, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
issue.Attachments = append(issue.Attachments, attachment)
|
||||
|
||||
if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, issue.Content, issue.ContentVersion); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attachment))
|
||||
}
|
||||
|
||||
// EditIssueAttachment updates the given attachment
|
||||
func EditIssueAttachment(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueEditIssueAttachment
|
||||
// ---
|
||||
// summary: Edit an issue attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// consumes:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: attachment_id
|
||||
// in: path
|
||||
// description: id of the attachment to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditAttachmentOptions"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
attachment := getIssueAttachmentSafeWrite(ctx)
|
||||
if attachment == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// do changes to attachment. only meaningful change is name.
|
||||
form := web.GetForm(ctx).(*api.EditAttachmentOptions)
|
||||
if form.Name != "" {
|
||||
attachment.Name = form.Name
|
||||
}
|
||||
|
||||
if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attachment); err != nil {
|
||||
if upload.IsErrFileTypeForbidden(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attachment))
|
||||
}
|
||||
|
||||
// DeleteIssueAttachment delete a given attachment
|
||||
func DeleteIssueAttachment(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/assets/{attachment_id} issue issueDeleteIssueAttachment
|
||||
// ---
|
||||
// summary: Delete an issue attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: attachment_id
|
||||
// in: path
|
||||
// description: id of the attachment to delete
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
attachment := getIssueAttachmentSafeWrite(ctx)
|
||||
if attachment == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_model.DeleteAttachment(ctx, attachment, true); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func getIssueFromContext(ctx *context.APIContext) *issues_model.Issue {
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
issue.Repo = ctx.Repo.Repository
|
||||
|
||||
return issue
|
||||
}
|
||||
|
||||
func getIssueAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment {
|
||||
issue := getIssueFromContext(ctx)
|
||||
if issue == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !canUserWriteIssueAttachment(ctx, issue) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return getIssueAttachmentSafeRead(ctx, issue)
|
||||
}
|
||||
|
||||
func getIssueAttachmentSafeRead(ctx *context.APIContext, issue *issues_model.Issue) *repo_model.Attachment {
|
||||
attachment, err := repo_model.GetAttachmentByID(ctx, ctx.PathParamInt64("attachment_id"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError(err)
|
||||
return nil
|
||||
}
|
||||
if !attachmentBelongsToRepoOrIssue(ctx, attachment, issue) {
|
||||
return nil
|
||||
}
|
||||
return attachment
|
||||
}
|
||||
|
||||
func canUserWriteIssueAttachment(ctx *context.APIContext, issue *issues_model.Issue) bool {
|
||||
canEditIssue := ctx.IsSigned && (ctx.Doer.ID == issue.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin() || ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull))
|
||||
if !canEditIssue {
|
||||
ctx.APIError(http.StatusForbidden, "user should have permission to write issue")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func attachmentBelongsToRepoOrIssue(ctx *context.APIContext, attachment *repo_model.Attachment, issue *issues_model.Issue) bool {
|
||||
if attachment.RepoID != ctx.Repo.Repository.ID {
|
||||
log.Debug("Requested attachment[%d] does not belong to repo[%-v].", attachment.ID, ctx.Repo.Repository)
|
||||
ctx.APIErrorNotFound("no such attachment in repo")
|
||||
return false
|
||||
}
|
||||
if attachment.IssueID == 0 {
|
||||
log.Debug("Requested attachment[%d] is not in an issue.", attachment.ID)
|
||||
ctx.APIErrorNotFound("no such attachment in issue")
|
||||
return false
|
||||
} else if issue != nil && attachment.IssueID != issue.ID {
|
||||
log.Debug("Requested attachment[%d] does not belong to issue[%d, #%d].", attachment.ID, issue.ID, issue.Index)
|
||||
ctx.APIErrorNotFound("no such attachment in issue")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,706 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
stdCtx "context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/optional"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
issue_service "gitea.dev/services/issue"
|
||||
)
|
||||
|
||||
// ListIssueComments list all the comments of an issue
|
||||
func ListIssueComments(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/comments issue issueGetComments
|
||||
// ---
|
||||
// summary: List all comments on an issue
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: since
|
||||
// in: query
|
||||
// description: if provided, only comments updated since the specified time are returned.
|
||||
// type: string
|
||||
// format: date-time
|
||||
// - name: before
|
||||
// in: query
|
||||
// description: if provided, only comments updated before the provided time are returned.
|
||||
// type: string
|
||||
// format: date-time
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/CommentList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
before, since, err := context.GetQueryBeforeSince(ctx.Base)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
issue.Repo = ctx.Repo.Repository
|
||||
|
||||
opts := &issues_model.FindCommentsOptions{
|
||||
IssueID: issue.ID,
|
||||
Since: since,
|
||||
Before: before,
|
||||
Type: issues_model.CommentTypeComment,
|
||||
}
|
||||
|
||||
comments, err := issues_model.FindComments(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
totalCount, err := issues_model.CountComments(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := comments.LoadPosters(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := comments.LoadAttachments(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiComments := make([]*api.Comment, len(comments))
|
||||
for i, comment := range comments {
|
||||
comment.Issue = issue
|
||||
apiComments[i] = convert.ToAPIComment(ctx, ctx.Repo.Repository, comments[i])
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(totalCount)
|
||||
ctx.JSON(http.StatusOK, &apiComments)
|
||||
}
|
||||
|
||||
// ListIssueCommentsAndTimeline list all the comments and events of an issue
|
||||
func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/timeline issue issueGetCommentsAndTimeline
|
||||
// ---
|
||||
// summary: List all comments and events on an issue
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: since
|
||||
// in: query
|
||||
// description: if provided, only comments updated since the specified time are returned.
|
||||
// type: string
|
||||
// format: date-time
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// - name: before
|
||||
// in: query
|
||||
// description: if provided, only comments updated before the provided time are returned.
|
||||
// type: string
|
||||
// format: date-time
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TimelineList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
before, since, err := context.GetQueryBeforeSince(ctx.Base)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
issue.Repo = ctx.Repo.Repository
|
||||
|
||||
opts := &issues_model.FindCommentsOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
IssueID: issue.ID,
|
||||
Since: since,
|
||||
Before: before,
|
||||
Type: issues_model.CommentTypeUndefined,
|
||||
}
|
||||
|
||||
comments, err := issues_model.FindComments(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := comments.LoadPosters(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
var apiComments []*api.TimelineComment
|
||||
for _, comment := range comments {
|
||||
if comment.Type != issues_model.CommentTypeCode && isXRefCommentAccessible(ctx, ctx.Doer, comment, issue.RepoID) {
|
||||
comment.Issue = issue
|
||||
apiComments = append(apiComments, convert.ToTimelineComment(ctx, issue.Repo, comment, ctx.Doer))
|
||||
}
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(int64(len(apiComments)))
|
||||
ctx.JSON(http.StatusOK, &apiComments)
|
||||
}
|
||||
|
||||
func isXRefCommentAccessible(ctx stdCtx.Context, user *user_model.User, c *issues_model.Comment, issueRepoID int64) bool {
|
||||
// Remove comments that the user has no permissions to see
|
||||
if issues_model.CommentTypeIsRef(c.Type) && c.RefRepoID != issueRepoID && c.RefRepoID != 0 {
|
||||
var err error
|
||||
// Set RefRepo for description in template
|
||||
c.RefRepo, err = repo_model.GetRepositoryByID(ctx, c.RefRepoID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
perm, err := access_model.GetDoerRepoPermission(ctx, c.RefRepo, user)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !perm.CanReadIssuesOrPulls(c.RefIsPull) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ListRepoIssueComments returns all issue-comments for a repo
|
||||
func ListRepoIssueComments(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/comments issue issueGetRepoComments
|
||||
// ---
|
||||
// summary: List all comments in a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: since
|
||||
// in: query
|
||||
// description: if provided, only comments updated since the provided time are returned.
|
||||
// type: string
|
||||
// format: date-time
|
||||
// - name: before
|
||||
// in: query
|
||||
// description: if provided, only comments updated before the provided time are returned.
|
||||
// type: string
|
||||
// format: date-time
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/CommentList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
before, since, err := context.GetQueryBeforeSince(ctx.Base)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
var isPull optional.Option[bool]
|
||||
canReadIssue := ctx.Repo.Permission.CanRead(unit.TypeIssues)
|
||||
canReadPull := ctx.Repo.Permission.CanRead(unit.TypePullRequests)
|
||||
if canReadIssue && canReadPull {
|
||||
isPull = optional.None[bool]()
|
||||
} else if canReadIssue {
|
||||
isPull = optional.Some(false)
|
||||
} else if canReadPull {
|
||||
isPull = optional.Some(true)
|
||||
} else {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
opts := &issues_model.FindCommentsOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Type: issues_model.CommentTypeComment,
|
||||
Since: since,
|
||||
Before: before,
|
||||
IsPull: isPull,
|
||||
}
|
||||
|
||||
comments, err := issues_model.FindComments(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
totalCount, err := issues_model.CountComments(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = comments.LoadPosters(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiComments := make([]*api.Comment, len(comments))
|
||||
if err := comments.LoadIssues(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if err := comments.LoadAttachments(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if _, err := comments.Issues().LoadRepositories(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
for i := range comments {
|
||||
apiComments[i] = convert.ToAPIComment(ctx, ctx.Repo.Repository, comments[i])
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(totalCount)
|
||||
ctx.JSON(http.StatusOK, &apiComments)
|
||||
}
|
||||
|
||||
// CreateIssueComment create a comment for an issue
|
||||
func CreateIssueComment(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/comments issue issueCreateComment
|
||||
// ---
|
||||
// summary: Add a comment to an issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateIssueCommentOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Comment"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateIssueCommentOption)
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
|
||||
ctx.APIError(http.StatusForbidden, errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked")))
|
||||
return
|
||||
}
|
||||
|
||||
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment))
|
||||
}
|
||||
|
||||
// GetIssueComment Get a comment by ID
|
||||
func GetIssueComment(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id} issue issueGetComment
|
||||
// ---
|
||||
// summary: Get a comment
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Comment"
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrCommentNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Type != issues_model.CommentTypeComment {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
if err := comment.LoadPoster(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment))
|
||||
}
|
||||
|
||||
// EditIssueComment modify a comment of an issue
|
||||
func EditIssueComment(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/issues/comments/{id} issue issueEditComment
|
||||
// ---
|
||||
// summary: Edit a comment
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditIssueCommentOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Comment"
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditIssueCommentOption)
|
||||
editIssueComment(ctx, *form)
|
||||
}
|
||||
|
||||
// EditIssueCommentDeprecated modify a comment of an issue
|
||||
func EditIssueCommentDeprecated(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/issues/{index}/comments/{id} issue issueEditCommentDeprecated
|
||||
// ---
|
||||
// summary: Edit a comment
|
||||
// deprecated: true
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: this parameter is ignored
|
||||
// type: integer
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditIssueCommentOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Comment"
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditIssueCommentOption)
|
||||
editIssueComment(ctx, *form)
|
||||
}
|
||||
|
||||
func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) {
|
||||
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrCommentNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !comment.Type.HasContentSupport() {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
if form.Body != comment.Content {
|
||||
oldContent := comment.Content
|
||||
comment.Content = form.Body
|
||||
if err := issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, oldContent); err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment))
|
||||
}
|
||||
|
||||
// DeleteIssueComment delete a comment from an issue
|
||||
func DeleteIssueComment(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id} issue issueDeleteComment
|
||||
// ---
|
||||
// summary: Delete a comment
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of comment to delete
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
deleteIssueComment(ctx)
|
||||
}
|
||||
|
||||
// DeleteIssueCommentDeprecated delete a comment from an issue
|
||||
func DeleteIssueCommentDeprecated(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/comments/{id} issue issueDeleteCommentDeprecated
|
||||
// ---
|
||||
// summary: Delete a comment
|
||||
// deprecated: true
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: this parameter is ignored
|
||||
// type: integer
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of comment to delete
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
deleteIssueComment(ctx)
|
||||
}
|
||||
|
||||
func deleteIssueComment(ctx *context.APIContext) {
|
||||
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrCommentNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
} else if !comment.Type.HasContentSupport() {
|
||||
ctx.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
attachment_service "gitea.dev/services/attachment"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/context/upload"
|
||||
"gitea.dev/services/convert"
|
||||
issue_service "gitea.dev/services/issue"
|
||||
)
|
||||
|
||||
// GetIssueCommentAttachment gets a single attachment of the comment
|
||||
func GetIssueCommentAttachment(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueGetIssueCommentAttachment
|
||||
// ---
|
||||
// summary: Get a comment attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: attachment_id
|
||||
// in: path
|
||||
// description: id of the attachment to get
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
comment := getIssueCommentSafe(ctx)
|
||||
if comment == nil {
|
||||
return
|
||||
}
|
||||
attachment := getIssueCommentAttachmentSafeRead(ctx, comment)
|
||||
if attachment == nil {
|
||||
return
|
||||
}
|
||||
if attachment.CommentID != comment.ID {
|
||||
log.Debug("User requested attachment[%d] is not in comment[%d].", attachment.ID, comment.ID)
|
||||
ctx.APIErrorNotFound("attachment not in comment")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIAttachment(ctx.Repo.Repository, attachment))
|
||||
}
|
||||
|
||||
// ListIssueCommentAttachments lists all attachments of the comment
|
||||
func ListIssueCommentAttachments(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/assets issue issueListIssueCommentAttachments
|
||||
// ---
|
||||
// summary: List comment's attachments
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/AttachmentList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
comment := getIssueCommentSafe(ctx)
|
||||
if comment == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := comment.LoadAttachments(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIAttachments(ctx.Repo.Repository, comment.Attachments))
|
||||
}
|
||||
|
||||
// CreateIssueCommentAttachment creates an attachment and saves the given file
|
||||
func CreateIssueCommentAttachment(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/comments/{id}/assets issue issueCreateIssueCommentAttachment
|
||||
// ---
|
||||
// summary: Create a comment attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: name
|
||||
// in: query
|
||||
// description: name of the attachment
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: attachment
|
||||
// in: formData
|
||||
// description: attachment to upload
|
||||
// type: file
|
||||
// required: true
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
// "413":
|
||||
// "$ref": "#/responses/error"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
// Check if comment exists and load comment
|
||||
comment := getIssueCommentSafe(ctx)
|
||||
if comment == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !canUserWriteIssueCommentAttachment(ctx, comment) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get uploaded file from request
|
||||
file, header, err := ctx.Req.FormFile("attachment")
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
filename := header.Filename
|
||||
if query := ctx.FormString("name"); query != "" {
|
||||
filename = query
|
||||
}
|
||||
|
||||
uploaderFile := attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
|
||||
attachment, err := attachment_service.UploadAttachmentForIssue(ctx, uploaderFile, &repo_model.Attachment{
|
||||
Name: filename,
|
||||
UploaderID: ctx.Doer.ID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IssueID: comment.IssueID,
|
||||
CommentID: comment.ID,
|
||||
})
|
||||
if err != nil {
|
||||
if upload.IsErrFileTypeForbidden(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
} else if errors.Is(err, util.ErrContentTooLarge) {
|
||||
ctx.APIError(http.StatusRequestEntityTooLarge, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := comment.LoadAttachments(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, comment.Content); err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attachment))
|
||||
}
|
||||
|
||||
// EditIssueCommentAttachment updates the given attachment
|
||||
func EditIssueCommentAttachment(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueEditIssueCommentAttachment
|
||||
// ---
|
||||
// summary: Edit a comment attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// consumes:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: attachment_id
|
||||
// in: path
|
||||
// description: id of the attachment to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditAttachmentOptions"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
attach := getIssueCommentAttachmentSafeWrite(ctx)
|
||||
if attach == nil {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditAttachmentOptions)
|
||||
if form.Name != "" {
|
||||
attach.Name = form.Name
|
||||
}
|
||||
|
||||
if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attach); err != nil {
|
||||
if upload.IsErrFileTypeForbidden(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
|
||||
}
|
||||
|
||||
// DeleteIssueCommentAttachment delete a given attachment
|
||||
func DeleteIssueCommentAttachment(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id} issue issueDeleteIssueCommentAttachment
|
||||
// ---
|
||||
// summary: Delete a comment attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: attachment_id
|
||||
// in: path
|
||||
// description: id of the attachment to delete
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
attach := getIssueCommentAttachmentSafeWrite(ctx)
|
||||
if attach == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_model.DeleteAttachment(ctx, attach, true); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
|
||||
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError(err)
|
||||
return nil
|
||||
}
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil
|
||||
}
|
||||
if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.APIError(http.StatusNotFound, "no matching issue comment found")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
return nil
|
||||
}
|
||||
|
||||
comment.Issue.Repo = ctx.Repo.Repository
|
||||
|
||||
return comment
|
||||
}
|
||||
|
||||
func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment {
|
||||
comment := getIssueCommentSafe(ctx)
|
||||
if comment == nil {
|
||||
return nil
|
||||
}
|
||||
if !canUserWriteIssueCommentAttachment(ctx, comment) {
|
||||
return nil
|
||||
}
|
||||
return getIssueCommentAttachmentSafeRead(ctx, comment)
|
||||
}
|
||||
|
||||
func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool {
|
||||
canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)
|
||||
if !canEditComment {
|
||||
ctx.APIError(http.StatusForbidden, "user should have permission to edit comment")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_model.Comment) *repo_model.Attachment {
|
||||
attachment, err := repo_model.GetAttachmentByID(ctx, ctx.PathParamInt64("attachment_id"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError(err)
|
||||
return nil
|
||||
}
|
||||
if !attachmentBelongsToRepoOrComment(ctx, attachment, comment) {
|
||||
return nil
|
||||
}
|
||||
return attachment
|
||||
}
|
||||
|
||||
func attachmentBelongsToRepoOrComment(ctx *context.APIContext, attachment *repo_model.Attachment, comment *issues_model.Comment) bool {
|
||||
if attachment.RepoID != ctx.Repo.Repository.ID {
|
||||
log.Debug("Requested attachment[%d] does not belong to repo[%-v].", attachment.ID, ctx.Repo.Repository)
|
||||
ctx.APIErrorNotFound("no such attachment in repo")
|
||||
return false
|
||||
}
|
||||
if attachment.IssueID == 0 || attachment.CommentID == 0 {
|
||||
log.Debug("Requested attachment[%d] is not in a comment.", attachment.ID)
|
||||
ctx.APIErrorNotFound("no such attachment in comment")
|
||||
return false
|
||||
}
|
||||
if comment != nil && attachment.CommentID != comment.ID {
|
||||
log.Debug("Requested attachment[%d] does not belong to comment[%d].", attachment.ID, comment.ID)
|
||||
ctx.APIErrorNotFound("no such attachment in comment")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,599 @@
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// GetIssueDependencies list an issue's dependencies
|
||||
func GetIssueDependencies(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/dependencies issue issueListIssueDependencies
|
||||
// ---
|
||||
// summary: List an issue's dependencies, i.e all issues that block this issue.
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/IssueList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
// If this issue's repository does not enable dependencies then there can be no dependencies by default
|
||||
if !ctx.Repo.Repository.IsDependenciesEnabled(ctx) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound("IsErrIssueNotExist", err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 1. We must be able to read this issue
|
||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
|
||||
|
||||
blockerIssues := make([]*issues_model.Issue, 0, min(listOptions.PageSize, setting.API.MaxResponseItems))
|
||||
|
||||
// 2. Get the issues this issue depends on, i.e. the `<#b>`: `<issue> <- <#b>`
|
||||
blockersInfo, total, err := issue.BlockedByDependencies(ctx, listOptions)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
repoPerms := make(map[int64]access_model.Permission)
|
||||
repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
|
||||
for _, blocker := range blockersInfo {
|
||||
// Get the permissions for this repository
|
||||
// If the repo ID exists in the map, return the exist permissions
|
||||
// else get the permission and add it to the map
|
||||
var perm access_model.Permission
|
||||
existPerm, ok := repoPerms[blocker.RepoID]
|
||||
if ok {
|
||||
perm = existPerm
|
||||
} else {
|
||||
var err error
|
||||
perm, err = access_model.GetDoerRepoPermission(ctx, &blocker.Repository, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
repoPerms[blocker.RepoID] = perm
|
||||
}
|
||||
|
||||
// check permission
|
||||
if !perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
|
||||
if !canWrite {
|
||||
hiddenBlocker := &issues_model.DependencyInfo{
|
||||
Issue: issues_model.Issue{
|
||||
Title: "HIDDEN",
|
||||
},
|
||||
}
|
||||
blocker = hiddenBlocker
|
||||
} else {
|
||||
confidentialBlocker := &issues_model.DependencyInfo{
|
||||
Issue: issues_model.Issue{
|
||||
RepoID: blocker.Issue.RepoID,
|
||||
Index: blocker.Index,
|
||||
Title: blocker.Title,
|
||||
IsClosed: blocker.IsClosed,
|
||||
IsPull: blocker.IsPull,
|
||||
},
|
||||
Repository: repo_model.Repository{
|
||||
ID: blocker.Issue.Repo.ID,
|
||||
Name: blocker.Issue.Repo.Name,
|
||||
OwnerName: blocker.Issue.Repo.OwnerName,
|
||||
},
|
||||
}
|
||||
confidentialBlocker.Issue.Repo = &confidentialBlocker.Repository
|
||||
blocker = confidentialBlocker
|
||||
}
|
||||
}
|
||||
blockerIssues = append(blockerIssues, &blocker.Issue)
|
||||
}
|
||||
ctx.SetLinkHeader(total, listOptions.PageSize)
|
||||
ctx.SetTotalCountHeader(total)
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, blockerIssues))
|
||||
}
|
||||
|
||||
// CreateIssueDependency create a new issue dependencies
|
||||
func CreateIssueDependency(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/dependencies issue issueCreateIssueDependencies
|
||||
// ---
|
||||
// summary: Make the issue in the url depend on the issue in the form.
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/IssueMeta"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Issue"
|
||||
// "404":
|
||||
// description: the issue does not exist
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
// We want to make <:index> depend on <Form>, i.e. <:index> is the target
|
||||
target := getParamsIssue(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
// and <Form> represents the dependency
|
||||
form := web.GetForm(ctx).(*api.IssueMeta)
|
||||
dependency := getFormIssue(ctx, form)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
dependencyPerm := getPermissionForRepo(ctx, dependency.Repo)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
createIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target))
|
||||
}
|
||||
|
||||
// RemoveIssueDependency remove an issue dependency
|
||||
func RemoveIssueDependency(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/dependencies issue issueRemoveIssueDependencies
|
||||
// ---
|
||||
// summary: Remove an issue dependency
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/IssueMeta"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Issue"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
// We want to make <:index> depend on <Form>, i.e. <:index> is the target
|
||||
target := getParamsIssue(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
// and <Form> represents the dependency
|
||||
form := web.GetForm(ctx).(*api.IssueMeta)
|
||||
dependency := getFormIssue(ctx, form)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
dependencyPerm := getPermissionForRepo(ctx, dependency.Repo)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
removeIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target))
|
||||
}
|
||||
|
||||
// GetIssueBlocks list issues that are blocked by this issue
|
||||
func GetIssueBlocks(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/blocks issue issueListBlocks
|
||||
// ---
|
||||
// summary: List issues that are blocked by this issue
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/IssueList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
// We need to list the issues that DEPEND on this issue not the other way round
|
||||
// Therefore whether dependencies are enabled or not in this repository is potentially irrelevant.
|
||||
|
||||
issue := getParamsIssue(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
limit := ctx.FormInt("limit")
|
||||
if limit <= 1 {
|
||||
limit = setting.API.DefaultPagingNum
|
||||
}
|
||||
|
||||
skip := (page - 1) * limit
|
||||
maxNum := page * limit
|
||||
|
||||
deps, err := issue.BlockingDependencies(ctx)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
var issues []*issues_model.Issue
|
||||
|
||||
repoPerms := make(map[int64]access_model.Permission)
|
||||
repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
|
||||
|
||||
for i, depMeta := range deps {
|
||||
if i < skip || i >= maxNum {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the permissions for this repository
|
||||
// If the repo ID exists in the map, return the exist permissions
|
||||
// else get the permission and add it to the map
|
||||
var perm access_model.Permission
|
||||
existPerm, ok := repoPerms[depMeta.RepoID]
|
||||
if ok {
|
||||
perm = existPerm
|
||||
} else {
|
||||
var err error
|
||||
perm, err = access_model.GetDoerRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
repoPerms[depMeta.RepoID] = perm
|
||||
}
|
||||
|
||||
if !perm.CanReadIssuesOrPulls(depMeta.Issue.IsPull) {
|
||||
continue
|
||||
}
|
||||
|
||||
depMeta.Issue.Repo = &depMeta.Repository
|
||||
issues = append(issues, &depMeta.Issue)
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
|
||||
}
|
||||
|
||||
// CreateIssueBlocking block the issue given in the body by the issue in path
|
||||
func CreateIssueBlocking(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/blocks issue issueCreateIssueBlocking
|
||||
// ---
|
||||
// summary: Block the issue given in the body by the issue in path
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/IssueMeta"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Issue"
|
||||
// "404":
|
||||
// description: the issue does not exist
|
||||
|
||||
dependency := getParamsIssue(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.IssueMeta)
|
||||
target := getFormIssue(ctx, form)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
targetPerm := getPermissionForRepo(ctx, target.Repo)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
createIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
|
||||
}
|
||||
|
||||
// RemoveIssueBlocking unblock the issue given in the body by the issue in path
|
||||
func RemoveIssueBlocking(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/blocks issue issueRemoveIssueBlocking
|
||||
// ---
|
||||
// summary: Unblock the issue given in the body by the issue in path
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/IssueMeta"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Issue"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
dependency := getParamsIssue(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.IssueMeta)
|
||||
target := getFormIssue(ctx, form)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
targetPerm := getPermissionForRepo(ctx, target.Repo)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
removeIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
|
||||
}
|
||||
|
||||
func getParamsIssue(ctx *context.APIContext) *issues_model.Issue {
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound("IsErrIssueNotExist", err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
issue.Repo = ctx.Repo.Repository
|
||||
return issue
|
||||
}
|
||||
|
||||
func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Issue {
|
||||
var repo *repo_model.Repository
|
||||
if form.Owner != ctx.Repo.Repository.OwnerName || form.Name != ctx.Repo.Repository.Name {
|
||||
if !setting.Service.AllowCrossRepositoryDependencies {
|
||||
ctx.JSON(http.StatusBadRequest, "CrossRepositoryDependencies not enabled")
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, form.Owner, form.Name)
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoNotExist(err) {
|
||||
ctx.APIErrorNotFound("IsErrRepoNotExist", err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
repo = ctx.Repo.Repository
|
||||
}
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, repo.ID, form.Index)
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound("IsErrIssueNotExist", err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
issue.Repo = repo
|
||||
return issue
|
||||
}
|
||||
|
||||
func getPermissionForRepo(ctx *context.APIContext, repo *repo_model.Repository) *access_model.Permission {
|
||||
if repo.ID == ctx.Repo.Repository.ID {
|
||||
return &ctx.Repo.Permission
|
||||
}
|
||||
|
||||
perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &perm
|
||||
}
|
||||
|
||||
func createIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) {
|
||||
if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) {
|
||||
// The target's repository doesn't have dependencies enabled
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) {
|
||||
// We can't write to the target
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) {
|
||||
// We can't read the dependency
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
err := issues_model.CreateIssueDependency(ctx, ctx.Doer, target, dependency)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func removeIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) {
|
||||
if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) {
|
||||
// The target's repository doesn't have dependencies enabled
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) {
|
||||
// We can't write to the target
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) {
|
||||
// We can't read the dependency
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
err := issues_model.RemoveIssueDependency(ctx, ctx.Doer, target, dependency, issues_model.DependencyTypeBlockedBy)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
issue_service "gitea.dev/services/issue"
|
||||
)
|
||||
|
||||
// ListIssueLabels list all the labels of an issue
|
||||
func ListIssueLabels(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/labels issue issueGetLabels
|
||||
// ---
|
||||
// summary: Get an issue's labels
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/LabelList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := issue.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToLabelList(issue.Labels, ctx.Repo.Repository, ctx.Repo.Owner))
|
||||
}
|
||||
|
||||
// AddIssueLabels add labels for an issue
|
||||
func AddIssueLabels(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/labels issue issueAddLabel
|
||||
// ---
|
||||
// summary: Add a label to an issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/IssueLabelsOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/LabelList"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
form := web.GetForm(ctx).(*api.IssueLabelsOption)
|
||||
issue, labels, err := prepareForReplaceOrAdd(ctx, *form)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = issue_service.AddLabels(ctx, issue, ctx.Doer, labels); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
labels, err = issues_model.GetLabelsByIssueID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToLabelList(labels, ctx.Repo.Repository, ctx.Repo.Owner))
|
||||
}
|
||||
|
||||
// DeleteIssueLabel delete a label for an issue
|
||||
func DeleteIssueLabel(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/labels/{id} issue issueRemoveLabel
|
||||
// ---
|
||||
// summary: Remove a label from an issue
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the label to remove
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
label, err := issues_model.GetLabelByID(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrLabelNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := issue_service.RemoveLabel(ctx, issue, ctx.Doer, label); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ReplaceIssueLabels replace labels for an issue
|
||||
func ReplaceIssueLabels(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /repos/{owner}/{repo}/issues/{index}/labels issue issueReplaceLabels
|
||||
// ---
|
||||
// summary: Replace an issue's labels
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/IssueLabelsOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/LabelList"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
form := web.GetForm(ctx).(*api.IssueLabelsOption)
|
||||
issue, labels, err := prepareForReplaceOrAdd(ctx, *form)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := issue_service.ReplaceLabels(ctx, issue, ctx.Doer, labels); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
labels, err = issues_model.GetLabelsByIssueID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToLabelList(labels, ctx.Repo.Repository, ctx.Repo.Owner))
|
||||
}
|
||||
|
||||
// ClearIssueLabels delete all the labels for an issue
|
||||
func ClearIssueLabels(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/labels issue issueClearLabels
|
||||
// ---
|
||||
// summary: Remove all labels from an issue
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if err := issue_service.ClearLabels(ctx, issue, ctx.Doer); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (*issues_model.Issue, []*issues_model.Label, error) {
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
ctx.APIError(http.StatusForbidden, "write permission is required")
|
||||
return nil, nil, errors.New("permission denied")
|
||||
}
|
||||
|
||||
var (
|
||||
labelIDs []int64
|
||||
labelNames []string
|
||||
)
|
||||
for _, label := range form.Labels {
|
||||
rv := reflect.ValueOf(label)
|
||||
switch rv.Kind() {
|
||||
case reflect.Float64:
|
||||
labelIDs = append(labelIDs, int64(rv.Float()))
|
||||
case reflect.String:
|
||||
labelNames = append(labelNames, rv.String())
|
||||
default:
|
||||
ctx.APIError(http.StatusBadRequest, "a label must be an integer or a string")
|
||||
return nil, nil, errors.New("invalid label")
|
||||
}
|
||||
}
|
||||
if len(labelIDs) > 0 && len(labelNames) > 0 {
|
||||
ctx.APIError(http.StatusBadRequest, "labels should be an array of strings or integers")
|
||||
return nil, nil, errors.New("invalid labels")
|
||||
}
|
||||
if len(labelNames) > 0 {
|
||||
repoLabelIDs, err := issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, nil, err
|
||||
}
|
||||
labelIDs = append(labelIDs, repoLabelIDs...)
|
||||
if ctx.Repo.Owner.IsOrganization() {
|
||||
orgLabelIDs, err := issues_model.GetLabelIDsInOrgByNames(ctx, ctx.Repo.Owner.ID, labelNames)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, nil, err
|
||||
}
|
||||
labelIDs = append(labelIDs, orgLabelIDs...)
|
||||
}
|
||||
}
|
||||
|
||||
labels, err := issues_model.GetLabelsByIDs(ctx, labelIDs, "id", "repo_id", "org_id", "name", "exclusive")
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return issue, labels, err
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// LockIssue lock an issue
|
||||
func LockIssue(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /repos/{owner}/{repo}/issues/{index}/lock issue issueLockIssue
|
||||
// ---
|
||||
// summary: Lock an issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/LockIssueOption"
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
reason := web.GetForm(ctx).(*api.LockIssueOption).Reason
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
ctx.APIError(http.StatusForbidden, errors.New("no permission to lock this issue"))
|
||||
return
|
||||
}
|
||||
|
||||
if !issue.IsLocked {
|
||||
opt := &issues_model.IssueLockOptions{
|
||||
Doer: ctx.ContextUser,
|
||||
Issue: issue,
|
||||
Reason: reason,
|
||||
}
|
||||
|
||||
issue.Repo = ctx.Repo.Repository
|
||||
err = issues_model.LockIssue(ctx, opt)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// UnlockIssue unlock an issue
|
||||
func UnlockIssue(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/lock issue issueUnlockIssue
|
||||
// ---
|
||||
// summary: Unlock an issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
ctx.APIError(http.StatusForbidden, errors.New("no permission to unlock this issue"))
|
||||
return
|
||||
}
|
||||
|
||||
if issue.IsLocked {
|
||||
opt := &issues_model.IssueLockOptions{
|
||||
Doer: ctx.ContextUser,
|
||||
Issue: issue,
|
||||
}
|
||||
|
||||
issue.Repo = ctx.Repo.Repository
|
||||
err = issues_model.UnlockIssue(ctx, opt)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// PinIssue pins a issue
|
||||
func PinIssue(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/pin issue pinIssue
|
||||
// ---
|
||||
// summary: Pin an Issue
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of issue to pin
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else if issues_model.IsErrIssueMaxPinReached(err) {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If we don't do this, it will crash when trying to add the pin event to the comment history
|
||||
err = issue.LoadRepo(ctx)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = issues_model.PinIssue(ctx, issue, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// UnpinIssue unpins a Issue
|
||||
func UnpinIssue(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/pin issue unpinIssue
|
||||
// ---
|
||||
// summary: Unpin an Issue
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of issue to unpin
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If we don't do this, it will crash when trying to add the unpin event to the comment history
|
||||
err = issue.LoadRepo(ctx)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
err = issues_model.UnpinIssue(ctx, issue, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// MoveIssuePin moves a pinned Issue to a new Position
|
||||
func MoveIssuePin(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/issues/{index}/pin/{position} issue moveIssuePin
|
||||
// ---
|
||||
// summary: Moves the Pin to the given Position
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: position
|
||||
// in: path
|
||||
// description: the new position
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err = issues_model.MovePin(ctx, issue, ctx.PathParamInt("position"))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ListPinnedIssues returns a list of all pinned Issues
|
||||
func ListPinnedIssues(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/pinned repository repoListPinnedIssues
|
||||
// ---
|
||||
// summary: List a repo's pinned issues
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/IssueList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
issues, err := issues_model.GetPinnedIssues(ctx, ctx.Repo.Repository.ID, false)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
|
||||
}
|
||||
|
||||
// ListPinnedPullRequests returns a list of all pinned PRs
|
||||
func ListPinnedPullRequests(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/pulls/pinned repository repoListPinnedPullRequests
|
||||
// ---
|
||||
// summary: List a repo's pinned pull requests
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/PullRequestList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
issues, err := issues_model.GetPinnedIssues(ctx, ctx.Repo.Repository.ID, true)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiPrs := make([]*api.PullRequest, len(issues))
|
||||
if err := issues.LoadPullRequests(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
for i, currentIssue := range issues {
|
||||
pr := currentIssue.PullRequest
|
||||
if err = pr.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = pr.LoadBaseRepo(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = pr.LoadHeadRepo(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiPrs[i] = convert.ToAPIPullRequest(ctx, pr, ctx.Doer)
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &apiPrs)
|
||||
}
|
||||
|
||||
// AreNewIssuePinsAllowed returns if new issues pins are allowed
|
||||
func AreNewIssuePinsAllowed(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/new_pin_allowed repository repoNewPinAllowed
|
||||
// ---
|
||||
// summary: Returns if new Issue Pins are allowed
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/RepoNewIssuePinsAllowed"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
pinsAllowed := api.NewIssuePinsAllowed{}
|
||||
var err error
|
||||
|
||||
pinsAllowed.Issues, err = issues_model.IsNewPinAllowed(ctx, ctx.Repo.Repository.ID, false)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
pinsAllowed.PullRequests, err = issues_model.IsNewPinAllowed(ctx, ctx.Repo.Repository.ID, true)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, pinsAllowed)
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
issue_service "gitea.dev/services/issue"
|
||||
)
|
||||
|
||||
// GetIssueCommentReactions list reactions of a comment from an issue
|
||||
func GetIssueCommentReactions(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issueGetCommentReactions
|
||||
// ---
|
||||
// summary: Get a list of reactions from a comment of an issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ReactionList"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrCommentNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions"))
|
||||
return
|
||||
}
|
||||
|
||||
reactions, _, err := issues_model.FindCommentReactions(ctx, comment.IssueID, comment.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
_, err = reactions.LoadUsers(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
var result []api.Reaction
|
||||
for _, r := range reactions {
|
||||
result = append(result, api.Reaction{
|
||||
User: convert.ToUser(ctx, r.User, ctx.Doer),
|
||||
Reaction: r.Type,
|
||||
Created: r.CreatedUnix.AsTime(),
|
||||
})
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// PostIssueCommentReaction add a reaction to a comment of an issue
|
||||
func PostIssueCommentReaction(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issuePostCommentReaction
|
||||
// ---
|
||||
// summary: Add a reaction to a comment of an issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: content
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditReactionOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Reaction"
|
||||
// "201":
|
||||
// "$ref": "#/responses/Reaction"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditReactionOption)
|
||||
|
||||
changeIssueCommentReaction(ctx, *form, true)
|
||||
}
|
||||
|
||||
// DeleteIssueCommentReaction remove a reaction from a comment of an issue
|
||||
func DeleteIssueCommentReaction(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/comments/{id}/reactions issue issueDeleteCommentReaction
|
||||
// ---
|
||||
// summary: Remove a reaction from a comment of an issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the comment to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: content
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditReactionOption"
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditReactionOption)
|
||||
|
||||
changeIssueCommentReaction(ctx, *form, false)
|
||||
}
|
||||
|
||||
func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
|
||||
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrCommentNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = comment.LoadIssue(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
|
||||
ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction"))
|
||||
return
|
||||
}
|
||||
|
||||
if isCreateType {
|
||||
// PostIssueCommentReaction part
|
||||
reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction)
|
||||
if err != nil {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
} else if issues_model.IsErrReactionAlreadyExist(err) {
|
||||
ctx.JSON(http.StatusOK, api.Reaction{
|
||||
User: convert.ToUser(ctx, ctx.Doer, ctx.Doer),
|
||||
Reaction: reaction.Type,
|
||||
Created: reaction.CreatedUnix.AsTime(),
|
||||
})
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, api.Reaction{
|
||||
User: convert.ToUser(ctx, ctx.Doer, ctx.Doer),
|
||||
Reaction: reaction.Type,
|
||||
Created: reaction.CreatedUnix.AsTime(),
|
||||
})
|
||||
} else {
|
||||
// DeleteIssueCommentReaction part
|
||||
err = issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// GetIssueReactions list reactions of an issue
|
||||
func GetIssueReactions(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/reactions issue issueGetIssueReactions
|
||||
// ---
|
||||
// summary: Get a list reactions of an issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ReactionList"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions"))
|
||||
return
|
||||
}
|
||||
|
||||
reactions, count, err := issues_model.FindIssueReactions(ctx, issue.ID, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
_, err = reactions.LoadUsers(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
var result []api.Reaction
|
||||
for _, r := range reactions {
|
||||
result = append(result, api.Reaction{
|
||||
User: convert.ToUser(ctx, r.User, ctx.Doer),
|
||||
Reaction: r.Type,
|
||||
Created: r.CreatedUnix.AsTime(),
|
||||
})
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// PostIssueReaction add a reaction to an issue
|
||||
func PostIssueReaction(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/reactions issue issuePostIssueReaction
|
||||
// ---
|
||||
// summary: Add a reaction to an issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: content
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditReactionOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Reaction"
|
||||
// "201":
|
||||
// "$ref": "#/responses/Reaction"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
form := web.GetForm(ctx).(*api.EditReactionOption)
|
||||
changeIssueReaction(ctx, *form, true)
|
||||
}
|
||||
|
||||
// DeleteIssueReaction remove a reaction from an issue
|
||||
func DeleteIssueReaction(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/reactions issue issueDeleteIssueReaction
|
||||
// ---
|
||||
// summary: Remove a reaction from an issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: content
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditReactionOption"
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
form := web.GetForm(ctx).(*api.EditReactionOption)
|
||||
changeIssueReaction(ctx, *form, false)
|
||||
}
|
||||
|
||||
func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
|
||||
issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction"))
|
||||
return
|
||||
}
|
||||
|
||||
if isCreateType {
|
||||
// PostIssueReaction part
|
||||
reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction)
|
||||
if err != nil {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
} else if issues_model.IsErrReactionAlreadyExist(err) {
|
||||
ctx.JSON(http.StatusOK, api.Reaction{
|
||||
User: convert.ToUser(ctx, ctx.Doer, ctx.Doer),
|
||||
Reaction: reaction.Type,
|
||||
Created: reaction.CreatedUnix.AsTime(),
|
||||
})
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, api.Reaction{
|
||||
User: convert.ToUser(ctx, ctx.Doer, ctx.Doer),
|
||||
Reaction: reaction.Type,
|
||||
Created: reaction.CreatedUnix.AsTime(),
|
||||
})
|
||||
} else {
|
||||
// DeleteIssueReaction part
|
||||
err = issues_model.DeleteIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Reaction)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// StartIssueStopwatch creates a stopwatch for the given issue.
|
||||
func StartIssueStopwatch(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/stopwatch/start issue issueStartStopWatch
|
||||
// ---
|
||||
// summary: Start stopwatch on an issue.
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue to create the stopwatch on
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// description: Not repo writer, user does not have rights to toggle stopwatch
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "409":
|
||||
// description: Cannot start a stopwatch again if it already exists
|
||||
|
||||
issue := prepareIssueForStopwatch(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if ok, err := issues_model.CreateIssueStopwatch(ctx, ctx.Doer, issue); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
} else if !ok {
|
||||
ctx.APIError(http.StatusConflict, "cannot start a stopwatch again if it already exists")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
// StopIssueStopwatch stops a stopwatch for the given issue.
|
||||
func StopIssueStopwatch(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/stopwatch/stop issue issueStopStopWatch
|
||||
// ---
|
||||
// summary: Stop an issue's existing stopwatch.
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue to stop the stopwatch on
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// description: Not repo writer, user does not have rights to toggle stopwatch
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "409":
|
||||
// description: Cannot stop a non-existent stopwatch
|
||||
|
||||
issue := prepareIssueForStopwatch(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if ok, err := issues_model.FinishIssueStopwatch(ctx, ctx.Doer, issue); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
} else if !ok {
|
||||
ctx.APIError(http.StatusConflict, "cannot stop a non-existent stopwatch")
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
// DeleteIssueStopwatch delete a specific stopwatch
|
||||
func DeleteIssueStopwatch(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/stopwatch/delete issue issueDeleteStopWatch
|
||||
// ---
|
||||
// summary: Delete an issue's existing stopwatch.
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue to stop the stopwatch on
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// description: Not repo writer, user does not have rights to toggle stopwatch
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "409":
|
||||
// description: Cannot cancel a non-existent stopwatch
|
||||
|
||||
issue := prepareIssueForStopwatch(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if ok, err := issues_model.CancelStopwatch(ctx, ctx.Doer, issue); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
} else if !ok {
|
||||
ctx.APIError(http.StatusConflict, "cannot cancel a non-existent stopwatch")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func prepareIssueForStopwatch(ctx *context.APIContext) *issues_model.Issue {
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return nil
|
||||
}
|
||||
return issue
|
||||
}
|
||||
|
||||
// GetStopwatches get all stopwatches
|
||||
func GetStopwatches(ctx *context.APIContext) {
|
||||
// swagger:operation GET /user/stopwatches user userGetStopWatches
|
||||
// ---
|
||||
// summary: Get list of all existing stopwatches
|
||||
// parameters:
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/StopWatchList"
|
||||
|
||||
sws, err := issues_model.GetUserStopwatches(ctx, ctx.Doer.ID, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := issues_model.CountUserStopwatches(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiSWs, err := convert.ToStopWatches(ctx, ctx.Doer, sws)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, apiSWs)
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// AddIssueSubscription Subscribe user to issue
|
||||
func AddIssueSubscription(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /repos/{owner}/{repo}/issues/{index}/subscriptions/{user} issue issueAddSubscription
|
||||
// ---
|
||||
// summary: Subscribe user to issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: user
|
||||
// in: path
|
||||
// description: username of the user to subscribe the issue to
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: Already subscribed
|
||||
// "201":
|
||||
// description: Successfully Subscribed
|
||||
// "304":
|
||||
// description: User can only subscribe itself if he is no admin
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
setIssueSubscription(ctx, true)
|
||||
}
|
||||
|
||||
// DelIssueSubscription Unsubscribe user from issue
|
||||
func DelIssueSubscription(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/subscriptions/{user} issue issueDeleteSubscription
|
||||
// ---
|
||||
// summary: Unsubscribe user from issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: user
|
||||
// in: path
|
||||
// description: username of the user to unsubscribe from an issue
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// description: Already unsubscribed
|
||||
// "201":
|
||||
// description: Successfully Unsubscribed
|
||||
// "304":
|
||||
// description: User can only subscribe itself if he is no admin
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
setIssueSubscription(ctx, false)
|
||||
}
|
||||
|
||||
func setIssueSubscription(ctx *context.APIContext, watch bool) {
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
user, err := user_model.GetUserByName(ctx, ctx.PathParam("user"))
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// only admin and user for itself can change subscription
|
||||
if user.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
|
||||
ctx.APIError(http.StatusForbidden, fmt.Errorf("%s is not permitted to change subscriptions for %s", ctx.Doer.Name, user.Name))
|
||||
return
|
||||
}
|
||||
|
||||
current, err := issues_model.CheckIssueWatch(ctx, user, issue)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// If watch state won't change
|
||||
if current == watch {
|
||||
ctx.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Update watch state
|
||||
if err := issues_model.CreateOrUpdateIssueWatch(ctx, user.ID, issue.ID, watch); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
// CheckIssueSubscription check if user is subscribed to an issue
|
||||
func CheckIssueSubscription(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/subscriptions/check issue issueCheckSubscription
|
||||
// ---
|
||||
// summary: Check if user is subscribed to an issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WatchInfo"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
watching, err := issues_model.CheckIssueWatch(ctx, ctx.Doer, issue)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, api.WatchInfo{
|
||||
Subscribed: watching,
|
||||
Ignored: !watching,
|
||||
Reason: nil,
|
||||
CreatedAt: issue.CreatedUnix.AsTime(),
|
||||
URL: issue.APIURL(ctx) + "/subscriptions",
|
||||
RepositoryURL: ctx.Repo.Repository.APIURL(),
|
||||
})
|
||||
}
|
||||
|
||||
// GetIssueSubscribers return subscribers of an issue
|
||||
func GetIssueSubscribers(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/subscriptions issue issueSubscriptions
|
||||
// ---
|
||||
// summary: Get users who subscribed on an issue.
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
iwl, err := issues_model.GetIssueWatchers(ctx, issue.ID, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
userIDs := make([]int64, 0, len(iwl))
|
||||
for _, iw := range iwl {
|
||||
userIDs = append(userIDs, iw.UserID)
|
||||
}
|
||||
|
||||
users, err := user_model.GetUsersByIDs(ctx, userIDs)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
apiUsers := make([]*api.User, 0, len(users))
|
||||
for _, v := range users {
|
||||
apiUsers = append(apiUsers, convert.ToUser(ctx, v, ctx.Doer))
|
||||
}
|
||||
|
||||
count, err := issues_model.CountIssueWatchers(ctx, issue.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, apiUsers)
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// ListTrackedTimes list all the tracked times of an issue
|
||||
func ListTrackedTimes(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/times issue issueTrackedTimes
|
||||
// ---
|
||||
// summary: List an issue's tracked times
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: user
|
||||
// in: query
|
||||
// description: optional filter by user (available for issue managers)
|
||||
// type: string
|
||||
// - name: since
|
||||
// in: query
|
||||
// description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
|
||||
// type: string
|
||||
// format: date-time
|
||||
// - name: before
|
||||
// in: query
|
||||
// description: Only show times updated before the given time. This is a timestamp in RFC 3339 format
|
||||
// type: string
|
||||
// format: date-time
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TrackedTimeList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
|
||||
ctx.APIErrorNotFound("Timetracker is disabled")
|
||||
return
|
||||
}
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
opts := &issues_model.FindTrackedTimesOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
RepositoryID: ctx.Repo.Repository.ID,
|
||||
IssueID: issue.ID,
|
||||
}
|
||||
|
||||
qUser := ctx.FormTrim("user")
|
||||
if qUser != "" {
|
||||
user, err := user_model.GetUserByName(ctx, qUser)
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
} else if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
opts.UserID = user.ID
|
||||
}
|
||||
|
||||
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
cantSetUser := !ctx.Doer.IsAdmin &&
|
||||
opts.UserID != ctx.Doer.ID &&
|
||||
!ctx.IsUserRepoWriter([]unit.Type{unit.TypeIssues})
|
||||
|
||||
if cantSetUser {
|
||||
if opts.UserID == 0 {
|
||||
opts.UserID = ctx.Doer.ID
|
||||
} else {
|
||||
ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
count, err := issues_model.CountTrackedTimes(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if err = trackedTimes.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
|
||||
}
|
||||
|
||||
// AddTime add time manual to the given issue
|
||||
func AddTime(ctx *context.APIContext) {
|
||||
// swagger:operation Post /repos/{owner}/{repo}/issues/{index}/times issue issueAddTime
|
||||
// ---
|
||||
// summary: Add tracked time to a issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/AddTimeOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TrackedTime"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
form := web.GetForm(ctx).(*api.AddTimeOption)
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) {
|
||||
if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
|
||||
ctx.APIError(http.StatusBadRequest, "time tracking disabled")
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.Doer
|
||||
if form.User != "" {
|
||||
if (ctx.IsUserRepoAdmin() && ctx.Doer.Name != form.User) || ctx.Doer.IsAdmin {
|
||||
// allow only RepoAdmin, Admin and User to add time
|
||||
user, err = user_model.GetUserByName(ctx, form.User)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
created := time.Time{}
|
||||
if !form.Created.IsZero() {
|
||||
created = form.Created
|
||||
}
|
||||
|
||||
trackedTime, err := issues_model.AddTime(ctx, user, issue, form.Time, created)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if err = trackedTime.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToTrackedTime(ctx, user, trackedTime))
|
||||
}
|
||||
|
||||
// ResetIssueTime reset time manual to the given issue
|
||||
func ResetIssueTime(ctx *context.APIContext) {
|
||||
// swagger:operation Delete /repos/{owner}/{repo}/issues/{index}/times issue issueResetTime
|
||||
// ---
|
||||
// summary: Reset a tracked time of an issue
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue to add tracked time to
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) {
|
||||
if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
|
||||
ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"})
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
err = issues_model.DeleteIssueUserTimes(ctx, issue, ctx.Doer)
|
||||
if err != nil {
|
||||
if db.IsErrNotExist(err) {
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// DeleteTime delete a specific time by id
|
||||
func DeleteTime(ctx *context.APIContext) {
|
||||
// swagger:operation Delete /repos/{owner}/{repo}/issues/{index}/times/{id} issue issueDeleteTime
|
||||
// ---
|
||||
// summary: Delete specific tracked time
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: index
|
||||
// in: path
|
||||
// description: index of the issue
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of time to delete
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) {
|
||||
if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
|
||||
ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"})
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
time, err := issues_model.GetTrackedTimeByID(ctx, issue.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if db.IsErrNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if time.Deleted {
|
||||
ctx.APIErrorNotFound("tracked time was already deleted")
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Doer.IsAdmin && time.UserID != ctx.Doer.ID {
|
||||
// Only Admin and User itself can delete their time
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
err = issues_model.DeleteTime(ctx, time)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ListTrackedTimesByUser lists all tracked times of the user
|
||||
func ListTrackedTimesByUser(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/times/{user} repository userTrackedTimes
|
||||
// ---
|
||||
// summary: List a user's tracked times in a repo
|
||||
// deprecated: true
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: user
|
||||
// in: path
|
||||
// description: username of the user whose tracked times are to be listed
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TrackedTimeList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
|
||||
ctx.APIError(http.StatusBadRequest, "time tracking disabled")
|
||||
return
|
||||
}
|
||||
user, err := user_model.GetUserByName(ctx, ctx.PathParam("timetrackingusername"))
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsUserRepoAdmin() && !ctx.Doer.IsAdmin && ctx.Doer.ID != user.ID {
|
||||
ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights"))
|
||||
return
|
||||
}
|
||||
|
||||
opts := &issues_model.FindTrackedTimesOptions{
|
||||
UserID: user.ID,
|
||||
RepositoryID: ctx.Repo.Repository.ID,
|
||||
}
|
||||
|
||||
trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if err = trackedTimes.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
|
||||
}
|
||||
|
||||
// ListTrackedTimesByRepository lists all tracked times of the repository
|
||||
func ListTrackedTimesByRepository(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/times repository repoTrackedTimes
|
||||
// ---
|
||||
// summary: List a repo's tracked times
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: user
|
||||
// in: query
|
||||
// description: optional filter by user (available for issue managers)
|
||||
// type: string
|
||||
// - name: since
|
||||
// in: query
|
||||
// description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
|
||||
// type: string
|
||||
// format: date-time
|
||||
// - name: before
|
||||
// in: query
|
||||
// description: Only show times updated before the given time. This is a timestamp in RFC 3339 format
|
||||
// type: string
|
||||
// format: date-time
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TrackedTimeList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
|
||||
ctx.APIError(http.StatusBadRequest, "time tracking disabled")
|
||||
return
|
||||
}
|
||||
|
||||
opts := &issues_model.FindTrackedTimesOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
RepositoryID: ctx.Repo.Repository.ID,
|
||||
}
|
||||
|
||||
// Filters
|
||||
qUser := ctx.FormTrim("user")
|
||||
if qUser != "" {
|
||||
user, err := user_model.GetUserByName(ctx, qUser)
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
} else if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
opts.UserID = user.ID
|
||||
}
|
||||
|
||||
var err error
|
||||
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
cantSetUser := !ctx.Doer.IsAdmin &&
|
||||
opts.UserID != ctx.Doer.ID &&
|
||||
!ctx.IsUserRepoWriter([]unit.Type{unit.TypeIssues})
|
||||
|
||||
if cantSetUser {
|
||||
if opts.UserID == 0 {
|
||||
opts.UserID = ctx.Doer.ID
|
||||
} else {
|
||||
ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
count, err := issues_model.CountTrackedTimes(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if err = trackedTimes.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
|
||||
}
|
||||
|
||||
// ListMyTrackedTimes lists all tracked times of the current user
|
||||
func ListMyTrackedTimes(ctx *context.APIContext) {
|
||||
// swagger:operation GET /user/times user userCurrentTrackedTimes
|
||||
// ---
|
||||
// summary: List the current user's tracked times
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// - name: since
|
||||
// in: query
|
||||
// description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
|
||||
// type: string
|
||||
// format: date-time
|
||||
// - name: before
|
||||
// in: query
|
||||
// description: Only show times updated before the given time. This is a timestamp in RFC 3339 format
|
||||
// type: string
|
||||
// format: date-time
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TrackedTimeList"
|
||||
|
||||
opts := &issues_model.FindTrackedTimesOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
UserID: ctx.Doer.ID,
|
||||
}
|
||||
|
||||
var err error
|
||||
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := issues_model.CountTrackedTimes(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = trackedTimes.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
stdCtx "context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/perm"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
asymkey_service "gitea.dev/services/asymkey"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// appendPrivateInformation appends the owner and key type information to api.PublicKey
|
||||
func appendPrivateInformation(ctx stdCtx.Context, apiKey *api.DeployKey, key *asymkey_model.DeployKey, repository *repo_model.Repository) (*api.DeployKey, error) {
|
||||
apiKey.ReadOnly = key.Mode == perm.AccessModeRead
|
||||
if repository.ID == key.RepoID {
|
||||
apiKey.Repository = convert.ToRepo(ctx, repository, access_model.Permission{AccessMode: key.Mode})
|
||||
} else {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, key.RepoID)
|
||||
if err != nil {
|
||||
return apiKey, err
|
||||
}
|
||||
apiKey.Repository = convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: key.Mode})
|
||||
}
|
||||
return apiKey, nil
|
||||
}
|
||||
|
||||
func composeDeployKeysAPILink(owner, name string) string {
|
||||
return setting.AppURL + "api/v1/repos/" + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/keys/"
|
||||
}
|
||||
|
||||
// ListDeployKeys list all the deploy keys of a repository
|
||||
func ListDeployKeys(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/keys repository repoListKeys
|
||||
// ---
|
||||
// summary: List a repository's keys
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: key_id
|
||||
// in: query
|
||||
// description: the key_id to search for
|
||||
// type: integer
|
||||
// - name: fingerprint
|
||||
// in: query
|
||||
// description: fingerprint of the key
|
||||
// type: string
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/DeployKeyList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
opts := asymkey_model.ListDeployKeysOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
KeyID: ctx.FormInt64("key_id"),
|
||||
Fingerprint: ctx.FormString("fingerprint"),
|
||||
}
|
||||
|
||||
keys, count, err := db.FindAndCount[asymkey_model.DeployKey](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
|
||||
apiKeys := make([]*api.DeployKey, len(keys))
|
||||
for i := range keys {
|
||||
if err := keys[i].GetContent(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
apiKeys[i] = convert.ToDeployKey(apiLink, keys[i])
|
||||
if ctx.Doer.IsAdmin || ((ctx.Repo.Repository.ID == keys[i].RepoID) && (ctx.Doer.ID == ctx.Repo.Owner.ID)) {
|
||||
apiKeys[i], _ = appendPrivateInformation(ctx, apiKeys[i], keys[i], ctx.Repo.Repository)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, &apiKeys)
|
||||
}
|
||||
|
||||
// GetDeployKey get a deploy key by id
|
||||
func GetDeployKey(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/keys/{id} repository repoGetKey
|
||||
// ---
|
||||
// summary: Get a repository's key by id
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the key to get
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/DeployKey"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
key, err := asymkey_model.GetDeployKeyByID(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if asymkey_model.IsErrDeployKeyNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// this check make it more consistent
|
||||
if key.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if err = key.GetContent(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
|
||||
apiKey := convert.ToDeployKey(apiLink, key)
|
||||
if ctx.Doer.IsAdmin || ((ctx.Repo.Repository.ID == key.RepoID) && (ctx.Doer.ID == ctx.Repo.Owner.ID)) {
|
||||
apiKey, _ = appendPrivateInformation(ctx, apiKey, key, ctx.Repo.Repository)
|
||||
}
|
||||
ctx.JSON(http.StatusOK, apiKey)
|
||||
}
|
||||
|
||||
// HandleCheckKeyStringError handle check key error
|
||||
func HandleCheckKeyStringError(ctx *context.APIContext, err error) {
|
||||
if db.IsErrSSHDisabled(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "SSH is disabled")
|
||||
} else if asymkey_model.IsErrKeyUnableVerify(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "Unable to verify key content")
|
||||
} else {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid key content: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
// HandleAddKeyError handle add key error
|
||||
func HandleAddKeyError(ctx *context.APIContext, err error) {
|
||||
switch {
|
||||
case asymkey_model.IsErrDeployKeyAlreadyExist(err):
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "This key has already been added to this repository")
|
||||
case asymkey_model.IsErrKeyAlreadyExist(err):
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "Key content has been used as non-deploy key")
|
||||
case asymkey_model.IsErrKeyNameAlreadyUsed(err):
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "Key title has been used")
|
||||
case asymkey_model.IsErrDeployKeyNameAlreadyUsed(err):
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "A key with the same name already exists")
|
||||
default:
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDeployKey create deploy key for a repository
|
||||
func CreateDeployKey(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/keys repository repoCreateKey
|
||||
// ---
|
||||
// summary: Add a key to a repository
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateKeyOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/DeployKey"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateKeyOption)
|
||||
content, err := asymkey_model.CheckPublicKeyString(form.Key)
|
||||
if err != nil {
|
||||
HandleCheckKeyStringError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
key, err := asymkey_model.AddDeployKey(ctx, ctx.Repo.Repository.ID, form.Title, content, form.ReadOnly)
|
||||
if err != nil {
|
||||
HandleAddKeyError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
key.Content = content
|
||||
apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
|
||||
ctx.JSON(http.StatusCreated, convert.ToDeployKey(apiLink, key))
|
||||
}
|
||||
|
||||
// DeleteDeploykey delete deploy key for a repository
|
||||
func DeleteDeploykey(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/keys/{id} repository repoDeleteKey
|
||||
// ---
|
||||
// summary: Delete a key from a repository
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the key to delete
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if err := asymkey_service.DeleteDeployKey(ctx, ctx.Repo.Repository, ctx.PathParamInt64("id")); err != nil {
|
||||
if asymkey_model.IsErrKeyAccessDenied(err) {
|
||||
ctx.APIError(http.StatusForbidden, "You do not have access to this key")
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/modules/label"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// ListLabels list all the labels of a repository
|
||||
func ListLabels(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/labels issue issueListLabels
|
||||
// ---
|
||||
// summary: Get all of a repository's labels
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/LabelList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := issues_model.CountLabelsByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, convert.ToLabelList(labels, ctx.Repo.Repository, nil))
|
||||
}
|
||||
|
||||
// GetLabel get label by repository and label id
|
||||
func GetLabel(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/labels/{id} issue issueGetLabel
|
||||
// ---
|
||||
// summary: Get a single label
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the label to get
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Label"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
var (
|
||||
l *issues_model.Label
|
||||
err error
|
||||
)
|
||||
strID := ctx.PathParam("id")
|
||||
if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil {
|
||||
l, err = issues_model.GetLabelInRepoByName(ctx, ctx.Repo.Repository.ID, strID)
|
||||
} else {
|
||||
l, err = issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, intID)
|
||||
}
|
||||
if err != nil {
|
||||
if issues_model.IsErrRepoLabelNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToLabel(l, ctx.Repo.Repository, nil))
|
||||
}
|
||||
|
||||
// CreateLabel create a label for a repository
|
||||
func CreateLabel(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/labels issue issueCreateLabel
|
||||
// ---
|
||||
// summary: Create a label
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateLabelOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Label"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateLabelOption)
|
||||
|
||||
color, err := label.NormalizeColor(form.Color)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
form.Color = color
|
||||
l := &issues_model.Label{
|
||||
Name: form.Name,
|
||||
Exclusive: form.Exclusive,
|
||||
Color: form.Color,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Description: form.Description,
|
||||
}
|
||||
l.SetArchived(form.IsArchived)
|
||||
if err := issues_model.NewLabel(ctx, l); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToLabel(l, ctx.Repo.Repository, nil))
|
||||
}
|
||||
|
||||
// EditLabel modify a label for a repository
|
||||
func EditLabel(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/labels/{id} issue issueEditLabel
|
||||
// ---
|
||||
// summary: Update a label
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the label to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditLabelOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Label"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditLabelOption)
|
||||
l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrRepoLabelNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if form.Name != nil {
|
||||
l.Name = *form.Name
|
||||
}
|
||||
if form.Exclusive != nil {
|
||||
l.Exclusive = *form.Exclusive
|
||||
}
|
||||
if form.Color != nil {
|
||||
color, err := label.NormalizeColor(*form.Color)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
l.Color = color
|
||||
}
|
||||
if form.Description != nil {
|
||||
l.Description = *form.Description
|
||||
}
|
||||
l.SetArchived(form.IsArchived != nil && *form.IsArchived)
|
||||
if err := issues_model.UpdateLabel(ctx, l); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToLabel(l, ctx.Repo.Repository, nil))
|
||||
}
|
||||
|
||||
// DeleteLabel delete a label for a repository
|
||||
func DeleteLabel(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/labels/{id} issue issueDeleteLabel
|
||||
// ---
|
||||
// summary: Delete a label
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the label to delete
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if err := issues_model.DeleteLabel(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
type languageResponse []*repo_model.LanguageStat
|
||||
|
||||
func (l languageResponse) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
if _, err := buf.WriteString("{"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, lang := range l {
|
||||
if i > 0 {
|
||||
if _, err := buf.WriteString(","); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if _, err := buf.WriteString(strconv.Quote(lang.Language)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := buf.WriteString(":"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := buf.WriteString(strconv.FormatInt(lang.Size, 10)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if _, err := buf.WriteString("}"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// GetLanguages returns languages and number of bytes of code written
|
||||
func GetLanguages(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/languages repository repoGetLanguages
|
||||
// ---
|
||||
// summary: Get languages and number of bytes of code written
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "200":
|
||||
// "$ref": "#/responses/LanguageStatistics"
|
||||
|
||||
langs, err := repo_model.GetLanguageStats(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
log.Error("GetLanguageStats failed: %v", err)
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := make(languageResponse, len(langs))
|
||||
copy(resp, langs)
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// GetLicenses returns licenses
|
||||
func GetLicenses(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/licenses repository repoGetLicenses
|
||||
// ---
|
||||
// summary: Get repo licenses
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "200":
|
||||
// "$ref": "#/responses/LicensesList"
|
||||
|
||||
licenses, err := repo_model.GetRepoLicenses(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
log.Error("GetRepoLicenses failed: %v", err)
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := make([]string, len(licenses))
|
||||
for i := range licenses {
|
||||
resp[i] = licenses[i].License
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/setting"
|
||||
webhook_service "gitea.dev/services/webhook"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m, &unittest.TestOptions{
|
||||
SetUp: func() error {
|
||||
setting.LoadQueueSettings()
|
||||
return webhook_service.Init()
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
gocontext "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/lfs"
|
||||
"gitea.dev/modules/log"
|
||||
base "gitea.dev/modules/migration"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
"gitea.dev/services/migrations"
|
||||
notify_service "gitea.dev/services/notify"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
// Migrate migrate remote git repository to gitea
|
||||
func Migrate(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/migrate repository repoMigrate
|
||||
// ---
|
||||
// summary: Migrate a remote git repository
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/MigrateRepoOptions"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Repository"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "409":
|
||||
// description: The repository with the same name already exists.
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.MigrateRepoOptions)
|
||||
|
||||
// get repoOwner
|
||||
var (
|
||||
repoOwner *user_model.User
|
||||
err error
|
||||
)
|
||||
if len(form.RepoOwner) != 0 {
|
||||
repoOwner, err = user_model.GetUserByName(ctx, form.RepoOwner)
|
||||
} else if form.RepoOwnerID != 0 {
|
||||
repoOwner, err = user_model.GetUserByID(ctx, form.RepoOwnerID)
|
||||
} else {
|
||||
repoOwner = ctx.Doer
|
||||
}
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Doer.IsAdmin {
|
||||
if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID {
|
||||
ctx.APIError(http.StatusForbidden, "Given user is not an organization.")
|
||||
return
|
||||
}
|
||||
|
||||
if repoOwner.IsOrganization() {
|
||||
// Check ownership of organization.
|
||||
isOwner, err := organization.OrgFromUser(repoOwner).IsOwnedBy(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
} else if !isOwner {
|
||||
ctx.APIError(http.StatusForbidden, "Given user is not owner of organization.")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remoteAddr, err := git.ParseRemoteAddr(form.CloneAddr, form.AuthUsername, form.AuthPassword)
|
||||
if err == nil {
|
||||
err = migrations.IsMigrateURLAllowed(remoteAddr, ctx.Doer)
|
||||
}
|
||||
if err != nil {
|
||||
handleRemoteAddrError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
gitServiceType := convert.ToGitServiceType(form.Service)
|
||||
|
||||
if form.Mirror && setting.Mirror.DisableNewPull {
|
||||
ctx.APIError(http.StatusForbidden, errors.New("the site administrator has disabled the creation of new pull mirrors"))
|
||||
return
|
||||
}
|
||||
|
||||
if setting.Repository.DisableMigrations {
|
||||
ctx.APIError(http.StatusForbidden, errors.New("the site administrator has disabled migrations"))
|
||||
return
|
||||
}
|
||||
|
||||
form.LFS = form.LFS && setting.LFS.StartServer
|
||||
|
||||
if form.LFS && len(form.LFSEndpoint) > 0 {
|
||||
ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
|
||||
if ep == nil {
|
||||
ctx.APIErrorInternal(errors.New("the LFS endpoint is not valid"))
|
||||
return
|
||||
}
|
||||
err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer)
|
||||
if err != nil {
|
||||
handleRemoteAddrError(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
opts := migrations.MigrateOptions{
|
||||
OriginalURL: form.CloneAddr,
|
||||
CloneAddr: remoteAddr,
|
||||
RepoName: form.RepoName,
|
||||
Description: form.Description,
|
||||
Private: form.Private || setting.Repository.ForcePrivate,
|
||||
Mirror: form.Mirror,
|
||||
LFS: form.LFS,
|
||||
LFSEndpoint: form.LFSEndpoint,
|
||||
AuthUsername: form.AuthUsername,
|
||||
AuthPassword: form.AuthPassword,
|
||||
AuthToken: form.AuthToken,
|
||||
Wiki: form.Wiki,
|
||||
Issues: form.Issues,
|
||||
Milestones: form.Milestones,
|
||||
Labels: form.Labels,
|
||||
Comments: form.Issues || form.PullRequests,
|
||||
PullRequests: form.PullRequests,
|
||||
Releases: form.Releases,
|
||||
GitServiceType: gitServiceType,
|
||||
MirrorInterval: form.MirrorInterval,
|
||||
}
|
||||
if opts.Mirror {
|
||||
opts.Issues = false
|
||||
opts.Milestones = false
|
||||
opts.Labels = false
|
||||
opts.Comments = false
|
||||
opts.PullRequests = false
|
||||
opts.Releases = false
|
||||
}
|
||||
if gitServiceType == api.CodeCommitService {
|
||||
opts.AWSAccessKeyID = form.AWSAccessKeyID
|
||||
opts.AWSSecretAccessKey = form.AWSSecretAccessKey
|
||||
}
|
||||
|
||||
createdRepo, err := repo_service.CreateRepositoryDirectly(ctx, ctx.Doer, repoOwner, repo_service.CreateRepoOptions{
|
||||
Name: opts.RepoName,
|
||||
Description: opts.Description,
|
||||
OriginalURL: form.CloneAddr,
|
||||
GitServiceType: gitServiceType,
|
||||
IsPrivate: opts.Private || setting.Repository.ForcePrivate,
|
||||
IsMirror: opts.Mirror,
|
||||
Status: repo_model.RepositoryBeingMigrated,
|
||||
}, false)
|
||||
if err != nil {
|
||||
handleMigrateError(ctx, repoOwner, err)
|
||||
return
|
||||
}
|
||||
|
||||
opts.MigrateToRepoID = createdRepo.ID
|
||||
|
||||
doLongTimeMigrate := func(ctx gocontext.Context, doer *user_model.User) (migratedRepo *repo_model.Repository, retErr error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
log.Error("MigrateRepository panic: %v\n%s", e, log.Stack(2))
|
||||
if errDelete := repo_service.DeleteRepositoryDirectly(ctx, createdRepo.ID); errDelete != nil {
|
||||
log.Error("Unable to delete repo after MigrateRepository panic: %v", errDelete)
|
||||
}
|
||||
retErr = errors.New("MigrateRepository panic") // no idea why it would happen, just legacy code
|
||||
}
|
||||
}()
|
||||
|
||||
migratedRepo, err := migrations.MigrateRepository(ctx, doer, repoOwner.Name, opts, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notify_service.MigrateRepository(ctx, doer, repoOwner, migratedRepo)
|
||||
return migratedRepo, nil
|
||||
}
|
||||
|
||||
// use a background context, don't cancel the migration even if the client goes away
|
||||
// HammerContext doesn't seem right (from https://github.com/go-gitea/gitea/pull/9335/files)
|
||||
// There are other abuses, maybe most HammerContext abuses should be fixed together in the future.
|
||||
migratedRepo, err := doLongTimeMigrate(graceful.GetManager().HammerContext(), ctx.Doer)
|
||||
if err != nil {
|
||||
handleMigrateError(ctx, repoOwner, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, migratedRepo, access_model.Permission{AccessMode: perm.AccessModeAdmin}))
|
||||
}
|
||||
|
||||
func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, err error) {
|
||||
switch {
|
||||
case repo_model.IsErrRepoAlreadyExist(err):
|
||||
ctx.APIError(http.StatusConflict, "The repository with the same name already exists.")
|
||||
case repo_model.IsErrRepoFilesAlreadyExist(err):
|
||||
ctx.APIError(http.StatusConflict, "Files already exist for this repository. Adopt them or delete them.")
|
||||
case migrations.IsRateLimitError(err):
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "Remote visit addressed rate limitation.")
|
||||
case migrations.IsTwoFactorAuthError(err):
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "Remote visit required two factors authentication.")
|
||||
case repo_model.IsErrReachLimitOfRepo(err):
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("You have already reached your limit of %d repositories.", repoOwner.MaxCreationLimit()))
|
||||
case db.IsErrNameReserved(err):
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("The username '%s' is reserved.", err.(db.ErrNameReserved).Name))
|
||||
case db.IsErrNameCharsNotAllowed(err):
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("The username '%s' contains invalid characters.", err.(db.ErrNameCharsNotAllowed).Name))
|
||||
case db.IsErrNamePatternNotAllowed(err):
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(db.ErrNamePatternNotAllowed).Pattern))
|
||||
case git.IsErrInvalidCloneAddr(err):
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
case base.IsErrNotSupported(err):
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
default:
|
||||
err = util.SanitizeErrorCredentialURLs(err)
|
||||
if strings.Contains(err.Error(), "Authentication failed") ||
|
||||
strings.Contains(err.Error(), "Bad credentials") ||
|
||||
strings.Contains(err.Error(), "could not read Username") {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Authentication failed: %v.", err))
|
||||
} else if strings.Contains(err.Error(), "fatal:") {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Migration failed: %v.", err))
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoteAddrError(ctx *context.APIContext, err error) {
|
||||
if git.IsErrInvalidCloneAddr(err) {
|
||||
addrErr := err.(*git.ErrInvalidCloneAddr)
|
||||
switch {
|
||||
case addrErr.IsURLError:
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "The provided URL is invalid.")
|
||||
case addrErr.IsPermissionDenied:
|
||||
if addrErr.LocalPath {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "You are not allowed to import local repositories.")
|
||||
} else {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "You can not import from disallowed hosts.")
|
||||
}
|
||||
case addrErr.IsInvalidPath:
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "Invalid local path, it does not exist or not a directory.")
|
||||
default:
|
||||
ctx.APIErrorInternal(fmt.Errorf("unknown error type (ErrInvalidCloneAddr): %w", err))
|
||||
}
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/routers/common"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// ListMilestones list milestones for a repository
|
||||
func ListMilestones(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/milestones issue issueGetMilestonesList
|
||||
// ---
|
||||
// summary: Get all of a repository's opened milestones
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: state
|
||||
// in: query
|
||||
// description: Milestone state, Recognized values are open, closed and all. Defaults to "open"
|
||||
// type: string
|
||||
// - name: name
|
||||
// in: query
|
||||
// description: filter by milestone name
|
||||
// type: string
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/MilestoneList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
||||
|
||||
milestones, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsClosed: isClosed,
|
||||
Name: ctx.FormString("name"),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiMilestones := make([]*api.Milestone, len(milestones))
|
||||
for i := range milestones {
|
||||
apiMilestones[i] = convert.ToAPIMilestone(milestones[i])
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(total)
|
||||
ctx.JSON(http.StatusOK, &apiMilestones)
|
||||
}
|
||||
|
||||
// GetMilestone get a milestone for a repository by ID and if not available by name
|
||||
func GetMilestone(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/milestones/{id} issue issueGetMilestone
|
||||
// ---
|
||||
// summary: Get a milestone
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: the milestone to get, identified by ID and if not available by name
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Milestone"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
milestone := getMilestoneByIDOrName(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIMilestone(milestone))
|
||||
}
|
||||
|
||||
// CreateMilestone create a milestone for a repository
|
||||
func CreateMilestone(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/milestones issue issueCreateMilestone
|
||||
// ---
|
||||
// summary: Create a milestone
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateMilestoneOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Milestone"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
form := web.GetForm(ctx).(*api.CreateMilestoneOption)
|
||||
|
||||
var deadlineUnix int64
|
||||
if form.Deadline != nil {
|
||||
deadlineUnix = form.Deadline.Unix()
|
||||
}
|
||||
|
||||
milestone := &issues_model.Milestone{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Name: form.Title,
|
||||
Content: form.Description,
|
||||
DeadlineUnix: timeutil.TimeStamp(deadlineUnix),
|
||||
}
|
||||
|
||||
if form.State == "closed" {
|
||||
milestone.IsClosed = true
|
||||
milestone.ClosedDateUnix = timeutil.TimeStampNow()
|
||||
}
|
||||
|
||||
if err := issues_model.NewMilestone(ctx, milestone); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIMilestone(milestone))
|
||||
}
|
||||
|
||||
// EditMilestone modify a milestone for a repository by ID and if not available by name
|
||||
func EditMilestone(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/milestones/{id} issue issueEditMilestone
|
||||
// ---
|
||||
// summary: Update a milestone
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: the milestone to edit, identified by ID and if not available by name
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditMilestoneOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Milestone"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
form := web.GetForm(ctx).(*api.EditMilestoneOption)
|
||||
milestone := getMilestoneByIDOrName(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if len(form.Title) > 0 {
|
||||
milestone.Name = form.Title
|
||||
}
|
||||
if form.Description != nil {
|
||||
milestone.Content = *form.Description
|
||||
}
|
||||
milestone.DeadlineUnix, _ = common.ParseAPIDeadlineToEndOfDay(form.Deadline)
|
||||
|
||||
oldIsClosed := milestone.IsClosed
|
||||
if form.State != nil {
|
||||
milestone.IsClosed = *form.State == string(api.StateClosed)
|
||||
}
|
||||
|
||||
if err := issues_model.UpdateMilestone(ctx, milestone, oldIsClosed); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIMilestone(milestone))
|
||||
}
|
||||
|
||||
// DeleteMilestone delete a milestone for a repository by ID and if not available by name
|
||||
func DeleteMilestone(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/milestones/{id} issue issueDeleteMilestone
|
||||
// ---
|
||||
// summary: Delete a milestone
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: the milestone to delete, identified by ID and if not available by name
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
m := getMilestoneByIDOrName(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := issues_model.DeleteMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, m.ID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// getMilestoneByIDOrName get milestone by ID and if not available by name
|
||||
func getMilestoneByIDOrName(ctx *context.APIContext) *issues_model.Milestone {
|
||||
mile := ctx.PathParam("id")
|
||||
mileID, _ := strconv.ParseInt(mile, 0, 64)
|
||||
|
||||
if mileID != 0 {
|
||||
milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, mileID)
|
||||
if err == nil {
|
||||
return milestone
|
||||
} else if !issues_model.IsErrMilestoneNotExist(err) {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
milestone, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, mile)
|
||||
if err != nil {
|
||||
if issues_model.IsErrMilestoneNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
return nil
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return milestone
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
"gitea.dev/services/migrations"
|
||||
mirror_service "gitea.dev/services/mirror"
|
||||
)
|
||||
|
||||
// MirrorSync adds a mirrored repository to the sync queue
|
||||
func MirrorSync(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/mirror-sync repository repoMirrorSync
|
||||
// ---
|
||||
// summary: Sync a mirrored repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo to sync
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo to sync
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
if !ctx.Repo.Permission.CanWrite(unit.TypeCode) {
|
||||
ctx.APIError(http.StatusForbidden, "Must have write access")
|
||||
}
|
||||
|
||||
if !setting.Mirror.Enabled {
|
||||
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := repo_model.GetMirrorByRepoID(ctx, repo.ID); err != nil {
|
||||
if errors.Is(err, repo_model.ErrMirrorNotExist) {
|
||||
ctx.APIError(http.StatusBadRequest, "Repository is not a mirror")
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
mirror_service.AddPullMirrorToQueue(repo.ID)
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// PushMirrorSync adds all push mirrored repositories to the sync queue
|
||||
func PushMirrorSync(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/push_mirrors-sync repository repoPushMirrorSync
|
||||
// ---
|
||||
// summary: Sync all push mirrored repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo to sync
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo to sync
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
if !setting.Mirror.Enabled {
|
||||
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
|
||||
return
|
||||
}
|
||||
// Get All push mirrors of a specific repo
|
||||
pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
failedPushMirrors := make([]string, 0)
|
||||
for _, mirror := range pushMirrors {
|
||||
ok := mirror_service.SyncPushMirror(ctx, mirror.ID)
|
||||
if !ok {
|
||||
failedPushMirrors = append(failedPushMirrors, mirror.RemoteName)
|
||||
}
|
||||
}
|
||||
if len(failedPushMirrors) != 0 {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "error occurred when syncing push mirrors: "+strings.Join(failedPushMirrors, ", "))
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// ListPushMirrors get list of push mirrors of a repository
|
||||
func ListPushMirrors(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/push_mirrors repository repoListPushMirrors
|
||||
// ---
|
||||
// summary: Get all push mirrors of the repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/PushMirrorList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if !setting.Mirror.Enabled {
|
||||
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
// Get all push mirrors for the specified repository.
|
||||
pushMirrors, count, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
responsePushMirrors := make([]*api.PushMirror, 0, len(pushMirrors))
|
||||
for _, mirror := range pushMirrors {
|
||||
m, err := convert.ToPushMirror(ctx, mirror)
|
||||
if err == nil {
|
||||
responsePushMirrors = append(responsePushMirrors, m)
|
||||
}
|
||||
}
|
||||
ctx.SetLinkHeader(int64(len(responsePushMirrors)), utils.GetListOptions(ctx).PageSize)
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, responsePushMirrors)
|
||||
}
|
||||
|
||||
// GetPushMirrorByName get push mirror of a repository by name
|
||||
func GetPushMirrorByName(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/push_mirrors/{name} repository repoGetPushMirrorByRemoteName
|
||||
// ---
|
||||
// summary: Get push mirror of the repository by remoteName
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: name
|
||||
// in: path
|
||||
// description: remote name of push mirror
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/PushMirror"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if !setting.Mirror.Enabled {
|
||||
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
mirrorName := ctx.PathParam("name")
|
||||
// Get push mirror of a specific repo by remoteName
|
||||
pushMirror, exist, err := db.Get[repo_model.PushMirror](ctx, repo_model.PushMirrorOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
RemoteName: mirrorName,
|
||||
}.ToConds())
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
} else if !exist {
|
||||
ctx.APIError(http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
m, err := convert.ToPushMirror(ctx, pushMirror)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, m)
|
||||
}
|
||||
|
||||
// AddPushMirror adds a push mirror to a repository
|
||||
func AddPushMirror(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/push_mirrors repository repoAddPushMirror
|
||||
// ---
|
||||
// summary: add a push mirror to the repository
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreatePushMirrorOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/PushMirror"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if !setting.Mirror.Enabled {
|
||||
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
pushMirror := web.GetForm(ctx).(*api.CreatePushMirrorOption)
|
||||
CreatePushMirror(ctx, pushMirror)
|
||||
}
|
||||
|
||||
// DeletePushMirrorByRemoteName deletes a push mirror from a repository by remoteName
|
||||
func DeletePushMirrorByRemoteName(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/push_mirrors/{name} repository repoDeletePushMirror
|
||||
// ---
|
||||
// summary: deletes a push mirror from a repository by remoteName
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: name
|
||||
// in: path
|
||||
// description: remote name of the pushMirror
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
if !setting.Mirror.Enabled {
|
||||
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
remoteName := ctx.PathParam("name")
|
||||
// Delete push mirror on repo by name.
|
||||
err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: remoteName})
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirrorOption) {
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
interval, err := time.ParseDuration(mirrorOption.Interval)
|
||||
if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
address, err := git.ParseRemoteAddr(mirrorOption.RemoteAddress, mirrorOption.RemoteUsername, mirrorOption.RemotePassword)
|
||||
if err == nil {
|
||||
err = migrations.IsMigrateURLAllowed(address, ctx.ContextUser)
|
||||
}
|
||||
if err != nil {
|
||||
HandleRemoteAddressError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
remoteSuffix := util.CryptoRandomString(10)
|
||||
|
||||
remoteAddress, err := util.SanitizeURL(mirrorOption.RemoteAddress)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
pushMirror := &repo_model.PushMirror{
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
RemoteName: "remote_mirror_" + remoteSuffix,
|
||||
Interval: interval,
|
||||
SyncOnCommit: mirrorOption.SyncOnCommit,
|
||||
RemoteAddress: remoteAddress,
|
||||
}
|
||||
|
||||
if err = db.Insert(ctx, pushMirror); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// if the registration of the push mirrorOption fails remove it from the database
|
||||
if err = mirror_service.AddPushMirrorRemote(ctx, pushMirror, address); err != nil {
|
||||
if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: pushMirror.ID, RepoID: pushMirror.RepoID}); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
m, err := convert.ToPushMirror(ctx, pushMirror)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, m)
|
||||
}
|
||||
|
||||
func HandleRemoteAddressError(ctx *context.APIContext, err error) {
|
||||
if git.IsErrInvalidCloneAddr(err) {
|
||||
addrErr := err.(*git.ErrInvalidCloneAddr)
|
||||
switch {
|
||||
case addrErr.IsProtocolInvalid:
|
||||
ctx.APIError(http.StatusBadRequest, "Invalid mirror protocol")
|
||||
case addrErr.IsURLError:
|
||||
ctx.APIError(http.StatusBadRequest, "Invalid Url ")
|
||||
case addrErr.IsPermissionDenied:
|
||||
ctx.APIError(http.StatusUnauthorized, "Permission denied")
|
||||
default:
|
||||
ctx.APIError(http.StatusBadRequest, "Unknown error")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestPushMirrorSync verifies the endpoint attempts every push mirror instead
|
||||
// of aborting on the first failure, reporting all failed remotes with a 422.
|
||||
// Each remote name is not a configured git remote, so SyncPushMirror fails fast
|
||||
// without any network access.
|
||||
func TestPushMirrorSync(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
|
||||
|
||||
for _, remoteName := range []string{"broken_remote_1", "broken_remote_2"} {
|
||||
assert.NoError(t, db.Insert(t.Context(), &repo_model.PushMirror{RepoID: 1, RemoteName: remoteName}))
|
||||
}
|
||||
|
||||
ctx, resp := contexttest.MockAPIContext(t, "user2/repo1")
|
||||
contexttest.LoadRepo(t, ctx, 1)
|
||||
|
||||
PushMirrorSync(ctx)
|
||||
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus())
|
||||
assert.Contains(t, resp.Body.String(), "broken_remote_1")
|
||||
assert.Contains(t, resp.Body.String(), "broken_remote_2")
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/modules/git"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// GetNote Get a note corresponding to a single commit from a repository
|
||||
func GetNote(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/git/notes/{sha} repository repoGetNote
|
||||
// ---
|
||||
// summary: Get a note corresponding to a single commit from a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: sha
|
||||
// in: path
|
||||
// description: a git ref or commit sha
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: verification
|
||||
// in: query
|
||||
// description: include verification for every commit (disable for speedup, default 'true')
|
||||
// type: boolean
|
||||
// - name: files
|
||||
// in: query
|
||||
// description: include a list of affected files for every commit (disable for speedup, default 'true')
|
||||
// type: boolean
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Note"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
sha := ctx.PathParam("sha")
|
||||
if !git.IsValidRefPattern(sha) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "no valid ref or sha: "+sha)
|
||||
return
|
||||
}
|
||||
getNote(ctx, sha)
|
||||
}
|
||||
|
||||
func getNote(ctx *context.APIContext, identifier string) {
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
ctx.APIErrorInternal(errors.New("no open git repo"))
|
||||
return
|
||||
}
|
||||
|
||||
commitID, err := ctx.Repo.GitRepo.ConvertToGitID(identifier)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var note git.Note
|
||||
if err := git.GetNote(ctx, ctx.Repo.GitRepo, commitID.String(), ¬e); err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.APIErrorNotFound("commit doesn't exist: " + identifier)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
|
||||
files := ctx.FormString("files") == "" || ctx.FormBool("files")
|
||||
|
||||
cmt, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, note.Commit, nil,
|
||||
convert.ToCommitOptions{
|
||||
Stat: true,
|
||||
Verification: verification,
|
||||
Files: files,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
apiNote := api.Note{Message: string(note.Message), Commit: cmt}
|
||||
ctx.JSON(http.StatusOK, apiNote)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/repository/files"
|
||||
)
|
||||
|
||||
// ApplyDiffPatch handles API call for applying a patch
|
||||
func ApplyDiffPatch(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/diffpatch repository repoApplyDiffPatch
|
||||
// ---
|
||||
// summary: Apply diff patch to repository
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ApplyDiffPatchFileOptions"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/FileResponse"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
apiOpts, changeRepoFileOpts := getAPIChangeRepoFileOptions[*api.ApplyDiffPatchFileOptions](ctx)
|
||||
opts := &files.ApplyDiffPatchOptions{
|
||||
Content: apiOpts.Content,
|
||||
Message: util.IfZero(apiOpts.Message, "apply-patch"),
|
||||
|
||||
OldBranch: changeRepoFileOpts.OldBranch,
|
||||
NewBranch: changeRepoFileOpts.NewBranch,
|
||||
Committer: changeRepoFileOpts.Committer,
|
||||
Author: changeRepoFileOpts.Author,
|
||||
Dates: changeRepoFileOpts.Dates,
|
||||
Signoff: changeRepoFileOpts.Signoff,
|
||||
}
|
||||
|
||||
fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts)
|
||||
if err != nil {
|
||||
handleChangeRepoFilesError(ctx, err)
|
||||
} else {
|
||||
ctx.JSON(http.StatusCreated, fileResponse)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,443 @@
|
||||
// Copyright 2016 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/modules/git"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
release_service "gitea.dev/services/release"
|
||||
)
|
||||
|
||||
func canAccessReleaseDraft(ctx *context.APIContext) bool {
|
||||
if !ctx.IsSigned || !ctx.Repo.Permission.CanWrite(unit.TypeReleases) {
|
||||
return false
|
||||
}
|
||||
if ctx.Data["IsApiToken"] != true {
|
||||
// not API token request, the request is from a user session with write access
|
||||
return true
|
||||
}
|
||||
// the request is from an access token with scope
|
||||
scope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
requiredScopes := auth_model.GetRequiredScopes(auth_model.Write, auth_model.AccessTokenScopeCategoryRepository)
|
||||
allow, _ := scope.HasScope(requiredScopes...) // err (invalid token) can be safely ignored
|
||||
return allow
|
||||
}
|
||||
|
||||
// GetRelease get a single release of a repository
|
||||
func GetRelease(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/releases/{id} repository repoGetRelease
|
||||
// ---
|
||||
// summary: Get a release
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the release to get
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Release"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
id := ctx.PathParamInt64("id")
|
||||
release, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id)
|
||||
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if err != nil && repo_model.IsErrReleaseNotExist(err) || release.IsTag {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if release.IsDraft && !canAccessReleaseDraft(ctx) { // only the users with write access can see draft releases
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if err := release.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, release))
|
||||
}
|
||||
|
||||
// GetLatestRelease gets the most recent non-prerelease, non-draft release of a repository, sorted by created_at
|
||||
func GetLatestRelease(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/releases/latest repository repoGetLatestRelease
|
||||
// ---
|
||||
// summary: Gets the most recent non-prerelease, non-draft release of a repository, sorted by created_at
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Release"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if err != nil && repo_model.IsErrReleaseNotExist(err) ||
|
||||
release.IsTag || release.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if err := release.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, release))
|
||||
}
|
||||
|
||||
// ListReleases list a repository's releases
|
||||
func ListReleases(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/releases repository repoListReleases
|
||||
// ---
|
||||
// summary: List a repo's releases
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: draft
|
||||
// in: query
|
||||
// description: filter (exclude / include) drafts, if you don't have repo write access none will show
|
||||
// type: boolean
|
||||
// - name: pre-release
|
||||
// in: query
|
||||
// description: filter (exclude / include) pre-releases
|
||||
// type: boolean
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ReleaseList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
opts := repo_model.FindReleasesOptions{
|
||||
ListOptions: listOptions,
|
||||
IncludeDrafts: canAccessReleaseDraft(ctx),
|
||||
IncludeTags: false,
|
||||
IsDraft: ctx.FormOptionalBool("draft"),
|
||||
IsPreRelease: ctx.FormOptionalBool("pre-release"),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
}
|
||||
|
||||
releases, err := db.Find[repo_model.Release](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
rels := make([]*api.Release, len(releases))
|
||||
for i, release := range releases {
|
||||
if err := release.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
rels[i] = convert.ToAPIRelease(ctx, ctx.Repo.Repository, release)
|
||||
}
|
||||
|
||||
filteredCount, err := db.Count[repo_model.Release](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(filteredCount, listOptions.PageSize)
|
||||
ctx.SetTotalCountHeader(filteredCount)
|
||||
ctx.JSON(http.StatusOK, rels)
|
||||
}
|
||||
|
||||
// CreateRelease create a release
|
||||
func CreateRelease(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/releases repository repoCreateRelease
|
||||
// ---
|
||||
// summary: Create a release
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateReleaseOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Release"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "409":
|
||||
// "$ref": "#/responses/error"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateReleaseOption)
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, errors.New("repo is empty"))
|
||||
return
|
||||
}
|
||||
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName)
|
||||
if err != nil {
|
||||
if !repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
// If target is not provided use default branch
|
||||
if len(form.Target) == 0 {
|
||||
form.Target = ctx.Repo.Repository.DefaultBranch
|
||||
}
|
||||
rel = &repo_model.Release{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
PublisherID: ctx.Doer.ID,
|
||||
Publisher: ctx.Doer,
|
||||
TagName: form.TagName,
|
||||
Target: form.Target,
|
||||
Title: form.Title,
|
||||
Note: form.Note,
|
||||
IsDraft: form.IsDraft,
|
||||
IsPrerelease: form.IsPrerelease,
|
||||
IsTag: false,
|
||||
Repo: ctx.Repo.Repository,
|
||||
}
|
||||
// GitHub doesn't have "tag_message", GitLab has: https://docs.gitlab.com/api/releases/#create-a-release
|
||||
// It doesn't need to be the same as the "release note"
|
||||
if err := release_service.CreateRelease(ctx.Repo.GitRepo, rel, nil, form.TagMessage); err != nil {
|
||||
if repo_model.IsErrReleaseAlreadyExist(err) {
|
||||
ctx.APIError(http.StatusConflict, err)
|
||||
} else if release_service.IsErrProtectedTagName(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
} else if git.IsErrNotExist(err) {
|
||||
ctx.APIError(http.StatusNotFound, fmt.Errorf("target \"%v\" not found: %w", rel.Target, err))
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !rel.IsTag {
|
||||
ctx.APIError(http.StatusConflict, "Release is has no Tag")
|
||||
return
|
||||
}
|
||||
|
||||
rel.Title = form.Title
|
||||
rel.Note = form.Note
|
||||
rel.IsDraft = form.IsDraft
|
||||
rel.IsPrerelease = form.IsPrerelease
|
||||
rel.PublisherID = ctx.Doer.ID
|
||||
rel.IsTag = false
|
||||
rel.Repo = ctx.Repo.Repository
|
||||
rel.Publisher = ctx.Doer
|
||||
rel.Target = form.Target
|
||||
|
||||
if err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIRelease(ctx, ctx.Repo.Repository, rel))
|
||||
}
|
||||
|
||||
// EditRelease edit a release
|
||||
func EditRelease(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/releases/{id} repository repoEditRelease
|
||||
// ---
|
||||
// summary: Update a release
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the release to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditReleaseOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Release"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditReleaseOption)
|
||||
id := ctx.PathParamInt64("id")
|
||||
rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id)
|
||||
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if err != nil && repo_model.IsErrReleaseNotExist(err) || rel.IsTag {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if len(form.TagName) > 0 {
|
||||
rel.TagName = form.TagName
|
||||
}
|
||||
if len(form.Target) > 0 {
|
||||
rel.Target = form.Target
|
||||
}
|
||||
if len(form.Title) > 0 {
|
||||
rel.Title = form.Title
|
||||
}
|
||||
if len(form.Note) > 0 {
|
||||
rel.Note = form.Note
|
||||
}
|
||||
if form.IsDraft != nil {
|
||||
rel.IsDraft = *form.IsDraft
|
||||
}
|
||||
if form.IsPrerelease != nil {
|
||||
rel.IsPrerelease = *form.IsPrerelease
|
||||
}
|
||||
if err := release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// reload data from database
|
||||
rel, err = repo_model.GetReleaseByID(ctx, id)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, rel))
|
||||
}
|
||||
|
||||
// DeleteRelease delete a release from a repository
|
||||
func DeleteRelease(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/releases/{id} repository repoDeleteRelease
|
||||
// ---
|
||||
// summary: Delete a release
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the release to delete
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
id := ctx.PathParamInt64("id")
|
||||
rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id)
|
||||
if err != nil && !repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if err != nil && repo_model.IsErrReleaseNotExist(err) || rel.IsTag {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
if err := release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, rel, ctx.Doer, false); err != nil {
|
||||
if release_service.IsErrProtectedTagName(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "user not allowed to delete protected tag")
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
attachment_service "gitea.dev/services/attachment"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/context/upload"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
func checkReleaseMatchRepo(ctx *context.APIContext, releaseID int64) bool {
|
||||
release, err := repo_model.GetReleaseByID(ctx, releaseID)
|
||||
if err != nil {
|
||||
if repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
return false
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return false
|
||||
}
|
||||
if release.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.APIErrorNotFound()
|
||||
return false
|
||||
}
|
||||
if release.IsDraft && !canAccessReleaseDraft(ctx) {
|
||||
ctx.APIErrorNotFound()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetReleaseAttachment gets a single attachment of the release
|
||||
func GetReleaseAttachment(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/releases/{id}/assets/{attachment_id} repository repoGetReleaseAttachment
|
||||
// ---
|
||||
// summary: Get a release attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the release
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: attachment_id
|
||||
// in: path
|
||||
// description: id of the attachment to get
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
releaseID := ctx.PathParamInt64("id")
|
||||
if !checkReleaseMatchRepo(ctx, releaseID) {
|
||||
return
|
||||
}
|
||||
|
||||
attachID := ctx.PathParamInt64("attachment_id")
|
||||
attach, err := repo_model.GetAttachmentByID(ctx, attachID)
|
||||
if err != nil {
|
||||
if repo_model.IsErrAttachmentNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if attach.ReleaseID != releaseID {
|
||||
log.Info("User requested attachment is not in release, release_id %v, attachment_id: %v", releaseID, attachID)
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
// FIXME Should prove the existence of the given repo, but results in unnecessary database requests
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
|
||||
}
|
||||
|
||||
// ListReleaseAttachments lists all attachments of the release
|
||||
func ListReleaseAttachments(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/releases/{id}/assets repository repoListReleaseAttachments
|
||||
// ---
|
||||
// summary: List release's attachments
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the release
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/AttachmentList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
releaseID := ctx.PathParamInt64("id")
|
||||
release, err := repo_model.GetReleaseByID(ctx, releaseID)
|
||||
if err != nil {
|
||||
if repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if release.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
if release.IsDraft && !canAccessReleaseDraft(ctx) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
if err := release.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, release).Attachments)
|
||||
}
|
||||
|
||||
// CreateReleaseAttachment creates an attachment and saves the given file
|
||||
func CreateReleaseAttachment(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/releases/{id}/assets repository repoCreateReleaseAttachment
|
||||
// ---
|
||||
// summary: Create a release attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
// - application/octet-stream
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the release
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: name
|
||||
// in: query
|
||||
// description: name of the attachment
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: attachment
|
||||
// in: formData
|
||||
// description: attachment to upload
|
||||
// type: file
|
||||
// required: false
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "413":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
// Check if attachments are enabled
|
||||
if !setting.Attachment.Enabled {
|
||||
ctx.APIErrorNotFound("Attachment is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if release exists an load release
|
||||
releaseID := ctx.PathParamInt64("id")
|
||||
if !checkReleaseMatchRepo(ctx, releaseID) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get uploaded file from request
|
||||
var filename string
|
||||
var uploaderFile *attachment_service.UploaderFile
|
||||
if strings.HasPrefix(strings.ToLower(ctx.Req.Header.Get("Content-Type")), "multipart/form-data") {
|
||||
file, header, err := ctx.Req.FormFile("attachment")
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
filename = header.Filename
|
||||
if name := ctx.FormString("name"); name != "" {
|
||||
filename = name
|
||||
}
|
||||
uploaderFile = attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
|
||||
} else {
|
||||
filename = ctx.FormString("name")
|
||||
uploaderFile = attachment_service.NewLimitedUploaderMaxBytesReader(ctx.Req.Body, ctx.Resp)
|
||||
}
|
||||
|
||||
if filename == "" {
|
||||
ctx.APIError(http.StatusBadRequest, "Could not determine name of attachment.")
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new attachment and save the file
|
||||
attach, err := attachment_service.UploadAttachmentForRelease(ctx, uploaderFile, &repo_model.Attachment{
|
||||
Name: filename,
|
||||
UploaderID: ctx.Doer.ID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
ReleaseID: releaseID,
|
||||
})
|
||||
if err != nil {
|
||||
if upload.IsErrFileTypeForbidden(err) {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, util.ErrContentTooLarge) {
|
||||
ctx.APIError(http.StatusRequestEntityTooLarge, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
|
||||
}
|
||||
|
||||
// EditReleaseAttachment updates the given attachment
|
||||
func EditReleaseAttachment(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/releases/{id}/assets/{attachment_id} repository repoEditReleaseAttachment
|
||||
// ---
|
||||
// summary: Edit a release attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// consumes:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the release
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: attachment_id
|
||||
// in: path
|
||||
// description: id of the attachment to edit
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditAttachmentOptions"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditAttachmentOptions)
|
||||
|
||||
// Check if release exists an load release
|
||||
releaseID := ctx.PathParamInt64("id")
|
||||
if !checkReleaseMatchRepo(ctx, releaseID) {
|
||||
return
|
||||
}
|
||||
|
||||
attachID := ctx.PathParamInt64("attachment_id")
|
||||
attach, err := repo_model.GetAttachmentByID(ctx, attachID)
|
||||
if err != nil {
|
||||
if repo_model.IsErrAttachmentNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if attach.ReleaseID != releaseID {
|
||||
log.Info("User requested attachment is not in release, release_id %v, attachment_id: %v", releaseID, attachID)
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
// FIXME Should prove the existence of the given repo, but results in unnecessary database requests
|
||||
if form.Name != "" {
|
||||
attach.Name = form.Name
|
||||
}
|
||||
|
||||
if err := attachment_service.UpdateAttachment(ctx, setting.Repository.Release.AllowedTypes, attach); err != nil {
|
||||
if upload.IsErrFileTypeForbidden(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
|
||||
}
|
||||
|
||||
// DeleteReleaseAttachment delete a given attachment
|
||||
func DeleteReleaseAttachment(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/releases/{id}/assets/{attachment_id} repository repoDeleteReleaseAttachment
|
||||
// ---
|
||||
// summary: Delete a release attachment
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the release
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: attachment_id
|
||||
// in: path
|
||||
// description: id of the attachment to delete
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
// Check if release exists an load release
|
||||
releaseID := ctx.PathParamInt64("id")
|
||||
if !checkReleaseMatchRepo(ctx, releaseID) {
|
||||
return
|
||||
}
|
||||
|
||||
attachID := ctx.PathParamInt64("attachment_id")
|
||||
attach, err := repo_model.GetAttachmentByID(ctx, attachID)
|
||||
if err != nil {
|
||||
if repo_model.IsErrAttachmentNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if attach.ReleaseID != releaseID {
|
||||
log.Info("User requested attachment is not in release, release_id %v, attachment_id: %v", releaseID, attachID)
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_model.DeleteAttachment(ctx, attach, true); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
unit_model "gitea.dev/models/unit"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
release_service "gitea.dev/services/release"
|
||||
)
|
||||
|
||||
// GetReleaseByTag get a single release of a repository by tag name
|
||||
func GetReleaseByTag(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/releases/tags/{tag} repository repoGetReleaseByTag
|
||||
// ---
|
||||
// summary: Get a release by tag name
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: tag
|
||||
// in: path
|
||||
// description: tag name of the release to get
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Release"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
tag := ctx.PathParam("tag")
|
||||
|
||||
release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tag)
|
||||
if err != nil {
|
||||
if repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if release.IsTag {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if release.IsDraft { // only the users with write access can see draft releases
|
||||
if !ctx.IsSigned || !ctx.Repo.Permission.CanWrite(unit_model.TypeReleases) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = release.LoadAttributes(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, release))
|
||||
}
|
||||
|
||||
// DeleteReleaseByTag delete a release from a repository by tag name
|
||||
func DeleteReleaseByTag(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/releases/tags/{tag} repository repoDeleteReleaseByTag
|
||||
// ---
|
||||
// summary: Delete a release by tag name
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: tag
|
||||
// in: path
|
||||
// description: tag name of the release to delete
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
tag := ctx.PathParam("tag")
|
||||
|
||||
release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tag)
|
||||
if err != nil {
|
||||
if repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if release.IsTag {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if err = release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, release, ctx.Doer, false); err != nil {
|
||||
if release_service.IsErrProtectedTagName(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "user not allowed to delete protected tag")
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRepoEdit(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
ctx, _ := contexttest.MockAPIContext(t, "user2/repo1")
|
||||
contexttest.LoadRepo(t, ctx, 1)
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
ctx.Repo.Owner = ctx.Doer
|
||||
description := "new description"
|
||||
website := "http://wwww.newwebsite.com"
|
||||
private := true
|
||||
hasIssues := false
|
||||
hasWiki := false
|
||||
defaultBranch := "master"
|
||||
hasPullRequests := true
|
||||
ignoreWhitespaceConflicts := true
|
||||
allowMerge := false
|
||||
allowRebase := false
|
||||
allowRebaseMerge := false
|
||||
allowSquashMerge := false
|
||||
allowFastForwardOnlyMerge := false
|
||||
archived := true
|
||||
opts := api.EditRepoOption{
|
||||
Name: &ctx.Repo.Repository.Name,
|
||||
Description: &description,
|
||||
Website: &website,
|
||||
Private: &private,
|
||||
HasIssues: &hasIssues,
|
||||
HasWiki: &hasWiki,
|
||||
DefaultBranch: &defaultBranch,
|
||||
HasPullRequests: &hasPullRequests,
|
||||
IgnoreWhitespaceConflicts: &ignoreWhitespaceConflicts,
|
||||
AllowMerge: &allowMerge,
|
||||
AllowRebase: &allowRebase,
|
||||
AllowRebaseMerge: &allowRebaseMerge,
|
||||
AllowSquash: &allowSquashMerge,
|
||||
AllowFastForwardOnly: &allowFastForwardOnlyMerge,
|
||||
Archived: &archived,
|
||||
}
|
||||
|
||||
web.SetForm(ctx, &opts)
|
||||
Edit(ctx)
|
||||
|
||||
assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
|
||||
ID: 1,
|
||||
}, unittest.Cond("name = ? AND is_archived = 1", *opts.Name))
|
||||
}
|
||||
|
||||
func TestRepoEditNameChange(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
ctx, _ := contexttest.MockAPIContext(t, "user2/repo1")
|
||||
contexttest.LoadRepo(t, ctx, 1)
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
ctx.Repo.Owner = ctx.Doer
|
||||
name := "newname"
|
||||
opts := api.EditRepoOption{
|
||||
Name: &name,
|
||||
}
|
||||
|
||||
web.SetForm(ctx, &opts)
|
||||
Edit(ctx)
|
||||
assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
|
||||
ID: 1,
|
||||
}, unittest.Cond("name = ?", opts.Name))
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// ListStargazers list a repository's stargazers
|
||||
func ListStargazers(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/stargazers repository repoListStargazers
|
||||
// ---
|
||||
// summary: List a repo's stargazers
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
|
||||
stargazers, err := repo_model.GetStargazers(ctx, ctx.Repo.Repository, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
users := make([]*api.User, len(stargazers))
|
||||
for i, stargazer := range stargazers {
|
||||
users[i] = convert.ToUser(ctx, stargazer, ctx.Doer)
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumStars))
|
||||
ctx.JSON(http.StatusOK, users)
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// Copyright 2017 Gitea. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
git_model "gitea.dev/models/git"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
commitstatus_service "gitea.dev/services/repository/commitstatus"
|
||||
)
|
||||
|
||||
// NewCommitStatus creates a new CommitStatus
|
||||
func NewCommitStatus(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/statuses/{sha} repository repoCreateStatus
|
||||
// ---
|
||||
// summary: Create a commit status
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: sha
|
||||
// in: path
|
||||
// description: sha of the commit
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateStatusOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/CommitStatus"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateStatusOption)
|
||||
sha := ctx.PathParam("sha")
|
||||
if len(sha) == 0 {
|
||||
ctx.APIError(http.StatusBadRequest, nil)
|
||||
return
|
||||
}
|
||||
status := &git_model.CommitStatus{
|
||||
State: form.State,
|
||||
TargetURL: form.TargetURL,
|
||||
Description: form.Description,
|
||||
Context: form.Context,
|
||||
}
|
||||
if err := commitstatus_service.CreateCommitStatus(ctx, ctx.Repo.Repository, ctx.Doer, sha, status); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToCommitStatus(ctx, status))
|
||||
}
|
||||
|
||||
// GetCommitStatuses returns all statuses for any given commit hash
|
||||
func GetCommitStatuses(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/statuses/{sha} repository repoListStatuses
|
||||
// ---
|
||||
// summary: Get a commit's statuses
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: sha
|
||||
// in: path
|
||||
// description: sha of the commit
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: sort
|
||||
// in: query
|
||||
// description: type of sort
|
||||
// type: string
|
||||
// enum: [oldest, recentupdate, leastupdate, leastindex, highestindex]
|
||||
// required: false
|
||||
// - name: state
|
||||
// in: query
|
||||
// description: type of state
|
||||
// type: string
|
||||
// enum: [pending, success, error, failure, warning]
|
||||
// required: false
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/CommitStatusList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
getCommitStatuses(ctx, ctx.PathParam("sha"))
|
||||
}
|
||||
|
||||
// GetCommitStatusesByRef returns all statuses for any given commit ref
|
||||
func GetCommitStatusesByRef(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/commits/{ref}/statuses repository repoListStatusesByRef
|
||||
// ---
|
||||
// summary: Get a commit's statuses, by branch/tag/commit reference
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: path
|
||||
// description: name of branch/tag/commit
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: sort
|
||||
// in: query
|
||||
// description: type of sort
|
||||
// type: string
|
||||
// enum: [oldest, recentupdate, leastupdate, leastindex, highestindex]
|
||||
// required: false
|
||||
// - name: state
|
||||
// in: query
|
||||
// description: type of state
|
||||
// type: string
|
||||
// enum: [pending, success, error, failure, warning]
|
||||
// required: false
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/CommitStatusList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
refCommit := resolveRefCommit(ctx, ctx.PathParam("ref"), 7)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
getCommitStatuses(ctx, refCommit.CommitID)
|
||||
}
|
||||
|
||||
func getCommitStatuses(ctx *context.APIContext, commitID string) {
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
statuses, maxResults, err := db.FindAndCount[git_model.CommitStatus](ctx, &git_model.CommitStatusOptions{
|
||||
ListOptions: listOptions,
|
||||
RepoID: repo.ID,
|
||||
SHA: commitID,
|
||||
SortType: ctx.FormTrim("sort"),
|
||||
State: ctx.FormTrim("state"),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(fmt.Errorf("GetCommitStatuses[%s, %s, %d]: %w", repo.FullName(), commitID, ctx.FormInt("page"), err))
|
||||
return
|
||||
}
|
||||
|
||||
apiStatuses := make([]*api.CommitStatus, 0, len(statuses))
|
||||
for _, status := range statuses {
|
||||
apiStatuses = append(apiStatuses, convert.ToCommitStatus(ctx, status))
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(maxResults, listOptions.PageSize)
|
||||
ctx.SetTotalCountHeader(maxResults)
|
||||
|
||||
ctx.JSON(http.StatusOK, apiStatuses)
|
||||
}
|
||||
|
||||
// GetCombinedCommitStatusByRef returns the combined status for any given commit hash
|
||||
func GetCombinedCommitStatusByRef(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/commits/{ref}/status repository repoGetCombinedStatusByRef
|
||||
// ---
|
||||
// summary: Get a commit's combined status, by branch/tag/commit reference
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: ref
|
||||
// in: path
|
||||
// description: name of branch/tag/commit
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/CombinedStatus"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
refCommit := resolveRefCommit(ctx, ctx.PathParam("ref"), 7)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, refCommit.Commit.ID.String(), listOptions)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(fmt.Errorf("GetLatestCommitStatus[%s, %s]: %w", repo.FullName(), refCommit.CommitID, err))
|
||||
return
|
||||
}
|
||||
|
||||
count, err := git_model.CountLatestCommitStatus(ctx, repo.ID, refCommit.Commit.ID.String())
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(fmt.Errorf("CountLatestCommitStatus[%s, %s]: %w", repo.FullName(), refCommit.CommitID, err))
|
||||
return
|
||||
}
|
||||
ctx.SetLinkHeader(count, listOptions.PageSize)
|
||||
ctx.SetTotalCountHeader(count)
|
||||
|
||||
combiStatus := convert.ToCombinedStatus(ctx, refCommit.Commit.ID.String(), statuses,
|
||||
convert.ToRepo(ctx, repo, ctx.Repo.Permission))
|
||||
ctx.JSON(http.StatusOK, combiStatus)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// ListSubscribers list a repo's subscribers (i.e. watchers)
|
||||
func ListSubscribers(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/subscribers repository repoListSubscribers
|
||||
// ---
|
||||
// summary: List a repo's watchers
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
subscribers, err := repo_model.GetRepoWatchers(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
users := make([]*api.User, len(subscribers))
|
||||
for i, subscriber := range subscribers {
|
||||
users[i] = convert.ToUser(ctx, subscriber, ctx.Doer)
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumWatches))
|
||||
ctx.JSON(http.StatusOK, users)
|
||||
}
|
||||
@@ -0,0 +1,641 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
git_model "gitea.dev/models/git"
|
||||
"gitea.dev/models/organization"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
release_service "gitea.dev/services/release"
|
||||
)
|
||||
|
||||
// ListTags list all the tags of a repository
|
||||
func ListTags(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/tags repository repoListTags
|
||||
// ---
|
||||
// summary: List a repository's tags
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results, default maximum page size is 50
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TagList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
listOpts := utils.GetListOptions(ctx)
|
||||
|
||||
tags, total, err := ctx.Repo.GitRepo.GetTagInfos(listOpts.Page, listOpts.PageSize)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiTags := make([]*api.Tag, len(tags))
|
||||
for i := range tags {
|
||||
apiTags[i] = convert.ToTag(ctx.Repo.Repository, tags[i])
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(int64(total))
|
||||
ctx.JSON(http.StatusOK, &apiTags)
|
||||
}
|
||||
|
||||
// GetAnnotatedTag get the tag of a repository.
|
||||
func GetAnnotatedTag(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/git/tags/{sha} repository GetAnnotatedTag
|
||||
// ---
|
||||
// summary: Gets the tag object of an annotated tag (not lightweight tags)
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: sha
|
||||
// in: path
|
||||
// description: sha of the tag. The Git tags API only supports annotated tag objects, not lightweight tags.
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/AnnotatedTag"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
sha := ctx.PathParam("sha")
|
||||
if len(sha) == 0 {
|
||||
ctx.APIError(http.StatusBadRequest, "SHA not provided")
|
||||
return
|
||||
}
|
||||
|
||||
tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
commit, err := ctx.Repo.GitRepo.GetTagCommit(tag.Name)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit))
|
||||
}
|
||||
|
||||
// GetTag get the tag of a repository
|
||||
func GetTag(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/tags/{tag} repository repoGetTag
|
||||
// ---
|
||||
// summary: Get the tag of a repository by tag name
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: tag
|
||||
// in: path
|
||||
// description: name of tag
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Tag"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
tagName := ctx.PathParam("*")
|
||||
|
||||
tag, err := ctx.Repo.GitRepo.GetTag(tagName)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound("tag doesn't exist: " + tagName)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToTag(ctx.Repo.Repository, tag))
|
||||
}
|
||||
|
||||
// CreateTag create a new git tag in a repository
|
||||
func CreateTag(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/tags repository repoCreateTag
|
||||
// ---
|
||||
// summary: Create a new git tag in a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateTagOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Tag"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "405":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "409":
|
||||
// "$ref": "#/responses/conflict"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
form := web.GetForm(ctx).(*api.CreateTagOption)
|
||||
|
||||
// If target is not provided use default branch
|
||||
if len(form.Target) == 0 {
|
||||
form.Target = ctx.Repo.Repository.DefaultBranch
|
||||
}
|
||||
|
||||
commit, err := ctx.Repo.GitRepo.GetCommit(form.Target)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusNotFound, fmt.Errorf("target not found: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, commit.ID.String(), form.TagName, form.Message); err != nil {
|
||||
if release_service.IsErrTagAlreadyExists(err) {
|
||||
ctx.APIError(http.StatusConflict, err)
|
||||
return
|
||||
}
|
||||
if release_service.IsErrProtectedTagName(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "user not allowed to create protected tag")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
tag, err := ctx.Repo.GitRepo.GetTag(form.TagName)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusCreated, convert.ToTag(ctx.Repo.Repository, tag))
|
||||
}
|
||||
|
||||
// DeleteTag delete a specific tag of in a repository by name
|
||||
func DeleteTag(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/tags/{tag} repository repoDeleteTag
|
||||
// ---
|
||||
// summary: Delete a repository's tag by name
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: tag
|
||||
// in: path
|
||||
// description: name of tag to delete
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "405":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "409":
|
||||
// "$ref": "#/responses/conflict"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
tagName := ctx.PathParam("*")
|
||||
|
||||
tag, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tagName)
|
||||
if err != nil {
|
||||
if repo_model.IsErrReleaseNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !tag.IsTag {
|
||||
ctx.APIError(http.StatusConflict, errors.New("a tag attached to a release cannot be deleted directly"))
|
||||
return
|
||||
}
|
||||
|
||||
if err = release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, tag, ctx.Doer, true); err != nil {
|
||||
if release_service.IsErrProtectedTagName(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "user not allowed to delete protected tag")
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ListTagProtection lists tag protections for a repo
|
||||
func ListTagProtection(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/tag_protections repository repoListTagProtection
|
||||
// ---
|
||||
// summary: List tag protections for a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TagProtectionList"
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
pts, err := git_model.GetProtectedTags(ctx, repo.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
apiPts := make([]*api.TagProtection, len(pts))
|
||||
for i := range pts {
|
||||
apiPts[i] = convert.ToTagProtection(ctx, pts[i], repo)
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, apiPts)
|
||||
}
|
||||
|
||||
// GetTagProtection gets a tag protection
|
||||
func GetTagProtection(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/tag_protections/{id} repository repoGetTagProtection
|
||||
// ---
|
||||
// summary: Get a specific tag protection for the repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the tag protect to get
|
||||
// type: integer
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TagProtection"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
id := ctx.PathParamInt64("id")
|
||||
pt, err := git_model.GetProtectedTagByID(ctx, id)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if pt == nil || repo.ID != pt.RepoID {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToTagProtection(ctx, pt, repo))
|
||||
}
|
||||
|
||||
// CreateTagProtection creates a tag protection for a repo
|
||||
func CreateTagProtection(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/tag_protections repository repoCreateTagProtection
|
||||
// ---
|
||||
// summary: Create a tag protections for a repository
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateTagProtectionOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/TagProtection"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateTagProtectionOption)
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
namePattern := strings.TrimSpace(form.NamePattern)
|
||||
if namePattern == "" {
|
||||
ctx.APIError(http.StatusBadRequest, "name_pattern are empty")
|
||||
return
|
||||
}
|
||||
|
||||
if len(form.WhitelistUsernames) == 0 && len(form.WhitelistTeams) == 0 {
|
||||
ctx.APIError(http.StatusBadRequest, "both whitelist_usernames and whitelist_teams are empty")
|
||||
return
|
||||
}
|
||||
|
||||
pt, err := git_model.GetProtectedTagByNamePattern(ctx, repo.ID, namePattern)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
} else if pt != nil {
|
||||
ctx.APIError(http.StatusForbidden, "Tag protection already exist")
|
||||
return
|
||||
}
|
||||
|
||||
var whitelistUsers, whitelistTeams []int64
|
||||
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if repo.Owner.IsOrganization() {
|
||||
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false)
|
||||
if err != nil {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
protectTag := &git_model.ProtectedTag{
|
||||
RepoID: repo.ID,
|
||||
NamePattern: strings.TrimSpace(namePattern),
|
||||
AllowlistUserIDs: whitelistUsers,
|
||||
AllowlistTeamIDs: whitelistTeams,
|
||||
}
|
||||
if err := git_model.InsertProtectedTag(ctx, protectTag); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
pt, err = git_model.GetProtectedTagByID(ctx, protectTag.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if pt == nil || pt.RepoID != repo.ID {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToTagProtection(ctx, pt, repo))
|
||||
}
|
||||
|
||||
// EditTagProtection edits a tag protection for a repo
|
||||
func EditTagProtection(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/tag_protections/{id} repository repoEditTagProtection
|
||||
// ---
|
||||
// summary: Edit a tag protections for a repository. Only fields that are set will be changed
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of protected tag
|
||||
// type: integer
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/EditTagProtectionOption"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TagProtection"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
form := web.GetForm(ctx).(*api.EditTagProtectionOption)
|
||||
|
||||
id := ctx.PathParamInt64("id")
|
||||
pt, err := git_model.GetProtectedTagByID(ctx, id)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if pt == nil || pt.RepoID != repo.ID {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if form.NamePattern != nil {
|
||||
pt.NamePattern = *form.NamePattern
|
||||
}
|
||||
|
||||
var whitelistUsers, whitelistTeams []int64
|
||||
if form.WhitelistTeams != nil {
|
||||
if repo.Owner.IsOrganization() {
|
||||
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false)
|
||||
if err != nil {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
pt.AllowlistTeamIDs = whitelistTeams
|
||||
}
|
||||
|
||||
if form.WhitelistUsernames != nil {
|
||||
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
pt.AllowlistUserIDs = whitelistUsers
|
||||
}
|
||||
|
||||
err = git_model.UpdateProtectedTag(ctx, pt)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
pt, err = git_model.GetProtectedTagByID(ctx, id)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if pt == nil || pt.RepoID != repo.ID {
|
||||
ctx.APIErrorInternal(errors.New("new tag protection not found"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToTagProtection(ctx, pt, repo))
|
||||
}
|
||||
|
||||
// DeleteTagProtection
|
||||
func DeleteTagProtection(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/tag_protections/{id} repository repoDeleteTagProtection
|
||||
// ---
|
||||
// summary: Delete a specific tag protection for the repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of protected tag
|
||||
// type: integer
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
id := ctx.PathParamInt64("id")
|
||||
pt, err := git_model.GetProtectedTagByID(ctx, id)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if pt == nil || pt.RepoID != repo.ID {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
err = git_model.DeleteProtectedTag(ctx, pt)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
// ListTeams list a repository's teams
|
||||
func ListTeams(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/teams repository repoListTeams
|
||||
// ---
|
||||
// summary: List a repository's teams
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TeamList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if !ctx.Repo.Owner.IsOrganization() {
|
||||
ctx.APIError(http.StatusMethodNotAllowed, "repo is not owned by an organization")
|
||||
return
|
||||
}
|
||||
|
||||
teams, err := organization.GetRepoTeams(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
apiTeams, err := convert.ToTeams(ctx, teams, false)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, apiTeams)
|
||||
}
|
||||
|
||||
// IsTeam check if a team is assigned to a repository
|
||||
func IsTeam(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/teams/{team} repository repoCheckTeam
|
||||
// ---
|
||||
// summary: Check if a team is assigned to a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: team
|
||||
// in: path
|
||||
// description: team name
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Team"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "405":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
if !ctx.Repo.Owner.IsOrganization() {
|
||||
ctx.APIError(http.StatusMethodNotAllowed, "repo is not owned by an organization")
|
||||
return
|
||||
}
|
||||
|
||||
team := getTeamByParam(ctx)
|
||||
if team == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if repo_service.HasRepository(ctx, team, ctx.Repo.Repository.ID) {
|
||||
apiTeam, err := convert.ToTeam(ctx, team)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, apiTeam)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.APIErrorNotFound()
|
||||
}
|
||||
|
||||
// AddTeam add a team to a repository
|
||||
func AddTeam(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /repos/{owner}/{repo}/teams/{team} repository repoAddTeam
|
||||
// ---
|
||||
// summary: Add a team to a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: team
|
||||
// in: path
|
||||
// description: team name
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "405":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
changeRepoTeam(ctx, true)
|
||||
}
|
||||
|
||||
// DeleteTeam delete a team from a repository
|
||||
func DeleteTeam(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/teams/{team} repository repoDeleteTeam
|
||||
// ---
|
||||
// summary: Delete a team from a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: team
|
||||
// in: path
|
||||
// description: team name
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "405":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
changeRepoTeam(ctx, false)
|
||||
}
|
||||
|
||||
func changeRepoTeam(ctx *context.APIContext, add bool) {
|
||||
if !ctx.Repo.Owner.IsOrganization() {
|
||||
ctx.APIError(http.StatusMethodNotAllowed, "repo is not owned by an organization")
|
||||
}
|
||||
if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.Permission.IsOwner() {
|
||||
ctx.APIError(http.StatusForbidden, "user is nor repo admin nor owner")
|
||||
return
|
||||
}
|
||||
|
||||
team := getTeamByParam(ctx)
|
||||
if team == nil {
|
||||
return
|
||||
}
|
||||
|
||||
repoHasTeam := repo_service.HasRepository(ctx, team, ctx.Repo.Repository.ID)
|
||||
var err error
|
||||
if add {
|
||||
if repoHasTeam {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team '%s' is already added to repo", team.Name))
|
||||
return
|
||||
}
|
||||
err = repo_service.TeamAddRepository(ctx, team, ctx.Repo.Repository)
|
||||
} else {
|
||||
if !repoHasTeam {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team '%s' was not added to repo", team.Name))
|
||||
return
|
||||
}
|
||||
err = repo_service.RemoveRepositoryFromTeam(ctx, team, ctx.Repo.Repository.ID)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func getTeamByParam(ctx *context.APIContext) *organization.Team {
|
||||
team, err := organization.GetTeam(ctx, ctx.Repo.Owner.ID, ctx.PathParam("team"))
|
||||
if err != nil {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
return nil
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil
|
||||
}
|
||||
return team
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/log"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/api/v1/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// ListTopics returns list of current topics for repo
|
||||
func ListTopics(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/topics repository repoListTopics
|
||||
// ---
|
||||
// summary: Get list of topics that a repository has
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TopicNames"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
opts := &repo_model.FindTopicOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
}
|
||||
|
||||
topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
topicNames := make([]string, len(topics))
|
||||
for i, topic := range topics {
|
||||
topicNames[i] = topic.Name
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(total)
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"topics": topicNames,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateTopics updates repo with a new set of topics
|
||||
func UpdateTopics(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /repos/{owner}/{repo}/topics repository repoUpdateTopics
|
||||
// ---
|
||||
// summary: Replace list of topics for a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/RepoTopicOptions"
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/invalidTopicsError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.RepoTopicOptions)
|
||||
topicNames := form.Topics
|
||||
validTopics, invalidTopics := repo_model.SanitizeAndValidateTopics(topicNames)
|
||||
|
||||
if len(validTopics) > 25 {
|
||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
|
||||
"invalidTopics": nil,
|
||||
"message": "Exceeding maximum number of topics per repo",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(invalidTopics) > 0 {
|
||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
|
||||
"invalidTopics": invalidTopics,
|
||||
"message": "Topic names are invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err := repo_model.SaveTopics(ctx, ctx.Repo.Repository.ID, validTopics...)
|
||||
if err != nil {
|
||||
log.Error("SaveTopics failed: %v", err)
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// AddTopic adds a topic name to a repo
|
||||
func AddTopic(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /repos/{owner}/{repo}/topics/{topic} repository repoAddTopic
|
||||
// ---
|
||||
// summary: Add a topic to a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: topic
|
||||
// in: path
|
||||
// description: name of the topic to add
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/invalidTopicsError"
|
||||
|
||||
topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic")))
|
||||
|
||||
if !repo_model.ValidateTopic(topicName) {
|
||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
|
||||
"invalidTopics": topicName,
|
||||
"message": "Topic name is invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent adding more topics than allowed to repo
|
||||
count, err := db.Count[repo_model.Topic](ctx, &repo_model.FindTopicOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("CountTopics failed: %v", err)
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if count >= 25 {
|
||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
|
||||
"message": "Exceeding maximum allowed topics per repo.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
_, err = repo_model.AddTopic(ctx, ctx.Repo.Repository.ID, topicName)
|
||||
if err != nil {
|
||||
log.Error("AddTopic failed: %v", err)
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// DeleteTopic removes topic name from repo
|
||||
func DeleteTopic(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/topics/{topic} repository repoDeleteTopic
|
||||
// ---
|
||||
// summary: Delete a topic from a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: topic
|
||||
// in: path
|
||||
// description: name of the topic to delete
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/invalidTopicsError"
|
||||
|
||||
topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic")))
|
||||
|
||||
if !repo_model.ValidateTopic(topicName) {
|
||||
ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
|
||||
"invalidTopics": topicName,
|
||||
"message": "Topic name is invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
topic, err := repo_model.DeleteTopic(ctx, ctx.Repo.Repository.ID, topicName)
|
||||
if err != nil {
|
||||
log.Error("DeleteTopic failed: %v", err)
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if topic == nil {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// TopicSearch search for creating topic
|
||||
func TopicSearch(ctx *context.APIContext) {
|
||||
// swagger:operation GET /topics/search repository topicSearch
|
||||
// ---
|
||||
// summary: search topics via keyword
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: q
|
||||
// in: query
|
||||
// description: keywords to search
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/TopicListResponse"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
opts := &repo_model.FindTopicOptions{
|
||||
Keyword: ctx.FormString("q"),
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
}
|
||||
|
||||
topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
topicResponses := make([]*api.TopicResponse, len(topics))
|
||||
for i, topic := range topics {
|
||||
topicResponses[i] = convert.ToTopicResponse(topic)
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(total)
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"topics": topicResponses,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
// Transfer transfers the ownership of a repository
|
||||
func Transfer(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/transfer repository repoTransfer
|
||||
// ---
|
||||
// summary: Transfer a repo ownership
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo to transfer
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo to transfer
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// description: "Transfer Options"
|
||||
// required: true
|
||||
// schema:
|
||||
// "$ref": "#/definitions/TransferRepoOption"
|
||||
// responses:
|
||||
// "202":
|
||||
// "$ref": "#/responses/Repository"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
opts := web.GetForm(ctx).(*api.TransferRepoOption)
|
||||
|
||||
newOwner, err := user_model.GetUserByName(ctx, opts.NewOwner)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.APIError(http.StatusNotFound, "The new owner does not exist or cannot be found")
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if newOwner.Type == user_model.UserTypeOrganization {
|
||||
if !ctx.Doer.IsAdmin && newOwner.Visibility == api.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) {
|
||||
// The user shouldn't know about this organization
|
||||
ctx.APIError(http.StatusNotFound, "The new owner does not exist or cannot be found")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var teams []*organization.Team
|
||||
if opts.TeamIDs != nil {
|
||||
if !newOwner.IsOrganization() {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, "Teams can only be added to organization-owned repositories")
|
||||
return
|
||||
}
|
||||
|
||||
org := convert.ToOrganization(ctx, organization.OrgFromUser(newOwner))
|
||||
for _, tID := range *opts.TeamIDs {
|
||||
team, err := organization.GetTeamByID(ctx, tID)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team %d not found", tID))
|
||||
return
|
||||
}
|
||||
|
||||
if team.OrgID != org.ID {
|
||||
ctx.APIError(http.StatusForbidden, fmt.Errorf("team %d belongs not to org %d", tID, org.ID))
|
||||
return
|
||||
}
|
||||
|
||||
teams = append(teams, team)
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Repo.GitRepo != nil {
|
||||
_ = ctx.Repo.GitRepo.Close()
|
||||
ctx.Repo.GitRepo = nil
|
||||
}
|
||||
|
||||
oldFullname := ctx.Repo.Repository.FullName()
|
||||
|
||||
if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil {
|
||||
switch {
|
||||
case repo_model.IsErrRepoTransferInProgress(err):
|
||||
ctx.APIError(http.StatusConflict, err)
|
||||
case repo_model.IsErrRepoAlreadyExist(err):
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
case repo_service.IsRepositoryLimitReached(err):
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
case errors.Is(err, user_model.ErrBlockedUser):
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
default:
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
|
||||
log.Trace("Repository transfer initiated: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
|
||||
ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeAdmin}))
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
|
||||
ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeAdmin}))
|
||||
}
|
||||
|
||||
// AcceptTransfer accept a repo transfer
|
||||
func AcceptTransfer(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/transfer/accept repository acceptRepoTransfer
|
||||
// ---
|
||||
// summary: Accept a repo transfer
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo to transfer
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo to transfer
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "202":
|
||||
// "$ref": "#/responses/Repository"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
err := repo_service.AcceptTransferOwnership(ctx, ctx.Repo.Repository, ctx.Doer)
|
||||
if err != nil {
|
||||
switch {
|
||||
case repo_model.IsErrNoPendingTransfer(err):
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
case errors.Is(err, util.ErrPermissionDenied):
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
case repo_service.IsRepositoryLimitReached(err):
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
default:
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission))
|
||||
}
|
||||
|
||||
// RejectTransfer reject a repo transfer
|
||||
func RejectTransfer(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/transfer/reject repository rejectRepoTransfer
|
||||
// ---
|
||||
// summary: Reject a repo transfer
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo to transfer
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo to transfer
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Repository"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
err := repo_service.RejectRepositoryTransfer(ctx, ctx.Repo.Repository, ctx.Doer)
|
||||
if err != nil {
|
||||
switch {
|
||||
case repo_model.IsErrNoPendingTransfer(err):
|
||||
ctx.APIError(http.StatusNotFound, err)
|
||||
case errors.Is(err, util.ErrPermissionDenied):
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
default:
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission))
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.dev/services/context"
|
||||
files_service "gitea.dev/services/repository/files"
|
||||
)
|
||||
|
||||
// GetTree get the tree of a repository.
|
||||
func GetTree(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/git/trees/{sha} repository GetTree
|
||||
// ---
|
||||
// summary: Gets the tree of a repository.
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: sha
|
||||
// in: path
|
||||
// description: sha of the commit
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: recursive
|
||||
// in: query
|
||||
// description: show all directories and files
|
||||
// required: false
|
||||
// type: boolean
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number; the 'truncated' field in the response will be true if there are still more items after this page, false if the last page
|
||||
// required: false
|
||||
// type: integer
|
||||
// - name: per_page
|
||||
// in: query
|
||||
// description: number of items per page
|
||||
// required: false
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/GitTreeResponse"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
sha := ctx.PathParam("sha")
|
||||
if len(sha) == 0 {
|
||||
ctx.APIError(http.StatusBadRequest, "sha not provided")
|
||||
return
|
||||
}
|
||||
if tree, err := files_service.GetTreeBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, sha, ctx.FormInt("page"), ctx.FormInt("per_page"), ctx.FormBool("recursive")); err != nil {
|
||||
ctx.APIError(http.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
ctx.SetTotalCountHeader(int64(tree.TotalCount))
|
||||
ctx.JSON(http.StatusOK, tree)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
notify_service "gitea.dev/services/notify"
|
||||
wiki_service "gitea.dev/services/wiki"
|
||||
)
|
||||
|
||||
// NewWikiPage response for wiki create request
|
||||
func NewWikiPage(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/wiki/new repository repoCreateWikiPage
|
||||
// ---
|
||||
// summary: Create a wiki page
|
||||
// consumes:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateWikiPageOptions"
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/WikiPage"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateWikiPageOptions)
|
||||
|
||||
if util.IsEmptyString(form.Title) {
|
||||
ctx.APIError(http.StatusBadRequest, nil)
|
||||
return
|
||||
}
|
||||
|
||||
wikiName := wiki_service.UserTitleToWebPath("", form.Title)
|
||||
|
||||
if len(form.Message) == 0 {
|
||||
form.Message = fmt.Sprintf("Add %q", form.Title)
|
||||
}
|
||||
|
||||
content, err := base64.StdEncoding.DecodeString(form.ContentBase64)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
form.ContentBase64 = string(content)
|
||||
|
||||
if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.ContentBase64, form.Message); err != nil {
|
||||
if repo_model.IsErrWikiReservedName(err) {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
} else if repo_model.IsErrWikiAlreadyExist(err) {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
wikiPage := getWikiPage(ctx, wikiName)
|
||||
|
||||
if !ctx.Written() {
|
||||
notify_service.NewWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(wikiName), form.Message)
|
||||
ctx.JSON(http.StatusCreated, wikiPage)
|
||||
}
|
||||
}
|
||||
|
||||
// EditWikiPage response for wiki modify request
|
||||
func EditWikiPage(ctx *context.APIContext) {
|
||||
// swagger:operation PATCH /repos/{owner}/{repo}/wiki/page/{pageName} repository repoEditWikiPage
|
||||
// ---
|
||||
// summary: Edit a wiki page
|
||||
// consumes:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: pageName
|
||||
// in: path
|
||||
// description: name of the page
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateWikiPageOptions"
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WikiPage"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateWikiPageOptions)
|
||||
|
||||
oldWikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
|
||||
newWikiName := wiki_service.UserTitleToWebPath("", form.Title)
|
||||
|
||||
if len(newWikiName) == 0 {
|
||||
newWikiName = oldWikiName
|
||||
}
|
||||
|
||||
if len(form.Message) == 0 {
|
||||
form.Message = fmt.Sprintf("Update %q", newWikiName)
|
||||
}
|
||||
|
||||
content, err := base64.StdEncoding.DecodeString(form.ContentBase64)
|
||||
if err != nil {
|
||||
ctx.APIError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
form.ContentBase64 = string(content)
|
||||
|
||||
if err := wiki_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, oldWikiName, newWikiName, form.ContentBase64, form.Message); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
wikiPage := getWikiPage(ctx, newWikiName)
|
||||
|
||||
if !ctx.Written() {
|
||||
notify_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(newWikiName), form.Message)
|
||||
ctx.JSON(http.StatusOK, wikiPage)
|
||||
}
|
||||
}
|
||||
|
||||
func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.WikiPage {
|
||||
wikiRepo, commit := findWikiRepoCommit(ctx)
|
||||
if wikiRepo != nil {
|
||||
defer wikiRepo.Close()
|
||||
}
|
||||
if ctx.Written() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// lookup filename in wiki - get filecontent, real filename
|
||||
content, pageFilename := wikiContentsByName(ctx, commit, wikiName, false)
|
||||
if ctx.Written() {
|
||||
return nil
|
||||
}
|
||||
|
||||
sidebarContent, _ := wikiContentsByName(ctx, commit, "_Sidebar", true)
|
||||
if ctx.Written() {
|
||||
return nil
|
||||
}
|
||||
|
||||
footerContent, _ := wikiContentsByName(ctx, commit, "_Footer", true)
|
||||
if ctx.Written() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// get commit count - wiki revisions
|
||||
commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
|
||||
|
||||
// Get last change information.
|
||||
lastCommit, err := wikiRepo.GetCommitByPath(pageFilename)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &api.WikiPage{
|
||||
WikiPageMetaData: wiki_service.ToWikiPageMetaData(wikiName, lastCommit, ctx.Repo.Repository),
|
||||
ContentBase64: content,
|
||||
CommitCount: commitsCount,
|
||||
Sidebar: sidebarContent,
|
||||
Footer: footerContent,
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteWikiPage delete wiki page
|
||||
func DeleteWikiPage(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/wiki/page/{pageName} repository repoDeleteWikiPage
|
||||
// ---
|
||||
// summary: Delete a wiki page
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: pageName
|
||||
// in: path
|
||||
// description: name of the page
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
wikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
|
||||
|
||||
if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil {
|
||||
if err.Error() == "file does not exist" {
|
||||
ctx.APIErrorNotFound(err)
|
||||
return
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
notify_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(wikiName))
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ListWikiPages get wiki pages list
|
||||
func ListWikiPages(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/wiki/pages repository repoGetWikiPages
|
||||
// ---
|
||||
// summary: Get all wiki pages
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WikiPageList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
wikiRepo, commit := findWikiRepoCommit(ctx)
|
||||
if wikiRepo != nil {
|
||||
defer wikiRepo.Close()
|
||||
}
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
limit := ctx.FormInt("limit")
|
||||
if limit <= 1 {
|
||||
limit = setting.API.DefaultPagingNum
|
||||
}
|
||||
|
||||
skip := (page - 1) * limit
|
||||
maxNum := page * limit
|
||||
|
||||
entries, err := commit.ListEntries()
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
pages := make([]*api.WikiPageMetaData, 0, len(entries))
|
||||
for i, entry := range entries {
|
||||
if i < skip || i >= maxNum || !entry.IsRegular() {
|
||||
continue
|
||||
}
|
||||
c, err := wikiRepo.GetCommitByPath(entry.Name())
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
wikiName, err := wiki_service.GitPathToWebPath(entry.Name())
|
||||
if err != nil {
|
||||
if repo_model.IsErrWikiInvalidFileName(err) {
|
||||
continue
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
pages = append(pages, wiki_service.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository))
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(int64(len(entries)), limit)
|
||||
ctx.SetTotalCountHeader(int64(len(entries)))
|
||||
ctx.JSON(http.StatusOK, pages)
|
||||
}
|
||||
|
||||
// GetWikiPage get single wiki page
|
||||
func GetWikiPage(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/wiki/page/{pageName} repository repoGetWikiPage
|
||||
// ---
|
||||
// summary: Get a wiki page
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: pageName
|
||||
// in: path
|
||||
// description: name of the page
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WikiPage"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
// get requested pagename
|
||||
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
|
||||
|
||||
wikiPage := getWikiPage(ctx, pageName)
|
||||
if !ctx.Written() {
|
||||
ctx.JSON(http.StatusOK, wikiPage)
|
||||
}
|
||||
}
|
||||
|
||||
// ListPageRevisions renders file revision list of wiki page
|
||||
func ListPageRevisions(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/wiki/revisions/{pageName} repository repoGetWikiPageRevisions
|
||||
// ---
|
||||
// summary: Get revisions of a wiki page
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: pageName
|
||||
// in: path
|
||||
// description: name of the page
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/WikiCommitList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
wikiRepo, commit := findWikiRepoCommit(ctx)
|
||||
if wikiRepo != nil {
|
||||
defer wikiRepo.Close()
|
||||
}
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
// get requested pagename
|
||||
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
|
||||
if len(pageName) == 0 {
|
||||
pageName = "Home"
|
||||
}
|
||||
|
||||
// lookup filename in wiki - get filecontent, gitTree entry , real filename
|
||||
_, pageFilename := wikiContentsByName(ctx, commit, pageName, false)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
// get commit count - wiki revisions
|
||||
commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
|
||||
// get Commit Count
|
||||
commitsHistory, err := wikiRepo.CommitsByFileAndRange(
|
||||
git.CommitsByFileAndRangeOptions{
|
||||
Revision: ctx.Repo.Repository.DefaultWikiBranch,
|
||||
File: pageFilename,
|
||||
Page: page,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// FIXME: SetLinkHeader missing
|
||||
ctx.SetTotalCountHeader(commitsCount)
|
||||
ctx.JSON(http.StatusOK, convert.ToWikiCommitList(commitsHistory, commitsCount))
|
||||
}
|
||||
|
||||
// findEntryForFile finds the tree entry for a target filepath.
|
||||
func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
|
||||
entry, err := commit.GetTreeEntryByPath(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry != nil {
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// Then the unescaped, shortest alternative
|
||||
var unescapedTarget string
|
||||
if unescapedTarget, err = url.QueryUnescape(target); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return commit.GetTreeEntryByPath(unescapedTarget)
|
||||
}
|
||||
|
||||
// findWikiRepoCommit opens the wiki repo and returns the latest commit, writing to context on error.
|
||||
// The caller is responsible for closing the returned repo again
|
||||
func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) {
|
||||
wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) || err.Error() == "no such file or directory" {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
ctx.APIErrorNotFound(err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return wikiRepo, nil
|
||||
}
|
||||
return wikiRepo, commit
|
||||
}
|
||||
|
||||
// wikiContentsByEntry returns the contents of the wiki page referenced by the
|
||||
// given tree entry, encoded with base64. Writes to ctx if an error occurs.
|
||||
func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string {
|
||||
blob := entry.Blob()
|
||||
if blob.Size() > setting.API.DefaultMaxBlobSize {
|
||||
return ""
|
||||
}
|
||||
content, err := blob.GetBlobContentBase64(nil)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return ""
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
// wikiContentsByName returns the contents of a wiki page, along with a boolean
|
||||
// indicating whether the page exists. Writes to ctx if an error occurs.
|
||||
func wikiContentsByName(ctx *context.APIContext, commit *git.Commit, wikiName wiki_service.WebPath, isSidebarOrFooter bool) (string, string) {
|
||||
gitFilename := wiki_service.WebPathToGitPath(wikiName)
|
||||
entry, err := findEntryForFile(commit, gitFilename)
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
if !isSidebarOrFooter {
|
||||
ctx.APIErrorNotFound()
|
||||
}
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
return wikiContentsByEntry(ctx, entry), gitFilename
|
||||
}
|
||||
Reference in New Issue
Block a user