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
@@ -6,9 +6,9 @@ package actions
import (
"net/http"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/actions/ping"
"code.gitea.io/gitea/routers/api/actions/runner"
"gitea.dev/modules/web"
"gitea.dev/routers/api/actions/ping"
"gitea.dev/routers/api/actions/runner"
)
func Routes(prefix string) *web.Router {
+1 -1
View File
@@ -760,7 +760,7 @@ const file_artifact_proto_rawDesc = "" +
"\x16DeleteArtifactResponse\x12\x0e\n" +
"\x02ok\x18\x01 \x01(\bR\x02ok\x12\x1f\n" +
"\vartifact_id\x18\x02 \x01(\x03R\n" +
"artifactIdB)Z'code.gitea.io/gitea/routers/api/actionsb\x06proto3"
"artifactIdB\x1fZ\x1dgitea.dev/routers/api/actionsb\x06proto3"
var (
file_artifact_proto_rawDescOnce sync.Once
@@ -5,7 +5,7 @@ import "google/protobuf/wrappers.proto";
package github.actions.results.api.v1;
option go_package = "code.gitea.io/gitea/routers/api/actions";
option go_package = "gitea.dev/routers/api/actions";
message CreateArtifactRequest {
string workflow_run_backend_id = 1;
@@ -66,21 +66,22 @@ import (
"errors"
"fmt"
"net/http"
"slices"
"strconv"
"strings"
"code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
web_types "code.gitea.io/gitea/modules/web/types"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
"gitea.dev/models/actions"
"gitea.dev/models/db"
"gitea.dev/modules/httplib"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/storage"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
web_types "gitea.dev/modules/web/types"
actions_service "gitea.dev/services/actions"
"gitea.dev/services/context"
)
const artifactRouteBase = "/_apis/pipelines/workflows/{run_id}/artifacts"
@@ -120,6 +121,9 @@ func ArtifactsRoutes(prefix string) *web.Router {
m.Get("/{artifact_id}/download", r.downloadArtifact)
})
// Job summary upload endpoint (GITHUB_STEP_SUMMARY).
m.Put(jobSummaryRouteBase, uploadJobSummary)
return m
}
@@ -310,7 +314,7 @@ func (ar artifactRoutes) confirmUploadArtifact(ctx *ArtifactContext) {
ctx.HTTPError(http.StatusBadRequest, "Error artifact name is empty")
return
}
if err := mergeChunksForRun(ctx, ar.fs, runID, artifactName); err != nil {
if err := mergeChunksForRun(ctx, ar.fs, runID, ctx.ActionTask.Job.RunAttemptID, artifactName); err != nil {
log.Error("Error merge chunks: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks")
return
@@ -332,14 +336,19 @@ type (
)
func (ar artifactRoutes) listArtifacts(ctx *ArtifactContext) {
_, runID, ok := validateRunID(ctx)
task, runID, ok := validateRunID(ctx)
if !ok {
return
}
attemptIDs, ok := readableArtifactAttemptIDs(ctx, task)
if !ok {
return
}
artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{
RunID: runID,
Status: int(actions.ArtifactStatusUploadConfirmed),
artifacts, err := actions.FindReadableArtifacts(ctx, actions.FindArtifactsOptions{
RunID: runID,
RunAttemptIDs: attemptIDs,
Status: int(actions.ArtifactStatusUploadConfirmed),
})
if err != nil {
log.Error("Error getting artifacts: %v", err)
@@ -392,7 +401,7 @@ type (
// getDownloadArtifactURL generates download url for each artifact
func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
_, runID, ok := validateRunID(ctx)
task, runID, ok := validateRunID(ctx)
if !ok {
return
}
@@ -402,10 +411,16 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
return
}
artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{
RunID: runID,
ArtifactName: itemPath,
Status: int(actions.ArtifactStatusUploadConfirmed),
attemptIDs, ok := readableArtifactAttemptIDs(ctx, task)
if !ok {
return
}
artifacts, err := actions.FindReadableArtifacts(ctx, actions.FindArtifactsOptions{
RunID: runID,
RunAttemptIDs: attemptIDs,
ArtifactName: itemPath,
Status: int(actions.ArtifactStatusUploadConfirmed),
})
if err != nil {
log.Error("Error getting artifacts: %v", err)
@@ -455,7 +470,7 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
// downloadArtifact downloads artifact content
func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) {
_, runID, ok := validateRunID(ctx)
task, runID, ok := validateRunID(ctx)
if !ok {
return
}
@@ -477,6 +492,18 @@ func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) {
ctx.HTTPError(http.StatusBadRequest)
return
}
// resolving the readable attempts costs a query, and an artifact of the task's own attempt never needs it
if artifact.RunAttemptID != task.Job.RunAttemptID {
attemptIDs, ok := readableArtifactAttemptIDs(ctx, task)
if !ok {
return
}
if !slices.Contains(attemptIDs, artifact.RunAttemptID) {
log.Error("Error artifact %d belongs to run attempt %d, which the task cannot read: %v", artifactID, artifact.RunAttemptID, attemptIDs)
ctx.HTTPError(http.StatusBadRequest)
return
}
}
if artifact.Status != actions.ArtifactStatusUploadConfirmed {
log.Error("Error artifact not found: %s", artifact.Status.ToString())
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
@@ -17,11 +17,11 @@ import (
"strings"
"time"
"code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"gitea.dev/models/actions"
"gitea.dev/models/db"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/storage"
)
type saveUploadChunkOptions struct {
@@ -257,11 +257,12 @@ func listOrderedChunksForArtifact(st storage.ObjectStorage, runID, artifactID in
return emptyListAsError(chunks)
}
func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int64, artifactName string) error {
func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID, runAttemptID int64, artifactName string) error {
// read all db artifacts by name
artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{
RunID: runID,
ArtifactName: artifactName,
RunID: runID,
RunAttemptIDs: []int64{runAttemptID},
ArtifactName: artifactName,
})
if err != nil {
return err
@@ -10,9 +10,9 @@ import (
"strconv"
"strings"
"code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
"gitea.dev/models/actions"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
)
const (
@@ -43,7 +43,7 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) {
return task, runID, true
}
func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) { //nolint:unparam // ActionTask is never used
func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) {
task := ctx.ActionTask
runID, err := strconv.ParseInt(rawRunID, 10, 64)
if err != nil || task.Job.RunID != runID {
@@ -54,6 +54,18 @@ func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask
return task, runID, true
}
// readableArtifactAttemptIDs resolves the attempts a task may read artifacts from:
// its own attempt, plus the attempts it inherits from when only a subset of the run's jobs was re-run.
func readableArtifactAttemptIDs(ctx *ArtifactContext, task *actions.ActionTask) ([]int64, bool) {
attemptIDs, err := actions.GetArtifactAttemptIDs(ctx, task.Job)
if err != nil {
log.Error("Error getting readable artifact attempts: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error getting readable artifact attempts")
return nil, false
}
return attemptIDs, true
}
func validateArtifactHash(ctx *ArtifactContext, artifactName string) bool {
paramHash := ctx.PathParam("artifact_hash")
// use artifact name to create upload url
@@ -102,16 +102,17 @@ import (
"strings"
"time"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
actions_module "gitea.dev/modules/actions"
"gitea.dev/modules/httplib"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/storage"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/services/actions"
"gitea.dev/services/context"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/reflect/protoreflect"
@@ -161,7 +162,7 @@ func ArtifactsV4Routes(prefix string) *web.Router {
}
func (r *artifactV4Routes) buildSignature(endpoint, expires, artifactName string, taskID, artifactID int64) []byte {
return actions.BuildSignature("v4", endpoint, expires, artifactName, strconv.FormatInt(taskID, 10), strconv.FormatInt(artifactID, 10))
return actions_module.BuildSignature("v4", endpoint, expires, artifactName, strconv.FormatInt(taskID, 10), strconv.FormatInt(artifactID, 10))
}
func (r *artifactV4Routes) buildArtifactURL(ctx *ArtifactContext, endpoint, artifactName string, taskID, artifactID int64) string {
@@ -260,9 +261,28 @@ func (r *artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*
return task, artifactName, true
}
func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions_model.ActionArtifact, error) {
// getOwnAttemptArtifactByName resolves an artifact of the given attempt whatever its status,
// since upload and finalize work on the pending row they just created.
func (r *artifactV4Routes) getOwnAttemptArtifactByName(ctx *ArtifactContext, runID, runAttemptID int64, name string) (*actions_model.ActionArtifact, error) {
return r.findArtifactByName(ctx, runID, []int64{runAttemptID}, name, nil)
}
// getDownloadableArtifactByName resolves the newest artifact with the given name within the attempts whose content can still be served,
// so a pending, deleted or expired row of a newer attempt does not shadow the confirmed copy inherited from an older one.
func (r *artifactV4Routes) getDownloadableArtifactByName(ctx *ArtifactContext, runID int64, runAttemptIDs []int64, name string) (*actions_model.ActionArtifact, error) {
return r.findArtifactByName(ctx, runID, runAttemptIDs, name, builder.Eq{"status": actions_model.ArtifactStatusUploadConfirmed})
}
func (r *artifactV4Routes) findArtifactByName(ctx *ArtifactContext, runID int64, runAttemptIDs []int64, name string, extraCond builder.Cond) (*actions_model.ActionArtifact, error) {
cond := builder.NewCond().
And(builder.Eq{"run_id": runID, "artifact_name": name}, builder.Like{"content_encoding", "%/%"}).
And(builder.In("run_attempt_id", runAttemptIDs))
if extraCond != nil {
cond = cond.And(extraCond)
}
var art actions_model.ActionArtifact
has, err := db.GetEngine(ctx).Where(builder.Eq{"run_id": runID, "artifact_name": name}, builder.Like{"content_encoding", "%/%"}).Get(&art)
has, err := db.GetEngine(ctx).Where(cond).OrderBy("run_attempt_id DESC, id DESC").Get(&art)
if err != nil {
return nil, err
} else if !has {
@@ -382,7 +402,7 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
switch comp {
case "block", "appendBlock":
// get artifact by name
artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName)
artifact, err := r.getOwnAttemptArtifactByName(ctx, task.Job.RunID, task.Job.RunAttemptID, artifactName)
if err != nil {
log.Error("Error artifact not found: %v", err)
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
@@ -469,7 +489,7 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) {
}
// get artifact by name
artifact, err := r.getArtifactByName(ctx, runID, req.Name)
artifact, err := r.getOwnAttemptArtifactByName(ctx, runID, ctx.ActionTask.Job.RunAttemptID, req.Name)
if err != nil {
log.Error("Error artifact not found: %v", err)
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
@@ -576,13 +596,18 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) {
if ok := r.parseProtobufBody(ctx, &req); !ok {
return
}
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
task, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
if !ok {
return
}
attemptIDs, ok := readableArtifactAttemptIDs(ctx, task)
if !ok {
return
}
artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
artifacts, err := actions_model.FindReadableArtifacts(ctx, actions_model.FindArtifactsOptions{
RunID: runID,
RunAttemptIDs: attemptIDs,
Status: int(actions_model.ArtifactStatusUploadConfirmed),
FinalizedArtifactsV4: true,
})
@@ -594,6 +619,8 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) {
list := []*ListArtifactsResponse_MonolithArtifact{}
// both filters pick from what this attempt may read, so they run after the shadowed artifacts are gone:
// a shadowed artifact is not downloadable either, GetSignedArtifactURL resolves by name
table := map[string]*ListArtifactsResponse_MonolithArtifact{}
for _, artifact := range artifacts {
if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value {
@@ -628,7 +655,11 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
if ok := r.parseProtobufBody(ctx, &req); !ok {
return
}
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
task, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
if !ok {
return
}
attemptIDs, ok := readableArtifactAttemptIDs(ctx, task)
if !ok {
return
}
@@ -636,17 +667,12 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
artifactName := req.Name
// get artifact by name
artifact, err := r.getArtifactByName(ctx, runID, artifactName)
artifact, err := r.getDownloadableArtifactByName(ctx, runID, attemptIDs, artifactName)
if err != nil {
log.Error("Error artifact not found: %v", err)
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
return
}
if artifact.Status != actions_model.ArtifactStatusUploadConfirmed {
log.Error("Error artifact not found: %s", artifact.Status.ToString())
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
return
}
respData := GetSignedArtifactURLResponse{}
@@ -668,16 +694,15 @@ func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) {
if !ok {
return
}
// get artifact by name
artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName)
if err != nil {
log.Error("Error artifact not found: %v", err)
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
attemptIDs, ok := readableArtifactAttemptIDs(ctx, task)
if !ok {
return
}
if artifact.Status != actions_model.ArtifactStatusUploadConfirmed {
log.Error("Error artifact not found: %s", artifact.Status.ToString())
// get artifact by name
artifact, err := r.getDownloadableArtifactByName(ctx, task.Job.RunID, attemptIDs, artifactName)
if err != nil {
log.Error("Error artifact not found: %v", err)
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
return
}
@@ -701,14 +726,14 @@ func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) {
}
// get artifact by name
artifact, err := r.getArtifactByName(ctx, runID, req.Name)
artifact, err := r.getOwnAttemptArtifactByName(ctx, runID, ctx.ActionTask.Job.RunAttemptID, req.Name)
if err != nil {
log.Error("Error artifact not found: %v", err)
ctx.HTTPError(http.StatusNotFound, "Error artifact not found")
return
}
err = actions_model.SetArtifactNeedDelete(ctx, runID, req.Name)
err = actions_model.SetArtifactNeedDeleteByRunAttempt(ctx, runID, ctx.ActionTask.Job.RunAttemptID, req.Name)
if err != nil {
log.Error("Error deleting artifacts: %v", err)
ctx.HTTPError(http.StatusInternalServerError, err.Error())
@@ -0,0 +1,104 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"errors"
"io"
"mime"
"net/http"
"slices"
"strconv"
actions_model "gitea.dev/models/actions"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
)
const jobSummaryRouteBase = "/_apis/pipelines/workflows/{run_id}/jobs/{job_id}/steps/{step_index}/summary"
func uploadJobSummary(ctx *ArtifactContext) {
task, _, ok := validateRunID(ctx)
if !ok {
return
}
jobID := ctx.PathParamInt64("job_id")
if jobID <= 0 || task.Job.ID != jobID {
ctx.HTTPError(http.StatusBadRequest, "job_id mismatch")
return
}
stepIndex, err := strconv.ParseInt(ctx.PathParam("step_index"), 10, 64)
if err != nil || stepIndex < 0 {
ctx.HTTPError(http.StatusBadRequest, "invalid step_index")
return
}
steps, err := actions_model.GetTaskStepsByTaskID(ctx, task.ID)
if err != nil {
log.Error("Error getting task steps: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error getting task steps")
return
}
if !slices.ContainsFunc(steps, func(s *actions_model.ActionTaskStep) bool { return s.Index == stepIndex }) {
ctx.HTTPError(http.StatusBadRequest, "step_index mismatch")
return
}
contentType, ok := normalizeJobSummaryContentType(ctx.Req.Header.Get("Content-Type"))
if !ok {
ctx.HTTPError(http.StatusBadRequest, "invalid summary content type")
return
}
body, err := io.ReadAll(io.LimitReader(ctx.Req.Body, actions_model.MaxJobSummarySize+1))
if err != nil {
log.Error("Error reading job summary request body: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "read request body")
return
}
message := "success"
if len(body) == 0 {
// PUT with an empty body clears any previously-stored summary for this step.
if err := actions_model.DeleteActionRunJobSummary(ctx, task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID, stepIndex); err != nil {
log.Error("Error deleting job summary: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error deleting job summary")
return
}
message = "cleared"
} else if err := actions_model.UpsertActionRunJobSummary(ctx, task.Job.RepoID, task.Job.RunID, task.Job.RunAttemptID, task.Job.ID, stepIndex, contentType, body); err != nil {
if errors.Is(err, actions_model.ErrJobSummaryAggregateExceeded) {
ctx.HTTPError(http.StatusBadRequest, "job summary aggregate size exceeded")
return
}
if errors.Is(err, util.ErrInvalidArgument) {
ctx.HTTPError(http.StatusBadRequest, "invalid summary")
return
}
log.Error("Error upsert job summary: %v", err)
ctx.HTTPError(http.StatusInternalServerError, "Error upsert job summary")
return
}
ctx.JSON(http.StatusOK, map[string]any{
"message": message,
"sizeBytes": len(body),
"runAttempt": task.Job.RunAttemptID,
})
}
func normalizeJobSummaryContentType(contentType string) (string, bool) {
if contentType == "" || contentType == "application/octet-stream" {
return actions_model.JobSummaryContentTypeMarkdown, true
}
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
return "", false
}
if mediaType != actions_model.JobSummaryContentTypeMarkdown {
return "", false
}
return actions_model.JobSummaryContentTypeMarkdown, true
}
@@ -8,10 +8,10 @@ import (
"fmt"
"net/http"
"code.gitea.io/gitea/modules/log"
pingv1 "gitea.dev/actions-proto-go/ping/v1"
"gitea.dev/actions-proto-go/ping/v1/pingv1connect"
"gitea.dev/modules/log"
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
"connectrpc.com/connect"
)
@@ -8,8 +8,9 @@ import (
"net/http/httptest"
"testing"
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
pingv1 "gitea.dev/actions-proto-go/ping/v1"
"gitea.dev/actions-proto-go/ping/v1/pingv1connect"
"connectrpc.com/connect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -8,12 +8,13 @@ import (
"crypto/subtle"
"errors"
"strings"
"time"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
actions_model "gitea.dev/models/actions"
auth_model "gitea.dev/models/auth"
"gitea.dev/modules/log"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"connectrpc.com/connect"
"google.golang.org/grpc/codes"
@@ -45,14 +46,26 @@ var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unar
return nil, status.Error(codes.Unauthenticated, "unregistered runner")
}
cols := []string{"last_online"}
runner.LastOnline = timeutil.TimeStampNow()
if methodName == "UpdateTask" || methodName == "UpdateLog" {
runner.LastActive = timeutil.TimeStampNow()
now := time.Now()
cols := make([]string, 0, 2)
// Debounce last_active too: while a runner streams logs, UpdateLog fires
// many times per second and writing on each is a major source of DB load.
// Persist only when stale enough to affect the active/idle status.
if (methodName == "UpdateTask" || methodName == "UpdateLog") &&
actions_model.ShouldPersistLastActive(runner.LastActive, now) {
runner.LastActive = timeutil.TimeStamp(now.Unix())
cols = append(cols, "last_active")
}
if err := actions_model.UpdateRunner(ctx, runner, cols...); err != nil {
log.Error("can't update runner status: %v", err)
// Debounce last_online: writing on every poll is a major source of DB load
// with many runners. Persist only when stale enough to affect offline status.
if actions_model.ShouldPersistLastOnline(runner.LastOnline, now) {
runner.LastOnline = timeutil.TimeStamp(now.Unix())
cols = append(cols, "last_online")
}
if len(cols) > 0 {
if err := actions_model.UpdateRunner(ctx, runner, cols...); err != nil {
log.Error("can't update runner status: %v", err)
}
}
ctx = context.WithValue(ctx, runnerCtxKey{}, runner)
@@ -7,22 +7,23 @@ import (
"context"
"errors"
"net/http"
"slices"
actions_model "code.gitea.io/gitea/models/actions"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
actions_service "code.gitea.io/gitea/services/actions"
notify_service "code.gitea.io/gitea/services/notify"
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
"gitea.dev/actions-proto-go/runner/v1/runnerv1connect"
actions_model "gitea.dev/models/actions"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/actions"
"gitea.dev/modules/log"
"gitea.dev/modules/util"
actions_service "gitea.dev/services/actions"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
"connectrpc.com/connect"
gouuid "github.com/google/uuid"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
func NewRunnerServiceHandler() (string, http.Handler) {
@@ -68,21 +69,21 @@ func (s *Service) Register(
}
labels := req.Msg.Labels
hasCancellingSupport := slices.Contains(req.Msg.GetCapabilities(), runnerCapabilityCancelling)
// create new runner
name := util.EllipsisDisplayString(req.Msg.Name, 255)
runner := &actions_model.ActionRunner{
UUID: gouuid.New().String(),
Name: name,
OwnerID: runnerToken.OwnerID,
RepoID: runnerToken.RepoID,
Version: req.Msg.Version,
AgentLabels: labels,
Ephemeral: req.Msg.Ephemeral,
}
if err := runner.GenerateToken(); err != nil {
return nil, errors.New("can't generate token")
UUID: gouuid.New().String(),
Name: name,
OwnerID: runnerToken.OwnerID,
RepoID: runnerToken.RepoID,
Version: req.Msg.Version,
AgentLabels: labels,
Ephemeral: req.Msg.Ephemeral,
HasCancellingSupport: hasCancellingSupport,
}
runner.GenerateAndFillToken()
// create new runner
if err := actions_model.CreateRunner(ctx, runner); err != nil {
@@ -110,18 +111,42 @@ func (s *Service) Register(
return res, nil
}
// runnerCapabilityCancelling is the wire string the runner advertises in its
// capabilities list to indicate it understands the transitional cancelling
// state and will run post-step cleanup before finalizing the task.
const runnerCapabilityCancelling = "cancelling"
type declareRequest interface {
proto.Message
GetVersion() string
GetLabels() []string
GetCapabilities() []string
}
func applyDeclareRequestToRunner(runner *actions_model.ActionRunner, req declareRequest) []string {
runner.AgentLabels = req.GetLabels()
runner.Version = req.GetVersion()
cols := []string{"agent_labels", "version"}
hasCancellingSupport := slices.Contains(req.GetCapabilities(), runnerCapabilityCancelling)
if runner.HasCancellingSupport != hasCancellingSupport {
runner.HasCancellingSupport = hasCancellingSupport
cols = append(cols, "has_cancelling_support")
}
return cols
}
func (s *Service) Declare(
ctx context.Context,
req *connect.Request[runnerv1.DeclareRequest],
) (*connect.Response[runnerv1.DeclareResponse], error) {
runner := GetRunner(ctx)
runner.AgentLabels = req.Msg.Labels
runner.Version = req.Msg.Version
if err := actions_model.UpdateRunner(ctx, runner, "agent_labels", "version"); err != nil {
if err := actions_model.UpdateRunner(ctx, runner, applyDeclareRequestToRunner(runner, req.Msg)...); err != nil {
return nil, status.Errorf(codes.Internal, "update runner: %v", err)
}
return connect.NewResponse(&runnerv1.DeclareResponse{
resp := connect.NewResponse(&runnerv1.DeclareResponse{
Runner: &runnerv1.Runner{
Id: runner.ID,
Uuid: runner.UUID,
@@ -130,7 +155,11 @@ func (s *Service) Declare(
Version: runner.Version,
Labels: runner.AgentLabels,
},
}), nil
})
// Capabilities are communicated via headers to avoid a hard dependency on a proto bump.
// Older runners ignore unknown headers; newer runners can use this for feature negotiation.
resp.Header().Set("X-Gitea-Actions-Capabilities", actions_model.RunnerCapabilities())
return resp, nil
}
// FetchTask assigns a task to the runner
@@ -165,9 +194,15 @@ func (s *Service) FetchTask(
// if the task version in request is not equal to the version in db,
// it means there may still be some tasks that haven't been assigned.
// try to pick a task for the runner that send the request.
if t, ok, err := actions_service.PickTask(ctx, freshRunner); err != nil {
if t, ok, throttled, err := actions_service.TryPickTask(ctx, freshRunner); err != nil {
log.Error("pick task failed: %v", err)
return nil, status.Errorf(codes.Internal, "pick task: %v", err)
} else if throttled {
// Concurrency limit reached: don't advance the runner's tasks version,
// so it retries on its next poll instead of sleeping until the next bump.
latestVersion = tasksVersion
// A steady stream here means MAX_CONCURRENT_TASK_PICKS is too low for the fleet.
log.Debug("task pick throttled for runner %q (id %d); it will retry on its next poll", freshRunner.Name, freshRunner.ID)
} else if ok {
task = t
}
@@ -226,7 +261,7 @@ func (s *Service) UpdateTask(
actions_service.CreateCommitStatusForRunJobs(ctx, task.Job.Run, task.Job)
if task.Status.IsDone() {
notify_service.WorkflowJobStatusUpdate(ctx, task.Job.Run.Repo, task.Job.Run.TriggerUser, task.Job, task)
actions_service.NotifyWorkflowJobStatusUpdateWithTask(ctx, task.Job, task)
}
if req.Msg.State.Result != runnerv1.Result_RESULT_UNSPECIFIED {
@@ -234,7 +269,7 @@ func (s *Service) UpdateTask(
log.Error("Emit ready jobs of run %d: %v", task.Job.RunID, err)
}
if task.Job.Run.Status.IsDone() {
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, task.Job)
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, task.Job.RepoID, task.Job.RunID)
}
}
@@ -270,6 +305,15 @@ func (s *Service) UpdateLog(
rows = req.Msg.Rows[ack-req.Msg.Index:]
}
// Ack a re-sent finalize idempotently. Appending new rows past the seal errors.
if task.LogInStorage {
if len(rows) > 0 {
return nil, status.Errorf(codes.AlreadyExists, "log file has been archived")
}
res.Msg.AckIndex = ack
return res, nil
}
// Bail unless we have new rows or a NoMore to finalize. Even with
// NoMore, bail when the runner has outrun the server — archiving a
// log with a gap is worse than asking it to retry.
@@ -278,10 +322,6 @@ func (s *Service) UpdateLog(
return res, nil
}
if task.LogInStorage {
return nil, status.Errorf(codes.AlreadyExists, "log file has been archived")
}
// WriteLogs is called even with no rows: with offset==0 it bootstraps
// an empty DBFS file so TransferLogs below has something to read when
// the runner finalizes a task that produced no log output.
@@ -0,0 +1,57 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package runner
import (
"testing"
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
actions_model "gitea.dev/models/actions"
"github.com/stretchr/testify/assert"
)
func TestApplyDeclareRequestToRunnerAdvertisedCapabilityEnablesCancelling(t *testing.T) {
runner := &actions_model.ActionRunner{}
req := &runnerv1.DeclareRequest{
Version: "1.2.3",
Labels: []string{"linux"},
Capabilities: []string{runnerCapabilityCancelling, "other"},
}
cols := applyDeclareRequestToRunner(runner, req)
assert.Equal(t, []string{"agent_labels", "version", "has_cancelling_support"}, cols)
assert.True(t, runner.HasCancellingSupport)
assert.Equal(t, "1.2.3", runner.Version)
assert.Equal(t, []string{"linux"}, runner.AgentLabels)
}
func TestApplyDeclareRequestToRunnerMissingCapabilityDisablesCancelling(t *testing.T) {
runner := &actions_model.ActionRunner{
HasCancellingSupport: true,
}
req := &runnerv1.DeclareRequest{
Version: "1.2.3",
Labels: []string{"linux"},
}
cols := applyDeclareRequestToRunner(runner, req)
assert.Equal(t, []string{"agent_labels", "version", "has_cancelling_support"}, cols)
assert.False(t, runner.HasCancellingSupport)
}
func TestApplyDeclareRequestToRunnerUnchangedCapabilityOmitsColumn(t *testing.T) {
runner := &actions_model.ActionRunner{
HasCancellingSupport: true,
}
req := &runnerv1.DeclareRequest{
Version: "1.2.3",
Labels: []string{"linux"},
Capabilities: []string{runnerCapabilityCancelling},
}
cols := applyDeclareRequestToRunner(runner, req)
assert.Equal(t, []string{"agent_labels", "version"}, cols)
assert.True(t, runner.HasCancellingSupport)
}
@@ -13,15 +13,15 @@ import (
"net/http"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/json"
packages_module "code.gitea.io/gitea/modules/packages"
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
alpine_service "code.gitea.io/gitea/services/packages/alpine"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/json"
packages_module "gitea.dev/modules/packages"
alpine_module "gitea.dev/modules/packages/alpine"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
alpine_service "gitea.dev/services/packages/alpine"
)
func apiError(ctx *context.Context, status int, obj any) {
@@ -67,15 +67,34 @@ func GetRepositoryFile(ctx *context.Context) {
return
}
branch := ctx.PathParam("branch")
repository := ctx.PathParam("repository")
architecture := ctx.PathParam("architecture")
s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion(
ctx,
pv,
&packages_service.PackageFileInfo{
Filename: alpine_service.IndexArchiveFilename,
CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.PathParam("branch"), ctx.PathParam("repository"), ctx.PathParam("architecture")),
CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture),
},
ctx.Req.Method,
)
// A repository that only contains "noarch" packages has no per-architecture
// index. Since noarch packages are installable on every architecture, fall
// back to the noarch index so clients requesting their own architecture
// (e.g. x86_64) can still discover them.
if errors.Is(err, util.ErrNotExist) && architecture != alpine_module.NoArch {
s, u, pf, err = packages_service.OpenFileForDownloadByPackageVersion(
ctx,
pv,
&packages_service.PackageFileInfo{
Filename: alpine_service.IndexArchiveFilename,
CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, alpine_module.NoArch),
},
ctx.Req.Method,
)
}
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
@@ -6,36 +6,36 @@ package packages
import (
"net/http"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/packages/alpine"
"code.gitea.io/gitea/routers/api/packages/arch"
"code.gitea.io/gitea/routers/api/packages/cargo"
"code.gitea.io/gitea/routers/api/packages/chef"
"code.gitea.io/gitea/routers/api/packages/composer"
"code.gitea.io/gitea/routers/api/packages/conan"
"code.gitea.io/gitea/routers/api/packages/conda"
"code.gitea.io/gitea/routers/api/packages/container"
"code.gitea.io/gitea/routers/api/packages/cran"
"code.gitea.io/gitea/routers/api/packages/debian"
"code.gitea.io/gitea/routers/api/packages/generic"
"code.gitea.io/gitea/routers/api/packages/goproxy"
"code.gitea.io/gitea/routers/api/packages/helm"
"code.gitea.io/gitea/routers/api/packages/maven"
"code.gitea.io/gitea/routers/api/packages/npm"
"code.gitea.io/gitea/routers/api/packages/nuget"
"code.gitea.io/gitea/routers/api/packages/pub"
"code.gitea.io/gitea/routers/api/packages/pypi"
"code.gitea.io/gitea/routers/api/packages/rpm"
"code.gitea.io/gitea/routers/api/packages/rubygems"
"code.gitea.io/gitea/routers/api/packages/swift"
"code.gitea.io/gitea/routers/api/packages/terraform"
"code.gitea.io/gitea/routers/api/packages/vagrant"
"code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/perm"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/web"
"gitea.dev/routers/api/packages/alpine"
"gitea.dev/routers/api/packages/arch"
"gitea.dev/routers/api/packages/cargo"
"gitea.dev/routers/api/packages/chef"
"gitea.dev/routers/api/packages/composer"
"gitea.dev/routers/api/packages/conan"
"gitea.dev/routers/api/packages/conda"
"gitea.dev/routers/api/packages/container"
"gitea.dev/routers/api/packages/cran"
"gitea.dev/routers/api/packages/debian"
"gitea.dev/routers/api/packages/generic"
"gitea.dev/routers/api/packages/goproxy"
"gitea.dev/routers/api/packages/helm"
"gitea.dev/routers/api/packages/maven"
"gitea.dev/routers/api/packages/npm"
"gitea.dev/routers/api/packages/nuget"
"gitea.dev/routers/api/packages/pub"
"gitea.dev/routers/api/packages/pypi"
"gitea.dev/routers/api/packages/rpm"
"gitea.dev/routers/api/packages/rubygems"
"gitea.dev/routers/api/packages/swift"
"gitea.dev/routers/api/packages/terraform"
"gitea.dev/routers/api/packages/vagrant"
"gitea.dev/services/auth"
"gitea.dev/services/context"
)
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
@@ -73,7 +73,9 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
}
if publicOnly {
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
// a public-only token must not reach limited-visibility owners either,
// matching how orgs/users are enforced elsewhere in this file
if ctx.Package != nil && !ctx.Package.Owner.Visibility.IsPublic() {
ctx.HTTPError(http.StatusForbidden, "reqToken", "token scope is limited to public packages")
return
}
@@ -133,7 +135,7 @@ func CommonRoutes() *web.Router {
r.Group("/{branch}/{repository}", func() {
r.Put("", reqPackageAccess(perm.AccessModeWrite), alpine.UploadPackageFile)
r.Group("/{architecture}", func() {
r.Get("/APKINDEX.tar.gz", alpine.GetRepositoryFile)
r.Methods("HEAD,GET", "/APKINDEX.tar.gz", alpine.GetRepositoryFile)
r.Group("/{filename}", func() {
r.Get("", alpine.DownloadPackageFile)
r.Delete("", reqPackageAccess(perm.AccessModeWrite), alpine.DeletePackageFile)
@@ -12,15 +12,15 @@ import (
"net/http"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/json"
packages_module "code.gitea.io/gitea/modules/packages"
arch_module "code.gitea.io/gitea/modules/packages/arch"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
arch_service "code.gitea.io/gitea/services/packages/arch"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/json"
packages_module "gitea.dev/modules/packages"
arch_module "gitea.dev/modules/packages/arch"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
arch_service "gitea.dev/services/packages/arch"
)
func apiError(ctx *context.Context, status int, obj any) {
@@ -6,10 +6,10 @@ package packages
import (
"net/http"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/packages"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/services/auth"
"gitea.dev/services/packages"
)
var _ auth.Method = &Auth{}
@@ -10,20 +10,20 @@ import (
"strconv"
"strings"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
cargo_module "code.gitea.io/gitea/modules/packages/cargo"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
packages_service "code.gitea.io/gitea/services/packages"
cargo_service "code.gitea.io/gitea/services/packages/cargo"
"gitea.dev/models/db"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
packages_module "gitea.dev/modules/packages"
cargo_module "gitea.dev/modules/packages/cargo"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
"gitea.dev/services/convert"
packages_service "gitea.dev/services/packages"
cargo_service "gitea.dev/services/packages/cargo"
)
// https://doc.rust-lang.org/cargo/reference/registries.html#web-api
@@ -24,10 +24,10 @@ import (
"strings"
"time"
user_model "code.gitea.io/gitea/models/user"
chef_module "code.gitea.io/gitea/modules/packages/chef"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth"
user_model "gitea.dev/models/user"
chef_module "gitea.dev/modules/packages/chef"
"gitea.dev/modules/util"
"gitea.dev/services/auth"
)
const (
@@ -13,16 +13,16 @@ import (
"strings"
"time"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
chef_module "code.gitea.io/gitea/modules/packages/chef"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
"gitea.dev/models/db"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/optional"
packages_module "gitea.dev/modules/packages"
chef_module "gitea.dev/modules/packages/chef"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
)
func apiError(ctx *context.Context, status int, obj any) {
@@ -8,11 +8,11 @@ import (
"net/url"
"time"
packages_model "code.gitea.io/gitea/models/packages"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/modules/log"
composer_module "code.gitea.io/gitea/modules/packages/composer"
"code.gitea.io/gitea/services/context"
packages_model "gitea.dev/models/packages"
access_model "gitea.dev/models/perm/access"
"gitea.dev/modules/log"
composer_module "gitea.dev/modules/packages/composer"
"gitea.dev/services/context"
)
// ServiceIndexResponse contains registry endpoints
@@ -10,17 +10,17 @@ import (
"net/url"
"strconv"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
composer_module "code.gitea.io/gitea/modules/packages/composer"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
packages_service "code.gitea.io/gitea/services/packages"
"gitea.dev/models/db"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/optional"
packages_module "gitea.dev/modules/packages"
composer_module "gitea.dev/modules/packages/composer"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
"gitea.dev/services/convert"
packages_service "gitea.dev/services/packages"
)
func apiError(ctx *context.Context, status int, obj any) {
@@ -12,21 +12,21 @@ import (
"strings"
"time"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
conan_model "code.gitea.io/gitea/models/packages/conan"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
conan_module "code.gitea.io/gitea/modules/packages/conan"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/api/packages/helper"
auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context"
notify_service "code.gitea.io/gitea/services/notify"
packages_service "code.gitea.io/gitea/services/packages"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/db"
packages_model "gitea.dev/models/packages"
conan_model "gitea.dev/models/packages/conan"
"gitea.dev/modules/container"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
packages_module "gitea.dev/modules/packages"
conan_module "gitea.dev/modules/packages/conan"
"gitea.dev/modules/setting"
"gitea.dev/routers/api/packages/helper"
auth_service "gitea.dev/services/auth"
"gitea.dev/services/context"
notify_service "gitea.dev/services/notify"
packages_service "gitea.dev/services/packages"
)
const (
@@ -8,11 +8,11 @@ import (
"net/http"
"strings"
conan_model "code.gitea.io/gitea/models/packages/conan"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
conan_module "code.gitea.io/gitea/modules/packages/conan"
"code.gitea.io/gitea/services/context"
conan_model "gitea.dev/models/packages/conan"
user_model "gitea.dev/models/user"
"gitea.dev/modules/json"
conan_module "gitea.dev/modules/packages/conan"
"gitea.dev/services/context"
)
// SearchResult contains the found recipe names
@@ -10,15 +10,15 @@ import (
"net/http"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
conda_model "code.gitea.io/gitea/models/packages/conda"
"code.gitea.io/gitea/modules/json"
packages_module "code.gitea.io/gitea/modules/packages"
conda_module "code.gitea.io/gitea/modules/packages/conda"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
packages_model "gitea.dev/models/packages"
conda_model "gitea.dev/models/packages/conda"
"gitea.dev/modules/json"
packages_module "gitea.dev/modules/packages"
conda_module "gitea.dev/modules/packages/conda"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
"github.com/dsnet/compress/bzip2"
)
@@ -11,15 +11,15 @@ import (
"os"
"strings"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
container_model "code.gitea.io/gitea/models/packages/container"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
container_module "code.gitea.io/gitea/modules/packages/container"
"code.gitea.io/gitea/modules/util"
packages_service "code.gitea.io/gitea/services/packages"
"gitea.dev/models/db"
packages_model "gitea.dev/models/packages"
container_model "gitea.dev/models/packages/container"
"gitea.dev/modules/globallock"
"gitea.dev/modules/log"
packages_module "gitea.dev/modules/packages"
container_module "gitea.dev/modules/packages/container"
"gitea.dev/modules/util"
packages_service "gitea.dev/services/packages"
"github.com/opencontainers/go-digest"
)
@@ -15,25 +15,25 @@ import (
"strings"
"sync"
auth_model "code.gitea.io/gitea/models/auth"
packages_model "code.gitea.io/gitea/models/packages"
container_model "code.gitea.io/gitea/models/packages/container"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
container_module "code.gitea.io/gitea/modules/packages/container"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
auth_service "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
container_service "code.gitea.io/gitea/services/packages/container"
auth_model "gitea.dev/models/auth"
packages_model "gitea.dev/models/packages"
container_model "gitea.dev/models/packages/container"
user_model "gitea.dev/models/user"
"gitea.dev/modules/httplib"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
packages_module "gitea.dev/modules/packages"
container_module "gitea.dev/modules/packages/container"
"gitea.dev/modules/setting"
"gitea.dev/modules/storage"
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
auth_service "gitea.dev/services/auth"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
container_service "gitea.dev/services/packages/container"
"github.com/opencontainers/go-digest"
)
@@ -11,19 +11,19 @@ import (
"os"
"strings"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
container_model "code.gitea.io/gitea/models/packages/container"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
container_module "code.gitea.io/gitea/modules/packages/container"
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify"
packages_service "code.gitea.io/gitea/services/packages"
container_service "code.gitea.io/gitea/services/packages/container"
"gitea.dev/models/db"
packages_model "gitea.dev/models/packages"
container_model "gitea.dev/models/packages/container"
user_model "gitea.dev/models/user"
"gitea.dev/modules/globallock"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
packages_module "gitea.dev/modules/packages"
container_module "gitea.dev/modules/packages/container"
"gitea.dev/modules/util"
notify_service "gitea.dev/services/notify"
packages_service "gitea.dev/services/packages"
container_service "gitea.dev/services/packages/container"
"github.com/opencontainers/go-digest"
oci "github.com/opencontainers/image-spec/specs-go/v1"
@@ -11,14 +11,14 @@ import (
"net/http"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
cran_model "code.gitea.io/gitea/models/packages/cran"
packages_module "code.gitea.io/gitea/modules/packages"
cran_module "code.gitea.io/gitea/modules/packages/cran"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
packages_model "gitea.dev/models/packages"
cran_model "gitea.dev/models/packages/cran"
packages_module "gitea.dev/modules/packages"
cran_module "gitea.dev/modules/packages/cran"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
)
func apiError(ctx *context.Context, status int, obj any) {
@@ -11,16 +11,16 @@ import (
"net/http"
"strings"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
packages_module "code.gitea.io/gitea/modules/packages"
debian_module "code.gitea.io/gitea/modules/packages/debian"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
notify_service "code.gitea.io/gitea/services/notify"
packages_service "code.gitea.io/gitea/services/packages"
debian_service "code.gitea.io/gitea/services/packages/debian"
"gitea.dev/models/db"
packages_model "gitea.dev/models/packages"
packages_module "gitea.dev/modules/packages"
debian_module "gitea.dev/modules/packages/debian"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
notify_service "gitea.dev/services/notify"
packages_service "gitea.dev/services/packages"
debian_service "gitea.dev/services/packages/debian"
)
func apiError(ctx *context.Context, status int, obj any) {
@@ -120,9 +120,9 @@ func GetRepositoryFileByHash(ctx *context.Context) {
}
func UploadPackageFile(ctx *context.Context) {
distribution := strings.TrimSpace(ctx.PathParam("distribution"))
component := strings.TrimSpace(ctx.PathParam("component"))
if distribution == "" || component == "" {
distribution := ctx.PathParam("distribution")
component := ctx.PathParam("component")
if !debian_module.IsValidDistributionOrComponent(distribution) || !debian_module.IsValidDistributionOrComponent(component) {
apiError(ctx, http.StatusBadRequest, "invalid distribution or component")
return
}
@@ -10,11 +10,11 @@ import (
"strings"
"unicode"
packages_model "code.gitea.io/gitea/models/packages"
packages_module "code.gitea.io/gitea/modules/packages"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
packages_model "gitea.dev/models/packages"
packages_module "gitea.dev/modules/packages"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
)
var (
@@ -11,14 +11,14 @@ import (
"sort"
"time"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
goproxy_module "code.gitea.io/gitea/modules/packages/goproxy"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/optional"
packages_module "gitea.dev/modules/packages"
goproxy_module "gitea.dev/modules/packages/goproxy"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
)
func apiError(ctx *context.Context, status int, obj any) {
@@ -12,18 +12,18 @@ import (
"strings"
"time"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
helm_module "code.gitea.io/gitea/modules/packages/helm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/json"
"gitea.dev/modules/optional"
packages_module "gitea.dev/modules/packages"
helm_module "gitea.dev/modules/packages/helm"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v4"
)
func apiError(ctx *context.Context, status int, obj any) {
@@ -9,10 +9,10 @@ import (
"net/http"
"net/url"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/services/context"
)
// ProcessErrorForUser logs the error and returns a user-error message for the end user.
@@ -7,7 +7,7 @@ import (
"encoding/xml"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
packages_model "gitea.dev/models/packages"
)
// MetadataResponse https://maven.apache.org/ref/3.2.5/maven-repository-metadata/repository-metadata.html
@@ -19,17 +19,19 @@ import (
"strconv"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/json"
packages_module "code.gitea.io/gitea/modules/packages"
maven_module "code.gitea.io/gitea/modules/packages/maven"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/globallock"
"gitea.dev/modules/json"
packages_module "gitea.dev/modules/packages"
maven_module "gitea.dev/modules/packages/maven"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
)
const maxChecksumSize = sha512.Size*2 + 1
const (
mavenMetadataFile = "maven-metadata.xml"
extensionMD5 = ".md5"
@@ -260,12 +262,21 @@ func UploadPackageFile(ctx *context.Context) {
}
defer releaser()
buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body)
ext := path.Ext(params.Filename)
reader := io.Reader(ctx.Req.Body)
if isChecksumExtension(ext) {
reader = io.LimitReader(reader, maxChecksumSize+1)
}
buf, err := packages_module.CreateHashedBufferFromReader(reader)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer buf.Close()
if isChecksumExtension(ext) && !isChecksumSizeAllowed(buf.Size()) {
apiError(ctx, http.StatusRequestEntityTooLarge, "checksum is too large")
return
}
pvci := &packages_service.PackageCreationInfo{
PackageInfo: packages_service.PackageInfo{
@@ -291,8 +302,6 @@ func UploadPackageFile(ctx *context.Context) {
}
}
ext := path.Ext(params.Filename)
// Do not upload checksum files but compare the hashes.
if isChecksumExtension(ext) {
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version)
@@ -404,6 +413,10 @@ func UploadPackageFile(ctx *context.Context) {
ctx.Status(http.StatusCreated)
}
func isChecksumSizeAllowed(size int64) bool {
return size <= maxChecksumSize
}
func isChecksumExtension(ext string) bool {
return ext == extensionMD5 || ext == extensionSHA1 || ext == extensionSHA256 || ext == extensionSHA512
}
@@ -0,0 +1,15 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package maven
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestChecksumSizeAllowed(t *testing.T) {
assert.True(t, isChecksumSizeAllowed(maxChecksumSize))
assert.False(t, isChecksumSizeAllowed(maxChecksumSize+1))
}
@@ -9,10 +9,11 @@ import (
"fmt"
"net/url"
"sort"
"time"
packages_model "code.gitea.io/gitea/models/packages"
npm_module "code.gitea.io/gitea/modules/packages/npm"
"code.gitea.io/gitea/modules/setting"
packages_model "gitea.dev/models/packages"
npm_module "gitea.dev/modules/packages/npm"
"gitea.dev/modules/setting"
)
func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *npm_module.PackageMetadata {
@@ -22,8 +23,14 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
versions := make(map[string]*npm_module.PackageMetadataVersion)
distTags := make(map[string]string)
times := make(map[string]time.Time)
firstPublished, lastPublished := pds[0].Version.CreatedUnix, pds[0].Version.CreatedUnix
for _, pd := range pds {
versions[pd.SemVer.String()] = createPackageMetadataVersion(registryURL, pd)
semVer := pd.SemVer.String()
versions[semVer] = createPackageMetadataVersion(registryURL, pd)
times[semVer] = pd.Version.CreatedUnix.AsTimeInLocation(time.UTC)
firstPublished = min(firstPublished, pd.Version.CreatedUnix)
lastPublished = max(lastPublished, pd.Version.CreatedUnix)
for _, pvp := range pd.VersionProperties {
if pvp.Name == npm_module.TagProperty {
@@ -32,6 +39,10 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
}
}
// npm derives both from the versions currently served, so a deletion moves them
times["created"] = firstPublished.AsTimeInLocation(time.UTC)
times["modified"] = lastPublished.AsTimeInLocation(time.UTC)
latest := pds[len(pds)-1]
metadata := latest.Metadata.(*npm_module.Metadata)
@@ -42,7 +53,10 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
DistTags: distTags,
Description: metadata.Description,
Readme: metadata.Readme,
Maintainers: []npm_module.User{{Name: latest.Owner.Name}},
Time: times,
Homepage: metadata.ProjectURL,
Keywords: metadata.Keywords,
Author: npm_module.User{Name: metadata.Author},
License: metadata.License,
Versions: versions,
@@ -61,8 +75,10 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package
Version: pd.Version.Version,
Description: metadata.Description,
Author: npm_module.User{Name: metadata.Author},
Maintainers: []npm_module.User{{Name: pd.Owner.Name}},
Homepage: metadata.ProjectURL,
License: metadata.License,
Keywords: metadata.Keywords,
Dependencies: metadata.Dependencies,
BundleDependencies: metadata.BundleDependencies,
DevDependencies: metadata.DevelopmentDependencies,
@@ -0,0 +1,46 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package npm
import (
"testing"
"time"
packages_model "gitea.dev/models/packages"
user_model "gitea.dev/models/user"
npm_module "gitea.dev/modules/packages/npm"
"gitea.dev/modules/timeutil"
"github.com/hashicorp/go-version"
"github.com/stretchr/testify/assert"
)
func TestCreatePackageMetadataResponse(t *testing.T) {
descriptor := func(v string, publishedUnix int64) *packages_model.PackageDescriptor {
return &packages_model.PackageDescriptor{
Package: &packages_model.Package{Name: "test"},
Owner: &user_model.User{Name: "alice"},
Version: &packages_model.PackageVersion{Version: v, CreatedUnix: timeutil.TimeStamp(publishedUnix)},
SemVer: version.Must(version.NewVersion(v)),
Metadata: &npm_module.Metadata{Keywords: []string{"gitea"}},
Files: []*packages_model.PackageFileDescriptor{{File: &packages_model.PackageFile{}, Blob: &packages_model.PackageBlob{}}},
}
}
result := createPackageMetadataResponse("https://gitea.dev/api/packages/alice/npm", []*packages_model.PackageDescriptor{
descriptor("1.1.0", 1000),
descriptor("1.0.0", 2000),
})
assert.Equal(t, map[string]time.Time{
"1.0.0": time.Unix(2000, 0).UTC(),
"1.1.0": time.Unix(1000, 0).UTC(),
"created": time.Unix(1000, 0).UTC(),
"modified": time.Unix(2000, 0).UTC(),
}, result.Time)
assert.Equal(t, []npm_module.User{{Name: "alice"}}, result.Maintainers)
assert.Equal(t, []string{"gitea"}, result.Keywords)
assert.Equal(t, []string{"gitea"}, result.Versions["1.0.0"].Keywords)
assert.Equal(t, []npm_module.User{{Name: "alice"}}, result.Versions["1.0.0"].Maintainers)
}
@@ -12,19 +12,19 @@ import (
"net/http"
"strings"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
npm_module "code.gitea.io/gitea/modules/packages/npm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
"gitea.dev/models/db"
packages_model "gitea.dev/models/packages"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/modules/optional"
packages_module "gitea.dev/modules/packages"
npm_module "gitea.dev/modules/packages/npm"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
"github.com/hashicorp/go-version"
)
@@ -333,11 +333,18 @@ func ListPackageTags(ctx *context.Context) {
func AddPackageTag(ctx *context.Context) {
packageName := packageNameFromParams(ctx)
body, err := io.ReadAll(ctx.Req.Body)
// the dist-tag body is only a quoted version string; bound it to avoid an unbounded
// read that could exhaust memory
const maxDistTagBodySize = 4 * 1024
body, err := io.ReadAll(io.LimitReader(ctx.Req.Body, maxDistTagBodySize+1))
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if len(body) > maxDistTagBodySize {
apiError(ctx, http.StatusRequestEntityTooLarge, errors.New("request body too large"))
return
}
version := strings.Trim(string(body), "\"") // is as "version" in the body
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName, version)
@@ -8,8 +8,8 @@ import (
"strings"
"time"
packages_model "code.gitea.io/gitea/models/packages"
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
packages_model "gitea.dev/models/packages"
nuget_module "gitea.dev/modules/packages/nuget"
)
type AtomTitle struct {
@@ -7,8 +7,8 @@ import (
"sort"
"time"
packages_model "code.gitea.io/gitea/models/packages"
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
packages_model "gitea.dev/models/packages"
nuget_module "gitea.dev/modules/packages/nuget"
"golang.org/x/text/collate"
"golang.org/x/text/language"
@@ -6,8 +6,8 @@ package nuget
import (
"net/http"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/services/auth"
user_model "gitea.dev/models/user"
"gitea.dev/services/auth"
)
var _ auth.Method = &Auth{}
@@ -14,17 +14,17 @@ import (
"strconv"
"strings"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
nuget_model "code.gitea.io/gitea/models/packages/nuget"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
"gitea.dev/models/db"
packages_model "gitea.dev/models/packages"
nuget_model "gitea.dev/models/packages/nuget"
"gitea.dev/modules/optional"
packages_module "gitea.dev/modules/packages"
nuget_module "gitea.dev/modules/packages/nuget"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
)
func apiError(ctx *context.Context, status int, obj any) {
@@ -13,15 +13,15 @@ import (
"strings"
"time"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/json"
packages_module "code.gitea.io/gitea/modules/packages"
pub_module "code.gitea.io/gitea/modules/packages/pub"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/json"
packages_module "gitea.dev/modules/packages"
pub_module "gitea.dev/modules/packages/pub"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
)
func jsonResponse(ctx *context.Context, status int, obj any) {
@@ -13,14 +13,14 @@ import (
"strings"
"unicode"
packages_model "code.gitea.io/gitea/models/packages"
packages_module "code.gitea.io/gitea/modules/packages"
pypi_module "code.gitea.io/gitea/modules/packages/pypi"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
packages_model "gitea.dev/models/packages"
packages_module "gitea.dev/modules/packages"
pypi_module "gitea.dev/modules/packages/pypi"
"gitea.dev/modules/setting"
"gitea.dev/modules/validation"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
)
// https://peps.python.org/pep-0426/#name
@@ -13,18 +13,18 @@ import (
"strings"
"time"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/json"
packages_module "code.gitea.io/gitea/modules/packages"
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
notify_service "code.gitea.io/gitea/services/notify"
packages_service "code.gitea.io/gitea/services/packages"
rpm_service "code.gitea.io/gitea/services/packages/rpm"
"gitea.dev/models/db"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/json"
packages_module "gitea.dev/modules/packages"
rpm_module "gitea.dev/modules/packages/rpm"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
notify_service "gitea.dev/services/notify"
packages_service "gitea.dev/services/packages"
rpm_service "gitea.dev/services/packages/rpm"
)
func apiError(ctx *context.Context, status int, obj any) {
@@ -13,15 +13,15 @@ import (
"net/http"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/cache"
"gitea.dev/modules/optional"
packages_module "gitea.dev/modules/packages"
rubygems_module "gitea.dev/modules/packages/rubygems"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
)
func apiError(ctx *context.Context, status int, obj any) {
@@ -7,7 +7,7 @@ import (
"strings"
"testing"
rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
rubygems_module "gitea.dev/modules/packages/rubygems"
"github.com/stretchr/testify/assert"
)
@@ -8,21 +8,22 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"sort"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
swift_module "code.gitea.io/gitea/modules/packages/swift"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
packages_module "gitea.dev/modules/packages"
swift_module "gitea.dev/modules/packages/swift"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
"github.com/hashicorp/go-version"
)
@@ -145,7 +146,7 @@ func EnumeratePackageVersions(ctx *context.Context) {
return pds[i].SemVer.LessThan(pds[j].SemVer)
})
baseURL := fmt.Sprintf("%sapi/packages/%s/swift/%s/%s/", setting.AppURL, ctx.Package.Owner.LowerName, packageScope, packageName)
baseURL := fmt.Sprintf("%sapi/packages/%s/swift/%s/%s/", setting.AppURL, url.PathEscape(ctx.Package.Owner.LowerName), url.PathEscape(packageScope), url.PathEscape(packageName))
releases := make(map[string]Release)
for _, pd := range pds {
@@ -156,7 +157,7 @@ func EnumeratePackageVersions(ctx *context.Context) {
}
setResponseHeaders(ctx.Resp, &headers{
Link: fmt.Sprintf(`<%s%s>; rel="latest-version"`, baseURL, pds[len(pds)-1].Version.Version),
Link: fmt.Sprintf(`<%s%s>; rel="latest-version"`, baseURL, url.PathEscape(pds[len(pds)-1].Version.Version)),
})
ctx.JSON(http.StatusOK, EnumeratePackageVersionsResponse{
@@ -322,16 +323,15 @@ func formFileOptionalReadCloser(ctx *context.Context, formKey string) (io.ReadCl
func UploadPackageFile(ctx *context.Context) {
packageScope := ctx.PathParam("scope")
packageName := ctx.PathParam("name")
packageVersion := ctx.PathParam("version")
v, err := version.NewVersion(ctx.PathParam("version"))
_, err := version.NewSemver(packageVersion)
if !scopePattern.MatchString(packageScope) || !namePattern.MatchString(packageName) || err != nil {
if err != nil || !scopePattern.MatchString(packageScope) || !namePattern.MatchString(packageName) {
apiError(ctx, http.StatusBadRequest, err)
return
}
packageVersion := v.Core().String()
file, err := formFileOptionalReadCloser(ctx, "source-archive")
if file == nil || err != nil {
apiError(ctx, http.StatusBadRequest, "unable to read source-archive file")
@@ -13,15 +13,15 @@ import (
"strings"
"unicode"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
terraform_module "code.gitea.io/gitea/modules/packages/terraform"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
packages_model "gitea.dev/models/packages"
"gitea.dev/modules/globallock"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
packages_module "gitea.dev/modules/packages"
terraform_module "gitea.dev/modules/packages/terraform"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
)
var packageNameRegex = regexp.MustCompile(`\A[-_+.\w]+\z`)
@@ -12,13 +12,13 @@ import (
"sort"
"strings"
packages_model "code.gitea.io/gitea/models/packages"
packages_module "code.gitea.io/gitea/modules/packages"
vagrant_module "code.gitea.io/gitea/modules/packages/vagrant"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/context"
packages_service "code.gitea.io/gitea/services/packages"
packages_model "gitea.dev/models/packages"
packages_module "gitea.dev/modules/packages"
vagrant_module "gitea.dev/modules/packages/vagrant"
"gitea.dev/modules/setting"
"gitea.dev/routers/api/packages/helper"
"gitea.dev/services/context"
packages_service "gitea.dev/services/packages"
"github.com/hashicorp/go-version"
)
@@ -6,7 +6,7 @@ package activitypub
import (
"net/http"
"code.gitea.io/gitea/services/context"
"gitea.dev/services/context"
)
func NotImplemented(ctx *context.APIContext) {
@@ -4,8 +4,8 @@
package admin
import (
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/services/context"
"gitea.dev/routers/api/v1/shared"
"gitea.dev/services/context"
)
// ListWorkflowJobs Lists all jobs
@@ -29,6 +29,14 @@ func 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"
@@ -36,8 +44,10 @@ func ListWorkflowJobs(ctx *context.APIContext) {
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.ListJobs(ctx, 0, 0, 0)
shared.ListJobs(ctx, 0, 0, 0, nil)
}
// ListWorkflowRuns Lists all runs
@@ -89,5 +99,5 @@ func ListWorkflowRuns(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
shared.ListRuns(ctx, 0, 0)
shared.ListRuns(ctx, 0, 0, "")
}
@@ -6,12 +6,12 @@ package admin
import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/gitrepo"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
repo_service "gitea.dev/services/repository"
)
// ListUnadoptedRepositories lists the unadopted repositories that match the provided names
@@ -6,12 +6,12 @@ package admin
import (
"net/http"
"code.gitea.io/gitea/modules/log"
"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"
"code.gitea.io/gitea/services/cron"
"gitea.dev/modules/log"
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/cron"
)
// ListCronTasks api for getting cron tasks
@@ -6,11 +6,11 @@ package admin
import (
"net/http"
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"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// GetAllEmails
@@ -7,15 +7,15 @@ import (
"errors"
"net/http"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/optional"
"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"
webhook_service "code.gitea.io/gitea/services/webhook"
"gitea.dev/models/webhook"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
webhook_service "gitea.dev/services/webhook"
)
// ListHooks list system's webhooks
@@ -7,14 +7,14 @@ package admin
import (
"net/http"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
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"
"gitea.dev/models/organization"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// CreateOrg api for create organization
@@ -48,7 +48,7 @@ func CreateOrg(ctx *context.APIContext) {
visibility := api.VisibleTypePublic
if form.Visibility != "" {
visibility = api.VisibilityModes[form.Visibility]
visibility = api.VisibilityModes[string(form.Visibility)]
}
org := &organization.Organization{
@@ -67,7 +67,7 @@ func CreateOrg(ctx *context.APIContext) {
db.IsErrNameReserved(err) ||
db.IsErrNameCharsNotAllowed(err) ||
db.IsErrNamePatternNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -4,10 +4,10 @@
package admin
import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/repo"
"code.gitea.io/gitea/services/context"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/repo"
"gitea.dev/services/context"
)
// CreateRepo api for creating a repository
@@ -4,8 +4,8 @@
package admin
import (
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/services/context"
"gitea.dev/routers/api/v1/shared"
"gitea.dev/services/context"
)
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
@@ -9,27 +9,27 @@ import (
"fmt"
"net/http"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
org_model "code.gitea.io/gitea/models/organization"
packages_model "code.gitea.io/gitea/models/packages"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/auth/password"
"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/web"
"code.gitea.io/gitea/routers/api/v1/user"
"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"
"code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user"
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/auth"
"gitea.dev/models/db"
org_model "gitea.dev/models/organization"
packages_model "gitea.dev/models/packages"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/auth/password"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/api/v1/utils"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/context"
"gitea.dev/services/convert"
"gitea.dev/services/mailer"
user_service "gitea.dev/services/user"
)
func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64) {
@@ -40,7 +40,7 @@ func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64
source, err := auth.GetSourceByID(ctx, sourceID)
if err != nil {
if auth.IsErrSourceNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -97,14 +97,12 @@ func CreateUser(ctx *context.APIContext) {
if u.LoginType == auth.Plain {
if len(form.Password) < setting.MinPasswordLength {
err := errors.New("PasswordIsRequired")
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, "PasswordIsRequired")
return
}
if !password.IsComplexEnough(form.Password) {
err := errors.New("PasswordComplexity")
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, "PasswordComplexity")
return
}
@@ -112,7 +110,7 @@ func CreateUser(ctx *context.APIContext) {
if password.IsErrIsPwnedRequest(err) {
log.Error(err.Error())
}
ctx.APIError(http.StatusBadRequest, errors.New("PasswordPwned"))
ctx.APIError(http.StatusBadRequest, "PasswordPwned")
return
}
}
@@ -123,7 +121,7 @@ func CreateUser(ctx *context.APIContext) {
}
if form.Visibility != "" {
visibility := api.VisibilityModes[form.Visibility]
visibility := api.VisibilityModes[string(form.Visibility)]
overwriteDefault.Visibility = &visibility
}
@@ -143,7 +141,7 @@ func CreateUser(ctx *context.APIContext) {
user_model.IsErrEmailCharIsNotSupported(err) ||
user_model.IsErrEmailInvalid(err) ||
db.IsErrNamePatternNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -196,7 +194,7 @@ func EditUser(ctx *context.APIContext) {
authOpts := &user_service.UpdateAuthOptions{
LoginSource: optional.FromNonDefault(form.SourceID),
LoginName: optional.Some(form.LoginName),
LoginName: optional.FromPtr(form.LoginName),
Password: optional.FromNonDefault(form.Password),
MustChangePassword: optional.FromPtr(form.MustChangePassword),
ProhibitLogin: optional.FromPtr(form.ProhibitLogin),
@@ -204,11 +202,11 @@ func EditUser(ctx *context.APIContext) {
if err := user_service.UpdateAuth(ctx, ctx.ContextUser, authOpts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
ctx.APIError(http.StatusBadRequest, fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("password must be at least %d characters", setting.MinPasswordLength))
case errors.Is(err, password.ErrComplexity):
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
case errors.Is(err, password.ErrIsPwned), password.IsErrIsPwnedRequest(err):
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
default:
ctx.APIErrorInternal(err)
}
@@ -222,9 +220,9 @@ func EditUser(ctx *context.APIContext) {
if !user_model.IsEmailDomainAllowed(*form.Email) {
err = fmt.Errorf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email)
}
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
case user_model.IsErrEmailAlreadyUsed(err):
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
default:
ctx.APIErrorInternal(err)
}
@@ -239,7 +237,7 @@ func EditUser(ctx *context.APIContext) {
Description: optional.FromPtr(form.Description),
IsActive: optional.FromPtr(form.Active),
IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin),
Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
Visibility: optional.FromMapLookup(api.VisibilityModes, string(form.Visibility)),
AllowGitHook: optional.FromPtr(form.AllowGitHook),
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
@@ -249,7 +247,7 @@ func EditUser(ctx *context.APIContext) {
if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil {
if user_model.IsErrDeleteLastAdminUser(err) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -289,13 +287,13 @@ func DeleteUser(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
if ctx.ContextUser.IsOrganization() {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
ctx.APIError(http.StatusUnprocessableEntity, "target is an organization but not user")
return
}
// admin should not delete themself
if ctx.ContextUser.ID == ctx.Doer.ID {
ctx.APIError(http.StatusUnprocessableEntity, errors.New("you cannot delete yourself"))
ctx.APIError(http.StatusUnprocessableEntity, "you cannot delete yourself")
return
}
@@ -304,7 +302,7 @@ func DeleteUser(ctx *context.APIContext) {
org_model.IsErrUserHasOrgs(err) ||
packages_model.IsErrUserOwnPackages(err) ||
user_model.IsErrDeleteLastAdminUser(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -464,35 +462,18 @@ func SearchUsers(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx)
orderBy := db.SearchOrderByAlphabetically
sortMode := ctx.FormString("sort")
if len(sortMode) > 0 {
sortOrder := ctx.FormString("order")
if len(sortOrder) == 0 {
sortOrder = "asc"
}
if searchModeMap, ok := user_model.AdminUserOrderByMap[sortOrder]; ok {
if order, ok := searchModeMap[sortMode]; ok {
orderBy = order
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
return
}
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
return
}
orderBy, ok := utils.ResolveSortOrder(ctx, user_model.AdminUserOrderByMap, db.SearchOrderByAlphabetically)
if !ok {
return
}
var visible []api.VisibleType
visibilityParam := ctx.FormString("visibility")
if len(visibilityParam) > 0 {
if visibility, ok := api.VisibilityModes[visibilityParam]; ok {
visible = []api.VisibleType{visibility}
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid visibility: \"%s\"", visibilityParam))
return
}
if visibility, ok := api.VisibilityModes[visibilityParam]; ok {
visible = []api.VisibleType{visibility}
} else if visibilityParam != "" {
ctx.APIError(http.StatusUnprocessableEntity, "invalid visibility")
return
}
searchOpts := user_model.SearchUserOptions{
@@ -566,7 +547,7 @@ func RenameUser(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
if ctx.ContextUser.IsOrganization() {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
ctx.APIError(http.StatusUnprocessableEntity, "target is an organization but not user")
return
}
@@ -575,7 +556,7 @@ func RenameUser(ctx *context.APIContext) {
// Check if username has been changed
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName, ctx.Doer); err != nil {
if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -6,10 +6,10 @@ package admin
import (
"net/http"
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/services/context"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
)
// ListUserBadges lists all badges belonging to a user
+176 -105
View File
@@ -68,36 +68,38 @@ import (
"net/http"
"strings"
auth_model "code.gitea.io/gitea/models/auth"
"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"
"code.gitea.io/gitea/models/unit"
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"
"code.gitea.io/gitea/routers/api/v1/activitypub"
"code.gitea.io/gitea/routers/api/v1/admin"
"code.gitea.io/gitea/routers/api/v1/misc"
"code.gitea.io/gitea/routers/api/v1/notify"
"code.gitea.io/gitea/routers/api/v1/org"
"code.gitea.io/gitea/routers/api/v1/packages"
"code.gitea.io/gitea/routers/api/v1/repo"
"code.gitea.io/gitea/routers/api/v1/settings"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
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/log"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/activitypub"
"gitea.dev/routers/api/v1/admin"
"gitea.dev/routers/api/v1/misc"
"gitea.dev/routers/api/v1/notify"
"gitea.dev/routers/api/v1/org"
"gitea.dev/routers/api/v1/packages"
"gitea.dev/routers/api/v1/repo"
"gitea.dev/routers/api/v1/settings"
"gitea.dev/routers/api/v1/token"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/common"
"gitea.dev/services/actions"
"gitea.dev/services/auth"
"gitea.dev/services/context"
"gitea.dev/services/forms"
_ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation
_ "gitea.dev/routers/api/v1/swagger" // for swagger generation
"gitea.com/go-chi/binding"
chi_middleware "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
)
@@ -151,7 +153,7 @@ func repoAssignment() func(ctx *context.APIContext) {
if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil {
context.RedirectToUser(ctx.Base, ctx.Doer, userName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.APIErrorNotFound("GetUserByName", err)
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
@@ -289,7 +291,9 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
return
}
case auth_model.AccessTokenScopeCategoryPackage:
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
// a public-only token must not reach limited-visibility owners either,
// matching the org/user public-only enforcement above
if ctx.Package != nil && !ctx.Package.Owner.Visibility.IsPublic() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
return
}
@@ -414,7 +418,7 @@ func reqSiteAdmin() func(ctx *context.APIContext) {
// reqOwner user should be the owner of the repo or site admin.
func reqOwner() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.Repo.IsOwner() && !ctx.IsUserSiteAdmin() {
if !ctx.Repo.Permission.IsOwner() && !ctx.IsUserSiteAdmin() {
ctx.APIError(http.StatusForbidden, "user should be the owner of the repo")
return
}
@@ -454,7 +458,7 @@ func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) {
// reqRepoReader user should have specific read permission or be a repo admin or a site admin
func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.Repo.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() {
if !ctx.Repo.Permission.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() {
ctx.APIError(http.StatusForbidden, "user should have specific read permission or be a repo admin or a site admin")
return
}
@@ -504,41 +508,94 @@ func reqOrgOwnership() func(ctx *context.APIContext) {
}
}
// reqTeamMembership user should be an team member, or a site admin
func reqTeamMembership() func(ctx *context.APIContext) {
// reqOrgVisible requires the organization to be visible to the doer, or a site admin
func reqOrgVisible() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if ctx.IsUserSiteAdmin() {
if ctx.Org.Organization == nil {
setting.PanicInDevOrTesting("reqOrgVisible: unprepared context")
ctx.APIErrorInternal(errors.New("reqOrgVisible: unprepared context"))
return
}
if ctx.Org.Team == nil {
setting.PanicInDevOrTesting("reqTeamMembership: unprepared context")
ctx.APIErrorInternal(errors.New("reqTeamMembership: unprepared context"))
if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
ctx.APIErrorNotFound()
return
}
}
}
func teamAccessPrivileged(ctx *context.APIContext) (orgID int64, privileged, ok bool) {
if ctx.IsUserSiteAdmin() {
return 0, true, true
}
if ctx.Org.Team == nil {
setting.PanicInDevOrTesting("teamAccess: unprepared context")
ctx.APIErrorInternal(errors.New("teamAccess: unprepared context"))
return 0, false, false
}
orgID = ctx.Org.Team.OrgID
isOwner, err := organization.IsOrganizationOwner(ctx, orgID, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return 0, false, false
} else if isOwner {
return orgID, true, true
}
isTeamMember, err := organization.IsTeamMember(ctx, orgID, ctx.Org.Team.ID, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return 0, false, false
}
return orgID, isTeamMember, true
}
func denyNonTeamMember(ctx *context.APIContext, orgID int64) {
isOrgMember, err := organization.IsOrganizationMember(ctx, orgID, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
} else if isOrgMember {
ctx.APIError(http.StatusForbidden, "Must be a team member")
} else {
ctx.APIErrorNotFound()
}
}
// reqTeamReadAccess allows callers who can list the team to read its metadata.
// Non-members are admitted by the team's visibility tier and parent org visibility.
// Not sufficient for mutations — use reqOrgOwnership() or reqTeamMembership() for those.
func reqTeamReadAccess() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
orgID, privileged, ok := teamAccessPrivileged(ctx)
if !ok || privileged {
return
}
if ctx.Org.Organization == nil {
setting.PanicInDevOrTesting("reqTeamReadAccess: organization not loaded")
ctx.APIErrorInternal(errors.New("reqTeamReadAccess: organization not loaded"))
return
}
orgID := ctx.Org.Team.OrgID
isOwner, err := organization.IsOrganizationOwner(ctx, orgID, ctx.Doer.ID)
visible, err := ctx.Org.Team.CanNonMemberReadMeta(ctx, ctx.Org.Organization.AsUser(), ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
} else if isOwner {
return
}
if !visible {
// Not admitted by visibility: 403 for org members, 404 otherwise.
denyNonTeamMember(ctx, orgID)
}
}
}
if isTeamMember, err := organization.IsTeamMember(ctx, orgID, ctx.Org.Team.ID, ctx.Doer.ID); err != nil {
ctx.APIErrorInternal(err)
return
} else if !isTeamMember {
isOrgMember, err := organization.IsOrganizationMember(ctx, orgID, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
} else if isOrgMember {
ctx.APIError(http.StatusForbidden, "Must be a team member")
} else {
ctx.APIErrorNotFound()
}
// reqTeamMembership user should be a team member, or a site admin
func reqTeamMembership() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
orgID, privileged, ok := teamAccessPrivileged(ctx)
if !ok || privileged {
return
}
denyNonTeamMember(ctx, orgID)
}
}
@@ -626,7 +683,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
if err == nil {
context.RedirectToUser(ctx.Base, ctx.Doer, ctx.PathParam("org"), redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.APIErrorNotFound("GetOrgByName", err)
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
@@ -648,12 +705,23 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
}
return
}
if ctx.Org.Organization == nil {
ctx.Org.Organization, err = organization.GetOrgByID(ctx, ctx.Org.Team.OrgID)
if err != nil {
if organization.IsErrOrgNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
}
}
}
}
func mustEnableIssues(ctx *context.APIContext) {
if !ctx.Repo.CanRead(unit.TypeIssues) {
if !ctx.Repo.Permission.CanRead(unit.TypeIssues) {
if log.IsTrace() {
if ctx.IsSigned {
log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+
@@ -676,7 +744,7 @@ func mustEnableIssues(ctx *context.APIContext) {
}
func mustAllowPulls(ctx *context.APIContext) {
if !(ctx.Repo.Repository.CanEnablePulls() && ctx.Repo.CanRead(unit.TypePullRequests)) {
if !(ctx.Repo.Repository.CanEnablePulls() && ctx.Repo.Permission.CanRead(unit.TypePullRequests)) {
if ctx.Repo.Repository.CanEnablePulls() && log.IsTrace() {
if ctx.IsSigned {
log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+
@@ -699,8 +767,8 @@ func mustAllowPulls(ctx *context.APIContext) {
}
func mustEnableIssuesOrPulls(ctx *context.APIContext) {
if !ctx.Repo.CanRead(unit.TypeIssues) &&
!(ctx.Repo.Repository.CanEnablePulls() && ctx.Repo.CanRead(unit.TypePullRequests)) {
if !ctx.Repo.Permission.CanRead(unit.TypeIssues) &&
!(ctx.Repo.Repository.CanEnablePulls() && ctx.Repo.Permission.CanRead(unit.TypePullRequests)) {
if ctx.Repo.Repository.CanEnablePulls() && log.IsTrace() {
if ctx.IsSigned {
log.Trace("Permission Denied: User %-v cannot read %-v and %-v in Repo %-v\n"+
@@ -725,7 +793,7 @@ func mustEnableIssuesOrPulls(ctx *context.APIContext) {
}
func mustEnableWiki(ctx *context.APIContext) {
if !(ctx.Repo.CanRead(unit.TypeWiki)) {
if !(ctx.Repo.Permission.CanRead(unit.TypeWiki)) {
ctx.APIErrorNotFound()
return
}
@@ -734,14 +802,14 @@ func mustEnableWiki(ctx *context.APIContext) {
// FIXME: for consistency, maybe most mustNotBeArchived checks should be replaced with mustEnableEditor
func mustNotBeArchived(ctx *context.APIContext) {
if ctx.Repo.Repository.IsArchived {
ctx.APIError(http.StatusLocked, fmt.Errorf("%s is archived", ctx.Repo.Repository.FullName()))
ctx.APIError(http.StatusLocked, "repo is archived")
return
}
}
func mustEnableEditor(ctx *context.APIContext) {
if !ctx.Repo.Repository.CanEnableEditor() {
ctx.APIError(http.StatusLocked, fmt.Errorf("%s is not allowed to edit", ctx.Repo.Repository.FullName()))
ctx.APIError(http.StatusLocked, "repo is not allowed to edit")
return
}
}
@@ -858,19 +926,8 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC
func individualPermsChecker(ctx *context.APIContext) {
// org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked.
if ctx.ContextUser.IsIndividual() {
switch ctx.ContextUser.Visibility {
case api.VisibleTypePrivate:
if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) {
ctx.APIErrorNotFound("Visit Project", nil)
return
}
case api.VisibleTypeLimited:
if ctx.Doer == nil {
ctx.APIErrorNotFound("Visit Project", nil)
return
}
}
if ctx.ContextUser.IsIndividual() && !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
ctx.APIErrorNotFound()
}
}
@@ -885,7 +942,9 @@ func checkDeprecatedAuthMethods(ctx *context.APIContext) {
func Routes() *web.Router {
m := web.NewRouter()
m.BeforeRouting(securityHeaders())
// redirect HEAD requests to GET if no HEAD handler is defined (RFC 9110 §9.3.2)
m.BeforeRouting(chi_middleware.GetHead)
if setting.CORSConfig.Enabled {
m.BeforeRouting(cors.Handler(cors.Options{
AllowedOrigins: setting.CORSConfig.AllowDomain,
@@ -977,6 +1036,11 @@ func Routes() *web.Router {
})
})
// Token introspection and deletion endpoint
m.Combo("/token").
Get(reqToken(), token.GetCurrentToken).
Delete(reqToken(), token.DeleteCurrentToken)
// Notifications (requires 'notifications' scope)
// The notifications API is not available for public-only tokens because a user's notifications mix
// public and private repository events in the same mailbox.
@@ -1027,7 +1091,7 @@ func Routes() *web.Router {
m.Get("/starred", reqStarsEnabled(), user.GetStarredRepos)
m.Get("/subscriptions", user.GetWatchedRepos)
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
}, context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
// Users (requires user scope)
@@ -1168,7 +1232,7 @@ func Routes() *web.Router {
m.Get("/search", repo.Search)
// (repo scope)
m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate)
m.Post("/migrate", reqToken(), rejectPublicOnly(), bind(api.MigrateRepoOptions{}), repo.Migrate)
m.Group("/{username}/{reponame}", func() {
m.Get("/compare/*", reqRepoReader(unit.TypeCode), repo.CompareDiff)
@@ -1189,6 +1253,7 @@ func Routes() *web.Router {
m.Group("/actions/workflows", func() {
m.Get("", repo.ActionsListRepositoryWorkflows)
m.Get("/{workflow_id}", repo.ActionsGetWorkflow)
m.Get("/{workflow_id}/runs", repo.ActionsListWorkflowRuns)
m.Put("/{workflow_id}/disable", reqRepoWriter(unit.TypeActions), repo.ActionsDisableWorkflow)
m.Put("/{workflow_id}/enable", reqRepoWriter(unit.TypeActions), repo.ActionsEnableWorkflow)
m.Post("/{workflow_id}/dispatches", reqRepoWriter(unit.TypeActions), bind(api.CreateActionWorkflowDispatch{}), repo.ActionsDispatchWorkflow)
@@ -1227,6 +1292,7 @@ func Routes() *web.Router {
})
}, reqToken())
m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees)
m.Get("/assignees/{assignee}", reqToken(), reqAnyRepoReader(), repo.CheckRepoIssueAssignee)
m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers)
m.Group("/teams", func() {
m.Get("", reqAnyRepoReader(), repo.ListTeams)
@@ -1278,6 +1344,10 @@ func Routes() *web.Router {
m.Group("/runs", func() {
m.Group("/{run}", func() {
m.Get("", repo.GetWorkflowRun)
m.Group("/attempts/{attempt}", func() {
m.Get("", repo.GetWorkflowRunAttempt)
m.Get("/jobs", repo.ListWorkflowRunAttemptJobs)
})
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
m.Post("/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowRun)
m.Post("/rerun-failed-jobs", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunFailedWorkflowRun)
@@ -1292,7 +1362,7 @@ func Routes() *web.Router {
m.Delete("", reqRepoWriter(unit.TypeActions), repo.DeleteArtifact)
})
m.Get("/artifacts/{artifact_id}/zip", repo.DownloadArtifact)
}, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true))
}, reqRepoReader(unit.TypeActions))
m.Group("/keys", func() {
m.Combo("").Get(repo.ListDeployKeys).
Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey)
@@ -1389,6 +1459,7 @@ func Routes() *web.Router {
m.Combo("/requested_reviewers", reqToken()).
Delete(bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests).
Post(bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests)
m.Post("/comments/{id}/replies", reqToken(), mustNotBeArchived, bind(api.CreatePullReviewCommentReplyOptions{}), repo.CreatePullReviewCommentReply)
})
m.Get("/{base}/*", repo.GetPullRequestByBaseHead)
}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
@@ -1513,6 +1584,10 @@ func Routes() *web.Router {
m.Combo("").Get(repo.GetIssue).
Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue).
Delete(reqToken(), reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue)
m.Combo("/assignees").
Post(reqToken(), mustNotBeArchived, bind(api.IssueAssigneesOption{}), repo.AddIssueAssignees).
Delete(reqToken(), mustNotBeArchived, bind(api.IssueAssigneesOption{}), repo.DeleteIssueAssignees)
m.Get("/assignees/{assignee}", repo.CheckIssueAssignee)
m.Group("/comments", func() {
m.Combo("").Get(repo.ListIssueComments).
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
@@ -1624,21 +1699,22 @@ func Routes() *web.Router {
m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker)
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization))
m.Get("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), org.GetAll)
m.Group("/orgs/{org}", func() {
m.Combo("").Get(org.Get).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
Delete(reqToken(), reqOrgOwnership(), org.Delete)
m.Post("/rename", reqToken(), reqOrgOwnership(), bind(api.RenameOrgOption{}), org.Rename)
m.Combo("/repos").Get(user.ListOrgRepos).
Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
Post(reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), bind(api.CreateRepoOption{}), repo.CreateOrgRepo).
Delete(reqToken(), reqOrgOwnership(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), org.DeleteOrgRepos)
m.Group("/members", func() {
m.Get("", reqToken(), org.ListMembers)
m.Combo("/{username}").Get(reqToken(), org.IsMember).
Delete(reqToken(), reqOrgOwnership(), org.DeleteMember)
})
}, reqOrgVisible())
addActionsRoutes(
m,
reqOrgMembership(),
@@ -1650,7 +1726,7 @@ func Routes() *web.Router {
m.Combo("/{username}").Get(org.IsPublicMember).
Put(reqToken(), reqOrgMembership(), org.PublicizeMember).
Delete(reqToken(), reqOrgMembership(), org.ConcealMember)
})
}, reqOrgVisible())
m.Group("/teams", func() {
m.Get("", org.ListTeams)
m.Post("", reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
@@ -1662,7 +1738,7 @@ func Routes() *web.Router {
m.Combo("/{id}").Get(reqToken(), org.GetLabel).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel).
Delete(reqToken(), reqOrgOwnership(), org.DeleteLabel)
})
}, reqOrgVisible())
m.Group("/hooks", func() {
m.Combo("").Get(org.ListHooks).
Post(bind(api.CreateHookOption{}), org.CreateHook)
@@ -1686,25 +1762,31 @@ func Routes() *web.Router {
}, reqToken(), reqOrgOwnership())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
m.Group("/teams/{teamid}", func() {
m.Combo("").Get(reqToken(), org.GetTeam).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
m.Combo("").Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
Delete(reqToken(), reqOrgOwnership(), org.DeleteTeam)
m.Group("", func() {
m.Get("", org.GetTeam)
m.Group("/members", func() {
m.Get("", reqOrgMembership(), org.GetTeamMembers)
m.Combo("/{username}").Get(reqOrgMembership(), org.GetTeamMember)
})
m.Group("/repos", func() {
m.Get("", org.GetTeamRepos)
m.Combo("/{org}/{reponame}").Get(org.GetTeamRepo)
})
m.Get("/activities/feeds", org.ListTeamActivityFeeds)
}, reqTeamReadAccess())
m.Group("/members", func() {
m.Get("", reqToken(), org.GetTeamMembers)
m.Combo("/{username}").
Get(reqToken(), org.GetTeamMember).
Put(reqToken(), reqOrgOwnership(), org.AddTeamMember).
Delete(reqToken(), reqOrgOwnership(), org.RemoveTeamMember)
})
m.Group("/repos", func() {
m.Get("", reqToken(), org.GetTeamRepos)
m.Combo("/{org}/{reponame}").
Put(reqToken(), org.AddTeamRepository).
Delete(reqToken(), org.RemoveTeamRepository).
Get(reqToken(), org.GetTeamRepo)
Put(reqToken(), reqTeamMembership(), org.AddTeamRepository).
Delete(reqToken(), reqTeamMembership(), org.RemoveTeamRepository)
})
m.Get("/activities/feeds", org.ListTeamActivityFeeds)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), checkTokenPublicOnly())
m.Group("/admin", func() {
m.Group("/cron", func() {
@@ -1767,14 +1849,3 @@ func Routes() *web.Router {
return m
}
func securityHeaders() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
// CORB: https://www.chromium.org/Home/chromium-security/corb-for-developers
// http://stackoverflow.com/a/3146618/244009
resp.Header().Set("x-content-type-options", "nosniff")
next.ServeHTTP(resp, req)
})
}
}
@@ -6,11 +6,11 @@ package misc
import (
"net/http"
"code.gitea.io/gitea/modules/options"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"gitea.dev/modules/options"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/services/context"
)
// Shows a list of all Gitignore templates
@@ -6,10 +6,10 @@ package misc
import (
"net/http"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/util"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// Shows a list of all Label templates
@@ -8,12 +8,12 @@ import (
"net/http"
"net/url"
"code.gitea.io/gitea/modules/options"
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/services/context"
"gitea.dev/modules/options"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/services/context"
)
// Returns a list of all License templates
@@ -4,15 +4,14 @@
package misc
import (
"net/http"
"io"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/common"
"gitea.dev/services/context"
)
// Markup render markup document to HTML
@@ -36,12 +35,6 @@ func Markup(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.MarkupOption)
if ctx.HasAPIError() {
ctx.APIError(http.StatusUnprocessableEntity, ctx.GetErrMsg())
return
}
mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated
common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, form.FilePath)
}
@@ -67,12 +60,6 @@ func Markdown(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.MarkdownOption)
if ctx.HasAPIError() {
ctx.APIError(http.StatusUnprocessableEntity, ctx.GetErrMsg())
return
}
mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated
common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, "")
}
@@ -98,9 +85,6 @@ func MarkdownRaw(ctx *context.APIContext) {
// "$ref": "#/responses/MarkdownRender"
// "422":
// "$ref": "#/responses/validationError"
defer ctx.Req.Body.Close()
if err := markdown.RenderRaw(markup.NewRenderContext(ctx), ctx.Req.Body, ctx.Resp); err != nil {
ctx.APIErrorInternal(err)
return
}
textBytes, _ := io.ReadAll(io.LimitReader(ctx.Req.Body, setting.UI.MaxDisplayFileSize))
common.RenderMarkup(ctx.Base, ctx.Repo, "markdown", util.UnsafeBytesToString(textBytes), "", "")
}
@@ -7,33 +7,22 @@ import (
go_context "context"
"io"
"net/http"
"os"
"path"
"strings"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
context_service "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/contexttest"
"gitea.dev/modules/markup"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/test"
"gitea.dev/modules/web"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
const AppURL = "http://localhost:3000/"
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
FixtureFiles: []string{"repository.yml", "user.yml"},
})
os.Exit(m.Run())
}
func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expectedBody string, expectedCode int) {
setting.AppURL = AppURL
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
@@ -49,13 +38,11 @@ func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expe
FilePath: filePath,
}
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
ctx.Repo = &context_service.Repository{}
ctx.Repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
web.SetForm(ctx, &options)
Markup(ctx)
assert.Equal(t, expectedBody, resp.Body.String())
assert.Equal(t, expectedCode, resp.Code)
resp.Body.Reset()
assert.Contains(t, resp.Header().Get("Content-Security-Policy"), "script-src * 'nonce-")
}
func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody string, responseCode int) {
@@ -76,11 +63,10 @@ func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody
Markdown(ctx)
assert.Equal(t, responseBody, resp.Body.String())
assert.Equal(t, responseCode, resp.Code)
resp.Body.Reset()
assert.Contains(t, resp.Header().Get("Content-Security-Policy"), "script-src * 'nonce-")
}
func TestAPI_RenderGFM(t *testing.T) {
unittest.PrepareTestEnv(t)
markup.Init(&markup.RenderHelperFuncs{
IsUsernameMentionable: func(ctx go_context.Context, username string) bool {
return username == "r-lyeh"
@@ -177,49 +163,34 @@ Here are some links to the most important topics. You can find the full list of
testRenderMarkup(t, "unknown", false, "", "## Test", "unsupported render mode: unknown\n", http.StatusUnprocessableEntity)
}
var simpleCases = []string{
// Guard wiki sidebar: special syntax
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
// rendered
`<p>[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]</p>
`,
// special syntax
`[[Name|Link]]`,
// rendered
`<p>[[Name|Link]]</p>
`,
// empty
``,
// rendered
``,
}
func TestAPI_RenderSimple(t *testing.T) {
setting.AppURL = AppURL
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
options := api.MarkdownOption{
Mode: "markdown",
Text: "",
Context: "/user2/repo1",
}
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
for i := 0; i < len(simpleCases); i += 2 {
options.Text = simpleCases[i]
web.SetForm(ctx, &options)
Markdown(ctx)
assert.Equal(t, simpleCases[i+1], resp.Body.String())
resp.Body.Reset()
}
}
func TestAPI_RenderRaw(t *testing.T) {
setting.AppURL = AppURL
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
for i := 0; i < len(simpleCases); i += 2 {
ctx.Req.Body = io.NopCloser(strings.NewReader(simpleCases[i]))
MarkdownRaw(ctx)
assert.Equal(t, simpleCases[i+1], resp.Body.String())
resp.Body.Reset()
testCases := []struct {
in, out string
mode string
}{
{in: "", out: ""},
{in: "[[special-syntax]]", out: "<p>[[special-syntax]]</p>\n", mode: "markdown"},
{in: "[[special|syntax]]", out: "<p>[[special|syntax]]</p>\n", mode: "markdown"},
{in: "01234567890123456789", out: "<p>01234567890123456789</p>\n", mode: "gfm"}, // commit-like content should not crash the render
}
t.Run("markdown", func(t *testing.T) {
for _, c := range testCases {
options := api.MarkdownOption{Mode: c.mode, Text: c.in, Context: "/user2/repo1"}
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
web.SetForm(ctx, &options)
Markdown(ctx)
assert.Equal(t, c.out, resp.Body.String())
}
})
t.Run("markdown-raw", func(t *testing.T) {
for _, c := range testCases {
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
ctx.Req.Body = io.NopCloser(strings.NewReader(c.in))
MarkdownRaw(ctx)
assert.Equal(t, c.out, resp.Body.String())
}
})
}
@@ -4,9 +4,9 @@
package misc
import (
"code.gitea.io/gitea/modules/git"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
"gitea.dev/modules/git"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/context"
)
func getSigningKey(ctx *context.APIContext, expectedFormat string) {
@@ -6,9 +6,9 @@ package misc
import (
"net/http"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
"gitea.dev/services/context"
)
// Version shows the version of the Gitea server
@@ -7,11 +7,11 @@ import (
"net/http"
"strings"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
)
// NewAvailable check if unread notifications exist
@@ -28,7 +28,7 @@ func NewAvailable(ctx *context.APIContext) {
Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread},
})
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
@@ -38,7 +38,7 @@ func NewAvailable(ctx *context.APIContext) {
func getFindNotificationOptions(ctx *context.APIContext) *activities_model.FindNotificationOptions {
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return nil
}
opts := &activities_model.FindNotificationOptions{
@@ -8,11 +8,11 @@ import (
"strings"
"time"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
"gitea.dev/modules/structs"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
func statusStringToNotificationStatus(status string) activities_model.NotificationStatus {
@@ -183,7 +183,7 @@ func ReadRepoNotifications(ctx *context.APIContext) {
if len(qLastRead) > 0 {
tmpLastRead, err := time.Parse(time.RFC3339, qLastRead)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
if !tmpLastRead.IsZero() {
@@ -7,11 +7,11 @@ import (
"fmt"
"net/http"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// GetThread get notification by ID
@@ -104,14 +104,14 @@ func getThread(ctx *context.APIContext) *activities_model.Notification {
n, err := activities_model.GetNotificationByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if db.IsErrNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
return nil
}
if n.UserID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
ctx.APIError(http.StatusForbidden, fmt.Errorf("only user itself and admin are allowed to read/change this thread %d", n.ID))
ctx.APIError(http.StatusForbidden, fmt.Sprintf("only user itself and admin are allowed to read/change this thread %d", n.ID))
return nil
}
return n
@@ -7,11 +7,11 @@ import (
"net/http"
"time"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
"gitea.dev/modules/structs"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListNotifications list users's notification threads
@@ -134,7 +134,7 @@ func ReadNotifications(ctx *context.APIContext) {
if len(qLastRead) > 0 {
tmpLastRead, err := time.Parse(time.RFC3339, qLastRead)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
if !tmpLastRead.IsZero() {
@@ -7,17 +7,17 @@ import (
"errors"
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
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"
secret_service "code.gitea.io/gitea/services/secrets"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
secret_model "gitea.dev/models/secret"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/shared"
"gitea.dev/routers/api/v1/utils"
actions_service "gitea.dev/services/actions"
"gitea.dev/services/context"
secret_service "gitea.dev/services/secrets"
)
// ListActionsSecrets list an organization's actions secrets
@@ -111,9 +111,9 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
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)
}
@@ -158,9 +158,9 @@ func (Action) DeleteSecret(ctx *context.APIContext) {
err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
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)
}
@@ -277,7 +277,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)
}
@@ -327,9 +327,9 @@ func (Action) DeleteVariable(ctx *context.APIContext) {
if err := actions_service.DeleteVariableByName(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("variablename")); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
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)
}
@@ -387,13 +387,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, ownerID, 0, 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)
}
@@ -445,7 +445,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)
}
@@ -462,7 +462,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)
}
@@ -624,7 +624,7 @@ func (Action) ListWorkflowJobs(ctx *context.APIContext) {
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListJobs(ctx, ctx.Org.Organization.ID, 0, 0)
shared.ListJobs(ctx, ctx.Org.Organization.ID, 0, 0, nil)
}
func (Action) ListWorkflowRuns(ctx *context.APIContext) {
@@ -679,7 +679,7 @@ func (Action) ListWorkflowRuns(ctx *context.APIContext) {
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListRuns(ctx, ctx.Org.Organization.ID, 0)
shared.ListRuns(ctx, ctx.Org.Organization.ID, 0, "")
}
var _ actions_service.API = new(Action)
@@ -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"
user_service "code.gitea.io/gitea/services/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
user_service "gitea.dev/services/user"
)
// UpdateAvatarupdates the Avatar of an Organisation
@@ -39,7 +39,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
}
@@ -4,8 +4,8 @@
package org
import (
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/services/context"
"gitea.dev/routers/api/v1/shared"
"gitea.dev/services/context"
)
func ListBlocks(ctx *context.APIContext) {
@@ -6,11 +6,11 @@ package org
import (
"net/http"
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"
webhook_service "code.gitea.io/gitea/services/webhook"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
webhook_service "gitea.dev/services/webhook"
)
// ListHooks list an organization's webhooks
@@ -8,13 +8,13 @@ import (
"strconv"
"strings"
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 an organization
@@ -90,7 +90,7 @@ func CreateLabel(ctx *context.APIContext) {
form.Color = strings.Trim(form.Color, " ")
color, err := label.NormalizeColor(form.Color)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
form.Color = color
@@ -209,7 +209,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
@@ -7,15 +7,15 @@ import (
"net/http"
"net/url"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
org_service "code.gitea.io/gitea/services/org"
"gitea.dev/models/organization"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
org_service "gitea.dev/services/org"
)
// listMembers list an organization's members
@@ -221,7 +221,7 @@ func checkCanChangeOrgUserStatus(ctx *context.APIContext, targetUser *user_model
// allow org owners to change status of members
isOwner, err := ctx.Org.Organization.IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIError(http.StatusInternalServerError, err)
ctx.APIErrorInternal(err)
} else if !isOwner {
ctx.APIError(http.StatusForbidden, "Cannot change member visibility")
}
+105 -24
View File
@@ -5,25 +5,32 @@
package org
import (
gocontext "context"
"errors"
"fmt"
"net/http"
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"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
"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/org"
user_service "code.gitea.io/gitea/services/user"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
system_model "gitea.dev/models/system"
user_model "gitea.dev/models/user"
"gitea.dev/modules/graceful"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
feed_service "gitea.dev/services/feed"
"gitea.dev/services/org"
repo_service "gitea.dev/services/repository"
user_service "gitea.dev/services/user"
)
func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
@@ -138,8 +145,15 @@ func GetUserOrgsPermissions(ctx *context.APIContext) {
op := api.OrganizationPermissions{}
// A public-only token must not disclose membership/permission details of a
// non-public org, even for the token owner's own private orgs.
if ctx.PublicOnly && !o.Visibility.IsPublic() {
ctx.APIErrorNotFound()
return
}
if !organization.HasOrgOrUserVisible(ctx, o, ctx.Doer) {
ctx.APIErrorNotFound("HasOrgOrUserVisible", nil)
ctx.APIErrorNotFound()
return
}
@@ -249,13 +263,13 @@ func Create(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateOrgOption)
if !ctx.Doer.CanCreateOrganization() {
ctx.APIError(http.StatusForbidden, nil)
ctx.APIError(http.StatusForbidden, "not allowed to create org")
return
}
visibility := api.VisibleTypePublic
if form.Visibility != "" {
visibility = api.VisibilityModes[form.Visibility]
visibility = api.VisibilityModes[string(form.Visibility)]
}
org := &organization.Organization{
@@ -275,7 +289,7 @@ func Create(ctx *context.APIContext) {
db.IsErrNameReserved(err) ||
db.IsErrNameCharsNotAllowed(err) ||
db.IsErrNamePatternNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -305,7 +319,7 @@ func Get(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
ctx.APIErrorNotFound("HasOrgOrUserVisible", nil)
ctx.APIErrorNotFound()
return
}
@@ -348,7 +362,7 @@ func Rename(ctx *context.APIContext) {
orgUser := ctx.Org.Organization.AsUser()
if err := user_service.RenameUser(ctx, orgUser, form.NewName, ctx.Doer); err != nil {
if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -387,7 +401,7 @@ func Edit(ctx *context.APIContext) {
if err := org.UpdateOrgEmailAddress(ctx, ctx.Org.Organization, form.Email); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -399,7 +413,7 @@ func Edit(ctx *context.APIContext) {
Description: optional.FromPtr(form.Description),
Website: optional.FromPtr(form.Website),
Location: optional.FromPtr(form.Location),
Visibility: optional.FromMapLookup(api.VisibilityModes, optional.FromPtr(form.Visibility).Value()),
Visibility: optional.FromMapLookup(api.VisibilityModes, string(optional.FromPtr(form.Visibility).Value())),
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
}
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
@@ -502,3 +516,70 @@ func ListOrgActivityFeeds(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
}
func deleteOrgReposBackground(ctx gocontext.Context, org *organization.Organization, repoIDs []int64, doer *user_model.User) {
defer func() {
if r := recover(); r != nil {
log.Error("panic during org repo deletion: %v, stack: %v", r, log.Stack(2))
}
}()
for _, repoID := range repoIDs {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
desc := fmt.Sprintf("Failed to get repository ID %d in org %s: %v", repoID, org.Name, err)
_ = system_model.CreateNotice(ctx, system_model.NoticeRepository, desc)
log.Error("GetRepositoryByID failed: %v", desc)
continue
}
if err := repo_service.DeleteRepository(ctx, doer, repo, true); err != nil {
desc := fmt.Sprintf("Failed to delete repository %s (ID: %d) in org %s: %v", repo.Name, repo.ID, org.Name, err)
_ = system_model.CreateNotice(ctx, system_model.NoticeRepository, desc)
log.Error("DeleteRepository failed: %v", desc)
continue
}
log.Info("Successfully deleted repository %s (ID: %d) in org %s", repo.Name, repo.ID, org.Name)
}
log.Info("Completed deletion of repositories in org %s", org.Name)
}
func DeleteOrgRepos(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/repos organization orgDeleteRepos
// ---
// summary: Delete all repositories in an organization
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "202":
// "$ref": "#/responses/empty"
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// Intentionally it only loads repository IDs to avoid loading too much data into memory
// There is no need to do pagination here as the number of repositories is expected to be manageable
repoIDs, err := repo_model.GetOrgRepositoryIDs(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if len(repoIDs) == 0 {
ctx.Status(http.StatusNoContent)
return
}
// Start deletion (slow) in background with detached context, so it can continue even if the request is canceled
go deleteOrgReposBackground(graceful.GetManager().ShutdownContext(), ctx.Org.Organization, repoIDs, ctx.Doer)
ctx.Status(http.StatusAccepted)
}
+96 -44
View File
@@ -8,23 +8,23 @@ import (
"errors"
"net/http"
activities_model "code.gitea.io/gitea/models/activities"
"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/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
feed_service "code.gitea.io/gitea/services/feed"
org_service "code.gitea.io/gitea/services/org"
repo_service "code.gitea.io/gitea/services/repository"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
unit_model "gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
feed_service "gitea.dev/services/feed"
org_service "gitea.dev/services/org"
repo_service "gitea.dev/services/repository"
)
// ListTeams list all the teams of an organization
@@ -55,10 +55,15 @@ func ListTeams(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
teams, count, err := organization.SearchTeam(ctx, &organization.SearchTeamOptions{
opts := &organization.SearchTeamOptions{
ListOptions: listOptions,
OrgID: ctx.Org.Organization.ID,
})
}
if err := organization.ApplyTeamListFilter(ctx, ctx.Org.Organization.ID, ctx.Doer, ctx.IsSigned, opts); err != nil {
ctx.APIErrorInternal(err)
return
}
teams, count, err := organization.SearchTeam(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -210,7 +215,7 @@ func CreateTeam(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateTeamOption)
teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin)
teamPermission := perm.ParseAccessMode(string(form.Permission), perm.AccessModeNone, perm.AccessModeAdmin)
team := &organization.Team{
OrgID: ctx.Org.Organization.ID,
Name: form.Name,
@@ -218,13 +223,14 @@ func CreateTeam(ctx *context.APIContext) {
IncludesAllRepositories: form.IncludesAllRepositories,
CanCreateOrgRepo: form.CanCreateOrgRepo,
AccessMode: teamPermission,
Visibility: organization.NormalizeTeamVisibility(string(form.Visibility)),
}
if team.AccessMode < perm.AccessModeAdmin {
if len(form.UnitsMap) > 0 {
attachTeamUnitsMap(team, form.UnitsMap)
} else if len(form.Units) > 0 {
unitPerm := perm.ParseAccessMode(form.Permission, perm.AccessModeRead, perm.AccessModeWrite)
unitPerm := perm.ParseAccessMode(string(form.Permission), perm.AccessModeRead, perm.AccessModeWrite)
attachTeamUnits(team, unitPerm, form.Units)
} else {
ctx.APIErrorInternal(errors.New("units permission should not be empty"))
@@ -236,7 +242,7 @@ func CreateTeam(ctx *context.APIContext) {
if err := org_service.NewTeam(ctx, team); err != nil {
if organization.IsErrTeamAlreadyExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -295,10 +301,14 @@ func EditTeam(ctx *context.APIContext) {
team.Description = *form.Description
}
if form.Visibility != nil && !team.IsOwnerTeam() {
team.Visibility = organization.NormalizeTeamVisibility(string(*form.Visibility))
}
isAuthChanged := false
isIncludeAllChanged := false
if !team.IsOwnerTeam() && len(form.Permission) != 0 {
teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin)
teamPermission := perm.ParseAccessMode(string(form.Permission), perm.AccessModeNone, perm.AccessModeAdmin)
if team.AccessMode != teamPermission {
isAuthChanged = true
team.AccessMode = teamPermission
@@ -314,7 +324,7 @@ func EditTeam(ctx *context.APIContext) {
if len(form.UnitsMap) > 0 {
attachTeamUnitsMap(team, form.UnitsMap)
} else if len(form.Units) > 0 {
unitPerm := perm.ParseAccessMode(form.Permission, perm.AccessModeRead, perm.AccessModeWrite)
unitPerm := perm.ParseAccessMode(string(form.Permission), perm.AccessModeRead, perm.AccessModeWrite)
attachTeamUnits(team, unitPerm, form.Units)
}
} else {
@@ -387,15 +397,6 @@ func GetTeamMembers(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
isMember, err := organization.IsOrganizationMember(ctx, ctx.Org.Team.OrgID, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
} else if !isMember && !ctx.Doer.IsAdmin {
ctx.APIErrorNotFound()
return
}
listOptions := utils.GetListOptions(ctx)
teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
ListOptions: listOptions,
@@ -490,7 +491,7 @@ func AddTeamMember(ctx *context.APIContext) {
}
if err := org_service.AddTeamMember(ctx, ctx.Org.Team, u); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -566,25 +567,43 @@ func GetTeamRepos(ctx *context.APIContext) {
team := ctx.Org.Team
listOptions := utils.GetListOptions(ctx)
teamRepos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{
// A public-only token must not expose (or count) private repos, even when the
// doer owning the token otherwise has access to them, so filter them out at the
// query level to keep the returned page and the total-count header consistent.
searchOpts := &repo_model.SearchTeamRepoOptions{
ListOptions: listOptions,
TeamID: team.ID,
})
PublicOnly: ctx.PublicOnly,
}
teamRepos, err := repo_model.GetTeamRepositories(ctx, searchOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
repos := make([]*api.Repository, len(teamRepos))
for i, repo := range teamRepos {
count, err := repo_model.CountTeamRepositories(ctx, searchOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
repos := make([]*api.Repository, 0, len(teamRepos))
for _, repo := range teamRepos {
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
repos[i] = convert.ToRepo(ctx, repo, permission)
// A team's repo list is reachable by non-team-members through the team's
// visibility tier, so never expose repos (incl. their names) the doer
// cannot access. This per-repo visibility trim can't be expressed in the
// SQL count above without regressing per-unit public access, so for such
// non-members the total-count header may be a small upper bound.
if !permission.HasAnyUnitAccessOrPublicAccess() {
continue
}
repos = append(repos, convert.ToRepo(ctx, repo, permission))
}
ctx.SetLinkHeader(int64(team.NumRepos), listOptions.PageSize)
ctx.SetTotalCountHeader(int64(team.NumRepos))
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, repos)
}
@@ -623,6 +642,12 @@ func GetTeamRepo(ctx *context.APIContext) {
return
}
// A public-only token must not confirm the existence of a private repo.
if !ctx.TokenCanAccessRepo(repo) {
ctx.APIErrorNotFound()
return
}
if !organization.HasTeamRepo(ctx, ctx.Org.Team.OrgID, ctx.Org.Team.ID, repo.ID) {
ctx.APIErrorNotFound()
return
@@ -633,6 +658,12 @@ func GetTeamRepo(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
// The team may be reachable by a non-team-member via its visibility tier;
// don't confirm the existence of a repo the doer cannot access.
if !permission.HasAnyUnitAccessOrPublicAccess() {
ctx.APIErrorNotFound()
return
}
ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, permission))
}
@@ -651,6 +682,19 @@ func getRepositoryByParams(ctx *context.APIContext) *repo_model.Repository {
return repo
}
func canChangeTeamRepository(ctx *context.APIContext) bool {
canChange, err := ctx.Org.Organization.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
}
// AddTeamRepository api for adding a repository to a team
func AddTeamRepository(ctx *context.APIContext) {
// swagger:operation PUT /teams/{id}/repos/{org}/{repo} organization orgAddTeamRepository
@@ -687,6 +731,9 @@ func AddTeamRepository(ctx *context.APIContext) {
if ctx.Written() {
return
}
if !canChangeTeamRepository(ctx) {
return
}
if access, err := access_model.AccessLevel(ctx, ctx.Doer, repo); err != nil {
ctx.APIErrorInternal(err)
return
@@ -739,6 +786,9 @@ func RemoveTeamRepository(ctx *context.APIContext) {
if ctx.Written() {
return
}
if !canChangeTeamRepository(ctx) {
return
}
if access, err := access_model.AccessLevel(ctx, ctx.Doer, repo); err != nil {
ctx.APIErrorInternal(err)
return
@@ -806,9 +856,9 @@ func SearchTeam(ctx *context.APIContext) {
ListOptions: listOptions,
}
// Only admin is allowed to search for all teams
if !ctx.Doer.IsAdmin {
opts.UserID = ctx.Doer.ID
if err := organization.ApplyTeamListFilter(ctx, ctx.Org.Organization.ID, ctx.Doer, ctx.IsSigned, opts); err != nil {
ctx.APIErrorInternal(err)
return
}
teams, maxResults, err := organization.SearchTeam(ctx, opts)
@@ -876,6 +926,8 @@ func ListTeamActivityFeeds(ctx *context.APIContext) {
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
// A public-only token must not receive private activity entries.
opts.ApplyPublicOnly(ctx.PublicOnly)
feeds, count, err := feed_service.GetFeeds(ctx, opts)
if err != nil {
@@ -7,15 +7,15 @@ import (
"errors"
"net/http"
"code.gitea.io/gitea/models/packages"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/optional"
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"
"code.gitea.io/gitea/services/convert"
packages_service "code.gitea.io/gitea/services/packages"
"gitea.dev/models/packages"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/optional"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
packages_service "gitea.dev/services/packages"
)
// ListPackages gets all packages of an owner
@@ -329,7 +329,7 @@ func GetLatestPackageVersion(ctx *context.APIContext) {
return
}
if len(pvs) == 0 {
ctx.APIError(http.StatusNotFound, err)
ctx.APIErrorNotFound()
return
}
@@ -383,7 +383,7 @@ func LinkPackage(ctx *context.APIContext) {
pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -393,7 +393,7 @@ func LinkPackage(ctx *context.APIContext) {
repo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ctx.PathParam("repo_name"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -404,9 +404,9 @@ func LinkPackage(ctx *context.APIContext) {
if err != nil {
switch {
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
case errors.Is(err, util.ErrPermissionDenied):
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
default:
ctx.APIErrorInternal(err)
}
@@ -445,7 +445,7 @@ func UnlinkPackage(ctx *context.APIContext) {
pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -456,9 +456,9 @@ func UnlinkPackage(ctx *context.APIContext) {
if err != nil {
switch {
case errors.Is(err, util.ErrPermissionDenied):
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
default:
ctx.APIErrorInternal(err)
}
@@ -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
}

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