feat: update gitea vendor
runner nix smoke / nix label and flake smoke (push) Failing after 1m28s

This commit is contained in:
2026-09-26 21:18:24 +00:00
parent c439c1b948
commit d9b2a4e787
3538 changed files with 116131 additions and 44340 deletions
@@ -16,23 +16,24 @@ import (
"strings"
"time"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
secret_model "code.gitea.io/gitea/models/secret"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/httplib"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
secret_service "code.gitea.io/gitea/services/secrets"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
secret_model "gitea.dev/models/secret"
"gitea.dev/modules/actions"
"gitea.dev/modules/httplib"
"gitea.dev/modules/optional"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/shared"
"gitea.dev/routers/api/v1/utils"
actions_service "gitea.dev/services/actions"
"gitea.dev/services/context"
"gitea.dev/services/convert"
secret_service "gitea.dev/services/secrets"
"github.com/nektos/act/pkg/model"
"gitea.com/gitea/runner/act/model"
)
// ListActionsSecrets list an repo's actions secrets
@@ -140,9 +141,9 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
_, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, repo.ID, ctx.PathParam("secretname"), opt.Data, opt.Description)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -194,9 +195,9 @@ func (Action) DeleteSecret(ctx *context.APIContext) {
err := secret_service.DeleteSecretByName(ctx, 0, repo.ID, ctx.PathParam("secretname"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -242,7 +243,7 @@ func (Action) GetVariable(ctx *context.APIContext) {
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -297,9 +298,9 @@ func (Action) DeleteVariable(ctx *context.APIContext) {
if err := actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParam("variablename")); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -360,13 +361,13 @@ func (Action) CreateVariable(ctx *context.APIContext) {
return
}
if v != nil && v.ID > 0 {
ctx.APIError(http.StatusConflict, util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
ctx.APIError(http.StatusConflict, "variable name already exists")
return
}
if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value, opt.Description); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -421,7 +422,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -438,7 +439,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -674,7 +675,7 @@ func (Action) UpdateRunner(ctx *context.APIContext) {
shared.UpdateRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
}
// GetWorkflowRunJobs Lists all jobs for a workflow run.
// ListWorkflowJobs Lists all jobs for a repository.
func (Action) ListWorkflowJobs(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/jobs repository listWorkflowJobs
// ---
@@ -705,6 +706,14 @@ func (Action) ListWorkflowJobs(ctx *context.APIContext) {
// in: query
// description: page size of results
// type: integer
// - name: sort
// in: query
// description: sort jobs by attribute. Supported values are "id". Default is "id"
// type: string
// - name: order
// in: query
// description: sort order, either "asc" (ascending) or "desc" (descending). Default is "asc"
// type: string
// responses:
// "200":
// "$ref": "#/responses/WorkflowJobsList"
@@ -712,10 +721,12 @@ func (Action) ListWorkflowJobs(ctx *context.APIContext) {
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
repoID := ctx.Repo.Repository.ID
shared.ListJobs(ctx, 0, repoID, 0)
shared.ListJobs(ctx, 0, repoID, 0, nil)
}
// ListWorkflowRuns Lists all runs for a repository run.
@@ -761,6 +772,11 @@ func (Action) ListWorkflowRuns(ctx *context.APIContext) {
// description: triggering sha of the workflow run
// type: string
// required: false
// - name: exclude_pull_requests
// in: query
// description: if true, the `pull_requests` field on each returned run is emptied
// type: boolean
// required: false
// - name: page
// in: query
// description: page number of results to return (1-based)
@@ -779,7 +795,7 @@ func (Action) ListWorkflowRuns(ctx *context.APIContext) {
repoID := ctx.Repo.Repository.ID
shared.ListRuns(ctx, 0, repoID)
shared.ListRuns(ctx, 0, repoID, "")
}
var _ actions_service.API = new(Action)
@@ -845,6 +861,12 @@ func ListActionTasks(ctx *context.APIContext) {
res := new(api.ActionTaskResponse)
res.TotalCount = total
taskList := actions_model.TaskList(tasks)
if err := taskList.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
res.Entries = make([]*api.ActionTask, len(tasks))
for i := range tasks {
convertedTask, err := convert.ToActionTask(ctx, tasks[i])
@@ -856,7 +878,7 @@ func ListActionTasks(ctx *context.APIContext) {
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total) // Duplicates api response field but it's better to set it for consistency
ctx.SetTotalCountHeader(total) // Duplicates api response field, but it's better to set it for consistency
ctx.JSON(http.StatusOK, &res)
}
@@ -940,7 +962,7 @@ func ActionsGetWorkflow(ctx *context.APIContext) {
workflow, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -950,6 +972,107 @@ func ActionsGetWorkflow(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, workflow)
}
func ActionsListWorkflowRuns(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs repository ActionsListWorkflowRuns
// ---
// summary: List runs for a workflow
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: workflow_id
// in: path
// description: id of the workflow, must be the workflow file name (e.g. `build.yml`)
// type: string
// required: true
// - name: event
// in: query
// description: workflow event name
// type: string
// required: false
// - name: branch
// in: query
// description: workflow branch
// type: string
// required: false
// - name: status
// in: query
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
// type: string
// required: false
// - name: actor
// in: query
// description: triggered by user
// type: string
// required: false
// - name: head_sha
// in: query
// description: triggering sha of the workflow run
// type: string
// required: false
// - name: exclude_pull_requests
// in: query
// description: if true, the `pull_requests` field on each returned run is emptied
// type: boolean
// required: false
// - name: scoped_workflow_source_repo_id
// description: For a scoped workflow, the ID of the source repository providing it; omit or 0 for a repo-level workflow.
// in: query
// type: integer
// format: int64
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/WorkflowRunsList"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
workflowID := ctx.PathParam("workflow_id")
scopedWorkflowSourceRepoID := ctx.FormInt64("scoped_workflow_source_repo_id")
// Existing runs prove the workflow is/was valid and cover historical workflows whose file was later removed.
// Repo-level never-run workflows fall back to a git lookup; scoped workflows are selected by source repo ID and may return an empty run list.
runExists, err := db.Exist[actions_model.ActionRun](ctx, actions_model.FindRunOptions{
RepoID: ctx.Repo.Repository.ID,
WorkflowID: workflowID,
WorkflowRepoID: scopedWorkflowSourceRepoID,
IsScopedRun: optional.Some(scopedWorkflowSourceRepoID > 0),
}.ToConds())
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !runExists {
if scopedWorkflowSourceRepoID == 0 {
if _, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID); err != nil {
ctx.APIErrorAuto(err)
return
}
}
}
shared.ListRuns(ctx, 0, ctx.Repo.Repository.ID, workflowID)
}
func ActionsDisableWorkflow(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/disable repository ActionsDisableWorkflow
// ---
@@ -988,7 +1111,7 @@ func ActionsDisableWorkflow(ctx *context.APIContext) {
err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, false)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -1028,6 +1151,11 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
// description: Whether the response should include the workflow run ID and URLs.
// in: query
// type: boolean
// - name: scoped_workflow_source_repo_id
// description: For a scoped workflow, the ID of the source repository providing it; omit or 0 for a repo-level workflow.
// in: query
// type: integer
// format: int64
// responses:
// "200":
// "$ref": "#/responses/RunDetails"
@@ -1045,11 +1173,13 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
workflowID := ctx.PathParam("workflow_id")
opt := web.GetForm(ctx).(*api.CreateActionWorkflowDispatch)
if opt.Ref == "" {
ctx.APIError(http.StatusUnprocessableEntity, util.NewInvalidArgumentErrorf("ref is required parameter"))
ctx.APIError(http.StatusUnprocessableEntity, "ref is required parameter")
return
}
runID, err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
// a non-zero scoped_workflow_source_repo_id dispatches a scoped workflow from that source repo; 0/absent is repo-level.
scopedWorkflowSourceRepoID := ctx.FormInt64("scoped_workflow_source_repo_id")
runID, err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, scopedWorkflowSourceRepoID, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
if strings.Contains(ctx.Req.Header.Get("Content-Type"), "form-urlencoded") {
// The chi framework's "Binding" doesn't support to bind the form map values into a map[string]string
// So we have to manually read the `inputs[key]` from the form
@@ -1071,9 +1201,11 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else if errors.Is(err, util.ErrPermissionDenied) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -1132,7 +1264,7 @@ func ActionsEnableWorkflow(ctx *context.APIContext) {
err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, true)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -1145,13 +1277,11 @@ func ActionsEnableWorkflow(ctx *context.APIContext) {
func getCurrentRepoActionRunByID(ctx *context.APIContext) *actions_model.ActionRun {
runID := ctx.PathParamInt64("run")
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound(err)
return nil
} else if err != nil {
ctx.APIErrorInternal(err)
if err != nil {
ctx.APIErrorAuto(err)
return nil
}
run.Repo = ctx.Repo.Repository
return run
}
@@ -1161,14 +1291,30 @@ func getCurrentRepoActionRunJobsByID(ctx *context.APIContext) (*actions_model.Ac
return nil, nil
}
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, run.RepoID, run.ID)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
}
jobs.SortMatrixGroupsByName()
return run, jobs
}
func getCurrentRepoActionRunAttemptByNumber(ctx *context.APIContext) (*actions_model.ActionRun, *actions_model.ActionRunAttempt) {
run := getCurrentRepoActionRunByID(ctx)
if ctx.Written() {
return nil, nil
}
attemptNum := ctx.PathParamInt64("attempt")
attempt, err := actions_model.GetRunAttemptByRunIDAndAttemptNum(ctx, run.ID, attemptNum)
if err != nil {
ctx.APIErrorAuto(err)
return nil, nil
}
return run, attempt
}
// GetWorkflowRun Gets a specific workflow run.
func GetWorkflowRun(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun
@@ -1205,7 +1351,56 @@ func GetWorkflowRun(ctx *context.APIContext) {
return
}
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run)
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil, false)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convertedRun)
}
// GetWorkflowRunAttempt Gets a specific workflow run attempt.
func GetWorkflowRunAttempt(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/attempts/{attempt} repository getWorkflowRunAttempt
// ---
// summary: Gets a specific workflow run attempt
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: run
// in: path
// description: id of the run
// type: integer
// required: true
// - name: attempt
// in: path
// description: logical attempt number of the run
// type: integer
// required: true
// responses:
// "200":
// "$ref": "#/responses/WorkflowRun"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
run, attempt := getCurrentRepoActionRunAttemptByNumber(ctx)
if ctx.Written() {
return
}
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, attempt, false)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -1245,6 +1440,8 @@ func RerunWorkflowRun(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
@@ -1253,12 +1450,12 @@ func RerunWorkflowRun(ctx *context.APIContext) {
return
}
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs); err != nil {
if _, err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, ctx.Doer, jobs); err != nil {
handleWorkflowRerunError(ctx, err)
return
}
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run)
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil, false)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -1296,6 +1493,8 @@ func RerunFailedWorkflowRun(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
@@ -1304,7 +1503,14 @@ func RerunFailedWorkflowRun(ctx *context.APIContext) {
return
}
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetFailedRerunJobs(jobs)); err != nil {
failedJobs := actions_service.GetFailedJobsForRerun(jobs)
// Empty failedJobs means no failed jobs to re-run
if len(failedJobs) == 0 {
ctx.APIError(http.StatusBadRequest, "this workflow run has no failed jobs to re-run")
return
}
if _, err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, ctx.Doer, failedJobs); err != nil {
handleWorkflowRerunError(ctx, err)
return
}
@@ -1349,6 +1555,8 @@ func RerunWorkflowJob(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
@@ -1360,17 +1568,33 @@ func RerunWorkflowJob(ctx *context.APIContext) {
jobID := ctx.PathParamInt64("job_id")
jobIdx := slices.IndexFunc(jobs, func(job *actions_model.ActionRunJob) bool { return job.ID == jobID })
if jobIdx == -1 {
ctx.APIErrorNotFound(util.NewNotExistErrorf("workflow job with id %d", jobID))
ctx.APIErrorNotFound("workflow job not found")
return
}
targetJob := jobs[jobIdx]
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetAllRerunJobs(targetJob, jobs)); err != nil {
newAttempt, err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, ctx.Doer, []*actions_model.ActionRunJob{targetJob})
if err != nil {
handleWorkflowRerunError(ctx, err)
return
}
convertedJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, targetJob)
// Legacy jobs had AttemptJobID=0 before the rerun; createOriginalAttemptForLegacyRun inside
// RerunWorkflowRunJobs has since backfilled it in the DB, so reload only in that case.
if targetJob.AttemptJobID == 0 {
targetJob, err = actions_model.GetRunJobByRepoAndID(ctx, run.RepoID, targetJob.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
rerunJob, err := actions_model.GetRunJobByAttemptJobID(ctx, run.ID, newAttempt.ID, targetJob.AttemptJobID)
if err != nil {
handleWorkflowRerunError(ctx, err)
return
}
convertedJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, rerunJob)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -1380,7 +1604,13 @@ func RerunWorkflowJob(ctx *context.APIContext) {
func handleWorkflowRerunError(ctx *context.APIContext, err error) {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
} else if errors.Is(err, util.ErrAlreadyExist) {
ctx.APIError(http.StatusConflict, err.Error())
return
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1422,6 +1652,83 @@ func ListWorkflowRunJobs(ctx *context.APIContext) {
// in: query
// description: page size of results
// type: integer
// - name: sort
// in: query
// description: sort jobs by attribute. Supported values are "id". Default is "id"
// type: string
// - name: order
// in: query
// description: sort order, either "asc" (ascending) or "desc" (descending). Default is "asc"
// type: string
// responses:
// "200":
// "$ref": "#/responses/WorkflowJobsList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
repoID, runID := ctx.Repo.Repository.ID, ctx.PathParamInt64("run")
// Avoid the list all jobs functionality for this api route to be used with a runID == 0.
if runID <= 0 {
ctx.APIError(http.StatusBadRequest, "runID must be a positive integer")
return
}
run, err := actions_model.GetRunByRepoAndID(ctx, repoID, runID)
if err != nil {
ctx.APIErrorAuto(err)
return
}
// runID is used as an additional filter next to repoID to ensure that we only list jobs for the specified repoID and runID.
// no additional checks for runID are needed here
shared.ListJobs(ctx, 0, repoID, runID, optional.Some(run.LatestAttemptID))
}
// ListWorkflowRunAttemptJobs Lists all jobs for a workflow run attempt.
func ListWorkflowRunAttemptJobs(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/attempts/{attempt}/jobs repository listWorkflowRunAttemptJobs
// ---
// summary: Lists all jobs for a workflow run attempt
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: run
// in: path
// description: id of the workflow run
// type: integer
// required: true
// - name: attempt
// in: path
// description: logical attempt number of the run
// type: integer
// required: true
// - name: status
// in: query
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
// type: string
// required: false
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/WorkflowJobsList"
@@ -1430,17 +1737,12 @@ func ListWorkflowRunJobs(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
repoID, runID := ctx.Repo.Repository.ID, ctx.PathParamInt64("run")
// Avoid the list all jobs functionality for this api route to be used with a runID == 0.
if runID <= 0 {
ctx.APIError(http.StatusBadRequest, util.NewInvalidArgumentErrorf("runID must be a positive integer"))
run, attempt := getCurrentRepoActionRunAttemptByNumber(ctx)
if ctx.Written() {
return
}
// runID is used as an additional filter next to repoID to ensure that we only list jobs for the specified repoID and runID.
// no additional checks for runID are needed here
shared.ListJobs(ctx, 0, repoID, runID)
shared.ListJobs(ctx, 0, run.RepoID, run.ID, optional.Some(attempt.ID))
}
// GetWorkflowJob Gets a specific workflow job for a workflow run.
@@ -1482,7 +1784,7 @@ func GetWorkflowJob(ctx *context.APIContext) {
}
if !has || job.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound(util.ErrNotExist)
ctx.APIErrorNotFound()
return
}
@@ -1706,7 +2008,7 @@ func GetArtifact(ctx *context.APIContext) {
return
}
if actions.IsArtifactV4(art) {
if actions_service.IsArtifactV4(art) {
convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, art)
if err != nil {
ctx.APIErrorInternal(err)
@@ -1755,8 +2057,8 @@ func DeleteArtifact(ctx *context.APIContext) {
return
}
if actions.IsArtifactV4(art) {
if err := actions_model.SetArtifactNeedDelete(ctx, art.RunID, art.ArtifactName); err != nil {
if actions_service.IsArtifactV4(art) {
if err := actions_model.SetArtifactNeedDeleteByID(ctx, art.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
@@ -1771,8 +2073,8 @@ func buildSignature(endp string, expires, artifactID int64) []byte {
return actions.BuildSignature("api", endp, strconv.FormatInt(expires, 10), strconv.FormatInt(artifactID, 10))
}
func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) string {
return fmt.Sprintf("api/v1/repos/%s/%s/actions/artifacts/%d/zip/raw", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), artifactID)
func buildDownloadRawEndpoint(ownerName, repoName string, artifactID int64) string {
return fmt.Sprintf("api/v1/repos/%s/%s/actions/artifacts/%d/zip/raw", url.PathEscape(ownerName), url.PathEscape(repoName), artifactID)
}
func buildSigURL(ctx go_context.Context, endPoint string, artifactID int64) string {
@@ -1824,16 +2126,16 @@ func DownloadArtifact(ctx *context.APIContext) {
return
}
if actions.IsArtifactV4(art) {
if actions_service.IsArtifactV4(art) {
// @actions/toolkit asserts that downloaded artifacts of a different runid return 302
// https://github.com/actions/toolkit/blob/44d43b5490b02998bd09b0c4ff369a4cc67876c2/packages/artifact/src/internal/download/download-artifact.ts#L203-L210
if actions.DownloadArtifactV4ServeDirect(ctx.Base, art) {
if actions_service.DownloadArtifactV4ServeDirect(ctx.Base, art) {
return
}
// @actions/toolkit asserts a 302 for the artifact download, so we have to build a signed URL and redirect to it
// TODO: a perma link to the code for reference
redirectURL := buildSigURL(ctx, buildDownloadRawEndpoint(ctx.Repo.Repository, art.ID), art.ID)
redirectURL := buildSigURL(ctx, buildDownloadRawEndpoint(ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, art.ID), art.ID)
ctx.Redirect(redirectURL, http.StatusFound)
return
}
@@ -1844,7 +2146,22 @@ func DownloadArtifact(ctx *context.APIContext) {
// DownloadArtifactRaw Downloads a specific artifact for a workflow run directly.
func DownloadArtifactRaw(ctx *context.APIContext) {
// it doesn't use repoAssignment middleware, so it needs to prepare the repo and check permission (sig) by itself
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ctx.PathParam("username"), ctx.PathParam("reponame"))
ownerName, repoName := ctx.PathParam("username"), ctx.PathParam("reponame")
query := ctx.Req.URL.Query()
sigBytes, _ := base64.RawURLEncoding.DecodeString(query.Get("sig"))
expires, _ := strconv.ParseInt(query.Get("expires"), 10, 64)
artifactID := ctx.PathParamInt64("artifact_id")
if !hmac.Equal(sigBytes, buildSignature(buildDownloadRawEndpoint(ownerName, repoName, artifactID), expires, artifactID)) {
ctx.APIErrorNotFound()
return
}
if time.Unix(expires, 0).Before(time.Now()) {
ctx.APIError(http.StatusUnauthorized, "Error link expired")
return
}
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound()
@@ -1858,29 +2175,13 @@ func DownloadArtifactRaw(ctx *context.APIContext) {
return
}
sigStr := ctx.Req.URL.Query().Get("sig")
expiresStr := ctx.Req.URL.Query().Get("expires")
sigBytes, _ := base64.RawURLEncoding.DecodeString(sigStr)
expires, _ := strconv.ParseInt(expiresStr, 10, 64)
expectedSig := buildSignature(buildDownloadRawEndpoint(repo, art.ID), expires, art.ID)
if !hmac.Equal(sigBytes, expectedSig) {
ctx.APIError(http.StatusUnauthorized, "Error unauthorized")
return
}
t := time.Unix(expires, 0)
if t.Before(time.Now()) {
ctx.APIError(http.StatusUnauthorized, "Error link expired")
return
}
// if artifacts status is not uploaded-confirmed, treat it as not found
if art.Status == actions_model.ArtifactStatusExpired {
ctx.APIError(http.StatusNotFound, "Artifact has expired")
return
}
if actions.IsArtifactV4(art) {
err := actions.DownloadArtifactV4(ctx.Base, art)
if actions_service.IsArtifactV4(art) {
err := actions_service.DownloadArtifactV4(ctx.Base, art)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -4,12 +4,9 @@
package repo
import (
"errors"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
actions_model "gitea.dev/models/actions"
"gitea.dev/routers/common"
"gitea.dev/services/context"
)
func DownloadActionsRunJobLogs(ctx *context.APIContext) {
@@ -45,11 +42,7 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) {
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)
}
ctx.APIErrorAuto(err)
return
}
if err = curJob.LoadRepo(ctx); err != nil {
@@ -59,10 +52,6 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) {
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)
}
ctx.APIErrorAuto(err)
}
}
@@ -7,10 +7,10 @@ import (
"encoding/base64"
"net/http"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
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
@@ -44,7 +44,7 @@ func UpdateAvatar(ctx *context.APIContext) {
content, err := base64.StdEncoding.DecodeString(form.Image)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
@@ -6,8 +6,8 @@ package repo
import (
"net/http"
"code.gitea.io/gitea/services/context"
files_service "code.gitea.io/gitea/services/repository/files"
"gitea.dev/services/context"
files_service "gitea.dev/services/repository/files"
)
// GetBlob get the blob of a repository file.
@@ -48,7 +48,7 @@ func GetBlob(ctx *context.APIContext) {
}
if blob, err := files_service.GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.JSON(http.StatusOK, blob)
}
@@ -8,24 +8,24 @@ import (
"errors"
"net/http"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
pull_service "code.gitea.io/gitea/services/pull"
release_service "code.gitea.io/gitea/services/release"
repo_service "code.gitea.io/gitea/services/repository"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
"gitea.dev/models/organization"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/optional"
repo_module "gitea.dev/modules/repository"
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"
pull_service "gitea.dev/services/pull"
release_service "gitea.dev/services/release"
repo_service "gitea.dev/services/repository"
)
// GetBranch get a branch of a repository
@@ -64,7 +64,7 @@ func GetBranch(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
} else if !exist {
ctx.APIErrorNotFound(err)
ctx.APIErrorNotFound()
return
}
@@ -80,7 +80,7 @@ func GetBranch(ctx *context.APIContext) {
return
}
br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branchName, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branchName, c, branchProtection, ctx.Doer, ctx.Repo.Permission.IsAdmin())
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -153,11 +153,11 @@ func DeleteBranch(ctx *context.APIContext) {
if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
switch {
case git.IsErrBranchNotExist(err):
ctx.APIErrorNotFound(err)
ctx.APIErrorNotFound()
case errors.Is(err, repo_service.ErrBranchIsDefault):
ctx.APIError(http.StatusForbidden, errors.New("can not delete default or pull request target branch"))
ctx.APIError(http.StatusForbidden, "can not delete default or pull request target branch")
case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.APIError(http.StatusForbidden, errors.New("branch protected"))
ctx.APIError(http.StatusForbidden, "branch protected")
default:
ctx.APIErrorInternal(err)
}
@@ -271,7 +271,7 @@ func CreateBranch(ctx *context.APIContext) {
return
}
br, err := convert.ToBranch(ctx, ctx.Repo.Repository, opt.BranchName, commit, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
br, err := convert.ToBranch(ctx, ctx.Repo.Repository, opt.BranchName, commit, branchProtection, ctx.Doer, ctx.Repo.Permission.IsAdmin())
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -306,6 +306,10 @@ func ListBranches(ctx *context.APIContext) {
// in: query
// description: page size of results
// type: integer
// - name: q
// in: query
// description: branch name substring to filter by
// type: string
// responses:
// "200":
// "$ref": "#/responses/BranchList"
@@ -314,6 +318,7 @@ func ListBranches(ctx *context.APIContext) {
var apiBranches []*api.Branch
listOptions := utils.GetListOptions(ctx)
keyword := ctx.FormString("q")
if !ctx.Repo.Repository.IsEmpty {
if ctx.Repo.GitRepo == nil {
@@ -325,6 +330,7 @@ func ListBranches(ctx *context.APIContext) {
ListOptions: listOptions,
RepoID: ctx.Repo.Repository.ID,
IsDeletedBranch: optional.Some(false),
Keyword: keyword,
}
var err error
totalNumOfBranches, err = db.Count[git_model.Branch](ctx, branchOpts)
@@ -366,7 +372,7 @@ func ListBranches(ctx *context.APIContext) {
}
branchProtection := rules.GetFirstMatched(branches[i].Name)
apiBranch, err := convert.ToBranch(ctx, ctx.Repo.Repository, branches[i].Name, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
apiBranch, err := convert.ToBranch(ctx, ctx.Repo.Repository, branches[i].Name, c, branchProtection, ctx.Doer, ctx.Repo.Permission.IsAdmin())
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -440,9 +446,9 @@ func UpdateBranch(ctx *context.APIContext) {
if err := repo_service.UpdateBranch(ctx, repo, ctx.Repo.GitRepo, ctx.Doer, branchName, opt.NewCommitID, opt.OldCommitID, opt.Force); err != nil {
switch {
case git_model.IsErrBranchNotExist(err):
ctx.APIErrorNotFound(err)
ctx.APIErrorNotFound()
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
case git.IsErrPushRejected(err):
rej := err.(*git.ErrPushRejected)
ctx.APIError(http.StatusForbidden, rej.Message)
@@ -678,7 +684,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
whitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -687,7 +693,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
forcePushAllowlistUsers, err := user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -696,7 +702,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
mergeWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -705,18 +711,30 @@ func CreateBranchProtection(ctx *context.APIContext) {
approvalsWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
var bypassAllowlistUsers []int64
if form.EnableBypassAllowlist {
bypassAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.BypassAllowlistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
}
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams, bypassAllowlistTeams []int64
if repo.Owner.IsOrganization() {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -725,7 +743,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
forcePushAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushAllowlistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -734,7 +752,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -743,12 +761,23 @@ func CreateBranchProtection(ctx *context.APIContext) {
approvalsWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ApprovalsWhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
if form.EnableBypassAllowlist {
bypassAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.BypassAllowlistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
}
}
protectBranch = &git_model.ProtectedBranch{
@@ -762,6 +791,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
ForcePushAllowlistDeployKeys: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistDeployKeys,
EnableMergeWhitelist: form.EnableMergeWhitelist,
EnableBypassAllowlist: form.EnableBypassAllowlist,
EnableStatusCheck: form.EnableStatusCheck,
StatusCheckContexts: form.StatusCheckContexts,
EnableApprovalsWhitelist: form.EnableApprovalsWhitelist,
@@ -786,6 +816,8 @@ func CreateBranchProtection(ctx *context.APIContext) {
MergeTeamIDs: mergeWhitelistTeams,
ApprovalsUserIDs: approvalsWhitelistUsers,
ApprovalsTeamIDs: approvalsWhitelistTeams,
BypassUserIDs: bypassAllowlistUsers,
BypassTeamIDs: bypassAllowlistTeams,
}); err != nil {
ctx.APIErrorInternal(err)
return
@@ -906,6 +938,10 @@ func EditBranchProtection(ctx *context.APIContext) {
protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist
}
if form.EnableBypassAllowlist != nil {
protectBranch.EnableBypassAllowlist = *form.EnableBypassAllowlist
}
if form.EnableStatusCheck != nil {
protectBranch.EnableStatusCheck = *form.EnableStatusCheck
}
@@ -958,12 +994,12 @@ func EditBranchProtection(ctx *context.APIContext) {
protectBranch.BlockAdminMergeOverride = *form.BlockAdminMergeOverride
}
var whitelistUsers, forcePushAllowlistUsers, mergeWhitelistUsers, approvalsWhitelistUsers []int64
var whitelistUsers, forcePushAllowlistUsers, mergeWhitelistUsers, approvalsWhitelistUsers, bypassAllowlistUsers []int64
if form.PushWhitelistUsernames != nil {
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -976,7 +1012,7 @@ func EditBranchProtection(ctx *context.APIContext) {
forcePushAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -989,7 +1025,7 @@ func EditBranchProtection(ctx *context.APIContext) {
mergeWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1002,7 +1038,7 @@ func EditBranchProtection(ctx *context.APIContext) {
approvalsWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1011,14 +1047,27 @@ func EditBranchProtection(ctx *context.APIContext) {
} else {
approvalsWhitelistUsers = protectBranch.ApprovalsWhitelistUserIDs
}
if form.BypassAllowlistUsernames != nil {
bypassAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.BypassAllowlistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
} else {
bypassAllowlistUsers = protectBranch.BypassAllowlistUserIDs
}
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams, bypassAllowlistTeams []int64
if repo.Owner.IsOrganization() {
if form.PushWhitelistTeams != nil {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1031,7 +1080,7 @@ func EditBranchProtection(ctx *context.APIContext) {
forcePushAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushAllowlistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1044,7 +1093,7 @@ func EditBranchProtection(ctx *context.APIContext) {
mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1057,7 +1106,7 @@ func EditBranchProtection(ctx *context.APIContext) {
approvalsWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ApprovalsWhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1066,6 +1115,23 @@ func EditBranchProtection(ctx *context.APIContext) {
} else {
approvalsWhitelistTeams = protectBranch.ApprovalsWhitelistTeamIDs
}
if form.BypassAllowlistTeams != nil {
bypassAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.BypassAllowlistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
} else {
bypassAllowlistTeams = protectBranch.BypassAllowlistTeamIDs
}
}
if !protectBranch.EnableBypassAllowlist {
bypassAllowlistUsers = nil
bypassAllowlistTeams = nil
}
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
@@ -1077,6 +1143,8 @@ func EditBranchProtection(ctx *context.APIContext) {
MergeTeamIDs: mergeWhitelistTeams,
ApprovalsUserIDs: approvalsWhitelistUsers,
ApprovalsTeamIDs: approvalsWhitelistTeams,
BypassUserIDs: bypassAllowlistUsers,
BypassTeamIDs: bypassAllowlistTeams,
})
if err != nil {
ctx.APIErrorInternal(err)
@@ -1263,10 +1331,13 @@ func MergeUpstream(ctx *context.APIContext) {
mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch, form.FfOnly)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
return
} else if errors.Is(err, util.ErrPermissionDenied) {
ctx.APIError(http.StatusForbidden, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -9,18 +9,18 @@ import (
"net/http"
"strings"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
"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
@@ -107,7 +107,7 @@ func IsCollaborator(ctx *context.APIContext) {
user, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -167,7 +167,7 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) {
collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -181,12 +181,12 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) {
p := perm.AccessModeWrite
if form.Permission != nil {
p = perm.ParseAccessMode(*form.Permission, perm.AccessModeRead, perm.AccessModeWrite, perm.AccessModeAdmin)
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)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -230,7 +230,7 @@ func DeleteCollaborator(ctx *context.APIContext) {
collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -284,7 +284,7 @@ func GetRepoPermissions(ctx *context.APIContext) {
collaborator, err := user_model.GetUserByName(ctx, collaboratorUsername)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -326,7 +326,7 @@ func GetReviewers(ctx *context.APIContext) {
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"))
ctx.APIError(http.StatusForbidden, "doer has no permission to get reviewers")
return
}
@@ -367,5 +367,42 @@ func GetAssignees(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToUsers(ctx, ctx.Doer, assignees))
}
// CheckRepoIssueAssignee check if a user can be assigned to issues in a repository
func CheckRepoIssueAssignee(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/assignees/{assignee} repository repoCheckAssignee
// ---
// summary: Check if a user can be assigned to issues 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: assignee
// in: path
// description: username of the user to check for being an assignee
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
if checkAssignableUser(ctx, ctx.PathParam("assignee"), ctx.Repo.Repository) {
ctx.Status(http.StatusNoContent)
}
}
@@ -10,15 +10,15 @@ import (
"strconv"
"time"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
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
@@ -217,7 +217,7 @@ func GetAllCommits(ctx *context.APIContext) {
// get commit specified by sha
baseCommit, err = ctx.Repo.GitRepo.GetCommit(sha)
if err != nil {
ctx.NotFoundOrServerError(err)
ctx.APIErrorAuto(err)
return
}
}
@@ -258,11 +258,30 @@ func GetAllCommits(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
} else if commitsCountTotal == 0 {
ctx.APIErrorNotFound("FileCommitsCount", nil)
return
// when date filters are active, a zero count may just mean no
// commits in the requested range — not that the path is invalid
if since == "" && until == "" {
ctx.APIErrorNotFound()
return
}
// verify the path actually exists in the revision history
totalWithoutDate, err := gitrepo.CommitsCount(ctx, ctx.Repo.Repository,
gitrepo.CommitsCountOptions{
Not: not,
Revision: []string{sha},
RelPath: []string{path},
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
if totalWithoutDate == 0 {
ctx.APIErrorNotFound()
return
}
}
commits, err = ctx.Repo.GitRepo.CommitsByFileAndRange(
commits, _, err = ctx.Repo.GitRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
Revision: sha,
File: path,
@@ -383,7 +402,7 @@ func GetCommitPullRequest(ctx *context.APIContext) {
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)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -6,11 +6,12 @@ package repo
import (
"net/http"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
user_model "gitea.dev/models/user"
"gitea.dev/modules/gitrepo"
api "gitea.dev/modules/structs"
"gitea.dev/services/context"
"gitea.dev/services/convert"
git_service "gitea.dev/services/git"
)
// CompareDiff compare two branches or commits
@@ -18,8 +19,12 @@ func CompareDiff(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/compare/{basehead} repository repoCompareDiff
// ---
// summary: Get commit comparison information
// description: |
// By default returns JSON commit comparison information. The raw diff or patch can be
// requested with the `output` query parameter set to `diff` or `patch` respectively.
// produces:
// - application/json
// - text/plain
// parameters:
// - name: owner
// in: path
@@ -33,12 +38,21 @@ func CompareDiff(ctx *context.APIContext) {
// required: true
// - name: basehead
// in: path
// description: compare two branches or commits
// description: compare two refs as `base...head` (or `base..head`); refs may be branches, tags, full or short SHAs (including branch names that contain slashes), optionally with a `^` or `~N` revision suffix.
// type: string
// required: true
// - name: output
// in: query
// description: return the raw comparison as `diff` or `patch` instead of JSON
// type: string
// enum:
// - diff
// - patch
// responses:
// "200":
// "$ref": "#/responses/Compare"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
@@ -57,6 +71,16 @@ func CompareDiff(ctx *context.APIContext) {
}
defer closer()
// ?output=diff|patch returns the raw output, otherwise the JSON comparison is returned.
switch ctx.FormString("output") {
case "diff":
downloadCompareDiffOrPatch(ctx, compareInfo, false)
return
case "patch":
downloadCompareDiffOrPatch(ctx, compareInfo, true)
return
}
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
files := ctx.FormString("files") == "" || ctx.FormBool("files")
@@ -88,3 +112,20 @@ func CompareDiff(ctx *context.APIContext) {
Commits: apiCommits,
})
}
// downloadCompareDiffOrPatch writes a comparison's raw diff or patch to the response.
func downloadCompareDiffOrPatch(ctx *context.APIContext, compareInfo *git_service.CompareInfo, patch bool) {
ctx.Resp.Header().Set("Content-Type", "text/plain; charset=utf-8")
compareArg := compareInfo.BaseCommitID + compareInfo.CompareSeparator + compareInfo.HeadCommitID
var err error
if patch {
err = compareInfo.HeadGitRepo.GetPatch(compareArg, ctx.Resp)
} else {
err = compareInfo.HeadGitRepo.GetDiff(compareArg, ctx.Resp)
}
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
@@ -7,19 +7,19 @@ import (
"errors"
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
archiver_service "code.gitea.io/gitea/services/repository/archiver"
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)
ctx.APIError(http.StatusBadRequest, err.Error())
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -28,7 +28,7 @@ func serveRepoArchive(ctx *context.APIContext, reqFileName string, paths []strin
err = archiver_service.ServeRepoArchive(ctx.Base, aReq)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -14,22 +14,22 @@ import (
"strings"
"time"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
pull_service "code.gitea.io/gitea/services/pull"
files_service "code.gitea.io/gitea/services/repository/files"
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"
@@ -213,7 +213,7 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEn
}
if entry.IsDir() || entry.IsSubModule() {
ctx.APIErrorNotFound("getBlobForEntry", nil)
ctx.APIErrorNotFound()
return nil, nil, nil
}
@@ -301,18 +301,14 @@ func GetEditorconfig(ctx *context.APIContext) {
ec, _, err := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
if err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
fileName := ctx.PathParam("filename")
def, err := ec.GetDefinitionForFilename(fileName)
if def == nil {
ctx.APIErrorNotFound(err)
if err != nil {
ctx.APIErrorNotFound(err.Error())
return
}
ctx.JSON(http.StatusOK, def)
@@ -409,7 +405,7 @@ func ChangeFiles(ctx *context.APIContext) {
for _, file := range apiOpts.Files {
contentReader, err := base64Reader(file.ContentBase64)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
// FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options
@@ -483,7 +479,7 @@ func CreateFile(ctx *context.APIContext) {
}
contentReader, err := base64Reader(apiOpts.ContentBase64)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
@@ -554,7 +550,7 @@ func UpdateFile(ctx *context.APIContext) {
}
contentReader, err := base64Reader(apiOpts.ContentBase64)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
willCreate := apiOpts.SHA == ""
@@ -584,17 +580,17 @@ func handleChangeRepoFilesError(ctx *context.APIContext, err error) {
return
}
if files_service.IsErrUserCannotCommit(err) || pull_service.IsErrFilePathProtected(err) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
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)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -699,10 +695,8 @@ func DeleteFile(ctx *context.APIContext) {
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)
if err != nil {
ctx.APIErrorAuto(err)
}
return refCommit
}
@@ -828,11 +822,8 @@ func getRepoContents(ctx *context.APIContext, opts files_service.GetContentsOrLi
}
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)
ctx.APIErrorAuto(err)
return nil
}
return &ret
}
@@ -8,19 +8,19 @@ import (
"errors"
"net/http"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_service "code.gitea.io/gitea/services/repository"
"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
@@ -55,7 +55,8 @@ func ListForks(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx))
listOptions := utils.GetListOptions(ctx)
forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, listOptions)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -79,6 +80,7 @@ func ListForks(ctx *context.APIContext) {
apiForks[i] = convert.ToRepo(ctx, fork, permission)
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, apiForks)
}
@@ -165,9 +167,9 @@ func CreateFork(ctx *context.APIContext) {
})
if err != nil {
if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -7,11 +7,11 @@ import (
"errors"
"net/http"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"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
@@ -8,10 +8,10 @@ import (
"net/http"
"net/url"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
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
@@ -7,19 +7,19 @@ package repo
import (
"net/http"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
webhook_service "code.gitea.io/gitea/services/webhook"
"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
@@ -177,7 +177,7 @@ func TestHook(ctx *context.APIContext) {
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{
if err := webhook_service.PrepareTestWebhook(ctx, hook, webhook_module.HookEventPush, &api.PushPayload{
Ref: ref,
Before: commitID,
After: commitID,
@@ -5,11 +5,13 @@ package repo
import (
"net/http"
"strconv"
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/services/contexttest"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
"gitea.dev/models/webhook"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
@@ -17,8 +19,17 @@ import (
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", "1")
ctx.SetPathParam("id", strconv.FormatInt(hook.ID, 10))
contexttest.LoadRepo(t, ctx, 1)
contexttest.LoadRepoCommit(t, ctx)
contexttest.LoadUser(t, ctx, 2)
@@ -26,6 +37,6 @@ func TestTestHook(t *testing.T) {
assert.Equal(t, http.StatusNoContent, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &webhook.HookTask{
HookID: 1,
HookID: hook.ID,
}, unittest.Cond("is_delivered=?", false))
}
+60 -122
View File
@@ -12,82 +12,26 @@ import (
"strings"
"time"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
"gitea.dev/models/db"
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"
issue_indexer "gitea.dev/modules/indexer/issues"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/routers/common"
"gitea.dev/services/context"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
)
// buildSearchIssuesRepoIDs builds the list of repository IDs for issue search based on query parameters.
// It returns repoIDs, allPublic flag, and any error that occurred.
func buildSearchIssuesRepoIDs(ctx *context.APIContext) (repoIDs []int64, allPublic bool, err error) {
opts := repo_model.SearchRepoOptions{
Private: false,
AllPublic: true,
TopicOnly: false,
Collaborate: optional.None[bool](),
// This needs to be a column that is not nil in fixtures or
// MySQL will return different results when sorting by null in some cases
OrderBy: db.SearchOrderByAlphabetically,
Actor: ctx.Doer,
}
if ctx.IsSigned {
opts.Private = true
opts.AllLimited = true
}
opts.ApplyPublicOnly(ctx.PublicOnly)
if ctx.FormString("owner") != "" {
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
if err != nil {
return nil, false, err
}
opts.OwnerID = owner.ID
opts.AllLimited = false
opts.AllPublic = false
opts.Collaborate = optional.Some(false)
}
if ctx.FormString("team") != "" {
if ctx.FormString("owner") == "" {
return nil, false, util.NewInvalidArgumentErrorf("owner organisation is required for filtering on team")
}
team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
if err != nil {
return nil, false, err
}
opts.TeamID = team.ID
}
if opts.AllPublic {
allPublic = true
opts.AllPublic = false // set it false to avoid returning too many repos, we could filter by indexer
}
repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
if err != nil {
return nil, false, err
}
if len(repoIDs) == 0 {
// no repos found, don't let the indexer return all repos
repoIDs = []int64{0}
}
return repoIDs, allPublic, nil
}
// SearchIssues searches for issues across the repositories that the user has access to
func SearchIssues(ctx *context.APIContext) {
// swagger:operation GET /repos/issues/search issue issueSearchIssues
@@ -187,16 +131,21 @@ func SearchIssues(ctx *context.APIContext) {
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
repoIDs, allPublic, err := buildSearchIssuesRepoIDs(ctx)
repoIDs, allPublic, err := common.SearchIssuesRepoIDs(ctx, common.SearchIssuesRepoIDsOptions{
Doer: ctx.Doer,
PublicOnly: ctx.PublicOnly,
OwnerName: ctx.FormString("owner"),
TeamName: ctx.FormString("team"),
})
if err != nil {
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -204,10 +153,6 @@ func SearchIssues(ctx *context.APIContext) {
}
keyword := ctx.FormTrim("q")
if strings.IndexByte(keyword, 0) >= 0 {
keyword = ""
}
isPull := common.ParseIssueFilterTypeIsPull(ctx.FormString("type"))
var includedAnyLabels []int64
@@ -384,15 +329,12 @@ func ListIssues(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
keyword := ctx.FormTrim("q")
if strings.IndexByte(keyword, 0) >= 0 {
keyword = ""
}
var labelIDs []int64
if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
@@ -435,22 +377,16 @@ func ListIssues(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx)
isPull := optional.None[bool]()
switch ctx.FormString("type") {
case "pulls":
isPull = optional.Some(true)
case "issues":
isPull = optional.Some(false)
}
isPull := common.ParseIssueFilterTypeIsPull(ctx.FormString("type"))
if isPull.Has() && !ctx.Repo.CanReadIssuesOrPulls(isPull.Value()) {
if isPull.Has() && !ctx.Repo.Permission.CanReadIssuesOrPulls(isPull.Value()) {
ctx.APIErrorNotFound()
return
}
if !isPull.Has() {
canReadIssues := ctx.Repo.CanRead(unit.TypeIssues)
canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests)
canReadIssues := ctx.Repo.Permission.CanRead(unit.TypeIssues)
canReadPulls := ctx.Repo.Permission.CanRead(unit.TypePullRequests)
if !canReadIssues && !canReadPulls {
ctx.APIErrorNotFound()
return
@@ -540,16 +476,10 @@ func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 {
}
user, err := user_model.GetUserByName(ctx, userName)
if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound(err)
return 0
}
if err != nil {
ctx.APIErrorInternal(err)
ctx.APIErrorAuto(err)
return 0
}
return user.ID
}
@@ -592,7 +522,7 @@ func GetIssue(ctx *context.APIContext) {
}
return
}
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return
}
@@ -639,7 +569,7 @@ func CreateIssue(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.CreateIssueOption)
var deadlineUnix timeutil.TimeStamp
if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) {
if form.Deadline != nil && ctx.Repo.Permission.CanWrite(unit.TypeIssues) {
deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
}
@@ -656,7 +586,7 @@ func CreateIssue(ctx *context.APIContext) {
assigneeIDs := make([]int64, 0)
var err error
if ctx.Repo.CanWrite(unit.TypeIssues) {
if ctx.Repo.Permission.CanWrite(unit.TypeIssues) {
issue.MilestoneID = form.Milestone
assigneeIDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees)
if err != nil {
@@ -676,13 +606,13 @@ func CreateIssue(ctx *context.APIContext) {
return
}
valid, err := access_model.CanBeAssigned(ctx, assignee, ctx.Repo.Repository, false)
valid, err := access_model.CanBeAssigned(ctx, assignee, ctx.Repo.Repository)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !valid {
ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name})
ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name}.Error())
return
}
}
@@ -691,11 +621,11 @@ func CreateIssue(ctx *context.APIContext) {
form.Labels = make([]int64, 0)
}
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.APIError(http.StatusBadRequest, err)
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, form.Projects); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err.Error())
} else if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -776,7 +706,7 @@ func EditIssue(ctx *context.APIContext) {
return
}
issue.Repo = ctx.Repo.Repository
canWrite := ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
err = issue.LoadAttributes(ctx)
if err != nil {
@@ -794,7 +724,7 @@ func EditIssue(ctx *context.APIContext) {
// handles concurrent requests.
// TODO: wrap all mutations in a transaction to fully prevent partial writes.
if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion {
ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged)
ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged.Error())
return
}
@@ -813,7 +743,7 @@ func EditIssue(ctx *context.APIContext) {
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion)
if err != nil {
if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
return
}
@@ -869,7 +799,7 @@ func EditIssue(ctx *context.APIContext) {
err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer)
if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -914,6 +844,18 @@ func EditIssue(ctx *context.APIContext) {
}
}
// Update projects if provided
if canWrite && form.Projects != nil {
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, *form.Projects); err != nil {
if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
return
}
}
// Refetch from database to assign some automatic values
issue, err = issues_model.GetIssueByID(ctx, issue.ID)
if err != nil {
@@ -957,11 +899,7 @@ func DeleteIssue(ctx *context.APIContext) {
// "$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)
}
ctx.APIErrorAuto(err)
return
}
@@ -1021,7 +959,7 @@ func UpdateIssueDeadline(ctx *context.APIContext) {
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, "Not repo writer")
return
}
@@ -0,0 +1,238 @@
// Copyright 2026 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"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
)
// AddIssueAssignees add assignees to an issue
func AddIssueAssignees(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/assignees issue issueAddAssignees
// ---
// summary: Add assignees 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
// required: true
// schema:
// "$ref": "#/definitions/IssueAssigneesOption"
// responses:
// "201":
// "$ref": "#/responses/Issue"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
opts := web.GetForm(ctx).(*api.IssueAssigneesOption)
updateIssueAssignees(ctx, *opts, true)
}
// DeleteIssueAssignees remove assignees from an issue
func DeleteIssueAssignees(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/assignees issue issueRemoveAssignees
// ---
// summary: Remove assignees 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: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/IssueAssigneesOption"
// responses:
// "200":
// "$ref": "#/responses/Issue"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
opts := web.GetForm(ctx).(*api.IssueAssigneesOption)
updateIssueAssignees(ctx, *opts, false)
}
// CheckIssueAssignee check if a user can be assigned to an issue
func CheckIssueAssignee(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assignees/{assignee} issue issueCheckAssignee
// ---
// summary: Check if a user can be assigned to 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: assignee
// in: path
// description: username of the user to check for being an assignee
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return
}
if checkAssignableUser(ctx, ctx.PathParam("assignee"), ctx.Repo.Repository) {
ctx.Status(http.StatusNoContent)
}
}
// checkAssignableUser resolves assigneeName and verifies the user can be assigned to issues in repo.
// Returns true only when the user resolves AND is assignable; the caller is responsible for writing the 204.
// On any failure it writes the appropriate API response and returns false.
func checkAssignableUser(ctx *context.APIContext, assigneeName string, repo *repo_model.Repository) bool {
assignee, err := user_model.GetUserByName(ctx, assigneeName)
if err != nil {
ctx.APIErrorAuto(err)
return false
}
canAssign, err := access_model.CanBeAssigned(ctx, assignee, repo)
if err != nil {
ctx.APIErrorAuto(err)
return false
}
if !canAssign {
ctx.APIErrorNotFound()
return false
}
return true
}
func updateIssueAssignees(ctx *context.APIContext, opts api.IssueAssigneesOption, isAdd bool) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
return
}
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return
}
if err := issue.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
assigneeIDs, err := user_model.GetUserIDsByNames(ctx, opts.Assignees, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorAuto(err)
return
}
if isAdd {
err = issue_service.AddAssignees(ctx, issue, ctx.Doer, assigneeIDs)
} else {
err = issue_service.RemoveAssignees(ctx, issue, ctx.Doer, assigneeIDs)
}
if err != nil {
ctx.APIErrorAuto(err)
return
}
issue, err = issues_model.GetIssueByID(ctx, issue.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
status := http.StatusOK
if isAdd {
status = http.StatusCreated
}
ctx.JSON(status, convert.ToAPIIssue(ctx, ctx.Doer, issue))
}
@@ -7,18 +7,18 @@ import (
"errors"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
attachment_service "code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
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
@@ -194,9 +194,9 @@ func CreateIssueAttachment(ctx *context.APIContext) {
})
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else if errors.Is(err, util.ErrContentTooLarge) {
ctx.APIError(http.StatusRequestEntityTooLarge, err)
ctx.APIError(http.StatusRequestEntityTooLarge, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -272,7 +272,7 @@ func EditIssueAttachment(ctx *context.APIContext) {
if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attachment); err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -336,7 +336,7 @@ func DeleteIssueAttachment(ctx *context.APIContext) {
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)
ctx.APIErrorAuto(err)
return nil
}
@@ -361,7 +361,7 @@ func getIssueAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment
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)
ctx.APIErrorAuto(err)
return nil
}
if !attachmentBelongsToRepoOrIssue(ctx, attachment, issue) {
@@ -371,7 +371,7 @@ func getIssueAttachmentSafeRead(ctx *context.APIContext, issue *issues_model.Iss
}
func canUserWriteIssueAttachment(ctx *context.APIContext, issue *issues_model.Issue) bool {
canEditIssue := ctx.IsSigned && (ctx.Doer.ID == issue.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull))
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
@@ -9,18 +9,18 @@ import (
"errors"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
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
@@ -65,7 +65,7 @@ func ListIssueComments(ctx *context.APIContext) {
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
@@ -73,7 +73,7 @@ func ListIssueComments(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return
}
@@ -169,7 +169,7 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
@@ -274,13 +274,13 @@ func ListRepoIssueComments(ctx *context.APIContext) {
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
var isPull optional.Option[bool]
canReadIssue := ctx.Repo.CanRead(unit.TypeIssues)
canReadPull := ctx.Repo.CanRead(unit.TypePullRequests)
canReadIssue := ctx.Repo.Permission.CanRead(unit.TypeIssues)
canReadPull := ctx.Repo.Permission.CanRead(unit.TypePullRequests)
if canReadIssue && canReadPull {
isPull = optional.None[bool]()
} else if canReadIssue {
@@ -386,20 +386,20 @@ func CreateIssueComment(ctx *context.APIContext) {
return
}
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return
}
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
ctx.APIError(http.StatusForbidden, errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked")))
if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
ctx.APIError(http.StatusForbidden, 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)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -447,15 +447,11 @@ func GetIssueComment(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)
}
ctx.APIErrorAuto(err)
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIErrorNotFound()
return
}
@@ -572,15 +568,11 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) {
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)
}
ctx.APIErrorAuto(err)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Status(http.StatusForbidden)
return
}
@@ -595,7 +587,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
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)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -681,15 +673,11 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) {
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)
}
ctx.APIErrorAuto(err)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
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() {
@@ -7,19 +7,19 @@ import (
"errors"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
attachment_service "code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
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
@@ -202,9 +202,9 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
})
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else if errors.Is(err, util.ErrContentTooLarge) {
ctx.APIError(http.StatusRequestEntityTooLarge, err)
ctx.APIError(http.StatusRequestEntityTooLarge, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -218,7 +218,7 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
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)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -285,7 +285,7 @@ func EditIssueCommentAttachment(ctx *context.APIContext) {
if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attach); err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -346,7 +346,7 @@ func DeleteIssueCommentAttachment(ctx *context.APIContext) {
func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
ctx.NotFoundOrServerError(err)
ctx.APIErrorAuto(err)
return nil
}
if err := comment.LoadIssue(ctx); err != nil {
@@ -358,7 +358,7 @@ func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
return nil
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
return nil
}
@@ -379,7 +379,7 @@ func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Att
}
func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool {
canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)
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
@@ -391,7 +391,7 @@ func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues
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)
ctx.APIErrorAuto(err)
return nil
}
if !attachmentBelongsToRepoOrComment(ctx, attachment, comment) {
@@ -7,15 +7,15 @@ package repo
import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
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
@@ -63,11 +63,7 @@ func GetIssueDependencies(ctx *context.APIContext) {
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)
}
ctx.APIErrorAuto(err)
return
}
@@ -487,11 +483,7 @@ func RemoveIssueBlocking(ctx *context.APIContext) {
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)
}
ctx.APIErrorAuto(err)
return nil
}
issue.Repo = ctx.Repo.Repository
@@ -508,11 +500,7 @@ func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Is
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)
}
ctx.APIErrorAuto(err)
return nil
}
} else {
@@ -521,11 +509,7 @@ func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Is
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)
}
ctx.APIErrorAuto(err)
return nil
}
issue.Repo = repo
@@ -9,12 +9,12 @@ import (
"net/http"
"reflect"
issues_model "code.gitea.io/gitea/models/issues"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
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
@@ -173,18 +173,17 @@ func DeleteIssueLabel(ctx *context.APIContext) {
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return
}
label, err := issues_model.GetLabelByID(ctx, ctx.PathParamInt64("id"))
// the label must belong to this repo (or its owning org); otherwise a foreign label ID
// is rejected the same way as a nonexistent one, closing a cross-repo enumeration oracle
labelID := ctx.PathParamInt64("id")
label, err := issues_model.GetLabelInRepoOrOrgByID(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner.ID, ctx.Repo.Owner.IsOrganization(), labelID)
if err != nil {
if issues_model.IsErrLabelNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -295,7 +294,7 @@ func ClearIssueLabels(ctx *context.APIContext) {
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return
}
@@ -319,7 +318,7 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
return nil, nil, err
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, "write permission is required")
return nil, nil, errors.New("permission denied")
}
@@ -4,13 +4,12 @@
package repo
import (
"errors"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
issues_model "gitea.dev/models/issues"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
)
// LockIssue lock an issue
@@ -54,16 +53,12 @@ func LockIssue(ctx *context.APIContext) {
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)
}
ctx.APIErrorAuto(err)
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to lock this issue"))
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, "no permission to lock this issue")
return
}
@@ -121,16 +116,12 @@ func UnlockIssue(ctx *context.APIContext) {
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)
}
ctx.APIErrorAuto(err)
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to unlock this issue"))
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, "no permission to unlock this issue")
return
}
@@ -6,10 +6,10 @@ package repo
import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issues_model "gitea.dev/models/issues"
api "gitea.dev/modules/structs"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// PinIssue pins a issue
@@ -46,7 +46,7 @@ func PinIssue(ctx *context.APIContext) {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else if issues_model.IsErrIssueMaxPinReached(err) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -7,14 +7,14 @@ import (
"errors"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
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
@@ -53,11 +53,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -71,8 +67,8 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions"))
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIError(http.StatusForbidden, "no permission to get reactions")
return
}
@@ -190,11 +186,7 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) {
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)
}
ctx.APIErrorAuto(err)
return
}
@@ -208,13 +200,13 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIErrorNotFound()
return
}
if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction"))
if comment.Issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIError(http.StatusForbidden, "no permission to change reaction")
return
}
@@ -223,7 +215,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
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)
ctx.APIError(http.StatusForbidden, err.Error())
} else if issues_model.IsErrReactionAlreadyExist(err) {
ctx.JSON(http.StatusOK, api.Reaction{
User: convert.ToUser(ctx, ctx.Doer, ctx.Doer),
@@ -304,8 +296,8 @@ func GetIssueReactions(ctx *context.APIContext) {
return
}
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions"))
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, "no permission to get reactions")
return
}
@@ -428,8 +420,8 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
return
}
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction"))
if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, "no permission to change reaction")
return
}
@@ -438,7 +430,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
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)
ctx.APIError(http.StatusForbidden, err.Error())
} else if issues_model.IsErrReactionAlreadyExist(err) {
ctx.JSON(http.StatusOK, api.Reaction{
User: convert.ToUser(ctx, ctx.Doer, ctx.Doer),
@@ -6,10 +6,10 @@ package repo
import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
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.
@@ -178,7 +178,7 @@ func prepareIssueForStopwatch(ctx *context.APIContext) *issues_model.Issue {
return nil
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return nil
}
@@ -7,12 +7,12 @@ import (
"fmt"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
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
@@ -128,7 +128,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) {
// 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))
ctx.APIError(http.StatusForbidden, fmt.Sprintf("%s is not permitted to change subscriptions for %s", ctx.Doer.Name, user.Name))
return
}
@@ -4,19 +4,19 @@
package repo
import (
"errors"
"net/http"
"time"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
"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
@@ -72,16 +72,12 @@ func ListTrackedTimes(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
ctx.APIErrorNotFound("Timetracker is disabled")
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)
}
ctx.APIErrorAuto(err)
return
}
@@ -95,7 +91,8 @@ func ListTrackedTimes(ctx *context.APIContext) {
if qUser != "" {
user, err := user_model.GetUserByName(ctx, qUser)
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
return
} else if err != nil {
ctx.APIErrorInternal(err)
return
@@ -104,7 +101,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
}
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
@@ -116,7 +113,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
if opts.UserID == 0 {
opts.UserID = ctx.Doer.ID
} else {
ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights"))
ctx.APIError(http.StatusForbidden, "query by user not allowed; not enough rights")
return
}
}
@@ -136,11 +133,33 @@ func ListTrackedTimes(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
trackedTimes, err = filterTrackedTimesByAccess(ctx, trackedTimes)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
}
func filterTrackedTimesByAccess(ctx *context.APIContext, trackedTimes issues_model.TrackedTimeList) (issues_model.TrackedTimeList, error) {
filtered := make(issues_model.TrackedTimeList, 0, len(trackedTimes))
for _, trackedTime := range trackedTimes {
if trackedTime.Issue == nil || trackedTime.Issue.Repo == nil {
continue
}
permission, err := access_model.GetIndividualUserRepoPermission(ctx, trackedTime.Issue.Repo, ctx.Doer)
if err != nil {
return nil, err
}
if permission.HasAnyUnitAccessOrPublicAccess() {
filtered = append(filtered, trackedTime)
}
}
return filtered, nil
}
// AddTime add time manual to the given issue
func AddTime(ctx *context.APIContext) {
// swagger:operation Post /repos/{owner}/{repo}/issues/{index}/times issue issueAddTime
@@ -183,11 +202,7 @@ func AddTime(ctx *context.APIContext) {
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)
}
ctx.APIErrorAuto(err)
return
}
@@ -206,7 +221,8 @@ func AddTime(ctx *context.APIContext) {
// allow only RepoAdmin, Admin and User to add time
user, err = user_model.GetUserByName(ctx, form.User)
if err != nil {
ctx.APIErrorInternal(err)
ctx.APIErrorAuto(err)
return
}
}
}
@@ -266,11 +282,7 @@ func ResetIssueTime(ctx *context.APIContext) {
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)
}
ctx.APIErrorAuto(err)
return
}
@@ -286,7 +298,7 @@ func ResetIssueTime(ctx *context.APIContext) {
err = issues_model.DeleteIssueUserTimes(ctx, issue, ctx.Doer)
if err != nil {
if db.IsErrNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -339,11 +351,7 @@ func DeleteTime(ctx *context.APIContext) {
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)
}
ctx.APIErrorAuto(err)
return
}
@@ -358,11 +366,7 @@ func DeleteTime(ctx *context.APIContext) {
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)
ctx.APIErrorAuto(err)
return
}
if time.Deleted {
@@ -424,11 +428,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
}
user, err := user_model.GetUserByName(ctx, ctx.PathParam("timetrackingusername"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
if user == nil {
@@ -437,7 +437,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
}
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"))
ctx.APIError(http.StatusForbidden, "query by user not allowed; not enough rights")
return
}
@@ -523,7 +523,8 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
if qUser != "" {
user, err := user_model.GetUserByName(ctx, qUser)
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
return
} else if err != nil {
ctx.APIErrorInternal(err)
return
@@ -533,7 +534,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
var err error
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
@@ -545,7 +546,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
if opts.UserID == 0 {
opts.UserID = ctx.Doer.ID
} else {
ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights"))
ctx.APIError(http.StatusForbidden, "query by user not allowed; not enough rights")
return
}
}
@@ -565,6 +566,11 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
trackedTimes, err = filterTrackedTimesByAccess(ctx, trackedTimes)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
@@ -607,7 +613,7 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
var err error
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
@@ -627,6 +633,11 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
trackedTimes, err = filterTrackedTimesByAccess(ctx, trackedTimes)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
+13 -13
View File
@@ -10,18 +10,18 @@ import (
"net/http"
"net/url"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
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
@@ -179,7 +179,7 @@ func HandleCheckKeyStringError(ctx *context.APIContext, err error) {
} 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))
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid key content: %v", err))
}
}
@@ -8,13 +8,13 @@ import (
"net/http"
"strconv"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/label"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
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
@@ -106,11 +106,7 @@ func GetLabel(ctx *context.APIContext) {
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)
}
ctx.APIErrorAuto(err)
return
}
@@ -153,7 +149,7 @@ func CreateLabel(ctx *context.APIContext) {
color, err := label.NormalizeColor(form.Color)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
form.Color = color
@@ -214,11 +210,7 @@ func EditLabel(ctx *context.APIContext) {
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)
}
ctx.APIErrorAuto(err)
return
}
@@ -231,7 +223,7 @@ func EditLabel(ctx *context.APIContext) {
if form.Color != nil {
color, err := label.NormalizeColor(*form.Color)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
l.Color = color
@@ -8,9 +8,9 @@ import (
"net/http"
"strconv"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/services/context"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/log"
"gitea.dev/services/context"
)
type languageResponse []*repo_model.LanguageStat
@@ -6,9 +6,9 @@ package repo
import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/services/context"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/log"
"gitea.dev/services/context"
)
// GetLicenses returns licenses
@@ -6,9 +6,9 @@ package repo
import (
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
webhook_service "code.gitea.io/gitea/services/webhook"
"gitea.dev/models/unittest"
"gitea.dev/modules/setting"
webhook_service "gitea.dev/services/webhook"
)
func TestMain(m *testing.M) {
@@ -10,26 +10,26 @@ import (
"net/http"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/migrations"
notify_service "code.gitea.io/gitea/services/notify"
repo_service "code.gitea.io/gitea/services/repository"
"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
@@ -72,18 +72,13 @@ func Migrate(ctx *context.APIContext) {
}
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
return
}
if ctx.HasAPIError() {
ctx.APIError(http.StatusUnprocessableEntity, ctx.GetErrMsg())
return
}
if !ctx.Doer.IsAdmin {
if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID {
ctx.APIError(http.StatusForbidden, "Given user is not an organization.")
@@ -115,12 +110,12 @@ func Migrate(ctx *context.APIContext) {
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"))
ctx.APIError(http.StatusForbidden, "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"))
ctx.APIError(http.StatusForbidden, "the site administrator has disabled migrations")
return
}
@@ -240,9 +235,9 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, err
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)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
case base.IsErrNotSupported(err):
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
default:
err = util.SanitizeErrorCredentialURLs(err)
if strings.Contains(err.Error(), "Authentication failed") ||
@@ -262,7 +257,7 @@ func handleRemoteAddrError(ctx *context.APIContext, err error) {
addrErr := err.(*git.ErrInvalidCloneAddr)
switch {
case addrErr.IsURLError:
ctx.APIError(http.StatusUnprocessableEntity, err)
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.")
@@ -8,15 +8,15 @@ import (
"net/http"
"strconv"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"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
@@ -5,22 +5,24 @@ package repo
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/migrations"
mirror_service "code.gitea.io/gitea/services/mirror"
"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
@@ -51,7 +53,7 @@ func MirrorSync(ctx *context.APIContext) {
repo := ctx.Repo.Repository
if !ctx.Repo.CanWrite(unit.TypeCode) {
if !ctx.Repo.Permission.CanWrite(unit.TypeCode) {
ctx.APIError(http.StatusForbidden, "Must have write access")
}
@@ -101,6 +103,8 @@ func PushMirrorSync(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
if !setting.Mirror.Enabled {
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
@@ -109,17 +113,21 @@ func PushMirrorSync(ctx *context.APIContext) {
// 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)
ctx.APIError(http.StatusNotFound, err.Error())
return
}
failedPushMirrors := make([]string, 0)
for _, mirror := range pushMirrors {
ok := mirror_service.SyncPushMirror(ctx, mirror.ID)
if !ok {
ctx.APIErrorInternal(errors.New("error occurred when syncing push mirror " + mirror.RemoteName))
return
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)
}
@@ -168,7 +176,7 @@ func ListPushMirrors(ctx *context.APIContext) {
// 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)
ctx.APIError(http.StatusNotFound, err.Error())
return
}
@@ -232,7 +240,7 @@ func GetPushMirrorByName(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
} else if !exist {
ctx.APIError(http.StatusNotFound, nil)
ctx.APIErrorNotFound()
return
}
@@ -327,7 +335,7 @@ func DeletePushMirrorByRemoteName(ctx *context.APIContext) {
// 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)
ctx.APIError(http.StatusNotFound, err.Error())
return
}
ctx.Status(http.StatusNoContent)
@@ -337,8 +345,12 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro
repo := ctx.Repo.Repository
interval, err := time.ParseDuration(mirrorOption.Interval)
if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
ctx.APIError(http.StatusBadRequest, err)
if err != nil {
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("invalid interval: %v", err))
return
}
if interval != 0 && interval < setting.Mirror.MinInterval {
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("interval is shorter than minimum %v", setting.Mirror.MinInterval.String()))
return
}
@@ -351,11 +363,7 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro
return
}
remoteSuffix, err := util.CryptoRandomString(10)
if err != nil {
ctx.APIErrorInternal(err)
return
}
remoteSuffix := util.CryptoRandomString(10)
remoteAddress, err := util.SanitizeURL(mirrorOption.RemoteAddress)
if err != nil {
@@ -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")
}
@@ -7,10 +7,10 @@ import (
"errors"
"net/http"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"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
@@ -68,11 +68,7 @@ func getNote(ctx *context.APIContext, identifier string) {
commitID, err := ctx.Repo.GitRepo.ConvertToGitID(identifier)
if err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -6,10 +6,10 @@ package repo
import (
"net/http"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/repository/files"
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
@@ -12,39 +12,38 @@ import (
"strings"
"time"
activities_model "code.gitea.io/gitea/models/activities"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
pull_model "code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/automerge"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/forms"
git_service "code.gitea.io/gitea/services/git"
"code.gitea.io/gitea/services/gitdiff"
issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
activities_model "gitea.dev/models/activities"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
pull_model "gitea.dev/models/pull"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/base"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/routers/common"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/automerge"
"gitea.dev/services/context"
"gitea.dev/services/convert"
"gitea.dev/services/forms"
git_service "gitea.dev/services/git"
"gitea.dev/services/gitdiff"
issue_service "gitea.dev/services/issue"
notify_service "gitea.dev/services/notify"
pull_service "gitea.dev/services/pull"
repo_service "gitea.dev/services/repository"
)
// ListPullRequests returns a list of all PRs
@@ -126,7 +125,7 @@ func ListPullRequests(ctx *context.APIContext) {
poster, err := user_model.GetUserByName(ctx, posterStr)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -448,7 +447,7 @@ func CreatePullRequest(ctx *context.APIContext) {
HeadBranch: existingPr.HeadBranch,
BaseBranch: existingPr.BaseBranch,
}
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
return
}
@@ -521,7 +520,7 @@ func CreatePullRequest(ctx *context.APIContext) {
BaseBranch: compareResult.BaseRef.ShortName(),
HeadRepo: compareResult.HeadRepo,
BaseRepo: repo,
MergeBase: compareResult.MergeBase,
MergeBase: compareResult.CompareBase,
Type: issues_model.PullRequestGitea,
}
@@ -545,13 +544,13 @@ func CreatePullRequest(ctx *context.APIContext) {
return
}
valid, err := access_model.CanBeAssigned(ctx, assignee, repo, true)
valid, err := access_model.CanBeAssigned(ctx, assignee, repo)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !valid {
ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}.Error())
return
}
}
@@ -570,11 +569,11 @@ func CreatePullRequest(ctx *context.APIContext) {
if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else if errors.Is(err, issues_model.ErrMustCollaborator) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -653,7 +652,7 @@ func EditPullRequest(ctx *context.APIContext) {
return
}
if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWrite(unit.TypePullRequests) {
if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.Permission.CanWrite(unit.TypePullRequests) {
ctx.Status(http.StatusForbidden)
return
}
@@ -663,7 +662,7 @@ func EditPullRequest(ctx *context.APIContext) {
// handles concurrent requests.
// TODO: wrap all mutations in a transaction to fully prevent partial writes.
if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion {
ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged)
ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged.Error())
return
}
@@ -682,7 +681,7 @@ func EditPullRequest(ctx *context.APIContext) {
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion)
if err != nil {
if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
return
}
@@ -715,13 +714,13 @@ func EditPullRequest(ctx *context.APIContext) {
// Pass one or more user logins to replace the set of assignees on this Issue.
// Send an empty array ([]) to clear all assignees from the Issue.
if ctx.Repo.CanWrite(unit.TypePullRequests) && (form.Assignees != nil || len(form.Assignee) > 0) {
if ctx.Repo.Permission.CanWrite(unit.TypePullRequests) && (form.Assignees != nil || len(form.Assignee) > 0) {
err = issue_service.UpdateAssignees(ctx, issue, form.Assignee, form.Assignees, ctx.Doer)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: [name: %s]", err))
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -729,7 +728,7 @@ func EditPullRequest(ctx *context.APIContext) {
}
}
if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Milestone != 0 &&
if ctx.Repo.Permission.CanWrite(unit.TypePullRequests) && form.Milestone != 0 &&
issue.MilestoneID != form.Milestone {
oldMilestoneID := issue.MilestoneID
issue.MilestoneID = form.Milestone
@@ -744,7 +743,7 @@ func EditPullRequest(ctx *context.APIContext) {
}
}
if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Labels != nil {
if ctx.Repo.Permission.CanWrite(unit.TypePullRequests) && form.Labels != nil {
labels, err := issues_model.GetLabelsInRepoByIDs(ctx, ctx.Repo.Repository.ID, form.Labels)
if err != nil {
ctx.APIErrorInternal(err)
@@ -788,18 +787,18 @@ func EditPullRequest(ctx *context.APIContext) {
return
}
if !branchExist {
ctx.APIError(http.StatusNotFound, fmt.Errorf("new base '%s' not exist", form.Base))
ctx.APIError(http.StatusNotFound, fmt.Sprintf("new base '%s' not exist", form.Base))
return
}
if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, form.Base); err != nil {
if issues_model.IsErrPullRequestAlreadyExists(err) {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
return
} else if issues_model.IsErrIssueIsClosed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
} else if pull_service.IsErrPullRequestHasMerged(err) {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -927,11 +926,7 @@ func MergePullRequest(ctx *context.APIContext) {
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound("GetPullRequestByIndex", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -965,7 +960,7 @@ func MergePullRequest(ctx *context.APIContext) {
}
// start with merging by checking
if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil {
if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, repo_model.MergeStyle(form.Do), form.ForceMerge); err != nil {
if errors.Is(err, pull_service.ErrIsClosed) {
ctx.APIErrorNotFound()
} else if errors.Is(err, pull_service.ErrNoPermissionToMerge) {
@@ -977,9 +972,11 @@ func MergePullRequest(ctx *context.APIContext) {
} else if errors.Is(err, pull_service.ErrNotMergeableState) {
ctx.APIError(http.StatusMethodNotAllowed, "Please try again later")
} else if errors.Is(err, pull_service.ErrNotReadyToMerge) {
ctx.APIError(http.StatusMethodNotAllowed, err)
ctx.APIError(http.StatusMethodNotAllowed, err.Error())
} else if asymkey_service.IsErrWontSign(err) {
ctx.APIError(http.StatusMethodNotAllowed, err)
ctx.APIError(http.StatusMethodNotAllowed, err.Error())
} else if errors.Is(err, pull_service.ErrHeadCommitsNotAllVerified) {
ctx.APIError(http.StatusMethodNotAllowed, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -990,11 +987,11 @@ func MergePullRequest(ctx *context.APIContext) {
if manuallyMerged {
if err := pull_service.MergedManually(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
if pull_service.IsErrInvalidMergeStyle(err) {
ctx.APIError(http.StatusMethodNotAllowed, fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
ctx.APIError(http.StatusMethodNotAllowed, fmt.Sprintf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
return
}
if strings.Contains(err.Error(), "Wrong commit ID") {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1032,7 +1029,7 @@ func MergePullRequest(ctx *context.APIContext) {
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, deleteBranchAfterMerge)
if err != nil {
if pull_model.IsErrAlreadyScheduledToAutoMerge(err) {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1044,9 +1041,9 @@ func MergePullRequest(ctx *context.APIContext) {
}
}
if err := pull_service.Merge(ctx, pr, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
if err := pull_service.Merge(pr, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
if pull_service.IsErrInvalidMergeStyle(err) {
ctx.APIError(http.StatusMethodNotAllowed, fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
ctx.APIError(http.StatusMethodNotAllowed, fmt.Sprintf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
} else if pull_service.IsErrMergeConflicts(err) {
conflictError := err.(pull_service.ErrMergeConflicts)
ctx.JSON(http.StatusConflict, conflictError)
@@ -1089,12 +1086,6 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git
baseRepo := ctx.Repo.Repository
compareReq := common.ParseCompareRouterParam(compareParam)
// remove the check when we support compare with carets
if compareReq.BaseOriRefSuffix != "" {
ctx.APIError(http.StatusBadRequest, "Unsupported comparison syntax: ref with suffix")
return nil, nil
}
_, headRepo, err := common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq)
switch {
case errors.Is(err, util.ErrInvalidArgument):
@@ -1107,6 +1098,10 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git
ctx.APIErrorInternal(err)
return nil, nil
}
if !ctx.TokenCanAccessRepo(headRepo) {
ctx.APIErrorNotFound()
return nil, nil
}
isSameRepo := baseRepo.ID == headRepo.ID
@@ -1154,10 +1149,18 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git
return nil, nil
}
baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx)))
headRef := headGitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch))
baseRef, err := common.ResolveRefWithSuffix(ctx.Repo.GitRepo, util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx)), compareReq.BaseOriRefSuffix)
if err != nil {
ctx.APIErrorAuto(err)
return nil, nil
}
headRef, err := common.ResolveRefWithSuffix(headGitRepo, util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch), compareReq.HeadOriRefSuffix)
if err != nil {
ctx.APIErrorAuto(err)
return nil, nil
}
log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.RelativePath(), compareReq.BaseOriRef, baseRef, compareReq.HeadOriRef, headRef)
log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.RelativePath(), compareReq.BaseOriRef+compareReq.BaseOriRefSuffix, baseRef, compareReq.HeadOriRef+compareReq.HeadOriRefSuffix, headRef)
baseRefValid := baseRef.IsBranch() || baseRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName), baseRef.ShortName())
headRefValid := headRef.IsBranch() || headRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(headRepo.ObjectFormatName), headRef.ShortName())
@@ -1173,7 +1176,7 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git
return nil, nil
}
return compareInfo, closer
return &compareInfo, closer
}
// UpdatePullRequest merge PR's baseBranch into headBranch
@@ -1228,7 +1231,7 @@ func UpdatePullRequest(ctx *context.APIContext) {
}
if pr.HasMerged {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, "pull request is already merged")
return
}
@@ -1238,7 +1241,7 @@ func UpdatePullRequest(ctx *context.APIContext) {
}
if pr.Issue.IsClosed {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, "pull request is already closed")
return
}
@@ -1251,15 +1254,24 @@ func UpdatePullRequest(ctx *context.APIContext) {
return
}
rebase := ctx.FormString("style") == "rebase"
// a public-only token must not update (push into) a private head repo,
// even when the base repo named in the route is public
if !ctx.TokenCanAccessRepo(pr.HeadRepo) {
ctx.APIErrorNotFound()
return
}
allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, pr, ctx.Doer)
// keep API back-compat: when no style is given, default to "merge" rather than the repo's DefaultUpdateStyle,
// so existing API clients keep getting a merge update.
rebase := repo_model.UpdateStyle(ctx.FormString("style", string(repo_model.UpdateStyleMerge))) == repo_model.UpdateStyleRebase
userUpdateStyles, err := pull_service.CheckUserAllowedToUpdate(ctx, pr, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) {
if (rebase && !userUpdateStyles.RebaseAllowed) || (!rebase && !userUpdateStyles.MergeAllowed) {
ctx.Status(http.StatusForbidden)
return
}
@@ -1267,7 +1279,7 @@ func UpdatePullRequest(ctx *context.APIContext) {
// default merge commit message
message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch)
if err = pull_service.Update(graceful.GetManager().ShutdownContext(), pr, ctx.Doer, message, rebase); err != nil {
if err = pull_service.Update(pr, ctx.Doer, message, rebase); err != nil {
if pull_service.IsErrMergeConflicts(err) {
ctx.APIError(http.StatusConflict, "merge failed because of conflict")
return
@@ -1417,7 +1429,6 @@ func GetPullRequestCommits(ctx *context.APIContext) {
return
}
var compareInfo *git_service.CompareInfo
baseGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
if err != nil {
ctx.APIErrorInternal(err)
@@ -1425,13 +1436,14 @@ func GetPullRequestCommits(ctx *context.APIContext) {
}
defer closer.Close()
var compareInfo git_service.CompareInfo
if pr.HasMerged {
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefName(pr.MergeBase), git.RefName(pr.GetGitHeadRefName()), false, false)
} else {
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), false, false)
}
if gitcmd.StderrHasPrefix(err, "fatal: bad revision") {
if gitcmd.IsStderr(err, gitcmd.StderrBadRevision) {
ctx.APIError(http.StatusNotFound, "invalid base branch or revision")
return
} else if err != nil {
@@ -1550,7 +1562,7 @@ func GetPullRequestFiles(ctx *context.APIContext) {
baseGitRepo := ctx.Repo.GitRepo
var compareInfo *git_service.CompareInfo
var compareInfo git_service.CompareInfo
if pr.HasMerged {
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefName(pr.MergeBase), git.RefName(pr.GetGitHeadRefName()), false, false)
} else {
@@ -1567,7 +1579,7 @@ func GetPullRequestFiles(ctx *context.APIContext) {
return
}
startCommitID := compareInfo.MergeBase
startCommitID := compareInfo.CompareBase
endCommitID := headCommitID
maxLines := setting.Git.MaxGitDiffLines
@@ -9,18 +9,18 @@ import (
"net/http"
"strings"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/organization"
access_model "gitea.dev/models/perm/access"
user_model "gitea.dev/models/user"
"gitea.dev/modules/gitrepo"
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"
)
// ListPullReviews lists all reviews of a pull request
@@ -63,11 +63,7 @@ func ListPullReviews(ctx *context.APIContext) {
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound("GetPullRequestByIndex", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -208,6 +204,88 @@ func GetPullReviewComments(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiComments)
}
// CreatePullReviewCommentReply replies to a pull request review comment.
// The URL mirrors GitHub's endpoint, {index} is verified against the parent comment's pull request.
func CreatePullReviewCommentReply(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/comments/{id}/replies repository repoCreatePullReviewCommentReply
// ---
// summary: Reply to a pull request review 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: index
// in: path
// description: index of the pull request
// type: integer
// format: int64
// required: true
// - name: id
// in: path
// description: id of the review comment to reply to
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/CreatePullReviewCommentReplyOptions"
// responses:
// "201":
// "$ref": "#/responses/PullReviewComment"
// "400":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
opts := web.GetForm(ctx).(*api.CreatePullReviewCommentReplyOptions)
parent := getPullReviewCommentToResolve(ctx)
if parent == nil {
return
}
if parent.Issue.Index != ctx.PathParamInt64("index") {
ctx.APIErrorNotFound()
return
}
if parent.ReviewID == 0 {
ctx.APIError(http.StatusBadRequest, "comment is not a review comment")
return
}
comment, err := pull_service.CreateCodeComment(ctx,
ctx.Doer, ctx.Repo.GitRepo, parent.Issue,
parent.Line, opts.Body, parent.TreePath,
false, parent.ReviewID,
"", nil,
)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err := comment.LoadPoster(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
comment.Issue = parent.Issue
ctx.JSON(http.StatusCreated, convert.ToPullReviewComment(ctx, comment, ctx.Doer))
}
// ResolvePullReviewComment resolves a review comment in a pull request
func ResolvePullReviewComment(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/comments/{id}/resolve repository repoResolvePullReviewComment
@@ -307,11 +385,7 @@ func updatePullReviewCommentResolve(ctx *context.APIContext, isResolve bool) {
func getPullReviewCommentToResolve(ctx *context.APIContext) *issues_model.Comment {
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound("GetCommentByID", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return nil
}
@@ -376,7 +450,7 @@ func DeletePullReview(ctx *context.APIContext) {
return
}
if !ctx.Doer.IsAdmin && ctx.Doer.ID != review.ReviewerID {
ctx.APIError(http.StatusForbidden, nil)
ctx.APIError(http.StatusForbidden, "no permission to delete comment")
return
}
@@ -392,7 +466,7 @@ func DeletePullReview(ctx *context.APIContext) {
func CreatePullReview(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews repository repoCreatePullReview
// ---
// summary: Create a review to an pull request
// summary: Create a review to a pull request
// produces:
// - application/json
// parameters:
@@ -428,11 +502,7 @@ func CreatePullReview(ctx *context.APIContext) {
opts := web.GetForm(ctx).(*api.CreatePullReviewOptions)
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound("GetPullRequestByIndex", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -493,7 +563,7 @@ func CreatePullReview(ctx *context.APIContext) {
review, _, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil)
if err != nil {
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -509,11 +579,11 @@ func CreatePullReview(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiReview)
}
// SubmitPullReview submit a pending review to an pull request
// SubmitPullReview submit a pending review to a pull request
func SubmitPullReview(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoSubmitPullReview
// ---
// summary: Submit a pending review to an pull request
// summary: Submit a pending review to a pull request
// produces:
// - application/json
// parameters:
@@ -559,7 +629,7 @@ func SubmitPullReview(ctx *context.APIContext) {
}
if review.Type != issues_model.ReviewTypePending {
ctx.APIError(http.StatusUnprocessableEntity, errors.New("only a pending review can be submitted"))
ctx.APIError(http.StatusUnprocessableEntity, "only a pending review can be submitted")
return
}
@@ -571,7 +641,7 @@ func SubmitPullReview(ctx *context.APIContext) {
// if review stay pending return
if reviewType == issues_model.ReviewTypePending {
ctx.APIError(http.StatusUnprocessableEntity, errors.New("review stay pending"))
ctx.APIError(http.StatusUnprocessableEntity, "review stay pending")
return
}
@@ -585,7 +655,7 @@ func SubmitPullReview(ctx *context.APIContext) {
review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil)
if err != nil {
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -616,7 +686,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest
case api.ReviewStateApproved:
// can not approve your own PR
if pr.Issue.IsPoster(ctx.Doer.ID) {
ctx.APIError(http.StatusUnprocessableEntity, errors.New("approve your own pull is not allowed"))
ctx.APIError(http.StatusUnprocessableEntity, "approve your own pull is not allowed")
return -1, true
}
reviewType = issues_model.ReviewTypeApprove
@@ -625,7 +695,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest
case api.ReviewStateRequestChanges:
// can not reject your own PR
if pr.Issue.IsPoster(ctx.Doer.ID) {
ctx.APIError(http.StatusUnprocessableEntity, errors.New("reject your own pull is not allowed"))
ctx.APIError(http.StatusUnprocessableEntity, "reject your own pull is not allowed")
return -1, true
}
reviewType = issues_model.ReviewTypeReject
@@ -635,7 +705,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest
needsBody = false
// if there is no body we need to ensure that there are comments
if !hasBody && !hasComments {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("review event %s requires a body or a comment", event))
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("review event %s requires a body or a comment", event))
return -1, true
}
default:
@@ -644,7 +714,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest
// reject reviews with empty body if a body is required for this call
if needsBody && !hasBody {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("review event %s requires a body", event))
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("review event %s requires a body", event))
return -1, true
}
@@ -655,33 +725,25 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest
func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues_model.PullRequest, bool) {
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound("GetPullRequestByIndex", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return nil, nil, true
}
review, err := issues_model.GetReviewByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrReviewNotExist(err) {
ctx.APIErrorNotFound("GetReviewByID", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return nil, nil, true
}
// validate the review is for the given PR
if review.IssueID != pr.IssueID {
ctx.APIErrorNotFound("ReviewNotInPR")
ctx.APIErrorNotFound()
return nil, nil, true
}
// make sure that the user has access to this review if it is pending
if review.Type == issues_model.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
ctx.APIErrorNotFound("GetReviewByID")
ctx.APIErrorNotFound()
return nil, nil, true
}
@@ -693,7 +755,7 @@ func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues
return review, pr, false
}
// CreateReviewRequests create review requests to an pull request
// CreateReviewRequests create review requests to a pull request
func CreateReviewRequests(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/requested_reviewers repository repoCreatePullReviewRequests
// ---
@@ -734,7 +796,7 @@ func CreateReviewRequests(ctx *context.APIContext) {
apiReviewRequest(ctx, *opts, true)
}
// DeleteReviewRequests delete review requests to an pull request
// DeleteReviewRequests delete review requests to a pull request
func DeleteReviewRequests(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/requested_reviewers repository repoDeletePullReviewRequests
// ---
@@ -788,7 +850,7 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound("UserNotExist", fmt.Sprintf("User '%s' not exist", r))
ctx.APIErrorNotFound("user doesn't exist: " + r)
return nil, nil
}
ctx.APIErrorInternal(err)
@@ -804,7 +866,7 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN
teamReviewer, err = organization.GetTeam(ctx, ctx.Repo.Owner.ID, t)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIErrorNotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t))
ctx.APIErrorNotFound("team doesn't exist: " + t)
return nil, nil
}
ctx.APIErrorInternal(err)
@@ -820,11 +882,7 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN
func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) {
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound("GetPullRequestByIndex", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -853,11 +911,11 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, &permDoer, reviewer, isAdd)
if err != nil {
if issues_model.IsErrReviewRequestOnClosedPR(err) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
return
}
if issues_model.IsErrNotValidReviewRequest(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -878,11 +936,11 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
comment, err := issue_service.TeamReviewRequest(ctx, pr.Issue, ctx.Doer, teamReviewer, isAdd)
if err != nil {
if issues_model.IsErrReviewRequestOnClosedPR(err) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
return
}
if issues_model.IsErrNotValidReviewRequest(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1003,7 +1061,7 @@ func UnDismissPullReview(ctx *context.APIContext) {
}
func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors bool) {
if !ctx.Repo.IsAdmin() {
if !ctx.Repo.Permission.IsAdmin() {
ctx.APIError(http.StatusForbidden, "Must be repo admin")
return
}
@@ -1020,7 +1078,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors
_, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss, dismissPriors)
if err != nil {
if pull_service.IsErrDismissRequestOnClosedPR(err) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -4,25 +4,23 @@
package repo
import (
"errors"
"fmt"
"net/http"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
release_service "code.gitea.io/gitea/services/release"
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.CanWrite(unit.TypeReleases) {
if !ctx.IsSigned || !ctx.Repo.Permission.CanWrite(unit.TypeReleases) {
return false
}
if ctx.Data["IsApiToken"] != true {
@@ -243,7 +241,7 @@ func CreateRelease(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.CreateReleaseOption)
if ctx.Repo.Repository.IsEmpty {
ctx.APIError(http.StatusUnprocessableEntity, errors.New("repo is empty"))
ctx.APIError(http.StatusUnprocessableEntity, "repo is empty")
return
}
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName)
@@ -273,11 +271,11 @@ func CreateRelease(ctx *context.APIContext) {
// 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)
ctx.APIError(http.StatusConflict, err.Error())
} else if release_service.IsErrProtectedTagName(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else if git.IsErrNotExist(err) {
ctx.APIError(http.StatusNotFound, fmt.Errorf("target \"%v\" not found: %w", rel.Target, err))
ctx.APIError(http.StatusNotFound, "target not found")
} else {
ctx.APIErrorInternal(err)
}
@@ -8,16 +8,16 @@ import (
"net/http"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
attachment_service "code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert"
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 {
@@ -205,7 +205,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
// Check if attachments are enabled
if !setting.Attachment.Enabled {
ctx.APIErrorNotFound("Attachment is not enabled")
ctx.APIErrorNotFound("attachment is not enabled")
return
}
@@ -250,12 +250,12 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
})
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
if errors.Is(err, util.ErrContentTooLarge) {
ctx.APIError(http.StatusRequestEntityTooLarge, err)
ctx.APIError(http.StatusRequestEntityTooLarge, err.Error())
return
}
@@ -340,7 +340,7 @@ func EditReleaseAttachment(ctx *context.APIContext) {
if err := attachment_service.UpdateAttachment(ctx, setting.Repository.Release.AllowedTypes, attach); err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -6,11 +6,11 @@ package repo
import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
release_service "code.gitea.io/gitea/services/release"
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
@@ -60,7 +60,7 @@ func GetReleaseByTag(ctx *context.APIContext) {
}
if release.IsDraft { // only the users with write access can see draft releases
if !ctx.IsSigned || !ctx.Repo.CanWrite(unit_model.TypeReleases) {
if !ctx.IsSigned || !ctx.Repo.Permission.CanWrite(unit_model.TypeReleases) {
ctx.APIErrorNotFound()
return
}
+117 -62
View File
@@ -13,31 +13,34 @@ import (
"strings"
"time"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/label"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
feed_service "code.gitea.io/gitea/services/feed"
"code.gitea.io/gitea/services/issue"
repo_service "code.gitea.io/gitea/services/repository"
activities_model "gitea.dev/models/activities"
"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"
unit_model "gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/label"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
actions_service "gitea.dev/services/actions"
"gitea.dev/services/context"
"gitea.dev/services/convert"
feed_service "gitea.dev/services/feed"
"gitea.dev/services/issue"
"gitea.dev/services/migrations"
mirror_service "gitea.dev/services/mirror"
repo_service "gitea.dev/services/repository"
)
// Search repositories via options
@@ -170,7 +173,7 @@ func Search(ctx *context.APIContext) {
opts.Collaborate = optional.Some(true)
case "":
default:
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid search mode: \"%s\"", mode))
ctx.APIError(http.StatusUnprocessableEntity, "invalid search mode")
return
}
@@ -182,24 +185,11 @@ func Search(ctx *context.APIContext) {
opts.IsPrivate = optional.Some(ctx.FormBool("is_private"))
}
sortMode := ctx.FormString("sort")
if len(sortMode) > 0 {
sortOrder := ctx.FormString("order")
if len(sortOrder) == 0 {
sortOrder = "asc"
}
if searchModeMap, ok := repo_model.OrderByMap[sortOrder]; ok {
if orderBy, ok := searchModeMap[sortMode]; ok {
opts.OrderBy = orderBy
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
return
}
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
return
}
orderBy, ok := utils.ResolveSortOrder(ctx, repo_model.OrderByMap, "")
if !ok {
return
}
opts.OrderBy = orderBy
repos, count, err := repo_model.SearchRepository(ctx, opts)
if err != nil {
@@ -244,7 +234,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
// If the readme template does not exist, a 400 will be returned.
if opt.AutoInit && len(opt.Readme) > 0 && !slices.Contains(repo_module.Readmes, opt.Readme) {
ctx.APIError(http.StatusBadRequest, fmt.Errorf("readme template does not exist, available templates: %v", repo_module.Readmes))
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("readme template does not exist, available templates: %v", repo_module.Readmes))
return
}
@@ -260,7 +250,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
DefaultBranch: opt.DefaultBranch,
TrustModel: repo_model.ToTrustModel(opt.TrustModel),
IsTemplate: opt.Template,
ObjectFormatName: opt.ObjectFormatName,
ObjectFormatName: string(opt.ObjectFormatName),
})
if err != nil {
if repo_model.IsErrRepoAlreadyExist(err) {
@@ -268,9 +258,9 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
} else if db.IsErrNameReserved(err) ||
db.IsErrNamePatternNotAllowed(err) ||
label.IsErrTemplateLoad(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else if errors.Is(err, util.ErrPermissionDenied) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -423,7 +413,7 @@ func Generate(ctx *context.APIContext) {
ctx.APIError(http.StatusConflict, "The repository with the same name already exists.")
} else if db.IsErrNameReserved(err) ||
db.IsErrNamePatternNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -630,7 +620,11 @@ func Edit(ctx *context.APIContext) {
}
}
if opts.MirrorInterval != nil || opts.EnablePrune != nil {
if opts.MirrorInterval != nil ||
opts.EnablePrune != nil ||
opts.MirrorUsername != nil ||
opts.MirrorPassword != nil ||
opts.MirrorToken != nil {
if err := updateMirror(ctx, opts); err != nil {
return
}
@@ -658,13 +652,13 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil {
switch {
case repo_model.IsErrRepoAlreadyExist(err):
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
case db.IsErrNameReserved(err):
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
case db.IsErrNamePatternNotAllowed(err):
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
default:
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("ChangeRepositoryName: %w", err))
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("ChangeRepositoryName: %v", err))
}
return err
}
@@ -698,7 +692,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.Doer.IsAdmin {
err := errors.New("cannot change private repository to public")
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
@@ -760,14 +754,14 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
if opts.HasIssues != nil {
if *opts.HasIssues && opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
// Check that values are valid
if !validation.IsValidExternalURL(opts.ExternalTracker.ExternalTrackerURL) {
if !validation.IsValidURL(opts.ExternalTracker.ExternalTrackerURL) {
err := errors.New("External tracker URL not valid")
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) {
err := errors.New("External tracker URL format not valid")
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
@@ -822,7 +816,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
if opts.HasWiki != nil {
if *opts.HasWiki && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
// Check that values are valid
if !validation.IsValidExternalURL(opts.ExternalWiki.ExternalWikiURL) {
if !validation.IsValidURL(opts.ExternalWiki.ExternalWikiURL) {
err := errors.New("External wiki URL not valid")
ctx.APIError(http.StatusUnprocessableEntity, "Invalid external wiki URL")
return err
@@ -893,10 +887,20 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
optional.AssignPtrValue(changed, &config.AllowFastForwardOnly, opts.AllowFastForwardOnly)
optional.AssignPtrValue(changed, &config.AllowManualMerge, opts.AllowManualMerge)
optional.AssignPtrValue(changed, &config.AutodetectManualMerge, opts.AutodetectManualMerge)
optional.AssignPtrValue(changed, &config.AllowMergeUpdate, opts.AllowMergeUpdate)
optional.AssignPtrValue(changed, &config.AllowRebaseUpdate, opts.AllowRebaseUpdate)
optional.AssignPtrValue(changed, &config.DefaultDeleteBranchAfterMerge, opts.DefaultDeleteBranchAfterMerge)
optional.AssignPtrValue(changed, &config.DefaultAllowMaintainerEdit, opts.DefaultAllowMaintainerEdit)
optional.AssignPtrString(changed, &config.DefaultMergeStyle, opts.DefaultMergeStyle)
optional.AssignPtrString(changed, &config.DefaultUpdateStyle, opts.DefaultUpdateStyle)
// only validate update-style fields when the caller is actually changing one of them,
// so unrelated PATCH calls don't reject historical configs.
if opts.AllowMergeUpdate != nil || opts.AllowRebaseUpdate != nil || opts.DefaultUpdateStyle != nil {
if err := config.ValidateUpdateSettings(); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
}
if *changed || mustInsertPullRequestUnit {
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
@@ -984,7 +988,7 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
if opts.Archived != nil {
if repo.IsMirror {
err := errors.New("repo is a mirror, cannot archive/un-archive")
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
if *opts.Archived {
@@ -1038,14 +1042,14 @@ func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error {
interval, err := time.ParseDuration(*opts.MirrorInterval)
if err != nil {
log.Error("Wrong format for MirrorInternal Sent: %s", err)
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
// Ensure the provided duration is not too short
if interval != 0 && interval < setting.Mirror.MinInterval {
err := fmt.Errorf("invalid mirror interval: %s is below minimum interval: %s", interval, setting.Mirror.MinInterval)
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
@@ -1061,10 +1065,61 @@ func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error {
log.Trace("Repository %s Mirror[%d] Set EnablePrune: %t", repo.FullName(), mirror.ID, mirror.EnablePrune)
}
authUpdateRequested := opts.MirrorPassword != nil || opts.MirrorToken != nil || opts.MirrorUsername != nil
if authUpdateRequested {
remoteURL, err := gitrepo.GitRemoteGetURL(ctx, repo, mirror.GetRemoteName())
if err != nil {
ctx.APIErrorInternal(err)
return err
}
authUsername := ""
if opts.MirrorUsername != nil {
authUsername = *opts.MirrorUsername
} else if remoteURL.User != nil {
authUsername = remoteURL.User.Username()
}
authPassword := ""
authToken := ""
if opts.MirrorPassword != nil {
authPassword = *opts.MirrorPassword
}
if opts.MirrorToken != nil {
authToken = *opts.MirrorToken
}
if opts.MirrorPassword == nil && opts.MirrorToken == nil && remoteURL.User != nil && (authUsername == "" || authUsername == remoteURL.User.Username()) {
authPassword, _ = remoteURL.User.Password()
}
if authToken != "" {
authPassword = authToken
}
composedAddress, err := git.ParseRemoteAddr(repo.OriginalURL, authUsername, authPassword)
if err == nil {
err = migrations.IsMigrateURLAllowed(composedAddress, ctx.Doer)
}
if err != nil {
handleRemoteAddrError(ctx, err)
return err
}
if err := mirror_service.UpdateAddress(ctx, mirror, composedAddress); err != nil {
ctx.APIErrorInternal(err)
return err
}
if sanitized, err := util.SanitizeURL(repo.OriginalURL); err == nil {
mirror.RemoteAddress = sanitized
}
}
// finally update the mirror in the DB
if err := repo_model.UpdateMirror(ctx, mirror); err != nil {
log.Error("Failed to Set Mirror Interval: %s", err)
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
@@ -7,11 +7,11 @@ import (
"net/http"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/contexttest"
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"
)
@@ -6,11 +6,11 @@ package repo
import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
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
@@ -7,14 +7,14 @@ import (
"fmt"
"net/http"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
"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
@@ -55,7 +55,7 @@ func NewCommitStatus(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.CreateStatusOption)
sha := ctx.PathParam("sha")
if len(sha) == 0 {
ctx.APIError(http.StatusBadRequest, nil)
ctx.APIError(http.StatusBadRequest, "sha not provided")
return
}
status := &git_model.CommitStatus{
@@ -6,11 +6,11 @@ package repo
import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
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)
+19 -19
View File
@@ -9,16 +9,16 @@ import (
"net/http"
"strings"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
release_service "code.gitea.io/gitea/services/release"
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
@@ -109,13 +109,13 @@ func GetAnnotatedTag(ctx *context.APIContext) {
tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
commit, err := ctx.Repo.GitRepo.GetTagCommit(tag.Name)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit))
@@ -203,13 +203,13 @@ func CreateTag(ctx *context.APIContext) {
commit, err := ctx.Repo.GitRepo.GetCommit(form.Target)
if err != nil {
ctx.APIError(http.StatusNotFound, fmt.Errorf("target not found: %w", err))
ctx.APIError(http.StatusNotFound, fmt.Sprintf("target not found: %v", 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)
ctx.APIError(http.StatusConflict, err.Error())
return
}
if release_service.IsErrProtectedTagName(err) {
@@ -278,7 +278,7 @@ func DeleteTag(ctx *context.APIContext) {
}
if !tag.IsTag {
ctx.APIError(http.StatusConflict, errors.New("a tag attached to a release cannot be deleted directly"))
ctx.APIError(http.StatusConflict, "a tag attached to a release cannot be deleted directly")
return
}
@@ -438,7 +438,7 @@ func CreateTagProtection(ctx *context.APIContext) {
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -449,7 +449,7 @@ func CreateTagProtection(ctx *context.APIContext) {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -546,7 +546,7 @@ func EditTagProtection(ctx *context.APIContext) {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -560,7 +560,7 @@ func EditTagProtection(ctx *context.APIContext) {
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -7,10 +7,10 @@ import (
"fmt"
"net/http"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_service "code.gitea.io/gitea/services/repository"
"gitea.dev/models/organization"
"gitea.dev/services/context"
"gitea.dev/services/convert"
repo_service "gitea.dev/services/repository"
)
// ListTeams list a repository's teams
@@ -137,6 +137,8 @@ func AddTeam(ctx *context.APIContext) {
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "405":
@@ -173,6 +175,8 @@ func DeleteTeam(ctx *context.APIContext) {
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "405":
@@ -186,9 +190,9 @@ func DeleteTeam(ctx *context.APIContext) {
func changeRepoTeam(ctx *context.APIContext, add bool) {
if !ctx.Repo.Owner.IsOrganization() {
ctx.APIError(http.StatusMethodNotAllowed, "repo is not owned by an organization")
return
}
if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
ctx.APIError(http.StatusForbidden, "user is nor repo admin nor owner")
if !canChangeRepoTeam(ctx) {
return
}
@@ -201,13 +205,13 @@ func changeRepoTeam(ctx *context.APIContext, add bool) {
var err error
if add {
if repoHasTeam {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team '%s' is already added to repo", team.Name))
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("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))
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("team '%s' was not added to repo", team.Name))
return
}
err = repo_service.RemoveRepositoryFromTeam(ctx, team, ctx.Repo.Repository.ID)
@@ -220,11 +224,24 @@ func changeRepoTeam(ctx *context.APIContext, add bool) {
ctx.Status(http.StatusNoContent)
}
func canChangeRepoTeam(ctx *context.APIContext) bool {
canChange, err := organization.OrgFromUser(ctx.Repo.Owner).CanChangeRepoTeamAccess(ctx, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return false
}
if !canChange {
ctx.APIError(http.StatusForbidden, "Must be an organization owner")
return false
}
return true
}
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)
ctx.APIError(http.StatusNotFound, err.Error())
return nil
}
ctx.APIErrorInternal(err)
@@ -7,14 +7,14 @@ import (
"net/http"
"strings"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"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
@@ -300,7 +300,7 @@ func TopicSearch(ctx *context.APIContext) {
}
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, map[string]any{
"topics": topicResponses,
ctx.JSON(http.StatusOK, api.TopicListResponse{
Topics: topicResponses,
})
}
@@ -8,18 +8,18 @@ import (
"fmt"
"net/http"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_service "code.gitea.io/gitea/services/repository"
"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
@@ -87,12 +87,12 @@ func Transfer(ctx *context.APIContext) {
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))
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("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))
ctx.APIError(http.StatusForbidden, fmt.Sprintf("team %d belongs not to org %d", tID, org.ID))
return
}
@@ -110,13 +110,13 @@ func Transfer(ctx *context.APIContext) {
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)
ctx.APIError(http.StatusConflict, err.Error())
case repo_model.IsErrRepoAlreadyExist(err):
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
case repo_service.IsRepositoryLimitReached(err):
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
case errors.Is(err, user_model.ErrBlockedUser):
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
default:
ctx.APIErrorInternal(err)
}
@@ -163,11 +163,11 @@ func AcceptTransfer(ctx *context.APIContext) {
if err != nil {
switch {
case repo_model.IsErrNoPendingTransfer(err):
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
case errors.Is(err, util.ErrPermissionDenied):
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
case repo_service.IsRepositoryLimitReached(err):
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
default:
ctx.APIErrorInternal(err)
}
@@ -207,9 +207,9 @@ func RejectTransfer(ctx *context.APIContext) {
if err != nil {
switch {
case repo_model.IsErrNoPendingTransfer(err):
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
case errors.Is(err, util.ErrPermissionDenied):
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
default:
ctx.APIErrorInternal(err)
}
@@ -6,8 +6,8 @@ package repo
import (
"net/http"
"code.gitea.io/gitea/services/context"
files_service "code.gitea.io/gitea/services/repository/files"
"gitea.dev/services/context"
files_service "gitea.dev/services/repository/files"
)
// GetTree get the tree of a repository.
@@ -9,17 +9,17 @@ import (
"net/http"
"net/url"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
notify_service "code.gitea.io/gitea/services/notify"
wiki_service "code.gitea.io/gitea/services/wiki"
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
@@ -59,7 +59,7 @@ func NewWikiPage(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.CreateWikiPageOptions)
if util.IsEmptyString(form.Title) {
ctx.APIError(http.StatusBadRequest, nil)
ctx.APIError(http.StatusBadRequest, "title is required")
return
}
@@ -71,16 +71,16 @@ func NewWikiPage(ctx *context.APIContext) {
content, err := base64.StdEncoding.DecodeString(form.ContentBase64)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
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)
ctx.APIError(http.StatusBadRequest, err.Error())
} else if repo_model.IsErrWikiAlreadyExist(err) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -149,7 +149,7 @@ func EditWikiPage(ctx *context.APIContext) {
content, err := base64.StdEncoding.DecodeString(form.ContentBase64)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
form.ContentBase64 = string(content)
@@ -245,11 +245,7 @@ func DeleteWikiPage(ctx *context.APIContext) {
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)
ctx.APIErrorAuto(err)
return
}
@@ -435,7 +431,7 @@ func ListPageRevisions(ctx *context.APIContext) {
page := max(ctx.FormInt("page"), 1)
// get Commit Count
commitsHistory, err := wikiRepo.CommitsByFileAndRange(
commitsHistory, _, err := wikiRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
Revision: ctx.Repo.Repository.DefaultWikiBranch,
File: pageFilename,
@@ -474,21 +470,13 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error)
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)
}
ctx.APIErrorAuto(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)
}
ctx.APIErrorAuto(err)
return wikiRepo, nil
}
return wikiRepo, commit