diff options
Diffstat (limited to 'routers/api')
150 files changed, 5703 insertions, 3101 deletions
diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 910edd6d58..6473659e5c 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -135,7 +135,7 @@ func ArtifactContexter() func(next http.Handler) http.Handler { // we should verify the ACTIONS_RUNTIME_TOKEN authHeader := req.Header.Get("Authorization") if len(authHeader) == 0 || !strings.HasPrefix(authHeader, "Bearer ") { - ctx.Error(http.StatusUnauthorized, "Bad authorization header") + ctx.HTTPError(http.StatusUnauthorized, "Bad authorization header") return } @@ -147,12 +147,12 @@ func ArtifactContexter() func(next http.Handler) http.Handler { task, err = actions.GetTaskByID(req.Context(), tID) if err != nil { log.Error("Error runner api getting task by ID: %v", err) - ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID") + ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task by ID") return } if task.Status != actions.StatusRunning { log.Error("Error runner api getting task: task is not running") - ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task: task is not running") return } } else { @@ -162,14 +162,14 @@ func ArtifactContexter() func(next http.Handler) http.Handler { task, err = actions.GetRunningTaskByToken(req.Context(), authToken) if err != nil { log.Error("Error runner api getting task: %v", err) - ctx.Error(http.StatusInternalServerError, "Error runner api getting task") + ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task") return } } if err := task.LoadJob(req.Context()); err != nil { log.Error("Error runner api getting job: %v", err) - ctx.Error(http.StatusInternalServerError, "Error runner api getting job") + ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting job") return } @@ -211,7 +211,7 @@ func (ar artifactRoutes) getUploadArtifactURL(ctx *ArtifactContext) { var req getUploadArtifactRequest if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil { log.Error("Error decode request body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error decode request body") + ctx.HTTPError(http.StatusInternalServerError, "Error decode request body") return } @@ -250,7 +250,7 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) { expiredDays, err = strconv.ParseInt(queryRetentionDays, 10, 64) if err != nil { log.Error("Error parse retention days: %v", err) - ctx.Error(http.StatusBadRequest, "Error parse retention days") + ctx.HTTPError(http.StatusBadRequest, "Error parse retention days") return } } @@ -261,7 +261,7 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) { artifact, err := actions.CreateArtifact(ctx, task, artifactName, artifactPath, expiredDays) if err != nil { log.Error("Error create or get artifact: %v", err) - ctx.Error(http.StatusInternalServerError, "Error create or get artifact") + ctx.HTTPError(http.StatusInternalServerError, "Error create or get artifact") return } @@ -271,7 +271,7 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) { chunksTotalSize, err := saveUploadChunk(ar.fs, ctx, artifact, contentLength, runID) if err != nil { log.Error("Error save upload chunk: %v", err) - ctx.Error(http.StatusInternalServerError, "Error save upload chunk") + ctx.HTTPError(http.StatusInternalServerError, "Error save upload chunk") return } @@ -285,7 +285,7 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) { artifact.ContentEncoding = ctx.Req.Header.Get("Content-Encoding") if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error update artifact: %v", err) - ctx.Error(http.StatusInternalServerError, "Error update artifact") + ctx.HTTPError(http.StatusInternalServerError, "Error update artifact") return } log.Debug("[artifact] update artifact size, artifact_id: %d, size: %d, compressed size: %d", @@ -307,12 +307,12 @@ func (ar artifactRoutes) comfirmUploadArtifact(ctx *ArtifactContext) { artifactName := ctx.Req.URL.Query().Get("artifactName") if artifactName == "" { log.Error("Error artifact name is empty") - ctx.Error(http.StatusBadRequest, "Error artifact name is empty") + ctx.HTTPError(http.StatusBadRequest, "Error artifact name is empty") return } if err := mergeChunksForRun(ctx, ar.fs, runID, artifactName); err != nil { log.Error("Error merge chunks: %v", err) - ctx.Error(http.StatusInternalServerError, "Error merge chunks") + ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks") return } ctx.JSON(http.StatusOK, map[string]string{ @@ -337,15 +337,18 @@ func (ar artifactRoutes) listArtifacts(ctx *ArtifactContext) { return } - artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID}) + artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{ + RunID: runID, + Status: int(actions.ArtifactStatusUploadConfirmed), + }) if err != nil { log.Error("Error getting artifacts: %v", err) - ctx.Error(http.StatusInternalServerError, err.Error()) + ctx.HTTPError(http.StatusInternalServerError, err.Error()) return } if len(artifacts) == 0 { log.Debug("[artifact] handleListArtifacts, no artifacts") - ctx.Error(http.StatusNotFound) + ctx.HTTPError(http.StatusNotFound) return } @@ -402,21 +405,22 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) { artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{ RunID: runID, ArtifactName: itemPath, + Status: int(actions.ArtifactStatusUploadConfirmed), }) if err != nil { log.Error("Error getting artifacts: %v", err) - ctx.Error(http.StatusInternalServerError, err.Error()) + ctx.HTTPError(http.StatusInternalServerError, err.Error()) return } if len(artifacts) == 0 { log.Debug("[artifact] getDownloadArtifactURL, no artifacts") - ctx.Error(http.StatusNotFound) + ctx.HTTPError(http.StatusNotFound) return } if itemPath != artifacts[0].ArtifactName { log.Error("Error dismatch artifact name, itemPath: %v, artifact: %v", itemPath, artifacts[0].ArtifactName) - ctx.Error(http.StatusBadRequest, "Error dismatch artifact name") + ctx.HTTPError(http.StatusBadRequest, "Error dismatch artifact name") return } @@ -460,24 +464,29 @@ func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) { artifact, exist, err := db.GetByID[actions.ActionArtifact](ctx, artifactID) if err != nil { log.Error("Error getting artifact: %v", err) - ctx.Error(http.StatusInternalServerError, err.Error()) + ctx.HTTPError(http.StatusInternalServerError, err.Error()) return } if !exist { log.Error("artifact with ID %d does not exist", artifactID) - ctx.Error(http.StatusNotFound, fmt.Sprintf("artifact with ID %d does not exist", artifactID)) + ctx.HTTPError(http.StatusNotFound, fmt.Sprintf("artifact with ID %d does not exist", artifactID)) return } if artifact.RunID != runID { log.Error("Error mismatch runID and artifactID, task: %v, artifact: %v", runID, artifactID) - ctx.Error(http.StatusBadRequest) + 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") return } fd, err := ar.fs.Open(artifact.StoragePath) if err != nil { log.Error("Error opening file: %v", err) - ctx.Error(http.StatusInternalServerError, err.Error()) + ctx.HTTPError(http.StatusInternalServerError, err.Error()) return } defer fd.Close() diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go index cf48da12aa..708931d1ac 100644 --- a/routers/api/actions/artifacts_chunks.go +++ b/routers/api/actions/artifacts_chunks.go @@ -51,7 +51,7 @@ func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext, log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String) // if md5 not match, delete the chunk if reqMd5String != chunkMd5String { - checkErr = fmt.Errorf("md5 not match") + checkErr = errors.New("md5 not match") } } if writtenSize != contentSize { @@ -261,7 +261,7 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st return fmt.Errorf("save merged file error: %v", err) } if written != artifact.FileCompressedSize { - return fmt.Errorf("merged file size is not equal to chunk length") + return errors.New("merged file size is not equal to chunk length") } defer func() { @@ -292,7 +292,7 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st } artifact.StoragePath = storagePath - artifact.Status = int64(actions.ArtifactStatusUploadConfirmed) + artifact.Status = actions.ArtifactStatusUploadConfirmed if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { return fmt.Errorf("update artifact error: %v", err) } diff --git a/routers/api/actions/artifacts_utils.go b/routers/api/actions/artifacts_utils.go index a4ca5797dc..35868c290e 100644 --- a/routers/api/actions/artifacts_utils.go +++ b/routers/api/actions/artifacts_utils.go @@ -26,7 +26,7 @@ var invalidArtifactNameChars = strings.Join([]string{"\\", "/", "\"", ":", "<", func validateArtifactName(ctx *ArtifactContext, artifactName string) bool { if strings.ContainsAny(artifactName, invalidArtifactNameChars) { log.Error("Error checking artifact name contains invalid character") - ctx.Error(http.StatusBadRequest, "Error checking artifact name contains invalid character") + ctx.HTTPError(http.StatusBadRequest, "Error checking artifact name contains invalid character") return false } return true @@ -37,18 +37,18 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) { runID := ctx.PathParamInt64("run_id") if task.Job.RunID != runID { log.Error("Error runID not match") - ctx.Error(http.StatusBadRequest, "run-id does not match") + ctx.HTTPError(http.StatusBadRequest, "run-id does not match") return nil, 0, false } return task, runID, true } -func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) { //nolint:unparam +func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) { //nolint:unparam // ActionTask is never used task := ctx.ActionTask runID, err := strconv.ParseInt(rawRunID, 10, 64) if err != nil || task.Job.RunID != runID { log.Error("Error runID not match") - ctx.Error(http.StatusBadRequest, "run-id does not match") + ctx.HTTPError(http.StatusBadRequest, "run-id does not match") return nil, 0, false } return task, runID, true @@ -62,7 +62,7 @@ func validateArtifactHash(ctx *ArtifactContext, artifactName string) bool { return true } log.Error("Invalid artifact hash: %s", paramHash) - ctx.Error(http.StatusBadRequest, "Invalid artifact hash") + ctx.HTTPError(http.StatusBadRequest, "Invalid artifact hash") return false } diff --git a/routers/api/actions/artifactsv4.go b/routers/api/actions/artifactsv4.go index 8917a7a8a2..e9e9fc6393 100644 --- a/routers/api/actions/artifactsv4.go +++ b/routers/api/actions/artifactsv4.go @@ -25,7 +25,7 @@ package actions // 1.3. Continue Upload Zip Content to Blobstorage (unauthenticated request), repeat until everything is uploaded // PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=appendBlock // 1.4. BlockList xml payload to Blobstorage (unauthenticated request) -// Files of about 800MB are parallel in parallel and / or out of order, this file is needed to enshure the correct order +// Files of about 800MB are parallel in parallel and / or out of order, this file is needed to ensure the correct order // PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=blockList // Request // <?xml version="1.0" encoding="UTF-8" standalone="yes"?> @@ -162,15 +162,15 @@ func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, tas mac.Write([]byte(endp)) mac.Write([]byte(expires)) mac.Write([]byte(artifactName)) - mac.Write([]byte(fmt.Sprint(taskID))) - mac.Write([]byte(fmt.Sprint(artifactID))) + fmt.Fprint(mac, taskID) + fmt.Fprint(mac, artifactID) return mac.Sum(nil) } func (r artifactV4Routes) buildArtifactURL(ctx *ArtifactContext, endp, artifactName string, taskID, artifactID int64) string { expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST") uploadURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/") + strings.TrimSuffix(r.prefix, "/") + - "/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID, artifactID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID) + "&artifactID=" + fmt.Sprint(artifactID) + "/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID, artifactID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + strconv.FormatInt(taskID, 10) + "&artifactID=" + strconv.FormatInt(artifactID, 10) return uploadURL } @@ -187,29 +187,29 @@ func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*a expecedsig := r.buildSignature(endp, expires, artifactName, taskID, artifactID) if !hmac.Equal(dsig, expecedsig) { log.Error("Error unauthorized") - ctx.Error(http.StatusUnauthorized, "Error unauthorized") + ctx.HTTPError(http.StatusUnauthorized, "Error unauthorized") return nil, "", false } t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires) if err != nil || t.Before(time.Now()) { log.Error("Error link expired") - ctx.Error(http.StatusUnauthorized, "Error link expired") + ctx.HTTPError(http.StatusUnauthorized, "Error link expired") return nil, "", false } task, err := actions.GetTaskByID(ctx, taskID) if err != nil { log.Error("Error runner api getting task by ID: %v", err) - ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID") + ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task by ID") return nil, "", false } if task.Status != actions.StatusRunning { log.Error("Error runner api getting task: task is not running") - ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task: task is not running") return nil, "", false } if err := task.LoadJob(ctx); err != nil { log.Error("Error runner api getting job: %v", err) - ctx.Error(http.StatusInternalServerError, "Error runner api getting job") + ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting job") return nil, "", false } return task, artifactName, true @@ -230,13 +230,13 @@ func (r *artifactV4Routes) parseProtbufBody(ctx *ArtifactContext, req protorefle body, err := io.ReadAll(ctx.Req.Body) if err != nil { log.Error("Error decode request body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error decode request body") + ctx.HTTPError(http.StatusInternalServerError, "Error decode request body") return false } err = protojson.Unmarshal(body, req) if err != nil { log.Error("Error decode request body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error decode request body") + ctx.HTTPError(http.StatusInternalServerError, "Error decode request body") return false } return true @@ -246,7 +246,7 @@ func (r *artifactV4Routes) sendProtbufBody(ctx *ArtifactContext, req protoreflec resp, err := protojson.Marshal(req) if err != nil { log.Error("Error encode response body: %v", err) - ctx.Error(http.StatusInternalServerError, "Error encode response body") + ctx.HTTPError(http.StatusInternalServerError, "Error encode response body") return } ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8") @@ -275,7 +275,7 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { artifact, err := actions.CreateArtifact(ctx, ctx.ActionTask, artifactName, artifactName+".zip", rententionDays) if err != nil { log.Error("Error create or get artifact: %v", err) - ctx.Error(http.StatusInternalServerError, "Error create or get artifact") + ctx.HTTPError(http.StatusInternalServerError, "Error create or get artifact") return } artifact.ContentEncoding = ArtifactV4ContentEncoding @@ -283,7 +283,7 @@ func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) { artifact.FileCompressedSize = 0 if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error UpdateArtifactByID: %v", err) - ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") + ctx.HTTPError(http.StatusInternalServerError, "Error UpdateArtifactByID") return } @@ -309,28 +309,28 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName) if err != nil { log.Error("Error artifact not found: %v", err) - ctx.Error(http.StatusNotFound, "Error artifact not found") + ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return } _, err = appendUploadChunk(r.fs, ctx, artifact, artifact.FileSize, ctx.Req.ContentLength, artifact.RunID) if err != nil { log.Error("Error runner api getting task: task is not running") - ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task: task is not running") return } artifact.FileCompressedSize += ctx.Req.ContentLength artifact.FileSize += ctx.Req.ContentLength if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { log.Error("Error UpdateArtifactByID: %v", err) - ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID") + ctx.HTTPError(http.StatusInternalServerError, "Error UpdateArtifactByID") return } } else { _, err := r.fs.Save(fmt.Sprintf("tmpv4%d/block-%d-%d-%s", task.Job.RunID, task.Job.RunID, ctx.Req.ContentLength, base64.URLEncoding.EncodeToString([]byte(blockid))), ctx.Req.Body, -1) if err != nil { log.Error("Error runner api getting task: task is not running") - ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task: task is not running") return } } @@ -341,7 +341,7 @@ func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) { _, err := r.fs.Save(fmt.Sprintf("tmpv4%d/%d-%d-blocklist", task.Job.RunID, task.Job.RunID, artifactID), ctx.Req.Body, -1) if err != nil { log.Error("Error runner api getting task: task is not running") - ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running") + ctx.HTTPError(http.StatusInternalServerError, "Error runner api getting task: task is not running") return } ctx.JSON(http.StatusCreated, "created") @@ -389,7 +389,7 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { artifact, err := r.getArtifactByName(ctx, runID, req.Name) if err != nil { log.Error("Error artifact not found: %v", err) - ctx.Error(http.StatusNotFound, "Error artifact not found") + ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return } @@ -400,20 +400,20 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { chunkMap, err := listChunksByRunID(r.fs, runID) if err != nil { log.Error("Error merge chunks: %v", err) - ctx.Error(http.StatusInternalServerError, "Error merge chunks") + ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks") return } chunks, ok = chunkMap[artifact.ID] if !ok { log.Error("Error merge chunks") - ctx.Error(http.StatusInternalServerError, "Error merge chunks") + ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks") return } } else { chunks, err = listChunksByRunIDV4(r.fs, runID, artifact.ID, blockList) if err != nil { log.Error("Error merge chunks: %v", err) - ctx.Error(http.StatusInternalServerError, "Error merge chunks") + ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks") return } artifact.FileSize = chunks[len(chunks)-1].End + 1 @@ -426,7 +426,7 @@ func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) { } if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, checksum); err != nil { log.Error("Error merge chunks: %v", err) - ctx.Error(http.StatusInternalServerError, "Error merge chunks") + ctx.HTTPError(http.StatusInternalServerError, "Error merge chunks") return } @@ -448,15 +448,13 @@ func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) { return } - artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID}) + artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{ + RunID: runID, + Status: int(actions.ArtifactStatusUploadConfirmed), + }) if err != nil { log.Error("Error getting artifacts: %v", err) - ctx.Error(http.StatusInternalServerError, err.Error()) - return - } - if len(artifacts) == 0 { - log.Debug("[artifact] handleListArtifacts, no artifacts") - ctx.Error(http.StatusNotFound) + ctx.HTTPError(http.StatusInternalServerError, err.Error()) return } @@ -507,7 +505,12 @@ func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) { artifact, err := r.getArtifactByName(ctx, runID, artifactName) if err != nil { log.Error("Error artifact not found: %v", err) - ctx.Error(http.StatusNotFound, "Error artifact not found") + ctx.HTTPError(http.StatusNotFound, "Error artifact not found") + return + } + if artifact.Status != actions.ArtifactStatusUploadConfirmed { + log.Error("Error artifact not found: %s", artifact.Status.ToString()) + ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return } @@ -535,7 +538,12 @@ func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) { artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName) if err != nil { log.Error("Error artifact not found: %v", err) - ctx.Error(http.StatusNotFound, "Error artifact not found") + ctx.HTTPError(http.StatusNotFound, "Error artifact not found") + return + } + if artifact.Status != actions.ArtifactStatusUploadConfirmed { + log.Error("Error artifact not found: %s", artifact.Status.ToString()) + ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return } @@ -559,14 +567,14 @@ func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) { artifact, err := r.getArtifactByName(ctx, runID, req.Name) if err != nil { log.Error("Error artifact not found: %v", err) - ctx.Error(http.StatusNotFound, "Error artifact not found") + ctx.HTTPError(http.StatusNotFound, "Error artifact not found") return } err = actions.SetArtifactNeedDelete(ctx, runID, req.Name) if err != nil { log.Error("Error deleting artifacts: %v", err) - ctx.Error(http.StatusInternalServerError, err.Error()) + ctx.HTTPError(http.StatusInternalServerError, err.Error()) return } diff --git a/routers/api/actions/ping/ping_test.go b/routers/api/actions/ping/ping_test.go index 098b003ea2..98d2dcb820 100644 --- a/routers/api/actions/ping/ping_test.go +++ b/routers/api/actions/ping/ping_test.go @@ -4,7 +4,6 @@ package ping import ( - "context" "net/http" "net/http/httptest" "testing" @@ -51,7 +50,7 @@ func MainServiceTest(t *testing.T, h http.Handler) { clients := []pingv1connect.PingServiceClient{connectClient, grpcClient, grpcWebClient} t.Run("ping request", func(t *testing.T) { for _, client := range clients { - result, err := client.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{ + result, err := client.Ping(t.Context(), connect.NewRequest(&pingv1.PingRequest{ Data: "foobar", })) require.NoError(t, err) diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go index c55b30f7eb..ce8137592d 100644 --- a/routers/api/actions/runner/runner.go +++ b/routers/api/actions/runner/runner.go @@ -15,6 +15,7 @@ import ( "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 "code.gitea.io/actions-proto-go/runner/v1" "code.gitea.io/actions-proto-go/runner/v1/runnerv1connect" @@ -77,6 +78,7 @@ func (s *Service) Register( 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") @@ -95,12 +97,13 @@ func (s *Service) Register( res := connect.NewResponse(&runnerv1.RegisterResponse{ Runner: &runnerv1.Runner{ - Id: runner.ID, - Uuid: runner.UUID, - Token: runner.Token, - Name: runner.Name, - Version: runner.Version, - Labels: runner.AgentLabels, + Id: runner.ID, + Uuid: runner.UUID, + Token: runner.Token, + Name: runner.Name, + Version: runner.Version, + Labels: runner.AgentLabels, + Ephemeral: runner.Ephemeral, }, }) @@ -156,7 +159,7 @@ 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 not be assgined. // try to pick a task for the runner that send the request. - if t, ok, err := pickTask(ctx, runner); err != nil { + if t, ok, err := actions_service.PickTask(ctx, runner); err != nil { log.Error("pick task failed: %v", err) return nil, status.Errorf(codes.Internal, "pick task: %v", err) } else if ok { @@ -210,7 +213,7 @@ func (s *Service) UpdateTask( if err := task.LoadJob(ctx); err != nil { return nil, status.Errorf(codes.Internal, "load job: %v", err) } - if err := task.Job.LoadRun(ctx); err != nil { + if err := task.Job.LoadAttributes(ctx); err != nil { return nil, status.Errorf(codes.Internal, "load run: %v", err) } @@ -219,6 +222,10 @@ func (s *Service) UpdateTask( actions_service.CreateCommitStatus(ctx, task.Job) } + if task.Status.IsDone() { + notify_service.WorkflowJobStatusUpdate(ctx, task.Job.Run.Repo, task.Job.Run.TriggerUser, task.Job, task) + } + if req.Msg.State.Result != runnerv1.Result_RESULT_UNSPECIFIED { if err := actions_service.EmitJobsIfReady(task.Job.RunID); err != nil { log.Error("Emit ready jobs of run %d: %v", task.Job.RunID, err) diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go deleted file mode 100644 index 0fd7ca5c44..0000000000 --- a/routers/api/actions/runner/utils.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package runner - -import ( - "context" - "fmt" - - actions_model "code.gitea.io/gitea/models/actions" - secret_model "code.gitea.io/gitea/models/secret" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/services/actions" - - runnerv1 "code.gitea.io/actions-proto-go/runner/v1" - "google.golang.org/protobuf/types/known/structpb" -) - -func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv1.Task, bool, error) { - t, ok, err := actions_model.CreateTaskForRunner(ctx, runner) - if err != nil { - return nil, false, fmt.Errorf("CreateTaskForRunner: %w", err) - } - if !ok { - return nil, false, nil - } - - secrets, err := secret_model.GetSecretsOfTask(ctx, t) - if err != nil { - return nil, false, fmt.Errorf("GetSecretsOfTask: %w", err) - } - - vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run) - if err != nil { - return nil, false, fmt.Errorf("GetVariablesOfRun: %w", err) - } - - actions.CreateCommitStatus(ctx, t.Job) - - task := &runnerv1.Task{ - Id: t.ID, - WorkflowPayload: t.Job.WorkflowPayload, - Context: generateTaskContext(t), - Secrets: secrets, - Vars: vars, - } - - if needs, err := findTaskNeeds(ctx, t); err != nil { - log.Error("Cannot find needs for task %v: %v", t.ID, err) - // Go on with empty needs. - // If return error, the task will be wild, which means the runner will never get it when it has been assigned to the runner. - // In contrast, missing needs is less serious. - // And the task will fail and the runner will report the error in the logs. - } else { - task.Needs = needs - } - - return task, true, nil -} - -func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { - giteaRuntimeToken, err := actions.CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID) - if err != nil { - log.Error("actions.CreateAuthorizationToken failed: %v", err) - } - - gitCtx := actions.GenerateGiteaContext(t.Job.Run, t.Job) - gitCtx["token"] = t.Token - gitCtx["gitea_runtime_token"] = giteaRuntimeToken - - taskContext, err := structpb.NewStruct(gitCtx) - if err != nil { - log.Error("structpb.NewStruct failed: %v", err) - } - - return taskContext -} - -func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[string]*runnerv1.TaskNeed, error) { - if err := task.LoadAttributes(ctx); err != nil { - return nil, fmt.Errorf("task LoadAttributes: %w", err) - } - taskNeeds, err := actions.FindTaskNeeds(ctx, task.Job) - if err != nil { - return nil, err - } - ret := make(map[string]*runnerv1.TaskNeed, len(taskNeeds)) - for jobID, taskNeed := range taskNeeds { - ret[jobID] = &runnerv1.TaskNeed{ - Outputs: taskNeed.Outputs, - Result: runnerv1.Result(taskNeed.Result), - } - } - return ret, nil -} diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go index f35cff3df2..ba4a4f23ce 100644 --- a/routers/api/packages/alpine/alpine.go +++ b/routers/api/packages/alpine/alpine.go @@ -68,7 +68,7 @@ func GetRepositoryFile(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetFileStreamByPackageVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion( ctx, pv, &packages_service.PackageFileInfo{ @@ -216,7 +216,7 @@ func DownloadPackageFile(ctx *context.Context) { } } - s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) + s, u, pf, err := packages_service.OpenFileForDownload(ctx, pfs[0]) if err != nil { if errors.Is(err, util.ErrNotExist) { apiError(ctx, http.StatusNotFound, err) diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 41c3eb95e9..878e0f9945 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -5,8 +5,6 @@ package packages import ( "net/http" - "regexp" - "strings" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/perm" @@ -46,35 +44,36 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) { if ok { // it's a personal access token but not oauth2 token scopeMatched := false var err error - if accessMode == perm.AccessModeRead { + switch accessMode { + case perm.AccessModeRead: scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeReadPackage) if err != nil { - ctx.Error(http.StatusInternalServerError, "HasScope", err.Error()) + ctx.HTTPError(http.StatusInternalServerError, "HasScope", err.Error()) return } - } else if accessMode == perm.AccessModeWrite { + case perm.AccessModeWrite: scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeWritePackage) if err != nil { - ctx.Error(http.StatusInternalServerError, "HasScope", err.Error()) + ctx.HTTPError(http.StatusInternalServerError, "HasScope", err.Error()) return } } if !scopeMatched { ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`) - ctx.Error(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin") + ctx.HTTPError(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin") return } // check if scope only applies to public resources publicOnly, err := scope.PublicOnly() if err != nil { - ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error()) + ctx.HTTPError(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error()) return } if publicOnly { if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() { - ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public packages") + ctx.HTTPError(http.StatusForbidden, "reqToken", "token scope is limited to public packages") return } } @@ -83,7 +82,7 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) { if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea Package API"`) - ctx.Error(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin") + ctx.HTTPError(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin") return } } @@ -100,7 +99,7 @@ func verifyAuth(r *web.Router, authMethods []auth.Method) { ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session) if err != nil { log.Error("Failed to verify user: %v", err) - ctx.Error(http.StatusUnauthorized, "authGroup.Verify") + ctx.HTTPError(http.StatusUnauthorized, "authGroup.Verify") return } ctx.IsSigned = ctx.Doer != nil @@ -281,42 +280,10 @@ func CommonRoutes() *web.Router { }) }) }, reqPackageAccess(perm.AccessModeRead)) - r.Group("/conda", func() { - var ( - downloadPattern = regexp.MustCompile(`\A(.+/)?(.+)/((?:[^/]+(?:\.tar\.bz2|\.conda))|(?:current_)?repodata\.json(?:\.bz2)?)\z`) - uploadPattern = regexp.MustCompile(`\A(.+/)?([^/]+(?:\.tar\.bz2|\.conda))\z`) - ) - - r.Get("/*", func(ctx *context.Context) { - m := downloadPattern.FindStringSubmatch(ctx.PathParam("*")) - if len(m) == 0 { - ctx.Status(http.StatusNotFound) - return - } - - ctx.SetPathParam("channel", strings.TrimSuffix(m[1], "/")) - ctx.SetPathParam("architecture", m[2]) - ctx.SetPathParam("filename", m[3]) - - switch m[3] { - case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2": - conda.EnumeratePackages(ctx) - default: - conda.DownloadPackageFile(ctx) - } - }) - r.Put("/*", reqPackageAccess(perm.AccessModeWrite), func(ctx *context.Context) { - m := uploadPattern.FindStringSubmatch(ctx.PathParam("*")) - if len(m) == 0 { - ctx.Status(http.StatusNotFound) - return - } - - ctx.SetPathParam("channel", strings.TrimSuffix(m[1], "/")) - ctx.SetPathParam("filename", m[2]) - - conda.UploadPackageFile(ctx) - }) + r.PathGroup("/conda/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/<architecture>/<filename>", conda.ListOrGetPackages) + g.MatchPath("GET", "/<channel:*>/<architecture>/<filename>", conda.ListOrGetPackages) + g.MatchPath("PUT", "/<channel:*>/<filename>", reqPackageAccess(perm.AccessModeWrite), conda.UploadPackageFile) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/cran", func() { r.Group("/src", func() { @@ -357,60 +324,15 @@ func CommonRoutes() *web.Router { }, reqPackageAccess(perm.AccessModeRead)) r.Group("/go", func() { r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage) - r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) { - ctx.Status(http.StatusNotFound) - }) + r.Get("/sumdb/sum.golang.org/supported", http.NotFound) - // Manual mapping of routes because the package name contains slashes which chi does not support // https://go.dev/ref/mod#goproxy-protocol - r.Get("/*", func(ctx *context.Context) { - path := ctx.PathParam("*") - - if strings.HasSuffix(path, "/@latest") { - ctx.SetPathParam("name", path[:len(path)-len("/@latest")]) - ctx.SetPathParam("version", "latest") - - goproxy.PackageVersionMetadata(ctx) - return - } - - parts := strings.SplitN(path, "/@v/", 2) - if len(parts) != 2 { - ctx.Status(http.StatusNotFound) - return - } - - ctx.SetPathParam("name", parts[0]) - - // <package/name>/@v/list - if parts[1] == "list" { - goproxy.EnumeratePackageVersions(ctx) - return - } - - // <package/name>/@v/<version>.zip - if strings.HasSuffix(parts[1], ".zip") { - ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".zip")]) - - goproxy.DownloadPackageFile(ctx) - return - } - // <package/name>/@v/<version>.info - if strings.HasSuffix(parts[1], ".info") { - ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".info")]) - - goproxy.PackageVersionMetadata(ctx) - return - } - // <package/name>/@v/<version>.mod - if strings.HasSuffix(parts[1], ".mod") { - ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".mod")]) - - goproxy.PackageVersionGoModContent(ctx) - return - } - - ctx.Status(http.StatusNotFound) + r.PathGroup("/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/<name:*>/@<version:latest>", goproxy.PackageVersionMetadata) + g.MatchPath("GET", "/<name:*>/@v/list", goproxy.EnumeratePackageVersions) + g.MatchPath("GET", "/<name:*>/@v/<version>.zip", goproxy.DownloadPackageFile) + g.MatchPath("GET", "/<name:*>/@v/<version>.info", goproxy.PackageVersionMetadata) + g.MatchPath("GET", "/<name:*>/@v/<version>.mod", goproxy.PackageVersionGoModContent) }) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/generic", func() { @@ -531,82 +453,26 @@ func CommonRoutes() *web.Router { }) }) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/pypi", func() { r.Post("/", reqPackageAccess(perm.AccessModeWrite), pypi.UploadPackageFile) r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile) r.Get("/simple/{id}", pypi.PackageMetadata) }, reqPackageAccess(perm.AccessModeRead)) - r.Group("/rpm", func() { - r.Group("/repository.key", func() { - r.Head("", rpm.GetRepositoryKey) - r.Get("", rpm.GetRepositoryKey) - }) - - var ( - repoPattern = regexp.MustCompile(`\A(.*?)\.repo\z`) - uploadPattern = regexp.MustCompile(`\A(.*?)/upload\z`) - filePattern = regexp.MustCompile(`\A(.*?)/package/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+\.rpm)|)\z`) - repoFilePattern = regexp.MustCompile(`\A(.*?)/repodata/([^/]+)\z`) - ) - - r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) { - path := ctx.PathParam("*") - isHead := ctx.Req.Method == "HEAD" - isGetHead := ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET" - isPut := ctx.Req.Method == "PUT" - isDelete := ctx.Req.Method == "DELETE" - - m := repoPattern.FindStringSubmatch(path) - if len(m) == 2 && isGetHead { - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - rpm.GetRepositoryConfig(ctx) - return - } - m = repoFilePattern.FindStringSubmatch(path) - if len(m) == 3 && isGetHead { - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - ctx.SetPathParam("filename", m[2]) - if isHead { - rpm.CheckRepositoryFileExistence(ctx) - } else { - rpm.GetRepositoryFile(ctx) - } - return - } - - m = uploadPattern.FindStringSubmatch(path) - if len(m) == 2 && isPut { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - rpm.UploadPackageFile(ctx) - return - } - - m = filePattern.FindStringSubmatch(path) - if len(m) == 6 && (isGetHead || isDelete) { - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - ctx.SetPathParam("name", m[2]) - ctx.SetPathParam("version", m[3]) - ctx.SetPathParam("architecture", m[4]) - if isGetHead { - rpm.DownloadPackageFile(ctx) - } else { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - rpm.DeletePackageFile(ctx) - } - return - } - - ctx.Status(http.StatusNotFound) - }) + r.Methods("HEAD,GET", "/rpm.repo", reqPackageAccess(perm.AccessModeRead), rpm.GetRepositoryConfig) + r.PathGroup("/rpm/*", func(g *web.RouterPathGroup) { + g.MatchPath("HEAD,GET", "/repository.key", rpm.GetRepositoryKey) + g.MatchPath("HEAD,GET", "/<group:*>.repo", rpm.GetRepositoryConfig) + g.MatchPath("HEAD", "/<group:*>/repodata/<filename>", rpm.CheckRepositoryFileExistence) + g.MatchPath("GET", "/<group:*>/repodata/<filename>", rpm.GetRepositoryFile) + g.MatchPath("PUT", "/<group:*>/upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile) + // this URL pattern is only used internally in the RPM index, it is generated by us, the filename part is not really used (can be anything) + g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>", rpm.DownloadPackageFile) + g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>/<filename>", rpm.DownloadPackageFile) + g.MatchPath("DELETE", "/<group:*>/package/<name>/<version>/<architecture>", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/rubygems", func() { r.Get("/specs.4.8.gz", rubygems.EnumeratePackages) r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest) @@ -620,6 +486,7 @@ func CommonRoutes() *web.Router { r.Delete("/yank", rubygems.DeletePackage) }, reqPackageAccess(perm.AccessModeWrite)) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/swift", func() { r.Group("", func() { // Needs to be unauthenticated. r.Post("", swift.CheckAuthenticate) @@ -631,31 +498,12 @@ func CommonRoutes() *web.Router { r.Get("", swift.EnumeratePackageVersions) r.Get(".json", swift.EnumeratePackageVersions) }, swift.CheckAcceptMediaType(swift.AcceptJSON)) - r.Group("/{version}", func() { - r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest) - r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile) - r.Get("", func(ctx *context.Context) { - // Can't use normal routes here: https://github.com/go-chi/chi/issues/781 - - version := ctx.PathParam("version") - if strings.HasSuffix(version, ".zip") { - swift.CheckAcceptMediaType(swift.AcceptZip)(ctx) - if ctx.Written() { - return - } - ctx.SetPathParam("version", version[:len(version)-4]) - swift.DownloadPackageFile(ctx) - } else { - swift.CheckAcceptMediaType(swift.AcceptJSON)(ctx) - if ctx.Written() { - return - } - if strings.HasSuffix(version, ".json") { - ctx.SetPathParam("version", version[:len(version)-5]) - } - swift.PackageVersionMetadata(ctx) - } - }) + r.PathGroup("/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/<version>.json", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.PackageVersionMetadata) + g.MatchPath("GET", "/<version>.zip", swift.CheckAcceptMediaType(swift.AcceptZip), swift.DownloadPackageFile) + g.MatchPath("GET", "/<version>/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest) + g.MatchPath("GET", "/<version>", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.PackageVersionMetadata) + g.MatchPath("PUT", "/<version>", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile) }) }) r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers) @@ -692,6 +540,8 @@ func ContainerRoutes() *web.Router { &container.Auth{}, }) + // TODO: Content Discovery / References (not implemented yet) + r.Get("", container.ReqContainerAccess, container.DetermineSupport) r.Group("/token", func() { r.Get("", container.Authenticate) @@ -700,26 +550,22 @@ func ContainerRoutes() *web.Router { r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList) r.Group("/{username}", func() { r.PathGroup("/*", func(g *web.RouterPathGroup) { - g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.InitiateUploadBlob) - g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagList) - g.MatchPath("GET,PATCH,PUT,DELETE", `/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, func(ctx *context.Context) { - if ctx.Req.Method == http.MethodGet { - container.GetUploadBlob(ctx) - } else if ctx.Req.Method == http.MethodPatch { - container.UploadBlob(ctx) - } else if ctx.Req.Method == http.MethodPut { - container.EndUploadBlob(ctx) - } else /* DELETE */ { - container.CancelUploadBlob(ctx) - } - }) + g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.PostBlobsUploads) + g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagsList) + + patternBlobsUploadsUUID := g.PatternRegexp(`/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName) + g.MatchPattern("GET", patternBlobsUploadsUUID, container.GetBlobsUpload) + g.MatchPattern("PATCH", patternBlobsUploadsUUID, container.PatchBlobsUpload) + g.MatchPattern("PUT", patternBlobsUploadsUUID, container.PutBlobsUpload) + g.MatchPattern("DELETE", patternBlobsUploadsUUID, container.DeleteBlobsUpload) + g.MatchPath("HEAD", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.HeadBlob) g.MatchPath("GET", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.GetBlob) g.MatchPath("DELETE", `/<image:*>/blobs/<digest>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob) g.MatchPath("HEAD", `/<image:*>/manifests/<reference>`, container.VerifyImageName, container.HeadManifest) g.MatchPath("GET", `/<image:*>/manifests/<reference>`, container.VerifyImageName, container.GetManifest) - g.MatchPath("PUT", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.UploadManifest) + g.MatchPath("PUT", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.PutManifest) g.MatchPath("DELETE", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteManifest) }) }, container.ReqContainerAccess, context.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead)) diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go index f5dc6c1d01..bf9cc3f1b8 100644 --- a/routers/api/packages/arch/arch.go +++ b/routers/api/packages/arch/arch.go @@ -239,7 +239,7 @@ func GetPackageOrRepositoryFile(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) + s, u, pf, err := packages_service.OpenFileForDownload(ctx, pfs[0]) if err != nil { if errors.Is(err, util.ErrNotExist) { apiError(ctx, http.StatusNotFound, err) diff --git a/routers/api/packages/cargo/cargo.go b/routers/api/packages/cargo/cargo.go index 42ef13476c..cfcf79244f 100644 --- a/routers/api/packages/cargo/cargo.go +++ b/routers/api/packages/cargo/cargo.go @@ -51,7 +51,7 @@ func apiError(ctx *context.Context, status int, obj any) { // https://rust-lang.github.io/rfcs/2789-sparse-index.html func RepositoryConfig(ctx *context.Context) { - ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInView || ctx.Package.Owner.Visibility != structs.VisibleTypePublic)) + ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInViewStrict || ctx.Package.Owner.Visibility != structs.VisibleTypePublic)) } func EnumeratePackageVersions(ctx *context.Context) { @@ -95,10 +95,7 @@ type SearchResultMeta struct { // https://doc.rust-lang.org/cargo/reference/registries.html#search func SearchPackages(ctx *context.Context) { - page := ctx.FormInt("page") - if page < 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) perPage := ctx.FormInt("per_page") paginator := db.ListOptions{ Page: page, @@ -168,7 +165,7 @@ func ListOwners(ctx *context.Context) { // DownloadPackageFile serves the content of a package func DownloadPackageFile(ctx *context.Context) { - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/packages/chef/auth.go b/routers/api/packages/chef/auth.go index a790e9a363..c6808300a2 100644 --- a/routers/api/packages/chef/auth.go +++ b/routers/api/packages/chef/auth.go @@ -12,6 +12,7 @@ import ( "crypto/x509" "encoding/base64" "encoding/pem" + "errors" "fmt" "hash" "math/big" @@ -121,7 +122,7 @@ func verifyTimestamp(req *http.Request) error { } if diff > maxTimeDifference { - return fmt.Errorf("time difference") + return errors.New("time difference") } return nil @@ -190,7 +191,7 @@ func getAuthorizationData(req *http.Request) ([]byte, error) { tmp := make([]string, len(valueList)) for k, v := range valueList { if k > len(tmp) { - return nil, fmt.Errorf("invalid X-Ops-Authorization headers") + return nil, errors.New("invalid X-Ops-Authorization headers") } tmp[k-1] = v } @@ -267,7 +268,7 @@ func verifyDataOld(signature, data []byte, pub *rsa.PublicKey) error { } if !slices.Equal(out[skip:], data) { - return fmt.Errorf("could not verify signature") + return errors.New("could not verify signature") } return nil diff --git a/routers/api/packages/chef/chef.go b/routers/api/packages/chef/chef.go index a0c8c5696c..1f11afe548 100644 --- a/routers/api/packages/chef/chef.go +++ b/routers/api/packages/chef/chef.go @@ -343,7 +343,7 @@ func DownloadPackage(ctx *context.Context) { pf := pd.Files[0].File - s, u, _, err := packages_service.GetPackageFileStream(ctx, pf) + s, u, _, err := packages_service.OpenFileForDownload(ctx, pf) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return diff --git a/routers/api/packages/composer/api.go b/routers/api/packages/composer/api.go index a3bcf80417..a3ea2c2f9a 100644 --- a/routers/api/packages/composer/api.go +++ b/routers/api/packages/composer/api.go @@ -66,6 +66,7 @@ type PackageMetadataResponse struct { } // PackageVersionMetadata contains package metadata +// https://getcomposer.org/doc/05-repositories.md#package type PackageVersionMetadata struct { *composer_module.Metadata Name string `json:"name"` @@ -73,6 +74,7 @@ type PackageVersionMetadata struct { Type string `json:"type"` Created time.Time `json:"time"` Dist Dist `json:"dist"` + Source Source `json:"source"` } // Dist contains package download information @@ -82,6 +84,13 @@ type Dist struct { Checksum string `json:"shasum"` } +// Source contains package source information +type Source struct { + URL string `json:"url"` + Type string `json:"type"` + Reference string `json:"reference"` +} + func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *PackageMetadataResponse { versions := make([]*PackageVersionMetadata, 0, len(pds)) @@ -94,7 +103,7 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac } } - versions = append(versions, &PackageVersionMetadata{ + pkg := PackageVersionMetadata{ Name: pd.Package.Name, Version: pd.Version.Version, Type: packageType, @@ -105,7 +114,16 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac URL: fmt.Sprintf("%s/files/%s/%s/%s", registryURL, url.PathEscape(pd.Package.LowerName), url.PathEscape(pd.Version.LowerVersion), url.PathEscape(pd.Files[0].File.LowerName)), Checksum: pd.Files[0].Blob.HashSHA1, }, - }) + } + if pd.Repository != nil { + pkg.Source = Source{ + URL: pd.Repository.HTMLURL(), + Type: "git", + Reference: pd.Version.Version, + } + } + + versions = append(versions, &pkg) } return &PackageMetadataResponse{ diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go index c6c14e5cf4..9daf0ffeff 100644 --- a/routers/api/packages/composer/composer.go +++ b/routers/api/packages/composer/composer.go @@ -53,10 +53,7 @@ func ServiceIndex(ctx *context.Context) { // SearchPackages searches packages, only "q" is supported // https://packagist.org/apidoc#search-packages func SearchPackages(ctx *context.Context) { - page := ctx.FormInt("page") - if page < 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) perPage := ctx.FormInt("per_page") paginator := db.ListOptions{ Page: page, @@ -163,7 +160,7 @@ func PackageMetadata(ctx *context.Context) { // DownloadPackageFile serves the content of a package func DownloadPackageFile(ctx *context.Context) { - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go index 8019eee9f7..fe70e02cd6 100644 --- a/routers/api/packages/conan/conan.go +++ b/routers/api/packages/conan/conan.go @@ -480,7 +480,7 @@ func downloadFile(ctx *context.Context, fileFilter container.Set[string], fileKe return } - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go index 7a46681235..cfe069d6db 100644 --- a/routers/api/packages/conda/conda.go +++ b/routers/api/packages/conda/conda.go @@ -36,6 +36,24 @@ func apiError(ctx *context.Context, status int, obj any) { }) } +func isCondaPackageFileName(filename string) bool { + return strings.HasSuffix(filename, ".tar.bz2") || strings.HasSuffix(filename, ".conda") +} + +func ListOrGetPackages(ctx *context.Context) { + filename := ctx.PathParam("filename") + switch filename { + case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2": + EnumeratePackages(ctx) + return + } + if isCondaPackageFileName(filename) { + DownloadPackageFile(ctx) + return + } + ctx.NotFound(nil) +} + func EnumeratePackages(ctx *context.Context) { type Info struct { Subdir string `json:"subdir"` @@ -174,6 +192,12 @@ func EnumeratePackages(ctx *context.Context) { } func UploadPackageFile(ctx *context.Context) { + filename := ctx.PathParam("filename") + if !isCondaPackageFileName(filename) { + apiError(ctx, http.StatusBadRequest, nil) + return + } + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -191,7 +215,7 @@ func UploadPackageFile(ctx *context.Context) { defer buf.Close() var pck *conda_module.Package - if strings.HasSuffix(strings.ToLower(ctx.PathParam("filename")), ".tar.bz2") { + if strings.HasSuffix(filename, ".tar.bz2") { pck, err = conda_module.ParsePackageBZ2(buf) } else { pck, err = conda_module.ParsePackageConda(buf, buf.Size()) @@ -293,7 +317,7 @@ func DownloadPackageFile(ctx *context.Context) { pf := pfs[0] - s, u, _, err := packages_service.GetPackageFileStream(ctx, pf) + s, u, _, err := packages_service.OpenFileForDownload(ctx, pf) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return diff --git a/routers/api/packages/container/auth.go b/routers/api/packages/container/auth.go index 1d8ae6af7d..1e1b87eb79 100644 --- a/routers/api/packages/container/auth.go +++ b/routers/api/packages/container/auth.go @@ -21,7 +21,7 @@ func (a *Auth) Name() string { } // Verify extracts the user from the Bearer token -// If it's an anonymous session a ghost user is returned +// If it's an anonymous session, a ghost user is returned func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) { packageMeta, err := packages.ParseAuthorizationRequest(req) if err != nil { diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go index 671803788a..4b7bcee9d0 100644 --- a/routers/api/packages/container/blob.go +++ b/routers/api/packages/container/blob.go @@ -20,11 +20,13 @@ import ( container_module "code.gitea.io/gitea/modules/packages/container" "code.gitea.io/gitea/modules/util" packages_service "code.gitea.io/gitea/services/packages" + + "github.com/opencontainers/go-digest" ) // saveAsPackageBlob creates a package blob from an upload // The uploaded blob gets stored in a special upload version to link them to the package/image -func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam +func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam // PackageBlob is never used pb := packages_service.NewPackageBlob(hsr) exists := false @@ -88,20 +90,18 @@ func mountBlob(ctx context.Context, pi *packages_service.PackageInfo, pb *packag }) } -func containerPkgName(piOwnerID int64, piName string) string { - return fmt.Sprintf("pkg_%d_container_%s", piOwnerID, strings.ToLower(piName)) +func containerGlobalLockKey(piOwnerID int64, piName, usage string) string { + return fmt.Sprintf("pkg_%d_container_%s_%s", piOwnerID, strings.ToLower(piName), usage) } func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageInfo) (*packages_model.PackageVersion, error) { - var uploadVersion *packages_model.PackageVersion - - releaser, err := globallock.Lock(ctx, containerPkgName(pi.Owner.ID, pi.Name)) + releaser, err := globallock.Lock(ctx, containerGlobalLockKey(pi.Owner.ID, pi.Name, "package")) if err != nil { return nil, err } defer releaser() - err = db.WithTx(ctx, func(ctx context.Context) error { + return db.WithTx2(ctx, func(ctx context.Context) (*packages_model.PackageVersion, error) { created := true p := &packages_model.Package{ OwnerID: pi.Owner.ID, @@ -113,7 +113,7 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { if !errors.Is(err, packages_model.ErrDuplicatePackage) { log.Error("Error inserting package: %v", err) - return err + return nil, err } created = false } @@ -121,35 +121,30 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI if created { if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, strings.ToLower(pi.Owner.LowerName+"/"+pi.Name)); err != nil { log.Error("Error setting package property: %v", err) - return err + return nil, err } } pv := &packages_model.PackageVersion{ PackageID: p.ID, CreatorID: pi.Owner.ID, - Version: container_model.UploadVersion, - LowerVersion: container_model.UploadVersion, + Version: container_module.UploadVersion, + LowerVersion: container_module.UploadVersion, IsInternal: true, MetadataJSON: "null", } if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil { if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { log.Error("Error inserting package: %v", err) - return err + return nil, err } } - - uploadVersion = pv - - return nil + return pv, nil }) - - return uploadVersion, err } func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, pb *packages_model.PackageBlob) error { - filename := strings.ToLower(fmt.Sprintf("sha256_%s", pb.HashSHA256)) + filename := strings.ToLower("sha256_" + pb.HashSHA256) pf := &packages_model.PackageFile{ VersionID: pv.ID, @@ -175,8 +170,8 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p return nil } -func deleteBlob(ctx context.Context, ownerID int64, image, digest string) error { - releaser, err := globallock.Lock(ctx, containerPkgName(ownerID, image)) +func deleteBlob(ctx context.Context, ownerID int64, image string, digest digest.Digest) error { + releaser, err := globallock.Lock(ctx, containerGlobalLockKey(ownerID, image, "blob")) if err != nil { return err } @@ -186,7 +181,7 @@ func deleteBlob(ctx context.Context, ownerID int64, image, digest string) error pfds, err := container_model.GetContainerBlobs(ctx, &container_model.BlobSearchOptions{ OwnerID: ownerID, Image: image, - Digest: digest, + Digest: string(digest), }) if err != nil { return err diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index bb14db9db7..d532f698ad 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -13,6 +13,7 @@ import ( "regexp" "strconv" "strings" + "sync" auth_model "code.gitea.io/gitea/models/auth" packages_model "code.gitea.io/gitea/models/packages" @@ -21,6 +22,7 @@ import ( "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" @@ -31,17 +33,21 @@ import ( packages_service "code.gitea.io/gitea/services/packages" container_service "code.gitea.io/gitea/services/packages/container" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" ) // maximum size of a container manifest // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests const maxManifestSize = 10 * 1024 * 1024 -var ( - imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`) - referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`) -) +var globalVars = sync.OnceValue(func() (ret struct { + imageNamePattern, referencePattern *regexp.Regexp +}, +) { + ret.imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`) + ret.referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`) + return ret +}) type containerHeaders struct { Status int @@ -50,7 +56,7 @@ type containerHeaders struct { Range string Location string ContentType string - ContentLength int64 + ContentLength optional.Option[int64] } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers @@ -64,8 +70,8 @@ func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) { if h.ContentType != "" { resp.Header().Set("Content-Type", h.ContentType) } - if h.ContentLength != 0 { - resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10)) + if h.ContentLength.Has() { + resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength.Value(), 10)) } if h.UploadUUID != "" { resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID) @@ -83,9 +89,7 @@ func jsonResponse(ctx *context.Context, status int, obj any) { Status: status, ContentType: "application/json", }) - if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil { - log.Error("JSON encode: %v", err) - } + _ = json.NewEncoder(ctx.Resp).Encode(obj) // ignore network errors } func apiError(ctx *context.Context, status int, err error) { @@ -126,14 +130,14 @@ func apiUnauthorizedError(ctx *context.Context) { // ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled) func ReqContainerAccess(ctx *context.Context) { - if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) { + if ctx.Doer == nil || (setting.Service.RequireSignInViewStrict && ctx.Doer.IsGhost()) { apiUnauthorizedError(ctx) } } // VerifyImageName is a middleware which checks if the image name is allowed func VerifyImageName(ctx *context.Context) { - if !imageNamePattern.MatchString(ctx.PathParam("image")) { + if !globalVars().imageNamePattern.MatchString(ctx.PathParam("image")) { apiErrorDefined(ctx, errNameInvalid) } } @@ -152,7 +156,7 @@ func Authenticate(ctx *context.Context) { u := ctx.Doer packageScope := auth_service.GetAccessScope(ctx.Data) if u == nil { - if setting.Service.RequireSignInView { + if setting.Service.RequireSignInViewStrict { apiUnauthorizedError(ctx) return } @@ -215,7 +219,7 @@ func GetRepositoryList(ctx *context.Context) { if len(repositories) == n { v := url.Values{} if n > 0 { - v.Add("n", strconv.Itoa(n)) + v.Add("n", strconv.Itoa(n)) // FIXME: "n" can't be zero here, the logic is inconsistent with GetTagsList } v.Add("last", repositories[len(repositories)-1]) @@ -230,7 +234,7 @@ func GetRepositoryList(ctx *context.Context) { // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks -func InitiateUploadBlob(ctx *context.Context) { +func PostBlobsUploads(ctx *context.Context) { image := ctx.PathParam("image") mount := ctx.FormTrim("mount") @@ -312,14 +316,14 @@ func InitiateUploadBlob(ctx *context.Context) { setResponseHeaders(ctx.Resp, &containerHeaders{ Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID), - Range: "0-0", UploadUUID: upload.ID, Status: http.StatusAccepted, }) } -// https://docs.docker.com/registry/spec/api/#get-blob-upload -func GetUploadBlob(ctx *context.Context) { +// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks +func GetBlobsUpload(ctx *context.Context) { + image := ctx.PathParam("image") uuid := ctx.PathParam("uuid") upload, err := packages_model.GetBlobUploadByID(ctx, uuid) @@ -332,15 +336,21 @@ func GetUploadBlob(ctx *context.Context) { return } - setResponseHeaders(ctx.Resp, &containerHeaders{ - Range: fmt.Sprintf("0-%d", upload.BytesReceived), + // FIXME: undefined behavior when the uploaded content is empty: https://github.com/opencontainers/distribution-spec/issues/578 + respHeaders := &containerHeaders{ + Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID), UploadUUID: upload.ID, Status: http.StatusNoContent, - }) + } + if upload.BytesReceived > 0 { + respHeaders.Range = fmt.Sprintf("0-%d", upload.BytesReceived-1) + } + setResponseHeaders(ctx.Resp, respHeaders) } +// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks -func UploadBlob(ctx *context.Context) { +func PatchBlobsUpload(ctx *context.Context) { image := ctx.PathParam("image") uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid")) @@ -376,16 +386,19 @@ func UploadBlob(ctx *context.Context) { return } - setResponseHeaders(ctx.Resp, &containerHeaders{ + respHeaders := &containerHeaders{ Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, uploader.ID), - Range: fmt.Sprintf("0-%d", uploader.Size()-1), UploadUUID: uploader.ID, Status: http.StatusAccepted, - }) + } + if uploader.Size() > 0 { + respHeaders.Range = fmt.Sprintf("0-%d", uploader.Size()-1) + } + setResponseHeaders(ctx.Resp, respHeaders) } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks -func EndUploadBlob(ctx *context.Context) { +func PutBlobsUpload(ctx *context.Context) { image := ctx.PathParam("image") digest := ctx.FormTrim("digest") @@ -403,12 +416,7 @@ func EndUploadBlob(ctx *context.Context) { } return } - doClose := true - defer func() { - if doClose { - uploader.Close() - } - }() + defer uploader.Close() if ctx.Req.Body != nil { if err := uploader.Append(ctx, ctx.Req.Body); err != nil { @@ -441,11 +449,10 @@ func EndUploadBlob(ctx *context.Context) { return } - if err := uploader.Close(); err != nil { - apiError(ctx, http.StatusInternalServerError, err) - return - } - doClose = false + // There was a strange bug: the "Close" fails with error "close .../tmp/package-upload/....: file already closed" + // AFAIK there should be no other "Close" call to the uploader between NewBlobUploader and this line. + // At least it's safe to call Close twice, so ignore the error. + _ = uploader.Close() if err := container_service.RemoveBlobUploadByID(ctx, uploader.ID); err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -460,7 +467,7 @@ func EndUploadBlob(ctx *context.Context) { } // https://docs.docker.com/registry/spec/api/#delete-blob-upload -func CancelUploadBlob(ctx *context.Context) { +func DeleteBlobsUpload(ctx *context.Context) { uuid := ctx.PathParam("uuid") _, err := packages_model.GetBlobUploadByID(ctx, uuid) @@ -484,16 +491,15 @@ func CancelUploadBlob(ctx *context.Context) { } func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { - d := ctx.PathParam("digest") - - if digest.Digest(d).Validate() != nil { + d := digest.Digest(ctx.PathParam("digest")) + if d.Validate() != nil { return nil, container_model.ErrContainerBlobNotExist } return workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{ OwnerID: ctx.Package.Owner.ID, Image: ctx.PathParam("image"), - Digest: d, + Digest: string(d), }) } @@ -511,7 +517,7 @@ func HeadBlob(ctx *context.Context) { setResponseHeaders(ctx.Resp, &containerHeaders{ ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest), - ContentLength: blob.Blob.Size, + ContentLength: optional.Some(blob.Blob.Size), Status: http.StatusOK, }) } @@ -533,9 +539,8 @@ func GetBlob(ctx *context.Context) { // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs func DeleteBlob(ctx *context.Context) { - d := ctx.PathParam("digest") - - if digest.Digest(d).Validate() != nil { + d := digest.Digest(ctx.PathParam("digest")) + if d.Validate() != nil { apiErrorDefined(ctx, errBlobUnknown) return } @@ -551,7 +556,7 @@ func DeleteBlob(ctx *context.Context) { } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests -func UploadManifest(ctx *context.Context) { +func PutManifest(ctx *context.Context) { reference := ctx.PathParam("reference") mci := &manifestCreationInfo{ @@ -563,7 +568,7 @@ func UploadManifest(ctx *context.Context) { IsTagged: digest.Digest(reference).Validate() != nil, } - if mci.IsTagged && !referencePattern.MatchString(reference) { + if mci.IsTagged && !globalVars().referencePattern.MatchString(reference) { apiErrorDefined(ctx, errManifestInvalid.WithMessage("Tag is invalid")) return } @@ -607,18 +612,18 @@ func UploadManifest(ctx *context.Context) { } func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.BlobSearchOptions, error) { - reference := ctx.PathParam("reference") - opts := &container_model.BlobSearchOptions{ OwnerID: ctx.Package.Owner.ID, Image: ctx.PathParam("image"), IsManifest: true, } - if digest.Digest(reference).Validate() == nil { - opts.Digest = reference - } else if referencePattern.MatchString(reference) { + reference := ctx.PathParam("reference") + if d := digest.Digest(reference); d.Validate() == nil { + opts.Digest = string(d) + } else if globalVars().referencePattern.MatchString(reference) { opts.Tag = reference + opts.OnlyLead = true } else { return nil, container_model.ErrContainerBlobNotExist } @@ -650,7 +655,7 @@ func HeadManifest(ctx *context.Context) { setResponseHeaders(ctx.Resp, &containerHeaders{ ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest), ContentType: manifest.Properties.GetByName(container_module.PropertyMediaType), - ContentLength: manifest.Blob.Size, + ContentLength: optional.Some(manifest.Blob.Size), Status: http.StatusOK, }) } @@ -705,7 +710,7 @@ func DeleteManifest(ctx *context.Context) { func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) { serveDirectReqParams := make(url.Values) serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType)) - s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob, serveDirectReqParams) + s, u, _, err := packages_service.OpenBlobForDownload(ctx, pfd.File, pfd.Blob, serveDirectReqParams) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -714,14 +719,14 @@ func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) headers := &containerHeaders{ ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest), ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType), - ContentLength: pfd.Blob.Size, + ContentLength: optional.Some(pfd.Blob.Size), Status: http.StatusOK, } if u != nil { headers.Status = http.StatusTemporaryRedirect headers.Location = u.String() - + headers.ContentLength = optional.None[int64]() // do not set Content-Length for redirect responses setResponseHeaders(ctx.Resp, headers) return } @@ -735,7 +740,7 @@ func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery -func GetTagList(ctx *context.Context) { +func GetTagsList(ctx *context.Context) { image := ctx.PathParam("image") if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil { @@ -780,7 +785,8 @@ func GetTagList(ctx *context.Context) { }) } -// FIXME: Workaround to be removed in v1.20 +// FIXME: Workaround to be removed in v1.20. +// Update maybe we should never really remote it, as long as there is legacy data? // https://github.com/go-gitea/gitea/issues/19586 func workaroundGetContainerBlob(ctx *context.Context, opts *container_model.BlobSearchOptions) (*packages_model.PackageFileDescriptor, error) { blob, err := container_model.GetContainerBlob(ctx, opts) diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go index ad035cf473..de40215aa7 100644 --- a/routers/api/packages/container/manifest.go +++ b/routers/api/packages/container/manifest.go @@ -10,11 +10,13 @@ import ( "io" "os" "strings" + "time" "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" @@ -22,23 +24,12 @@ import ( "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" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" oci "github.com/opencontainers/image-spec/specs-go/v1" ) -func isValidMediaType(mt string) bool { - return strings.HasPrefix(mt, "application/vnd.docker.") || strings.HasPrefix(mt, "application/vnd.oci.") -} - -func isImageManifestMediaType(mt string) bool { - return strings.EqualFold(mt, oci.MediaTypeImageManifest) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.v2+json") -} - -func isImageIndexMediaType(mt string) bool { - return strings.EqualFold(mt, oci.MediaTypeImageIndex) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.list.v2+json") -} - // manifestCreationInfo describes a manifest to create type manifestCreationInfo struct { MediaType string @@ -55,71 +46,71 @@ func processManifest(ctx context.Context, mci *manifestCreationInfo, buf *packag if err := json.NewDecoder(buf).Decode(&index); err != nil { return "", err } - if index.SchemaVersion != 2 { return "", errUnsupported.WithMessage("Schema version is not supported") } - if _, err := buf.Seek(0, io.SeekStart); err != nil { return "", err } - if !isValidMediaType(mci.MediaType) { + if !container_module.IsMediaTypeValid(mci.MediaType) { mci.MediaType = index.MediaType - if !isValidMediaType(mci.MediaType) { + if !container_module.IsMediaTypeValid(mci.MediaType) { return "", errManifestInvalid.WithMessage("MediaType not recognized") } } - if isImageManifestMediaType(mci.MediaType) { - return processImageManifest(ctx, mci, buf) - } else if isImageIndexMediaType(mci.MediaType) { - return processImageManifestIndex(ctx, mci, buf) + // .../container/manifest.go:453:createManifestBlob() [E] Error inserting package blob: Error 1062 (23000): Duplicate entry '..........' for key 'package_blob.UQE_package_blob_md5' + releaser, err := globallock.Lock(ctx, containerGlobalLockKey(mci.Owner.ID, mci.Image, "manifest")) + if err != nil { + return "", err + } + defer releaser() + + if container_module.IsMediaTypeImageManifest(mci.MediaType) { + return processOciImageManifest(ctx, mci, buf) + } else if container_module.IsMediaTypeImageIndex(mci.MediaType) { + return processOciImageIndex(ctx, mci, buf) } return "", errManifestInvalid } -func processImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) { - manifestDigest := "" - - err := func() error { - var manifest oci.Manifest - if err := json.NewDecoder(buf).Decode(&manifest); err != nil { - return err - } - - if _, err := buf.Seek(0, io.SeekStart); err != nil { - return err - } - - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - configDescriptor, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{ - OwnerID: mci.Owner.ID, - Image: mci.Image, - Digest: string(manifest.Config.Digest), - }) - if err != nil { - return err - } +type processManifestTxRet struct { + pv *packages_model.PackageVersion + pb *packages_model.PackageBlob + created bool + digest string +} - configReader, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(configDescriptor.Blob.HashSHA256)) - if err != nil { - return err +func handleCreateManifestResult(ctx context.Context, err error, mci *manifestCreationInfo, contentStore *packages_module.ContentStore, txRet *processManifestTxRet) (string, error) { + if err != nil && txRet.created && txRet.pb != nil { + if err := contentStore.Delete(packages_module.BlobHash256Key(txRet.pb.HashSHA256)); err != nil { + log.Error("Error deleting package blob from content store: %v", err) } - defer configReader.Close() + return "", err + } + pd, err := packages_model.GetPackageDescriptor(ctx, txRet.pv) + if err != nil { + log.Error("Error getting package descriptor: %v", err) // ignore this error + } else { + notify_service.PackageCreate(ctx, mci.Creator, pd) + } + return txRet.digest, nil +} - metadata, err := container_module.ParseImageConfig(manifest.Config.MediaType, configReader) - if err != nil { - return err - } +func processOciImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (manifestDigest string, errRet error) { + manifest, configDescriptor, metadata, err := container_service.ParseManifestMetadata(ctx, buf, mci.Owner.ID, mci.Image) + if err != nil { + return "", err + } + if _, err = buf.Seek(0, io.SeekStart); err != nil { + return "", err + } + contentStore := packages_module.NewContentStore() + var txRet processManifestTxRet + err = db.WithTx(ctx, func(ctx context.Context) (err error) { blobReferences := make([]*blobReference, 0, 1+len(manifest.Layers)) - blobReferences = append(blobReferences, &blobReference{ Digest: manifest.Config.Digest, MediaType: manifest.Config.MediaType, @@ -150,78 +141,43 @@ func processImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *p return err } - uploadVersion, err := packages_model.GetInternalVersionByNameAndVersion(ctx, mci.Owner.ID, packages_model.TypeContainer, mci.Image, container_model.UploadVersion) - if err != nil && err != packages_model.ErrPackageNotExist { + uploadVersion, err := packages_model.GetInternalVersionByNameAndVersion(ctx, mci.Owner.ID, packages_model.TypeContainer, mci.Image, container_module.UploadVersion) + if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) { return err } for _, ref := range blobReferences { - if err := createFileFromBlobReference(ctx, pv, uploadVersion, ref); err != nil { + if _, err = createFileFromBlobReference(ctx, pv, uploadVersion, ref); err != nil { return err } } + txRet.pv = pv + txRet.pb, txRet.created, txRet.digest, err = createManifestBlob(ctx, contentStore, mci, pv, buf) + return err + }) - pb, created, digest, err := createManifestBlob(ctx, mci, pv, buf) - removeBlob := false - defer func() { - if removeBlob { - contentStore := packages_module.NewContentStore() - if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil { - log.Error("Error deleting package blob from content store: %v", err) - } - } - }() - if err != nil { - removeBlob = created - return err - } - - if err := committer.Commit(); err != nil { - removeBlob = created - return err - } - - if err := notifyPackageCreate(ctx, mci.Creator, pv); err != nil { - return err - } - - manifestDigest = digest + return handleCreateManifestResult(ctx, err, mci, contentStore, &txRet) +} - return nil - }() - if err != nil { +func processOciImageIndex(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (manifestDigest string, errRet error) { + var index oci.Index + if err := json.NewDecoder(buf).Decode(&index); err != nil { + return "", err + } + if _, err := buf.Seek(0, io.SeekStart); err != nil { return "", err } - return manifestDigest, nil -} - -func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) { - manifestDigest := "" - - err := func() error { - var index oci.Index - if err := json.NewDecoder(buf).Decode(&index); err != nil { - return err - } - - if _, err := buf.Seek(0, io.SeekStart); err != nil { - return err - } - - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - + contentStore := packages_module.NewContentStore() + var txRet processManifestTxRet + err := db.WithTx(ctx, func(ctx context.Context) (err error) { metadata := &container_module.Metadata{ Type: container_module.TypeOCI, Manifests: make([]*container_module.Manifest, 0, len(index.Manifests)), } for _, manifest := range index.Manifests { - if !isImageManifestMediaType(manifest.MediaType) { + if !container_module.IsMediaTypeImageManifest(manifest.MediaType) { return errManifestInvalid } @@ -265,50 +221,12 @@ func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, b return err } - pb, created, digest, err := createManifestBlob(ctx, mci, pv, buf) - removeBlob := false - defer func() { - if removeBlob { - contentStore := packages_module.NewContentStore() - if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil { - log.Error("Error deleting package blob from content store: %v", err) - } - } - }() - if err != nil { - removeBlob = created - return err - } - - if err := committer.Commit(); err != nil { - removeBlob = created - return err - } - - if err := notifyPackageCreate(ctx, mci.Creator, pv); err != nil { - return err - } - - manifestDigest = digest - - return nil - }() - if err != nil { - return "", err - } - - return manifestDigest, nil -} - -func notifyPackageCreate(ctx context.Context, doer *user_model.User, pv *packages_model.PackageVersion) error { - pd, err := packages_model.GetPackageDescriptor(ctx, pv) - if err != nil { + txRet.pv = pv + txRet.pb, txRet.created, txRet.digest, err = createManifestBlob(ctx, contentStore, mci, pv, buf) return err - } - - notify_service.PackageCreate(ctx, doer, pd) + }) - return nil + return handleCreateManifestResult(ctx, err, mci, contentStore, &txRet) } func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, metadata *container_module.Metadata) (*packages_model.PackageVersion, error) { @@ -349,24 +267,31 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met LowerVersion: strings.ToLower(mci.Reference), MetadataJSON: string(metadataJSON), } - var pv *packages_model.PackageVersion - if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { + pv, err := packages_model.GetOrInsertVersion(ctx, _pv) + if err != nil { if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { log.Error("Error inserting package: %v", err) return nil, err } - if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { - return nil, err - } - - // keep download count on overwrite - _pv.DownloadCount = pv.DownloadCount - - if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { - if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { - log.Error("Error inserting package: %v", err) - return nil, err + if container_module.IsMediaTypeImageIndex(mci.MediaType) { + if pv.CreatedUnix.AsTime().Before(time.Now().Add(-24 * time.Hour)) { + if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { + return nil, err + } + // keep download count on overwriting + _pv.DownloadCount = pv.DownloadCount + if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { + if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { + log.Error("Error inserting package: %v", err) + return nil, err + } + } + } else { + err = packages_model.UpdateVersion(ctx, &packages_model.PackageVersion{ID: pv.ID, MetadataJSON: _pv.MetadataJSON}) + if err != nil { + return nil, err + } } } } @@ -376,14 +301,20 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met } if mci.IsTagged { - if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil { - log.Error("Error setting package version property: %v", err) + if err = packages_model.InsertOrUpdateProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil { + return nil, err + } + } else { + if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged); err != nil { return nil, err } } + + if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference); err != nil { + return nil, err + } for _, manifest := range metadata.Manifests { - if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil { - log.Error("Error setting package version property: %v", err) + if _, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil { return nil, err } } @@ -400,30 +331,31 @@ type blobReference struct { IsLead bool } -func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *packages_model.PackageVersion, ref *blobReference) error { +func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *packages_model.PackageVersion, ref *blobReference) (*packages_model.PackageFile, error) { if ref.File.Blob.Size != ref.ExpectedSize { - return errSizeInvalid + return nil, errSizeInvalid } if ref.Name == "" { - ref.Name = strings.ToLower(fmt.Sprintf("sha256_%s", ref.File.Blob.HashSHA256)) + ref.Name = strings.ToLower("sha256_" + ref.File.Blob.HashSHA256) } pf := &packages_model.PackageFile{ - VersionID: pv.ID, - BlobID: ref.File.Blob.ID, - Name: ref.Name, - LowerName: ref.Name, - IsLead: ref.IsLead, + VersionID: pv.ID, + BlobID: ref.File.Blob.ID, + Name: ref.Name, + LowerName: ref.Name, + CompositeKey: string(ref.Digest), + IsLead: ref.IsLead, } var err error if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil { if errors.Is(err, packages_model.ErrDuplicatePackageFile) { // Skip this blob because the manifest contains the same filesystem layer multiple times. - return nil + return pf, nil } log.Error("Error inserting package file: %v", err) - return err + return nil, err } props := map[string]string{ @@ -433,21 +365,21 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package for name, value := range props { if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, name, value); err != nil { log.Error("Error setting package file property: %v", err) - return err + return nil, err } } - // Remove the file from the blob upload version + // Remove the ref file (old file) from the blob upload version if uploadVersion != nil && ref.File.File != nil && uploadVersion.ID == ref.File.File.VersionID { if err := packages_service.DeletePackageFile(ctx, ref.File.File); err != nil { - return err + return nil, err } } - return nil + return pf, nil } -func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *packages_model.PackageVersion, buf *packages_module.HashedBuffer) (*packages_model.PackageBlob, bool, string, error) { +func createManifestBlob(ctx context.Context, contentStore *packages_module.ContentStore, mci *manifestCreationInfo, pv *packages_model.PackageVersion, buf *packages_module.HashedBuffer) (_ *packages_model.PackageBlob, created bool, manifestDigest string, _ error) { pb, exists, err := packages_model.GetOrInsertBlob(ctx, packages_service.NewPackageBlob(buf)) if err != nil { log.Error("Error inserting package blob: %v", err) @@ -456,29 +388,48 @@ func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *pack // FIXME: Workaround to be removed in v1.20 // https://github.com/go-gitea/gitea/issues/19586 if exists { - err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(pb.HashSHA256)) + err = contentStore.Has(packages_module.BlobHash256Key(pb.HashSHA256)) if err != nil && (errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist)) { log.Debug("Package registry inconsistent: blob %s does not exist on file system", pb.HashSHA256) exists = false } } if !exists { - contentStore := packages_module.NewContentStore() if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), buf, buf.Size()); err != nil { log.Error("Error saving package blob in content store: %v", err) return nil, false, "", err } } - manifestDigest := digestFromHashSummer(buf) - err = createFileFromBlobReference(ctx, pv, nil, &blobReference{ + manifestDigest = digestFromHashSummer(buf) + pf, err := createFileFromBlobReference(ctx, pv, nil, &blobReference{ Digest: digest.Digest(manifestDigest), MediaType: mci.MediaType, - Name: container_model.ManifestFilename, + Name: container_module.ManifestFilename, File: &packages_model.PackageFileDescriptor{Blob: pb}, ExpectedSize: pb.Size, IsLead: true, }) + if err != nil { + return nil, false, "", err + } + oldManifestFiles, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ + OwnerID: mci.Owner.ID, + PackageType: packages_model.TypeContainer, + VersionID: pv.ID, + Query: container_module.ManifestFilename, + }) + if err != nil { + return nil, false, "", err + } + for _, oldManifestFile := range oldManifestFiles { + if oldManifestFile.ID != pf.ID && oldManifestFile.IsLead { + err = packages_model.UpdateFile(ctx, &packages_model.PackageFile{ID: oldManifestFile.ID, IsLead: false}, []string{"is_lead"}) + if err != nil { + return nil, false, "", err + } + } + } return pb, !exists, manifestDigest, err } diff --git a/routers/api/packages/cran/cran.go b/routers/api/packages/cran/cran.go index 8a20072cb6..732acd215f 100644 --- a/routers/api/packages/cran/cran.go +++ b/routers/api/packages/cran/cran.go @@ -250,7 +250,7 @@ func downloadPackageFile(ctx *context.Context, opts *cran_model.SearchOptions) { return } - s, u, _, err := packages_service.GetPackageFileStream(ctx, pf) + s, u, _, err := packages_service.OpenFileForDownload(ctx, pf) if err != nil { if errors.Is(err, util.ErrNotExist) { apiError(ctx, http.StatusNotFound, err) diff --git a/routers/api/packages/debian/debian.go b/routers/api/packages/debian/debian.go index fec34c91a6..346f71fa5d 100644 --- a/routers/api/packages/debian/debian.go +++ b/routers/api/packages/debian/debian.go @@ -59,7 +59,7 @@ func GetRepositoryFile(ctx *context.Context) { key += "|" + component + "|" + architecture } - s, u, pf, err := packages_service.GetFileStreamByPackageVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion( ctx, pv, &packages_service.PackageFileInfo{ @@ -106,7 +106,7 @@ func GetRepositoryFileByHash(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) + s, u, pf, err := packages_service.OpenFileForDownload(ctx, pfs[0]) if err != nil { if errors.Is(err, util.ErrNotExist) { apiError(ctx, http.StatusNotFound, err) @@ -210,7 +210,7 @@ func DownloadPackageFile(ctx *context.Context) { name := ctx.PathParam("name") version := ctx.PathParam("version") - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go index 0b5daa7334..db7aeace50 100644 --- a/routers/api/packages/generic/generic.go +++ b/routers/api/packages/generic/generic.go @@ -31,7 +31,7 @@ func apiError(ctx *context.Context, status int, obj any) { // DownloadPackageFile serves the specific generic package. func DownloadPackageFile(ctx *context.Context) { - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/packages/goproxy/goproxy.go b/routers/api/packages/goproxy/goproxy.go index bde29df739..89ec86bce9 100644 --- a/routers/api/packages/goproxy/goproxy.go +++ b/routers/api/packages/goproxy/goproxy.go @@ -106,7 +106,7 @@ func DownloadPackageFile(ctx *context.Context) { return } - s, u, _, err := packages_service.GetPackageFileStream(ctx, pfs[0]) + s, u, _, err := packages_service.OpenFileForDownload(ctx, pfs[0]) if err != nil { if errors.Is(err, util.ErrNotExist) { apiError(ctx, http.StatusNotFound, err) diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go index fb12daaa46..39c34f4da4 100644 --- a/routers/api/packages/helm/helm.go +++ b/routers/api/packages/helm/helm.go @@ -122,7 +122,7 @@ func DownloadPackageFile(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetFileStreamByPackageVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion( ctx, pvs[0], &packages_service.PackageFileInfo{ diff --git a/routers/api/packages/maven/api.go b/routers/api/packages/maven/api.go index 167fe42b56..ec6b9cfb0e 100644 --- a/routers/api/packages/maven/api.go +++ b/routers/api/packages/maven/api.go @@ -8,7 +8,6 @@ import ( "strings" packages_model "code.gitea.io/gitea/models/packages" - maven_module "code.gitea.io/gitea/modules/packages/maven" ) // MetadataResponse https://maven.apache.org/ref/3.2.5/maven-repository-metadata/repository-metadata.html @@ -22,7 +21,7 @@ type MetadataResponse struct { } // pds is expected to be sorted ascending by CreatedUnix -func createMetadataResponse(pds []*packages_model.PackageDescriptor) *MetadataResponse { +func createMetadataResponse(pds []*packages_model.PackageDescriptor, groupID, artifactID string) *MetadataResponse { var release *packages_model.PackageDescriptor versions := make([]string, 0, len(pds)) @@ -35,11 +34,9 @@ func createMetadataResponse(pds []*packages_model.PackageDescriptor) *MetadataRe latest := pds[len(pds)-1] - metadata := latest.Metadata.(*maven_module.Metadata) - resp := &MetadataResponse{ - GroupID: metadata.GroupID, - ArtifactID: metadata.ArtifactID, + GroupID: groupID, + ArtifactID: artifactID, Latest: latest.Version.Version, Version: versions, } diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go index 4d04d4d1e9..40a8ff8242 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -84,16 +84,20 @@ func handlePackageFile(ctx *context.Context, serveContent bool) { } func serveMavenMetadata(ctx *context.Context, params parameters) { - // /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512] - - pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName()) - if errors.Is(err, util.ErrNotExist) { - pvs, err = packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy()) + // path pattern: /com/foo/project/maven-metadata.xml[.md5/.sha1/.sha256/.sha512] + // in case there are legacy package names ("GroupID-ArtifactID") we need to check both, new packages always use ":" as separator("GroupID:ArtifactID") + pvsLegacy, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy()) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return } + pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName()) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return } + pvs = append(pvsLegacy, pvs...) + if len(pvs) == 0 { apiError(ctx, http.StatusNotFound, packages_model.ErrPackageNotExist) return @@ -110,7 +114,7 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix }) - xmlMetadata, err := xml.Marshal(createMetadataResponse(pds)) + xmlMetadata, err := xml.Marshal(createMetadataResponse(pds, params.GroupID, params.ArtifactID)) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -219,7 +223,7 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool return } - s, u, _, err := packages_service.GetPackageBlobStream(ctx, pf, pb, nil) + s, u, _, err := packages_service.OpenBlobForDownload(ctx, pf, pb, nil) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go index 6ec46bcb36..1f09816d32 100644 --- a/routers/api/packages/npm/npm.go +++ b/routers/api/packages/npm/npm.go @@ -85,7 +85,7 @@ func DownloadPackageFile(ctx *context.Context) { packageVersion := ctx.PathParam("version") filename := ctx.PathParam("filename") - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, @@ -132,7 +132,7 @@ func DownloadPackageFileByName(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetFileStreamByPackageVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion( ctx, pvs[0], &packages_service.PackageFileInfo{ diff --git a/routers/api/packages/nuget/api_v2.go b/routers/api/packages/nuget/api_v2.go index a726065ad0..801c60af13 100644 --- a/routers/api/packages/nuget/api_v2.go +++ b/routers/api/packages/nuget/api_v2.go @@ -246,21 +246,30 @@ type TypedValue[T any] struct { } type FeedEntryProperties struct { - Version string `xml:"d:Version"` - NormalizedVersion string `xml:"d:NormalizedVersion"` Authors string `xml:"d:Authors"` + Copyright string `xml:"d:Copyright,omitempty"` + Created TypedValue[time.Time] `xml:"d:Created"` Dependencies string `xml:"d:Dependencies"` Description string `xml:"d:Description"` - VersionDownloadCount TypedValue[int64] `xml:"d:VersionDownloadCount"` + DevelopmentDependency TypedValue[bool] `xml:"d:DevelopmentDependency"` DownloadCount TypedValue[int64] `xml:"d:DownloadCount"` - PackageSize TypedValue[int64] `xml:"d:PackageSize"` - Created TypedValue[time.Time] `xml:"d:Created"` + ID string `xml:"d:Id"` + IconURL string `xml:"d:IconUrl,omitempty"` + Language string `xml:"d:Language,omitempty"` LastUpdated TypedValue[time.Time] `xml:"d:LastUpdated"` - Published TypedValue[time.Time] `xml:"d:Published"` + LicenseURL string `xml:"d:LicenseUrl,omitempty"` + MinClientVersion string `xml:"d:MinClientVersion,omitempty"` + NormalizedVersion string `xml:"d:NormalizedVersion"` + Owners string `xml:"d:Owners,omitempty"` + PackageSize TypedValue[int64] `xml:"d:PackageSize"` ProjectURL string `xml:"d:ProjectUrl,omitempty"` + Published TypedValue[time.Time] `xml:"d:Published"` ReleaseNotes string `xml:"d:ReleaseNotes,omitempty"` RequireLicenseAcceptance TypedValue[bool] `xml:"d:RequireLicenseAcceptance"` - Title string `xml:"d:Title"` + Tags string `xml:"d:Tags,omitempty"` + Title string `xml:"d:Title,omitempty"` + Version string `xml:"d:Version"` + VersionDownloadCount TypedValue[int64] `xml:"d:VersionDownloadCount"` } type FeedEntry struct { @@ -353,21 +362,30 @@ func createEntry(l *linkBuilder, pd *packages_model.PackageDescriptor, withNames Author: metadata.Authors, Content: content, Properties: &FeedEntryProperties{ - Version: pd.Version.Version, - NormalizedVersion: pd.Version.Version, Authors: metadata.Authors, + Copyright: metadata.Copyright, + Created: createdValue, Dependencies: buildDependencyString(metadata), Description: metadata.Description, - VersionDownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount}, + DevelopmentDependency: TypedValue[bool]{Type: "Edm.Boolean", Value: metadata.DevelopmentDependency}, DownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount}, - PackageSize: TypedValue[int64]{Type: "Edm.Int64", Value: pd.CalculateBlobSize()}, - Created: createdValue, + ID: pd.Package.Name, + IconURL: metadata.IconURL, + Language: metadata.Language, LastUpdated: createdValue, - Published: createdValue, + LicenseURL: metadata.LicenseURL, + MinClientVersion: metadata.MinClientVersion, + NormalizedVersion: pd.Version.Version, + Owners: metadata.Owners, + PackageSize: TypedValue[int64]{Type: "Edm.Int64", Value: pd.CalculateBlobSize()}, ProjectURL: metadata.ProjectURL, + Published: createdValue, ReleaseNotes: metadata.ReleaseNotes, RequireLicenseAcceptance: TypedValue[bool]{Type: "Edm.Boolean", Value: metadata.RequireLicenseAcceptance}, - Title: pd.Package.Name, + Tags: metadata.Tags, + Title: metadata.Title, + Version: pd.Version.Version, + VersionDownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount}, }, } diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go index 07a8de0a68..92d62d90b1 100644 --- a/routers/api/packages/nuget/nuget.go +++ b/routers/api/packages/nuget/nuget.go @@ -36,7 +36,7 @@ func apiError(ctx *context.Context, status int, obj any) { }) } -func xmlResponse(ctx *context.Context, status int, obj any) { //nolint:unparam +func xmlResponse(ctx *context.Context, status int, obj any) { //nolint:unparam // status is always StatusOK ctx.Resp.Header().Set("Content-Type", "application/atom+xml; charset=utf-8") ctx.Resp.WriteHeader(status) if _, err := ctx.Resp.Write([]byte(xml.Header)); err != nil { @@ -405,7 +405,7 @@ func DownloadPackageFile(ctx *context.Context) { packageVersion := ctx.PathParam("version") filename := ctx.PathParam("filename") - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, @@ -488,7 +488,7 @@ func UploadPackage(ctx *context.Context) { pv, &packages_service.PackageFileCreationInfo{ PackageFileInfo: packages_service.PackageFileInfo{ - Filename: strings.ToLower(fmt.Sprintf("%s.nuspec", np.ID)), + Filename: strings.ToLower(np.ID + ".nuspec"), }, Data: nuspecBuf, }, @@ -669,7 +669,7 @@ func DownloadSymbolFile(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) + s, u, pf, err := packages_service.OpenFileForDownload(ctx, pfs[0]) if err != nil { if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) diff --git a/routers/api/packages/pub/pub.go b/routers/api/packages/pub/pub.go index e7b07aefd0..4bd36e94b6 100644 --- a/routers/api/packages/pub/pub.go +++ b/routers/api/packages/pub/pub.go @@ -274,7 +274,7 @@ func DownloadPackageFile(ctx *context.Context) { pf := pd.Files[0].File - s, u, _, err := packages_service.GetPackageFileStream(ctx, pf) + s, u, _, err := packages_service.OpenFileForDownload(ctx, pf) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go index 199f4e7478..9b5ae6c89d 100644 --- a/routers/api/packages/pypi/pypi.go +++ b/routers/api/packages/pypi/pypi.go @@ -82,7 +82,7 @@ func DownloadPackageFile(ctx *context.Context) { packageVersion := ctx.PathParam("version") filename := ctx.PathParam("filename") - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go index a00a61c079..938c35341d 100644 --- a/routers/api/packages/rpm/rpm.go +++ b/routers/api/packages/rpm/rpm.go @@ -96,7 +96,7 @@ func GetRepositoryFile(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetFileStreamByPackageVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion( ctx, pv, &packages_service.PackageFileInfo{ @@ -220,7 +220,7 @@ func DownloadPackageFile(ctx *context.Context) { name := ctx.PathParam("name") version := ctx.PathParam("version") - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index de8c7ef3ed..774d5520fd 100644 --- a/routers/api/packages/rubygems/rubygems.go +++ b/routers/api/packages/rubygems/rubygems.go @@ -14,6 +14,7 @@ import ( "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" @@ -177,7 +178,7 @@ func DownloadPackageFile(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetFileStreamByPackageVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion( ctx, pvs[0], &packages_service.PackageFileInfo{ @@ -309,7 +310,7 @@ func GetPackageInfo(ctx *context.Context) { apiError(ctx, http.StatusNotFound, nil) return } - infoContent, err := makePackageInfo(ctx, versions) + infoContent, err := makePackageInfo(ctx, versions, cache.NewEphemeralCache()) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -317,7 +318,7 @@ func GetPackageInfo(ctx *context.Context) { ctx.PlainText(http.StatusOK, infoContent) } -// GetAllPackagesVersions returns a custom text based format containing information about all versions of all rubygems. +// GetAllPackagesVersions returns a custom text-based format containing information about all versions of all rubygems. // ref: https://guides.rubygems.org/rubygems-org-compact-index-api/ func GetAllPackagesVersions(ctx *context.Context) { packages, err := packages_model.GetPackagesByType(ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems) @@ -326,6 +327,7 @@ func GetAllPackagesVersions(ctx *context.Context) { return } + ephemeralCache := cache.NewEphemeralCache() out := &strings.Builder{} out.WriteString("---\n") for _, pkg := range packages { @@ -338,7 +340,7 @@ func GetAllPackagesVersions(ctx *context.Context) { continue } - info, err := makePackageInfo(ctx, versions) + info, err := makePackageInfo(ctx, versions, ephemeralCache) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -348,7 +350,14 @@ func GetAllPackagesVersions(ctx *context.Context) { _, _ = fmt.Fprintf(out, "%s ", pkg.Name) for i, v := range versions { sep := util.Iif(i == len(versions)-1, "", ",") - _, _ = fmt.Fprintf(out, "%s%s", v.Version, sep) + pd, err := packages_model.GetPackageDescriptorWithCache(ctx, v, ephemeralCache) + if errors.Is(err, util.ErrNotExist) { + continue + } else if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + writePackageVersionForList(pd.Metadata, v.Version, sep, out) } _, _ = fmt.Fprintf(out, " %x\n", md5.Sum([]byte(info))) } @@ -356,6 +365,16 @@ func GetAllPackagesVersions(ctx *context.Context) { ctx.PlainText(http.StatusOK, out.String()) } +func writePackageVersionForList(metadata any, version, sep string, out *strings.Builder) { + if metadata, _ := metadata.(*rubygems_module.Metadata); metadata != nil && metadata.Platform != "" && metadata.Platform != "ruby" { + // VERSION_PLATFORM (see comment above in GetAllPackagesVersions) + _, _ = fmt.Fprintf(out, "%s_%s%s", version, metadata.Platform, sep) + } else { + // VERSION only + _, _ = fmt.Fprintf(out, "%s%s", version, sep) + } +} + func writePackageVersionRequirements(prefix string, reqs []rubygems_module.VersionRequirement, out *strings.Builder) { out.WriteString(prefix) if len(reqs) == 0 { @@ -367,11 +386,21 @@ func writePackageVersionRequirements(prefix string, reqs []rubygems_module.Versi } } -func makePackageVersionDependency(ctx *context.Context, version *packages_model.PackageVersion) (string, error) { +func writePackageVersionForDependency(version, platform string, out *strings.Builder) { + if platform != "" && platform != "ruby" { + // VERSION-PLATFORM (see comment below in makePackageVersionDependency) + _, _ = fmt.Fprintf(out, "%s-%s ", version, platform) + } else { + // VERSION only + _, _ = fmt.Fprintf(out, "%s ", version) + } +} + +func makePackageVersionDependency(ctx *context.Context, version *packages_model.PackageVersion, c *cache.EphemeralCache) (string, error) { // format: VERSION[-PLATFORM] [DEPENDENCY[,DEPENDENCY,...]]|REQUIREMENT[,REQUIREMENT,...] // DEPENDENCY: GEM:CONSTRAINT[&CONSTRAINT] // REQUIREMENT: KEY:VALUE (always contains "checksum") - pd, err := packages_model.GetPackageDescriptor(ctx, version) + pd, err := packages_model.GetPackageDescriptorWithCache(ctx, version, c) if err != nil { return "", err } @@ -388,8 +417,7 @@ func makePackageVersionDependency(ctx *context.Context, version *packages_model. } buf := &strings.Builder{} - buf.WriteString(version.Version) - buf.WriteByte(' ') + writePackageVersionForDependency(version.Version, metadata.Platform, buf) for i, dep := range metadata.RuntimeDependencies { sep := util.Iif(i == 0, "", ",") writePackageVersionRequirements(fmt.Sprintf("%s%s:", sep, dep.Name), dep.Version, buf) @@ -404,10 +432,10 @@ func makePackageVersionDependency(ctx *context.Context, version *packages_model. return buf.String(), nil } -func makePackageInfo(ctx *context.Context, versions []*packages_model.PackageVersion) (string, error) { +func makePackageInfo(ctx *context.Context, versions []*packages_model.PackageVersion, c *cache.EphemeralCache) (string, error) { ret := "---\n" for _, v := range versions { - dep, err := makePackageVersionDependency(ctx, v) + dep, err := makePackageVersionDependency(ctx, v, c) if err != nil { return "", err } diff --git a/routers/api/packages/rubygems/rubygems_test.go b/routers/api/packages/rubygems/rubygems_test.go new file mode 100644 index 0000000000..a07e12a7d3 --- /dev/null +++ b/routers/api/packages/rubygems/rubygems_test.go @@ -0,0 +1,41 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package rubygems + +import ( + "strings" + "testing" + + rubygems_module "code.gitea.io/gitea/modules/packages/rubygems" + + "github.com/stretchr/testify/assert" +) + +func TestWritePackageVersion(t *testing.T) { + buf := &strings.Builder{} + + writePackageVersionForList(nil, "1.0", " ", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForList(&rubygems_module.Metadata{Platform: "ruby"}, "1.0", " ", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForList(&rubygems_module.Metadata{Platform: "linux"}, "1.0", " ", buf) + assert.Equal(t, "1.0_linux ", buf.String()) + buf.Reset() + + writePackageVersionForDependency("1.0", "", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForDependency("1.0", "ruby", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForDependency("1.0", "os", buf) + assert.Equal(t, "1.0-os ", buf.String()) + buf.Reset() +} diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go index 4d7fb8b1a6..bf542f33a7 100644 --- a/routers/api/packages/swift/swift.go +++ b/routers/api/packages/swift/swift.go @@ -290,7 +290,24 @@ func DownloadManifest(ctx *context.Context) { }) } -// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6 +// formFileOptionalReadCloser returns (nil, nil) if the formKey is not present. +func formFileOptionalReadCloser(ctx *context.Context, formKey string) (io.ReadCloser, error) { + multipartFile, _, err := ctx.Req.FormFile(formKey) + if err != nil && !errors.Is(err, http.ErrMissingFile) { + return nil, err + } + if multipartFile != nil { + return multipartFile, nil + } + + content := ctx.Req.FormValue(formKey) + if content == "" { + return nil, nil + } + return io.NopCloser(strings.NewReader(content)), nil +} + +// UploadPackageFile refers to https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6 func UploadPackageFile(ctx *context.Context) { packageScope := ctx.PathParam("scope") packageName := ctx.PathParam("name") @@ -304,9 +321,9 @@ func UploadPackageFile(ctx *context.Context) { packageVersion := v.Core().String() - file, _, err := ctx.Req.FormFile("source-archive") - if err != nil { - apiError(ctx, http.StatusBadRequest, err) + file, err := formFileOptionalReadCloser(ctx, "source-archive") + if file == nil || err != nil { + apiError(ctx, http.StatusBadRequest, "unable to read source-archive file") return } defer file.Close() @@ -318,10 +335,13 @@ func UploadPackageFile(ctx *context.Context) { } defer buf.Close() - var mr io.Reader - metadata := ctx.Req.FormValue("metadata") - if metadata != "" { - mr = strings.NewReader(metadata) + mr, err := formFileOptionalReadCloser(ctx, "metadata") + if err != nil { + apiError(ctx, http.StatusBadRequest, "unable to read metadata file") + return + } + if mr != nil { + defer mr.Close() } pck, err := swift_module.ParsePackage(buf, buf.Size(), mr) @@ -409,7 +429,7 @@ func DownloadPackageFile(ctx *context.Context) { pf := pd.Files[0].File - s, u, _, err := packages_service.GetPackageFileStream(ctx, pf) + s, u, _, err := packages_service.OpenFileForDownload(ctx, pf) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return diff --git a/routers/api/packages/vagrant/vagrant.go b/routers/api/packages/vagrant/vagrant.go index 3afaa5de1f..9eb67e5397 100644 --- a/routers/api/packages/vagrant/vagrant.go +++ b/routers/api/packages/vagrant/vagrant.go @@ -218,7 +218,7 @@ func UploadPackageFile(ctx *context.Context) { } func DownloadPackageFile(ctx *context.Context) { - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/v1/activitypub/person.go b/routers/api/v1/activitypub/person.go index 995a148f0b..0b0db011c6 100644 --- a/routers/api/v1/activitypub/person.go +++ b/routers/api/v1/activitypub/person.go @@ -41,14 +41,14 @@ func Person(ctx *context.APIContext) { person.Name = ap.NaturalLanguageValuesNew() err := person.Name.Set("en", ap.Content(ctx.ContextUser.FullName)) if err != nil { - ctx.ServerError("Set Name", err) + ctx.APIErrorInternal(err) return } person.PreferredUsername = ap.NaturalLanguageValuesNew() err = person.PreferredUsername.Set("en", ap.Content(ctx.ContextUser.Name)) if err != nil { - ctx.ServerError("Set PreferredUsername", err) + ctx.APIErrorInternal(err) return } @@ -68,14 +68,14 @@ func Person(ctx *context.APIContext) { publicKeyPem, err := activitypub.GetPublicKey(ctx, ctx.ContextUser) if err != nil { - ctx.ServerError("GetPublicKey", err) + ctx.APIErrorInternal(err) return } person.PublicKey.PublicKeyPem = publicKeyPem binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(person) if err != nil { - ctx.ServerError("MarshalJSON", err) + ctx.APIErrorInternal(err) return } ctx.Resp.Header().Add("Content-Type", activitypub.ActivityStreamsContentType) diff --git a/routers/api/v1/activitypub/reqsignature.go b/routers/api/v1/activitypub/reqsignature.go index 853c3c0b59..4eff51782f 100644 --- a/routers/api/v1/activitypub/reqsignature.go +++ b/routers/api/v1/activitypub/reqsignature.go @@ -7,6 +7,7 @@ import ( "crypto" "crypto/x509" "encoding/pem" + "errors" "fmt" "io" "net/http" @@ -34,7 +35,7 @@ func getPublicKeyFromResponse(b []byte, keyID *url.URL) (p crypto.PublicKey, err pubKeyPem := pubKey.PublicKeyPem block, _ := pem.Decode([]byte(pubKeyPem)) if block == nil || block.Type != "PUBLIC KEY" { - return nil, fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type") + return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type") } p, err = x509.ParsePKIXPublicKey(block.Bytes) return p, err @@ -89,9 +90,9 @@ func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, er func ReqHTTPSignature() func(ctx *gitea_context.APIContext) { return func(ctx *gitea_context.APIContext) { if authenticated, err := verifyHTTPSignatures(ctx); err != nil { - ctx.ServerError("verifyHttpSignatures", err) + ctx.APIErrorInternal(err) } else if !authenticated { - ctx.Error(http.StatusForbidden, "reqSignature", "request signature verification failed") + ctx.APIError(http.StatusForbidden, "request signature verification failed") } } } diff --git a/routers/api/v1/admin/action.go b/routers/api/v1/admin/action.go new file mode 100644 index 0000000000..2fbb8e1a95 --- /dev/null +++ b/routers/api/v1/admin/action.go @@ -0,0 +1,93 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package admin + +import ( + "code.gitea.io/gitea/routers/api/v1/shared" + "code.gitea.io/gitea/services/context" +) + +// ListWorkflowJobs Lists all jobs +func ListWorkflowJobs(ctx *context.APIContext) { + // swagger:operation GET /admin/actions/jobs admin listAdminWorkflowJobs + // --- + // summary: Lists all jobs + // produces: + // - application/json + // parameters: + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WorkflowJobsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + shared.ListJobs(ctx, 0, 0, 0) +} + +// ListWorkflowRuns Lists all runs +func ListWorkflowRuns(ctx *context.APIContext) { + // swagger:operation GET /admin/actions/runs admin listAdminWorkflowRuns + // --- + // summary: Lists all runs + // produces: + // - application/json + // parameters: + // - name: event + // in: query + // description: workflow event name + // type: string + // required: false + // - name: branch + // in: query + // description: workflow branch + // type: string + // required: false + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: actor + // in: query + // description: triggered by user + // type: string + // required: false + // - name: head_sha + // in: query + // description: triggering sha of the workflow run + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WorkflowRunsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + shared.ListRuns(ctx, 0, 0) +} diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go index 55ea8c6758..c2efed7490 100644 --- a/routers/api/v1/admin/adopt.go +++ b/routers/api/v1/admin/adopt.go @@ -46,7 +46,7 @@ func ListUnadoptedRepositories(ctx *context.APIContext) { } repoNames, count, err := repo_service.ListUnadoptedRepositories(ctx, ctx.FormString("query"), &listOptions) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -86,33 +86,33 @@ func AdoptRepository(ctx *context.APIContext) { ctxUser, err := user_model.GetUserByName(ctx, ownerName) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() return } - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } // check not a repo has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } isDir, err := util.IsDir(repo_model.RepoPath(ctxUser.Name, repoName)) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } if has || !isDir { - ctx.NotFound() + ctx.APIErrorNotFound() return } if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{ Name: repoName, IsPrivate: true, }); err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -148,31 +148,31 @@ func DeleteUnadoptedRepository(ctx *context.APIContext) { ctxUser, err := user_model.GetUserByName(ctx, ownerName) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() return } - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } // check not a repo has, err := repo_model.IsRepositoryModelExist(ctx, ctxUser, repoName) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } isDir, err := util.IsDir(repo_model.RepoPath(ctxUser.Name, repoName)) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } if has || !isDir { - ctx.NotFound() + ctx.APIErrorNotFound() return } if err := repo_service.DeleteUnadoptedRepository(ctx, ctx.Doer, ctxUser, repoName); err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/admin/cron.go b/routers/api/v1/admin/cron.go index 962e007776..b4dae11095 100644 --- a/routers/api/v1/admin/cron.go +++ b/routers/api/v1/admin/cron.go @@ -76,7 +76,7 @@ func PostCronTask(ctx *context.APIContext) { // "$ref": "#/responses/notFound" task := cron.GetTask(ctx.PathParam("task")) if task == nil { - ctx.NotFound() + ctx.APIErrorNotFound() return } task.Run() diff --git a/routers/api/v1/admin/email.go b/routers/api/v1/admin/email.go index 3de94d6868..ad078347a4 100644 --- a/routers/api/v1/admin/email.go +++ b/routers/api/v1/admin/email.go @@ -42,7 +42,7 @@ func GetAllEmails(ctx *context.APIContext) { ListOptions: listOptions, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetAllEmails", err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go index c812ca182d..a687541be5 100644 --- a/routers/api/v1/admin/hooks.go +++ b/routers/api/v1/admin/hooks.go @@ -51,22 +51,23 @@ func ListHooks(ctx *context.APIContext) { // for compatibility the default value is true isSystemWebhook := optional.Some(true) typeValue := ctx.FormString("type") - if typeValue == "default" { + switch typeValue { + case "default": isSystemWebhook = optional.Some(false) - } else if typeValue == "all" { + case "all": isSystemWebhook = optional.None[bool]() } sysHooks, err := webhook.GetSystemOrDefaultWebhooks(ctx, isSystemWebhook) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetSystemWebhooks", err) + ctx.APIErrorInternal(err) return } hooks := make([]*api.Hook, len(sysHooks)) for i, hook := range sysHooks { h, err := webhook_service.ToHook(setting.AppURL+"/-/admin", hook) if err != nil { - ctx.Error(http.StatusInternalServerError, "convert.ToHook", err) + ctx.APIErrorInternal(err) return } hooks[i] = h @@ -96,15 +97,15 @@ func GetHook(ctx *context.APIContext) { hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err) + ctx.APIErrorInternal(err) } return } h, err := webhook_service.ToHook("/-/admin/", hook) if err != nil { - ctx.Error(http.StatusInternalServerError, "convert.ToHook", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, h) @@ -186,9 +187,9 @@ func DeleteHook(ctx *context.APIContext) { hookID := ctx.PathParamInt64("id") if err := webhook.DeleteDefaultSystemWebhook(ctx, hookID); err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "DeleteDefaultSystemWebhook", err) + ctx.APIErrorInternal(err) } return } diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go index a5c299bbf0..c3473372f2 100644 --- a/routers/api/v1/admin/org.go +++ b/routers/api/v1/admin/org.go @@ -29,7 +29,7 @@ func CreateOrg(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of the user that will own the created organization + // description: username of the user who will own the created organization // type: string // required: true // - name: organization @@ -67,9 +67,9 @@ func CreateOrg(ctx *context.APIContext) { db.IsErrNameReserved(err) || db.IsErrNameCharsNotAllowed(err) || db.IsErrNamePatternNotAllowed(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "CreateOrganization", err) + ctx.APIErrorInternal(err) } return } @@ -101,7 +101,7 @@ func GetAllOrgs(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) - users, maxResults, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ + users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Type: user_model.UserTypeOrganization, OrderBy: db.SearchOrderByAlphabetically, @@ -109,7 +109,7 @@ func GetAllOrgs(ctx *context.APIContext) { Visible: []api.VisibleType{api.VisibleTypePublic, api.VisibleTypeLimited, api.VisibleTypePrivate}, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "SearchOrganizations", err) + ctx.APIErrorInternal(err) return } orgs := make([]*api.Organization, len(users)) diff --git a/routers/api/v1/admin/repo.go b/routers/api/v1/admin/repo.go index c119d5390a..12a78c9c4b 100644 --- a/routers/api/v1/admin/repo.go +++ b/routers/api/v1/admin/repo.go @@ -22,7 +22,7 @@ func CreateRepo(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of the user. This user will own the created repository + // description: username of the user who will own the created repository // type: string // required: true // - name: repository diff --git a/routers/api/v1/admin/runners.go b/routers/api/v1/admin/runners.go index 329242d9f6..736c421229 100644 --- a/routers/api/v1/admin/runners.go +++ b/routers/api/v1/admin/runners.go @@ -24,3 +24,81 @@ func GetRegistrationToken(ctx *context.APIContext) { shared.GetRegistrationToken(ctx, 0, 0) } + +// CreateRegistrationToken returns the token to register global runners +func CreateRegistrationToken(ctx *context.APIContext) { + // swagger:operation POST /admin/actions/runners/registration-token admin adminCreateRunnerRegistrationToken + // --- + // summary: Get an global actions runner registration token + // produces: + // - application/json + // parameters: + // responses: + // "200": + // "$ref": "#/responses/RegistrationToken" + + shared.GetRegistrationToken(ctx, 0, 0) +} + +// ListRunners get all runners +func ListRunners(ctx *context.APIContext) { + // swagger:operation GET /admin/actions/runners admin getAdminRunners + // --- + // summary: Get all runners + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/definitions/ActionRunnersResponse" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.ListRunners(ctx, 0, 0) +} + +// GetRunner get an global runner +func GetRunner(ctx *context.APIContext) { + // swagger:operation GET /admin/actions/runners/{runner_id} admin getAdminRunner + // --- + // summary: Get an global runner + // produces: + // - application/json + // parameters: + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/definitions/ActionRunner" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.GetRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id")) +} + +// DeleteRunner delete an global runner +func DeleteRunner(ctx *context.APIContext) { + // swagger:operation DELETE /admin/actions/runners/{runner_id} admin deleteAdminRunner + // --- + // summary: Delete an global runner + // produces: + // - application/json + // parameters: + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "204": + // description: runner has been deleted + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.DeleteRunner(ctx, 0, 0, ctx.PathParamInt64("runner_id")) +} diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 21cb2f9ccd..8a267cc418 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -40,9 +40,9 @@ 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.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "auth.GetSourceByID", err) + ctx.APIErrorInternal(err) } return } @@ -98,13 +98,13 @@ func CreateUser(ctx *context.APIContext) { if u.LoginType == auth.Plain { if len(form.Password) < setting.MinPasswordLength { err := errors.New("PasswordIsRequired") - ctx.Error(http.StatusBadRequest, "PasswordIsRequired", err) + ctx.APIError(http.StatusBadRequest, err) return } if !password.IsComplexEnough(form.Password) { err := errors.New("PasswordComplexity") - ctx.Error(http.StatusBadRequest, "PasswordComplexity", err) + ctx.APIError(http.StatusBadRequest, err) return } @@ -112,7 +112,7 @@ func CreateUser(ctx *context.APIContext) { if password.IsErrIsPwnedRequest(err) { log.Error(err.Error()) } - ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned")) + ctx.APIError(http.StatusBadRequest, errors.New("PasswordPwned")) return } } @@ -143,9 +143,9 @@ func CreateUser(ctx *context.APIContext) { user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) || db.IsErrNamePatternNotAllowed(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "CreateUser", err) + ctx.APIErrorInternal(err) } return } @@ -175,7 +175,7 @@ func EditUser(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user to edit + // description: username of the user whose data is to be edited // type: string // required: true // - name: body @@ -204,13 +204,13 @@ func EditUser(ctx *context.APIContext) { if err := user_service.UpdateAuth(ctx, ctx.ContextUser, authOpts); err != nil { switch { case errors.Is(err, password.ErrMinLength): - ctx.Error(http.StatusBadRequest, "PasswordTooShort", fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength)) + ctx.APIError(http.StatusBadRequest, fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength)) case errors.Is(err, password.ErrComplexity): - ctx.Error(http.StatusBadRequest, "PasswordComplexity", err) + ctx.APIError(http.StatusBadRequest, err) case errors.Is(err, password.ErrIsPwned), password.IsErrIsPwnedRequest(err): - ctx.Error(http.StatusBadRequest, "PasswordIsPwned", err) + ctx.APIError(http.StatusBadRequest, err) default: - ctx.Error(http.StatusInternalServerError, "UpdateAuth", err) + ctx.APIErrorInternal(err) } return } @@ -219,11 +219,11 @@ func EditUser(ctx *context.APIContext) { if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil { switch { case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err): - ctx.Error(http.StatusBadRequest, "EmailInvalid", err) + ctx.APIError(http.StatusBadRequest, err) case user_model.IsErrEmailAlreadyUsed(err): - ctx.Error(http.StatusBadRequest, "EmailUsed", err) + ctx.APIError(http.StatusBadRequest, err) default: - ctx.Error(http.StatusInternalServerError, "AddOrSetPrimaryEmailAddress", err) + ctx.APIErrorInternal(err) } return } @@ -239,7 +239,7 @@ func EditUser(ctx *context.APIContext) { Location: optional.FromPtr(form.Location), Description: optional.FromPtr(form.Description), IsActive: optional.FromPtr(form.Active), - IsAdmin: optional.FromPtr(form.Admin), + IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin), Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]), AllowGitHook: optional.FromPtr(form.AllowGitHook), AllowImportLocal: optional.FromPtr(form.AllowImportLocal), @@ -250,9 +250,9 @@ func EditUser(ctx *context.APIContext) { if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil { if user_model.IsErrDeleteLastAdminUser(err) { - ctx.Error(http.StatusBadRequest, "LastAdmin", err) + ctx.APIError(http.StatusBadRequest, err) } else { - ctx.Error(http.StatusInternalServerError, "UpdateUser", err) + ctx.APIErrorInternal(err) } return } @@ -272,7 +272,7 @@ func DeleteUser(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user to delete + // description: username of the user to delete // type: string // required: true // - name: purge @@ -290,13 +290,13 @@ func DeleteUser(ctx *context.APIContext) { // "$ref": "#/responses/validationError" if ctx.ContextUser.IsOrganization() { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name)) return } // admin should not delete themself if ctx.ContextUser.ID == ctx.Doer.ID { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("you cannot delete yourself")) + ctx.APIError(http.StatusUnprocessableEntity, errors.New("you cannot delete yourself")) return } @@ -305,9 +305,9 @@ func DeleteUser(ctx *context.APIContext) { org_model.IsErrUserHasOrgs(err) || packages_model.IsErrUserOwnPackages(err) || user_model.IsErrDeleteLastAdminUser(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "DeleteUser", err) + ctx.APIErrorInternal(err) } return } @@ -328,7 +328,7 @@ func CreatePublicKey(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of the user + // description: username of the user who is to receive a public key // type: string // required: true // - name: key @@ -358,7 +358,7 @@ func DeleteUserPublicKey(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose public key is to be deleted // type: string // required: true // - name: id @@ -377,11 +377,11 @@ func DeleteUserPublicKey(ctx *context.APIContext) { if err := asymkey_service.DeletePublicKey(ctx, ctx.ContextUser, ctx.PathParamInt64("id")); err != nil { if asymkey_model.IsErrKeyNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else if asymkey_model.IsErrKeyAccessDenied(err) { - ctx.Error(http.StatusForbidden, "", "You do not have access to this key") + ctx.APIError(http.StatusForbidden, "You do not have access to this key") } else { - ctx.Error(http.StatusInternalServerError, "DeleteUserPublicKey", err) + ctx.APIErrorInternal(err) } return } @@ -405,7 +405,7 @@ func SearchUsers(ctx *context.APIContext) { // format: int64 // - name: login_name // in: query - // description: user's login name to search for + // description: identifier of the user, provided by the external authenticator // type: string // - name: page // in: query @@ -423,7 +423,7 @@ func SearchUsers(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) - users, maxResults, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ + users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Type: user_model.UserTypeIndividual, LoginName: ctx.FormTrim("login_name"), @@ -432,7 +432,7 @@ func SearchUsers(ctx *context.APIContext) { ListOptions: listOptions, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "SearchUsers", err) + ctx.APIErrorInternal(err) return } @@ -456,7 +456,7 @@ func RenameUser(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: existing username of user + // description: current username of the user // type: string // required: true // - name: body @@ -473,30 +473,20 @@ func RenameUser(ctx *context.APIContext) { // "$ref": "#/responses/validationError" if ctx.ContextUser.IsOrganization() { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name)) return } - oldName := ctx.ContextUser.Name newName := web.GetForm(ctx).(*api.RenameUserOption).NewName - // Check if user name has been changed + // Check if username has been changed if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil { - switch { - case user_model.IsErrUserAlreadyExist(err): - ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken")) - case db.IsErrNameReserved(err): - ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_reserved", newName)) - case db.IsErrNamePatternNotAllowed(err): - ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_pattern_not_allowed", newName)) - case db.IsErrNameCharsNotAllowed(err): - ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_chars_not_allowed", newName)) - default: - ctx.ServerError("ChangeUserName", err) + if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + } else { + ctx.APIErrorInternal(err) } return } - - log.Trace("User name changed: %s -> %s", oldName, newName) ctx.Status(http.StatusNoContent) } diff --git a/routers/api/v1/admin/user_badge.go b/routers/api/v1/admin/user_badge.go index 99e20877fd..ce32f455b0 100644 --- a/routers/api/v1/admin/user_badge.go +++ b/routers/api/v1/admin/user_badge.go @@ -22,7 +22,7 @@ func ListUserBadges(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose badges are to be listed // type: string // required: true // responses: @@ -33,7 +33,7 @@ func ListUserBadges(ctx *context.APIContext) { badges, maxResults, err := user_model.GetUserBadges(ctx, ctx.ContextUser) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserBadges", err) + ctx.APIErrorInternal(err) return } @@ -53,7 +53,7 @@ func AddUserBadges(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user to whom a badge is to be added // type: string // required: true // - name: body @@ -70,7 +70,7 @@ func AddUserBadges(ctx *context.APIContext) { badges := prepareBadgesForReplaceOrAdd(*form) if err := user_model.AddUserBadges(ctx, ctx.ContextUser, badges); err != nil { - ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err) + ctx.APIErrorInternal(err) return } @@ -87,7 +87,7 @@ func DeleteUserBadges(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose badge is to be deleted // type: string // required: true // - name: body @@ -106,7 +106,7 @@ func DeleteUserBadges(ctx *context.APIContext) { badges := prepareBadgesForReplaceOrAdd(*form) if err := user_model.RemoveUserBadges(ctx, ctx.ContextUser, badges); err != nil { - ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index b1a42a85e6..4a4bf12657 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -7,8 +7,6 @@ // This documentation describes the Gitea API. // // Schemes: https, http -// BasePath: /api/v1 -// Version: {{AppVer | JSEscape}} // License: MIT http://opensource.org/licenses/MIT // // Consumes: @@ -66,6 +64,8 @@ package v1 import ( + gocontext "context" + "errors" "fmt" "net/http" "strings" @@ -116,9 +116,9 @@ func sudo() func(ctx *context.APIContext) { user, err := user_model.GetUserByName(ctx, sudo) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) } return } @@ -154,12 +154,12 @@ func repoAssignment() func(ctx *context.APIContext) { if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil { context.RedirectToUser(ctx.Base, userName, redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { - ctx.NotFound("GetUserByName", err) + ctx.APIErrorNotFound("GetUserByName", err) } else { - ctx.Error(http.StatusInternalServerError, "LookupUserRedirect", err) + ctx.APIErrorInternal(err) } } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) } return } @@ -175,12 +175,12 @@ func repoAssignment() func(ctx *context.APIContext) { if err == nil { context.RedirectToRepo(ctx.Base, redirectRepoID) } else if repo_model.IsErrRedirectNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "LookupRepoRedirect", err) + ctx.APIErrorInternal(err) } } else { - ctx.Error(http.StatusInternalServerError, "GetRepositoryByName", err) + ctx.APIErrorInternal(err) } return } @@ -192,11 +192,11 @@ func repoAssignment() func(ctx *context.APIContext) { taskID := ctx.Data["ActionsTaskID"].(int64) task, err := actions_model.GetTaskByID(ctx, taskID) if err != nil { - ctx.Error(http.StatusInternalServerError, "actions_model.GetTaskByID", err) + ctx.APIErrorInternal(err) return } if task.RepoID != repo.ID { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -207,29 +207,52 @@ func repoAssignment() func(ctx *context.APIContext) { } if err := ctx.Repo.Repository.LoadUnits(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadUnits", err) + ctx.APIErrorInternal(err) return } ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode) } else { - ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + needTwoFactor, err := doerNeedTwoFactorAuth(ctx, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + ctx.APIErrorInternal(err) return } + if needTwoFactor { + ctx.Repo.Permission = access_model.PermissionNoAccess() + } else { + ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) + if err != nil { + ctx.APIErrorInternal(err) + return + } + } } - if !ctx.Repo.Permission.HasAnyUnitAccess() { - ctx.NotFound() + if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() { + ctx.APIErrorNotFound() return } } } +func doerNeedTwoFactorAuth(ctx gocontext.Context, doer *user_model.User) (bool, error) { + if !setting.TwoFactorAuthEnforced { + return false, nil + } + if doer == nil { + return false, nil + } + has, err := auth_model.HasTwoFactorOrWebAuthn(ctx, doer.ID) + if err != nil { + return false, err + } + return !has, nil +} + func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { - ctx.Error(http.StatusForbidden, "reqPackageAccess", "user should have specific permission or be a site admin") + ctx.APIError(http.StatusForbidden, "user should have specific permission or be a site admin") return } } @@ -250,41 +273,41 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) { switch { case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository): if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { - ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public repos") + ctx.APIError(http.StatusForbidden, "token scope is limited to public repos") return } case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryIssue): if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { - ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public issues") + ctx.APIError(http.StatusForbidden, "token scope is limited to public issues") return } case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization): if ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic { - ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs") + ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs") return } if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && ctx.ContextUser.Visibility != api.VisibleTypePublic { - ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs") + ctx.APIError(http.StatusForbidden, "token scope is limited to public orgs") return } case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser): - if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic { - ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public users") + if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic { + ctx.APIError(http.StatusForbidden, "token scope is limited to public users") return } case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub): - if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic { - ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public activitypub") + if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic { + ctx.APIError(http.StatusForbidden, "token scope is limited to public activitypub") return } case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryNotification): if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { - ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public notifications") + ctx.APIError(http.StatusForbidden, "token scope is limited to public notifications") return } case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryPackage): if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() { - ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public packages") + ctx.APIError(http.StatusForbidden, "token scope is limited to public packages") return } } @@ -308,7 +331,7 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC // use the http method to determine the access level requiredScopeLevel := auth_model.Read - if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || ctx.Req.Method == "DELETE" { + if ctx.Req.Method == http.MethodPost || ctx.Req.Method == http.MethodPut || ctx.Req.Method == http.MethodPatch || ctx.Req.Method == http.MethodDelete { requiredScopeLevel = auth_model.Write } @@ -316,12 +339,12 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC requiredScopes := auth_model.GetRequiredScopes(requiredScopeLevel, requiredScopeCategories...) allow, err := scope.HasScope(requiredScopes...) if err != nil { - ctx.Error(http.StatusForbidden, "tokenRequiresScope", "checking scope failed: "+err.Error()) + ctx.APIError(http.StatusForbidden, "checking scope failed: "+err.Error()) return } if !allow { - ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s), required=%v, token scope=%v", requiredScopes, scope)) + ctx.APIError(http.StatusForbidden, fmt.Sprintf("token does not have at least one of required scope(s), required=%v, token scope=%v", requiredScopes, scope)) return } @@ -330,7 +353,7 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC // check if scope only applies to public resources publicOnly, err := scope.PublicOnly() if err != nil { - ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error()) + ctx.APIError(http.StatusForbidden, "parsing public resource scope failed: "+err.Error()) return } @@ -350,14 +373,14 @@ func reqToken() func(ctx *context.APIContext) { if ctx.IsSigned { return } - ctx.Error(http.StatusUnauthorized, "reqToken", "token is required") + ctx.APIError(http.StatusUnauthorized, "token is required") } } func reqExploreSignIn() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if (setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned { - ctx.Error(http.StatusUnauthorized, "reqExploreSignIn", "you must be signed in to search for users") + if (setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView) && !ctx.IsSigned { + ctx.APIError(http.StatusUnauthorized, "you must be signed in to search for users") } } } @@ -365,7 +388,7 @@ func reqExploreSignIn() func(ctx *context.APIContext) { func reqUsersExploreEnabled() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if setting.Service.Explore.DisableUsersPage { - ctx.NotFound() + ctx.APIErrorNotFound() } } } @@ -376,7 +399,7 @@ func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) { return } if !ctx.IsBasicAuth { - ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required") + ctx.APIError(http.StatusUnauthorized, "auth required") return } } @@ -386,7 +409,7 @@ func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) { func reqSiteAdmin() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.IsUserSiteAdmin() { - ctx.Error(http.StatusForbidden, "reqSiteAdmin", "user should be the site admin") + ctx.APIError(http.StatusForbidden, "user should be the site admin") return } } @@ -396,7 +419,7 @@ func reqSiteAdmin() func(ctx *context.APIContext) { func reqOwner() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.Repo.IsOwner() && !ctx.IsUserSiteAdmin() { - ctx.Error(http.StatusForbidden, "reqOwner", "user should be the owner of the repo") + ctx.APIError(http.StatusForbidden, "user should be the owner of the repo") return } } @@ -406,7 +429,7 @@ func reqOwner() func(ctx *context.APIContext) { func reqSelfOrAdmin() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.IsUserSiteAdmin() && ctx.ContextUser != ctx.Doer { - ctx.Error(http.StatusForbidden, "reqSelfOrAdmin", "doer should be the site admin or be same as the contextUser") + ctx.APIError(http.StatusForbidden, "doer should be the site admin or be same as the contextUser") return } } @@ -416,7 +439,7 @@ func reqSelfOrAdmin() func(ctx *context.APIContext) { func reqAdmin() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { - ctx.Error(http.StatusForbidden, "reqAdmin", "user should be an owner or a collaborator with admin write of a repository") + ctx.APIError(http.StatusForbidden, "user should be an owner or a collaborator with admin write of a repository") return } } @@ -426,26 +449,17 @@ func reqAdmin() func(ctx *context.APIContext) { func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.IsUserRepoWriter(unitTypes) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { - ctx.Error(http.StatusForbidden, "reqRepoWriter", "user should have a permission to write to a repo") + ctx.APIError(http.StatusForbidden, "user should have a permission to write to a repo") return } } } -// reqRepoBranchWriter user should have a permission to write to a branch, or be a site admin -func reqRepoBranchWriter(ctx *context.APIContext) { - options, ok := web.GetForm(ctx).(api.FileOptionInterface) - if !ok || (!ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, options.Branch()) && !ctx.IsUserSiteAdmin()) { - ctx.Error(http.StatusForbidden, "reqRepoBranchWriter", "user should have a permission to write to this branch") - return - } -} - // 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() { - ctx.Error(http.StatusForbidden, "reqRepoReader", "user should have specific read permission or be a repo admin or a site admin") + ctx.APIError(http.StatusForbidden, "user should have specific read permission or be a repo admin or a site admin") return } } @@ -455,7 +469,7 @@ func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) { func reqAnyRepoReader() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.Repo.Permission.HasAnyUnitAccess() && !ctx.IsUserSiteAdmin() { - ctx.Error(http.StatusForbidden, "reqAnyRepoReader", "user should have any permission to read repository or permissions of site admin") + ctx.APIError(http.StatusForbidden, "user should have any permission to read repository or permissions of site admin") return } } @@ -474,19 +488,20 @@ func reqOrgOwnership() func(ctx *context.APIContext) { } else if ctx.Org.Team != nil { orgID = ctx.Org.Team.OrgID } else { - ctx.Error(http.StatusInternalServerError, "", "reqOrgOwnership: unprepared context") + setting.PanicInDevOrTesting("reqOrgOwnership: unprepared context") + ctx.APIErrorInternal(errors.New("reqOrgOwnership: unprepared context")) return } isOwner, err := organization.IsOrganizationOwner(ctx, orgID, ctx.Doer.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "IsOrganizationOwner", err) + ctx.APIErrorInternal(err) return } else if !isOwner { if ctx.Org.Organization != nil { - ctx.Error(http.StatusForbidden, "", "Must be an organization owner") + ctx.APIError(http.StatusForbidden, "Must be an organization owner") } else { - ctx.NotFound() + ctx.APIErrorNotFound() } return } @@ -500,30 +515,31 @@ func reqTeamMembership() func(ctx *context.APIContext) { return } if ctx.Org.Team == nil { - ctx.Error(http.StatusInternalServerError, "", "reqTeamMembership: unprepared context") + setting.PanicInDevOrTesting("reqTeamMembership: unprepared context") + ctx.APIErrorInternal(errors.New("reqTeamMembership: unprepared context")) return } orgID := ctx.Org.Team.OrgID isOwner, err := organization.IsOrganizationOwner(ctx, orgID, ctx.Doer.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "IsOrganizationOwner", err) + ctx.APIErrorInternal(err) return } else if isOwner { return } if isTeamMember, err := organization.IsTeamMember(ctx, orgID, ctx.Org.Team.ID, ctx.Doer.ID); err != nil { - ctx.Error(http.StatusInternalServerError, "IsTeamMember", err) + ctx.APIErrorInternal(err) return } else if !isTeamMember { isOrgMember, err := organization.IsOrganizationMember(ctx, orgID, ctx.Doer.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "IsOrganizationMember", err) + ctx.APIErrorInternal(err) } else if isOrgMember { - ctx.Error(http.StatusForbidden, "", "Must be a team member") + ctx.APIError(http.StatusForbidden, "Must be a team member") } else { - ctx.NotFound() + ctx.APIErrorNotFound() } return } @@ -543,18 +559,19 @@ func reqOrgMembership() func(ctx *context.APIContext) { } else if ctx.Org.Team != nil { orgID = ctx.Org.Team.OrgID } else { - ctx.Error(http.StatusInternalServerError, "", "reqOrgMembership: unprepared context") + setting.PanicInDevOrTesting("reqOrgMembership: unprepared context") + ctx.APIErrorInternal(errors.New("reqOrgMembership: unprepared context")) return } if isMember, err := organization.IsOrganizationMember(ctx, orgID, ctx.Doer.ID); err != nil { - ctx.Error(http.StatusInternalServerError, "IsOrganizationMember", err) + ctx.APIErrorInternal(err) return } else if !isMember { if ctx.Org.Organization != nil { - ctx.Error(http.StatusForbidden, "", "Must be an organization member") + ctx.APIError(http.StatusForbidden, "Must be an organization member") } else { - ctx.NotFound() + ctx.APIErrorNotFound() } return } @@ -564,7 +581,7 @@ func reqOrgMembership() func(ctx *context.APIContext) { func reqGitHook() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.Doer.CanEditGitHook() { - ctx.Error(http.StatusForbidden, "", "must be allowed to edit Git hooks") + ctx.APIError(http.StatusForbidden, "must be allowed to edit Git hooks") return } } @@ -574,7 +591,17 @@ func reqGitHook() func(ctx *context.APIContext) { func reqWebhooksEnabled() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if setting.DisableWebhooks { - ctx.Error(http.StatusForbidden, "", "webhooks disabled by administrator") + ctx.APIError(http.StatusForbidden, "webhooks disabled by administrator") + return + } + } +} + +// reqStarsEnabled requires Starring to be enabled in the config. +func reqStarsEnabled() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if setting.Repository.DisableStars { + ctx.APIError(http.StatusForbidden, "stars disabled by administrator") return } } @@ -603,12 +630,12 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { if err == nil { context.RedirectToUser(ctx.Base, ctx.PathParam("org"), redirectUserID) } else if user_model.IsErrUserRedirectNotExist(err) { - ctx.NotFound("GetOrgByName", err) + ctx.APIErrorNotFound("GetOrgByName", err) } else { - ctx.Error(http.StatusInternalServerError, "LookupUserRedirect", err) + ctx.APIErrorInternal(err) } } else { - ctx.Error(http.StatusInternalServerError, "GetOrgByName", err) + ctx.APIErrorInternal(err) } return } @@ -619,9 +646,9 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { ctx.Org.Team, err = organization.GetTeamByID(ctx, ctx.PathParamInt64("teamid")) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetTeamById", err) + ctx.APIErrorInternal(err) } return } @@ -647,7 +674,7 @@ func mustEnableIssues(ctx *context.APIContext) { ctx.Repo.Permission) } } - ctx.NotFound() + ctx.APIErrorNotFound() return } } @@ -670,7 +697,7 @@ func mustAllowPulls(ctx *context.APIContext) { ctx.Repo.Permission) } } - ctx.NotFound() + ctx.APIErrorNotFound() return } } @@ -696,28 +723,36 @@ func mustEnableIssuesOrPulls(ctx *context.APIContext) { ctx.Repo.Permission) } } - ctx.NotFound() + ctx.APIErrorNotFound() return } } func mustEnableWiki(ctx *context.APIContext) { if !(ctx.Repo.CanRead(unit.TypeWiki)) { - ctx.NotFound() + ctx.APIErrorNotFound() return } } +// FIXME: for consistency, maybe most mustNotBeArchived checks should be replaced with mustEnableEditor func mustNotBeArchived(ctx *context.APIContext) { if ctx.Repo.Repository.IsArchived { - ctx.Error(http.StatusLocked, "RepoArchived", fmt.Errorf("%s is archived", ctx.Repo.Repository.LogString())) + ctx.APIError(http.StatusLocked, fmt.Errorf("%s is archived", ctx.Repo.Repository.FullName())) + 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())) return } } func mustEnableAttachments(ctx *context.APIContext) { if !setting.Attachment.Enabled { - ctx.NotFound() + ctx.APIErrorNotFound() return } } @@ -728,7 +763,7 @@ func bind[T any](_ T) any { theObj := new(T) // create a new form obj for every request but not use obj directly errs := binding.Bind(ctx.Req, theObj) if len(errs) > 0 { - ctx.Error(http.StatusUnprocessableEntity, "validationError", fmt.Sprintf("%s: %s", errs[0].FieldNames, errs[0].Error())) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("%s: %s", errs[0].FieldNames, errs[0].Error())) return } web.SetForm(ctx, theObj) @@ -756,7 +791,7 @@ func apiAuth(authMethod auth.Method) func(*context.APIContext) { return func(ctx *context.APIContext) { ar, err := common.AuthShared(ctx.Base, nil, authMethod) if err != nil { - ctx.Error(http.StatusUnauthorized, "APIAuth", err) + ctx.APIError(http.StatusUnauthorized, err) return } ctx.Doer = ar.Doer @@ -830,15 +865,15 @@ 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 { - case ctx.ContextUser.Visibility == api.VisibleTypePrivate: + switch ctx.ContextUser.Visibility { + case api.VisibleTypePrivate: if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) { - ctx.NotFound("Visit Project", nil) + ctx.APIErrorNotFound("Visit Project", nil) return } - case ctx.ContextUser.Visibility == api.VisibleTypeLimited: + case api.VisibleTypeLimited: if ctx.Doer == nil { - ctx.NotFound("Visit Project", nil) + ctx.APIErrorNotFound("Visit Project", nil) return } } @@ -874,7 +909,7 @@ func Routes() *web.Router { m.Use(apiAuth(buildAuthGroup())) m.Use(verifyAuthWithOptions(&common.VerifyOptions{ - SignInRequired: setting.Service.RequireSignInView, + SignInRequired: setting.Service.RequireSignInViewStrict, })) addActionsRoutes := func( @@ -900,8 +935,14 @@ func Routes() *web.Router { }) m.Group("/runners", func() { + m.Get("", reqToken(), reqChecker, act.ListRunners) m.Get("/registration-token", reqToken(), reqChecker, act.GetRegistrationToken) + m.Post("/registration-token", reqToken(), reqChecker, act.CreateRegistrationToken) + m.Get("/{runner_id}", reqToken(), reqChecker, act.GetRunner) + m.Delete("/{runner_id}", reqToken(), reqChecker, act.DeleteRunner) }) + m.Get("/runs", reqToken(), reqChecker, act.ListWorkflowRuns) + m.Get("/jobs", reqToken(), reqChecker, act.ListWorkflowJobs) }) } @@ -931,7 +972,8 @@ func Routes() *web.Router { // Misc (public accessible) m.Group("", func() { m.Get("/version", misc.Version) - m.Get("/signing-key.gpg", misc.SigningKey) + m.Get("/signing-key.gpg", misc.SigningKeyGPG) + m.Get("/signing-key.pub", misc.SigningKeySSH) m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup) m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown) m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw) @@ -995,7 +1037,7 @@ func Routes() *web.Router { m.Get("/{target}", user.CheckFollowing) }) - m.Get("/starred", user.GetStarredRepos) + m.Get("/starred", reqStarsEnabled(), user.GetStarredRepos) m.Get("/subscriptions", user.GetWatchedRepos) }, context.UserAssignmentAPI(), checkTokenPublicOnly()) @@ -1031,8 +1073,15 @@ func Routes() *web.Router { }) m.Group("/runners", func() { + m.Get("", reqToken(), user.ListRunners) m.Get("/registration-token", reqToken(), user.GetRegistrationToken) + m.Post("/registration-token", reqToken(), user.CreateRegistrationToken) + m.Get("/{runner_id}", reqToken(), user.GetRunner) + m.Delete("/{runner_id}", reqToken(), user.DeleteRunner) }) + + m.Get("/runs", reqToken(), user.ListWorkflowRuns) + m.Get("/jobs", reqToken(), user.ListWorkflowJobs) }) m.Get("/followers", user.ListMyFollowers) @@ -1086,7 +1135,7 @@ func Routes() *web.Router { m.Put("", user.Star) m.Delete("", user.Unstar) }, repoAssignment(), checkTokenPublicOnly()) - }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) + }, reqStarsEnabled(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) m.Get("/times", repo.ListMyTrackedTimes) m.Get("/stopwatches", repo.GetStopwatches) m.Get("/subscriptions", user.GetMyWatchedRepos) @@ -1145,11 +1194,22 @@ func Routes() *web.Router { m.Post("/accept", repo.AcceptTransfer) m.Post("/reject", repo.RejectTransfer) }, reqToken()) - addActionsRoutes( - m, - reqOwner(), - repo.NewAction(), - ) + + addActionsRoutes(m, reqOwner(), repo.NewAction()) // it adds the routes for secrets/variables and runner management + + m.Group("/actions/workflows", func() { + m.Get("", repo.ActionsListRepositoryWorkflows) + m.Get("/{workflow_id}", repo.ActionsGetWorkflow) + 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) + }, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions)) + + m.Group("/actions/jobs", func() { + m.Get("/{job_id}", repo.GetWorkflowJob) + m.Get("/{job_id}/logs", repo.DownloadActionsRunJobLogs) + }, reqToken(), reqRepoReader(unit.TypeActions)) + m.Group("/hooks/git", func() { m.Combo("").Get(repo.ListGitHooks) m.Group("/{id}", func() { @@ -1187,7 +1247,7 @@ func Routes() *web.Router { }, reqToken()) m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile) m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS) - m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive) + m.Methods("HEAD,GET", "/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive) m.Combo("/forks").Get(repo.ListForks). Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork) m.Post("/merge-upstream", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeCode), bind(api.MergeUpstreamRequest{}), repo.MergeUpstream) @@ -1225,6 +1285,20 @@ func Routes() *web.Router { }, reqToken(), reqAdmin()) m.Group("/actions", func() { m.Get("/tasks", repo.ListActionTasks) + m.Group("/runs", func() { + m.Group("/{run}", func() { + m.Get("", repo.GetWorkflowRun) + m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun) + m.Get("/jobs", repo.ListWorkflowRunJobs) + m.Get("/artifacts", repo.GetArtifactsOfRun) + }) + }) + m.Get("/artifacts", repo.GetArtifacts) + m.Group("/artifacts/{artifact_id}", func() { + m.Get("", repo.GetArtifact) + m.Delete("", reqRepoWriter(unit.TypeActions), repo.DeleteArtifact) + }) + m.Get("/artifacts/{artifact_id}/zip", repo.DownloadArtifact) }, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true)) m.Group("/keys", func() { m.Combo("").Get(repo.ListDeployKeys). @@ -1248,7 +1322,7 @@ func Routes() *web.Router { m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup) m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown) m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw) - m.Get("/stargazers", repo.ListStargazers) + m.Get("/stargazers", reqStarsEnabled(), repo.ListStargazers) m.Get("/subscribers", repo.ListSubscribers) m.Group("/subscription", func() { m.Get("", user.IsWatching) @@ -1349,18 +1423,29 @@ func Routes() *web.Router { m.Get("/tags/{sha}", repo.GetAnnotatedTag) m.Get("/notes/{sha}", repo.GetNote) }, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode)) - m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, repo.ApplyDiffPatch) m.Group("/contents", func() { m.Get("", repo.GetContentsList) - m.Post("", reqToken(), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.ChangeFiles) m.Get("/*", repo.GetContents) - m.Group("/*", func() { - m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.CreateFile) - m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.UpdateFile) - m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.DeleteFile) - }, reqToken()) - }, reqRepoReader(unit.TypeCode)) - m.Get("/signing-key.gpg", misc.SigningKey) + m.Group("", func() { + // "change file" operations, need permission to write to the target branch provided by the form + m.Post("", bind(api.ChangeFilesOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.ChangeFiles) + m.Group("/*", func() { + m.Post("", bind(api.CreateFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.CreateFile) + m.Put("", bind(api.UpdateFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.UpdateFile) + m.Delete("", bind(api.DeleteFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.DeleteFile) + }) + m.Post("/diffpatch", bind(api.ApplyDiffPatchFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.ApplyDiffPatch) + }, mustEnableEditor, reqToken()) + }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()) + m.Group("/contents-ext", func() { + m.Get("", repo.GetContentsExt) + m.Get("/*", repo.GetContentsExt) + }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()) + m.Combo("/file-contents", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()). + Get(repo.GetFileContentsGet). + Post(bind(api.GetFilesOptions{}), repo.GetFileContentsPost) // the POST method requires "write" permission, so we also support "GET" method above + m.Get("/signing-key.gpg", misc.SigningKeyGPG) + m.Get("/signing-key.pub", misc.SigningKeySSH) m.Group("/topics", func() { m.Combo("").Get(repo.ListTopics). Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics) @@ -1381,10 +1466,14 @@ func Routes() *web.Router { m.Delete("", repo.DeleteAvatar) }, reqAdmin(), reqToken()) - m.Get("/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive) + m.Methods("HEAD,GET", "/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive) }, repoAssignment(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) + // Artifacts direct download endpoint authenticates via signed url + // it is protected by the "sig" parameter (to help to access private repo), so no need to use other middlewares + m.Get("/repos/{username}/{reponame}/actions/artifacts/{artifact_id}/zip/raw", repo.DownloadArtifactRaw) + // Notifications (requires notifications scope) m.Group("/repos", func() { m.Group("/{username}/{reponame}", func() { @@ -1489,6 +1578,11 @@ func Routes() *web.Router { Delete(reqToken(), reqAdmin(), repo.UnpinIssue) m.Patch("/{position}", reqToken(), reqAdmin(), repo.MoveIssuePin) }) + m.Group("/lock", func() { + m.Combo(""). + Put(bind(api.LockIssueOption{}), repo.LockIssue). + Delete(repo.UnlockIssue) + }, reqToken(), reqAdmin()) }) }, mustEnableIssuesOrPulls) m.Group("/labels", func() { @@ -1510,13 +1604,24 @@ func Routes() *web.Router { // NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs m.Group("/packages/{username}", func() { - m.Group("/{type}/{name}/{version}", func() { - m.Get("", reqToken(), packages.GetPackage) - m.Delete("", reqToken(), reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) - m.Get("/files", reqToken(), packages.ListPackageFiles) + m.Group("/{type}/{name}", func() { + m.Get("/", packages.ListPackageVersions) + + m.Group("/{version}", func() { + m.Get("", packages.GetPackage) + m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) + m.Get("/files", packages.ListPackageFiles) + }) + + m.Group("/-", func() { + m.Get("/latest", packages.GetLatestPackageVersion) + m.Post("/link/{repo_name}", reqPackageAccess(perm.AccessModeWrite), packages.LinkPackage) + m.Post("/unlink", reqPackageAccess(perm.AccessModeWrite), packages.UnlinkPackage) + }) }) - m.Get("/", reqToken(), packages.ListPackages) - }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly()) + + m.Get("/", packages.ListPackages) + }, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly()) // Organizations m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs) @@ -1530,6 +1635,7 @@ func Routes() *web.Router { 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) m.Group("/members", func() { @@ -1644,6 +1750,16 @@ func Routes() *web.Router { Patch(bind(api.EditHookOption{}), admin.EditHook). Delete(admin.DeleteHook) }) + m.Group("/actions", func() { + m.Group("/runners", func() { + m.Get("", admin.ListRunners) + m.Post("/registration-token", admin.CreateRegistrationToken) + m.Get("/{runner_id}", admin.GetRunner) + m.Delete("/{runner_id}", admin.DeleteRunner) + }) + m.Get("/runs", admin.ListWorkflowRuns) + m.Get("/jobs", admin.ListWorkflowJobs) + }) m.Group("/runners", func() { m.Get("/registration-token", admin.GetRegistrationToken) }) diff --git a/routers/api/v1/misc/gitignore.go b/routers/api/v1/misc/gitignore.go index b0bf00a921..1ff2628ce8 100644 --- a/routers/api/v1/misc/gitignore.go +++ b/routers/api/v1/misc/gitignore.go @@ -48,7 +48,7 @@ func GetGitignoreTemplateInfo(ctx *context.APIContext) { text, err := options.Gitignore(name) if err != nil { - ctx.NotFound() + ctx.APIErrorNotFound() return } diff --git a/routers/api/v1/misc/label_templates.go b/routers/api/v1/misc/label_templates.go index f105b4c684..95c156c4ab 100644 --- a/routers/api/v1/misc/label_templates.go +++ b/routers/api/v1/misc/label_templates.go @@ -52,7 +52,7 @@ func GetLabelTemplate(ctx *context.APIContext) { labels, err := repo_module.LoadTemplateLabelsByDisplayName(name) if err != nil { - ctx.NotFound() + ctx.APIErrorNotFound() return } diff --git a/routers/api/v1/misc/licenses.go b/routers/api/v1/misc/licenses.go index d99b276232..12670afef9 100644 --- a/routers/api/v1/misc/licenses.go +++ b/routers/api/v1/misc/licenses.go @@ -37,7 +37,6 @@ func ListLicenseTemplates(ctx *context.APIContext) { ctx.JSON(http.StatusOK, response) } -// Returns information about a gitignore template func GetLicenseTemplateInfo(ctx *context.APIContext) { // swagger:operation GET /licenses/{name} miscellaneous getLicenseTemplateInfo // --- @@ -59,7 +58,7 @@ func GetLicenseTemplateInfo(ctx *context.APIContext) { text, err := options.License(name) if err != nil { - ctx.NotFound() + ctx.APIErrorNotFound() return } diff --git a/routers/api/v1/misc/markup.go b/routers/api/v1/misc/markup.go index 7b3633552f..909310b4c8 100644 --- a/routers/api/v1/misc/markup.go +++ b/routers/api/v1/misc/markup.go @@ -38,11 +38,11 @@ func Markup(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.MarkupOption) if ctx.HasAPIError() { - ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg()) + ctx.APIError(http.StatusUnprocessableEntity, ctx.GetErrMsg()) return } - mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck + 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) } @@ -69,11 +69,11 @@ func Markdown(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.MarkdownOption) if ctx.HasAPIError() { - ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg()) + ctx.APIError(http.StatusUnprocessableEntity, ctx.GetErrMsg()) return } - mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck + 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, "") } @@ -100,7 +100,7 @@ func MarkdownRaw(ctx *context.APIContext) { // "$ref": "#/responses/validationError" defer ctx.Req.Body.Close() if err := markdown.RenderRaw(markup.NewRenderContext(ctx), ctx.Req.Body, ctx.Resp); err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } } diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go index 6063e54cdc..38a1a3be9e 100644 --- a/routers/api/v1/misc/markup_test.go +++ b/routers/api/v1/misc/markup_test.go @@ -134,7 +134,7 @@ Here are some links to the most important topics. You can find the full list of <h2 id="user-content-quick-links">Quick Links</h2> <p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p> <p><a href="http://localhost:3000/user2/repo1/wiki/Configuration" rel="nofollow">Configuration</a> -<a href="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p> +<a href="http://localhost:3000/user2/repo1/wiki/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p> `, } @@ -158,19 +158,19 @@ Here are some links to the most important topics. You can find the full list of input := "[Link](test.md)\n" testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a> -<a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p> +<a href="http://localhost:3000/user2/repo1/src/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p> `, http.StatusOK) testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a> -<a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p> +<a href="http://localhost:3000/user2/repo1/src/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p> `, http.StatusOK) testRenderMarkup(t, "gfm", false, "", input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a> -<a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p> +<a href="http://localhost:3000/user2/repo1/src/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p> `, http.StatusOK) testRenderMarkup(t, "file", false, "path/new-file.md", input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/path/test.md" rel="nofollow">Link</a> -<a href="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" alt="Image"/></a></p> +<a href="http://localhost:3000/user2/repo1/src/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" alt="Image"/></a></p> `, http.StatusOK) testRenderMarkup(t, "file", false, "path/test.unknown", "## Test", "unsupported file to render: \"path/test.unknown\"\n", http.StatusUnprocessableEntity) diff --git a/routers/api/v1/misc/nodeinfo.go b/routers/api/v1/misc/nodeinfo.go index 5973724782..ffe50e9fda 100644 --- a/routers/api/v1/misc/nodeinfo.go +++ b/routers/api/v1/misc/nodeinfo.go @@ -52,7 +52,7 @@ func NodeInfo(ctx *context.APIContext) { } if err := ctx.Cache.PutJSON(cacheKeyNodeInfoUsage, nodeInfoUsage, 180); err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } } diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go index 24a46c1e70..db70e04b8f 100644 --- a/routers/api/v1/misc/signing.go +++ b/routers/api/v1/misc/signing.go @@ -4,15 +4,35 @@ package misc import ( - "fmt" - "net/http" - + "code.gitea.io/gitea/modules/git" asymkey_service "code.gitea.io/gitea/services/asymkey" "code.gitea.io/gitea/services/context" ) -// SigningKey returns the public key of the default signing key if it exists -func SigningKey(ctx *context.APIContext) { +func getSigningKey(ctx *context.APIContext, expectedFormat string) { + // if the handler is in the repo's route group, get the repo's signing key + // otherwise, get the global signing key + path := "" + if ctx.Repo != nil && ctx.Repo.Repository != nil { + path = ctx.Repo.Repository.RepoPath() + } + content, format, err := asymkey_service.PublicSigningKey(ctx, path) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if format == "" { + ctx.APIErrorNotFound("no signing key") + return + } else if format != expectedFormat { + ctx.APIErrorNotFound("signing key format is " + format) + return + } + _, _ = ctx.Write([]byte(content)) +} + +// SigningKeyGPG returns the public key of the default signing key if it exists +func SigningKeyGPG(ctx *context.APIContext) { // swagger:operation GET /signing-key.gpg miscellaneous getSigningKey // --- // summary: Get default signing-key.gpg @@ -45,19 +65,42 @@ func SigningKey(ctx *context.APIContext) { // description: "GPG armored public key" // schema: // type: string + getSigningKey(ctx, git.SigningKeyFormatOpenPGP) +} - path := "" - if ctx.Repo != nil && ctx.Repo.Repository != nil { - path = ctx.Repo.Repository.RepoPath() - } +// SigningKeySSH returns the public key of the default signing key if it exists +func SigningKeySSH(ctx *context.APIContext) { + // swagger:operation GET /signing-key.pub miscellaneous getSigningKeySSH + // --- + // summary: Get default signing-key.pub + // produces: + // - text/plain + // responses: + // "200": + // description: "ssh public key" + // schema: + // type: string - content, err := asymkey_service.PublicSigningKey(ctx, path) - if err != nil { - ctx.Error(http.StatusInternalServerError, "gpg export", err) - return - } - _, err = ctx.Write([]byte(content)) - if err != nil { - ctx.Error(http.StatusInternalServerError, "gpg export", fmt.Errorf("Error writing key content %w", err)) - } + // swagger:operation GET /repos/{owner}/{repo}/signing-key.pub repository repoSigningKeySSH + // --- + // summary: Get signing-key.pub for given repository + // produces: + // - text/plain + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // description: "ssh public key" + // schema: + // type: string + getSigningKey(ctx, git.SigningKeyFormatSSH) } diff --git a/routers/api/v1/notify/notifications.go b/routers/api/v1/notify/notifications.go index 46b3c7f5e7..4e4c7dc6dd 100644 --- a/routers/api/v1/notify/notifications.go +++ b/routers/api/v1/notify/notifications.go @@ -28,7 +28,7 @@ func NewAvailable(ctx *context.APIContext) { Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread}, }) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, "db.Count[activities_model.Notification]", err) + ctx.APIError(http.StatusUnprocessableEntity, err) 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.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return nil } opts := &activities_model.FindNotificationOptions{ diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go index 1744426ee8..e87054e26c 100644 --- a/routers/api/v1/notify/repo.go +++ b/routers/api/v1/notify/repo.go @@ -110,18 +110,18 @@ func ListRepoNotifications(ctx *context.APIContext) { totalCount, err := db.Count[activities_model.Notification](ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } nl, err := db.Find[activities_model.Notification](ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } err = activities_model.NotificationList(nl).LoadAttributes(ctx) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -183,7 +183,7 @@ func ReadRepoNotifications(ctx *context.APIContext) { if len(qLastRead) > 0 { tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) if err != nil { - ctx.Error(http.StatusBadRequest, "Parse", err) + ctx.APIError(http.StatusBadRequest, err) return } if !tmpLastRead.IsZero() { @@ -203,7 +203,7 @@ func ReadRepoNotifications(ctx *context.APIContext) { } nl, err := db.Find[activities_model.Notification](ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -217,7 +217,7 @@ func ReadRepoNotifications(ctx *context.APIContext) { for _, n := range nl { notif, err := activities_model.SetNotificationStatus(ctx, n.ID, ctx.Doer, targetStatus) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } _ = notif.LoadAttributes(ctx) diff --git a/routers/api/v1/notify/threads.go b/routers/api/v1/notify/threads.go index 58a38cfd18..dd77e4aae4 100644 --- a/routers/api/v1/notify/threads.go +++ b/routers/api/v1/notify/threads.go @@ -42,7 +42,7 @@ func GetThread(ctx *context.APIContext) { return } if err := n.LoadAttributes(ctx); err != nil && !issues_model.IsErrCommentNotExist(err) { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -90,11 +90,11 @@ func ReadThread(ctx *context.APIContext) { notif, err := activities_model.SetNotificationStatus(ctx, n.ID, ctx.Doer, targetStatus) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } if err = notif.LoadAttributes(ctx); err != nil && !issues_model.IsErrCommentNotExist(err) { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusResetContent, convert.ToNotificationThread(ctx, notif)) @@ -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.Error(http.StatusNotFound, "GetNotificationByID", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) } return nil } if n.UserID != ctx.Doer.ID && !ctx.Doer.IsAdmin { - ctx.Error(http.StatusForbidden, "GetNotificationByID", fmt.Errorf("only user itself and admin are allowed to read/change this thread %d", n.ID)) + ctx.APIError(http.StatusForbidden, fmt.Errorf("only user itself and admin are allowed to read/change this thread %d", n.ID)) return nil } return n diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go index 879f484cce..3ebb678835 100644 --- a/routers/api/v1/notify/user.go +++ b/routers/api/v1/notify/user.go @@ -71,18 +71,18 @@ func ListNotifications(ctx *context.APIContext) { totalCount, err := db.Count[activities_model.Notification](ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } nl, err := db.Find[activities_model.Notification](ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } err = activities_model.NotificationList(nl).LoadAttributes(ctx) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -133,7 +133,7 @@ func ReadNotifications(ctx *context.APIContext) { if len(qLastRead) > 0 { tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) if err != nil { - ctx.Error(http.StatusBadRequest, "Parse", err) + ctx.APIError(http.StatusBadRequest, err) return } if !tmpLastRead.IsZero() { @@ -150,7 +150,7 @@ func ReadNotifications(ctx *context.APIContext) { } nl, err := db.Find[activities_model.Notification](ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -164,7 +164,7 @@ func ReadNotifications(ctx *context.APIContext) { for _, n := range nl { notif, err := activities_model.SetNotificationStatus(ctx, n.ID, ctx.Doer, targetStatus) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } _ = notif.LoadAttributes(ctx) diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go index 199ee7d777..3ae5e60585 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -54,15 +54,16 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) { secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } apiSecrets := make([]*api.Secret, len(secrets)) for k, v := range secrets { apiSecrets[k] = &api.Secret{ - Name: v.Name, - Created: v.CreatedUnix.AsTime(), + Name: v.Name, + Description: v.Description, + Created: v.CreatedUnix.AsTime(), } } @@ -106,14 +107,14 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) { opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption) - _, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"), opt.Data) + _, 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.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err) + ctx.APIError(http.StatusBadRequest, err) } else if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "CreateOrUpdateSecret", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err) + ctx.APIErrorInternal(err) } return } @@ -156,11 +157,11 @@ 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.Error(http.StatusBadRequest, "DeleteSecret", err) + ctx.APIError(http.StatusBadRequest, err) } else if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "DeleteSecret", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "DeleteSecret", err) + ctx.APIErrorInternal(err) } return } @@ -189,6 +190,27 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) { shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0) } +// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization +// CreateRegistrationToken returns the token to register org runners +func (Action) CreateRegistrationToken(ctx *context.APIContext) { + // swagger:operation POST /orgs/{org}/actions/runners/registration-token organization orgCreateRunnerRegistrationToken + // --- + // summary: Get an organization's actions runner registration token + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/RegistrationToken" + + shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0) +} + // ListVariables list org-level variables func (Action) ListVariables(ctx *context.APIContext) { // swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList @@ -223,17 +245,18 @@ func (Action) ListVariables(ctx *context.APIContext) { ListOptions: utils.GetListOptions(ctx), }) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindVariables", err) + ctx.APIErrorInternal(err) return } variables := make([]*api.ActionVariable, len(vars)) for i, v := range vars { variables[i] = &api.ActionVariable{ - OwnerID: v.OwnerID, - RepoID: v.RepoID, - Name: v.Name, - Data: v.Data, + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + Description: v.Description, } } @@ -273,18 +296,19 @@ func (Action) GetVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "GetVariable", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "GetVariable", err) + ctx.APIErrorInternal(err) } return } variable := &api.ActionVariable{ - OwnerID: v.OwnerID, - RepoID: v.RepoID, - Name: v.Name, - Data: v.Data, + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + Description: v.Description, } ctx.JSON(http.StatusOK, variable) @@ -322,11 +346,11 @@ 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.Error(http.StatusBadRequest, "DeleteVariableByName", err) + ctx.APIError(http.StatusBadRequest, err) } else if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "DeleteVariableByName", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err) + ctx.APIErrorInternal(err) } return } @@ -360,13 +384,13 @@ func (Action) CreateVariable(ctx *context.APIContext) { // "$ref": "#/definitions/CreateVariableOption" // responses: // "201": - // description: response when creating an org-level variable - // "204": - // description: response when creating an org-level variable + // description: successfully created the org-level variable // "400": // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/notFound" + // "409": + // description: variable name already exists. + // "500": + // "$ref": "#/responses/error" opt := web.GetForm(ctx).(*api.CreateVariableOption) @@ -378,24 +402,24 @@ func (Action) CreateVariable(ctx *context.APIContext) { Name: variableName, }) if err != nil && !errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusInternalServerError, "GetVariable", err) + ctx.APIErrorInternal(err) return } if v != nil && v.ID > 0 { - ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + ctx.APIError(http.StatusConflict, util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) return } - if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil { + if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value, opt.Description); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.Error(http.StatusBadRequest, "CreateVariable", err) + ctx.APIError(http.StatusBadRequest, err) } else { - ctx.Error(http.StatusInternalServerError, "CreateVariable", err) + ctx.APIErrorInternal(err) } return } - ctx.Status(http.StatusNoContent) + ctx.Status(http.StatusCreated) } // UpdateVariable update an org-level variable @@ -440,9 +464,9 @@ func (Action) UpdateVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "GetVariable", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "GetVariable", err) + ctx.APIErrorInternal(err) } return } @@ -450,11 +474,16 @@ func (Action) UpdateVariable(ctx *context.APIContext) { if opt.Name == "" { opt.Name = ctx.PathParam("variablename") } - if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil { + + v.Name = opt.Name + v.Data = opt.Value + v.Description = opt.Description + + if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.Error(http.StatusBadRequest, "UpdateVariable", err) + ctx.APIError(http.StatusBadRequest, err) } else { - ctx.Error(http.StatusInternalServerError, "UpdateVariable", err) + ctx.APIErrorInternal(err) } return } @@ -462,6 +491,175 @@ func (Action) UpdateVariable(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } +// ListRunners get org-level runners +func (Action) ListRunners(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/runners organization getOrgRunners + // --- + // summary: Get org-level runners + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/definitions/ActionRunnersResponse" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.ListRunners(ctx, ctx.Org.Organization.ID, 0) +} + +// GetRunner get an org-level runner +func (Action) GetRunner(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/runners/{runner_id} organization getOrgRunner + // --- + // summary: Get an org-level runner + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/definitions/ActionRunner" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.GetRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id")) +} + +// DeleteRunner delete an org-level runner +func (Action) DeleteRunner(ctx *context.APIContext) { + // swagger:operation DELETE /orgs/{org}/actions/runners/{runner_id} organization deleteOrgRunner + // --- + // summary: Delete an org-level runner + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "204": + // description: runner has been deleted + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.DeleteRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id")) +} + +func (Action) ListWorkflowJobs(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/jobs organization getOrgWorkflowJobs + // --- + // summary: Get org-level workflow jobs + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WorkflowJobsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.ListJobs(ctx, ctx.Org.Organization.ID, 0, 0) +} + +func (Action) ListWorkflowRuns(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/runs organization getOrgWorkflowRuns + // --- + // summary: Get org-level workflow runs + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: event + // in: query + // description: workflow event name + // type: string + // required: false + // - name: branch + // in: query + // description: workflow branch + // type: string + // required: false + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: actor + // in: query + // description: triggered by user + // type: string + // required: false + // - name: head_sha + // in: query + // description: triggering sha of the workflow run + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WorkflowRunsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.ListRuns(ctx, ctx.Org.Organization.ID, 0) +} + var _ actions_service.API = new(Action) // Action implements actions_service.API diff --git a/routers/api/v1/org/avatar.go b/routers/api/v1/org/avatar.go index f11eb6c1cd..0eb771b2cd 100644 --- a/routers/api/v1/org/avatar.go +++ b/routers/api/v1/org/avatar.go @@ -39,13 +39,13 @@ func UpdateAvatar(ctx *context.APIContext) { content, err := base64.StdEncoding.DecodeString(form.Image) if err != nil { - ctx.Error(http.StatusBadRequest, "DecodeImage", err) + ctx.APIError(http.StatusBadRequest, err) return } err = user_service.UploadAvatar(ctx, ctx.Org.Organization.AsUser(), content) if err != nil { - ctx.Error(http.StatusInternalServerError, "UploadAvatar", err) + ctx.APIErrorInternal(err) return } @@ -72,7 +72,7 @@ func DeleteAvatar(ctx *context.APIContext) { // "$ref": "#/responses/notFound" err := user_service.DeleteAvatar(ctx, ctx.Org.Organization.AsUser()) if err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteAvatar", err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/org/block.go b/routers/api/v1/org/block.go index 69a5222a20..6b2f3dc615 100644 --- a/routers/api/v1/org/block.go +++ b/routers/api/v1/org/block.go @@ -47,7 +47,7 @@ func CheckUserBlock(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: user to check + // description: username of the user to check // type: string // required: true // responses: @@ -71,7 +71,7 @@ func BlockUser(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: user to block + // description: username of the user to block // type: string // required: true // - name: note @@ -101,7 +101,7 @@ func UnblockUser(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: user to unblock + // description: username of the user to unblock // type: string // required: true // responses: diff --git a/routers/api/v1/org/hook.go b/routers/api/v1/org/hook.go index df82f4e5a2..f9e0684a97 100644 --- a/routers/api/v1/org/hook.go +++ b/routers/api/v1/org/hook.go @@ -78,7 +78,7 @@ func GetHook(ctx *context.APIContext) { apiHook, err := webhook_service.ToHook(ctx.ContextUser.HomeLink(), hook) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, apiHook) diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go index 2a9bd92e87..b5b70bdc7d 100644 --- a/routers/api/v1/org/label.go +++ b/routers/api/v1/org/label.go @@ -46,13 +46,13 @@ func ListLabels(ctx *context.APIContext) { labels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Org.Organization.ID, ctx.FormString("sort"), utils.GetListOptions(ctx)) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLabelsByOrgID", err) + ctx.APIErrorInternal(err) return } count, err := issues_model.CountLabelsByOrgID(ctx, ctx.Org.Organization.ID) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -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.Error(http.StatusUnprocessableEntity, "Color", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } form.Color = color @@ -103,7 +103,7 @@ func CreateLabel(ctx *context.APIContext) { Description: form.Description, } if err := issues_model.NewLabel(ctx, label); err != nil { - ctx.Error(http.StatusInternalServerError, "NewLabel", err) + ctx.APIErrorInternal(err) return } @@ -147,9 +147,9 @@ func GetLabel(ctx *context.APIContext) { } if err != nil { if issues_model.IsErrOrgLabelNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetLabelByOrgID", err) + ctx.APIErrorInternal(err) } return } @@ -193,9 +193,9 @@ func EditLabel(ctx *context.APIContext) { l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrOrgLabelNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err) + ctx.APIErrorInternal(err) } return } @@ -209,7 +209,7 @@ func EditLabel(ctx *context.APIContext) { if form.Color != nil { color, err := label.NormalizeColor(*form.Color) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, "Color", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } l.Color = color @@ -219,7 +219,7 @@ func EditLabel(ctx *context.APIContext) { } l.SetArchived(form.IsArchived != nil && *form.IsArchived) if err := issues_model.UpdateLabel(ctx, l); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateLabel", err) + ctx.APIErrorInternal(err) return } @@ -250,7 +250,7 @@ func DeleteLabel(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if err := issues_model.DeleteLabel(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id")); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteLabel", err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index 23c7da3d96..1c12b0cc94 100644 --- a/routers/api/v1/org/member.go +++ b/routers/api/v1/org/member.go @@ -8,6 +8,7 @@ import ( "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" @@ -28,13 +29,13 @@ func listMembers(ctx *context.APIContext, isMember bool) { count, err := organization.CountOrgMembers(ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } members, _, err := organization.FindOrgMembers(ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -82,7 +83,7 @@ func ListMembers(ctx *context.APIContext) { if ctx.Doer != nil { isMember, err = ctx.Org.Organization.IsOrgMember(ctx, ctx.Doer.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "IsOrgMember", err) + ctx.APIErrorInternal(err) return } } @@ -132,7 +133,7 @@ func IsMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the user + // description: username of the user to check for an organization membership // type: string // required: true // responses: @@ -150,20 +151,20 @@ func IsMember(ctx *context.APIContext) { if ctx.Doer != nil { userIsMember, err := ctx.Org.Organization.IsOrgMember(ctx, ctx.Doer.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "IsOrgMember", err) + ctx.APIErrorInternal(err) return } else if userIsMember || ctx.Doer.IsAdmin { userToCheckIsMember, err := ctx.Org.Organization.IsOrgMember(ctx, userToCheck.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "IsOrgMember", err) + ctx.APIErrorInternal(err) } else if userToCheckIsMember { ctx.Status(http.StatusNoContent) } else { - ctx.NotFound() + ctx.APIErrorNotFound() } return } else if ctx.Doer.ID == userToCheck.ID { - ctx.NotFound() + ctx.APIErrorNotFound() return } } @@ -185,7 +186,7 @@ func IsPublicMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the user + // description: username of the user to check for a public organization membership // type: string // required: true // responses: @@ -200,13 +201,27 @@ func IsPublicMember(ctx *context.APIContext) { } is, err := organization.IsPublicMembership(ctx, ctx.Org.Organization.ID, userToCheck.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "IsPublicMembership", err) + ctx.APIErrorInternal(err) return } if is { ctx.Status(http.StatusNoContent) } else { - ctx.NotFound() + ctx.APIErrorNotFound() + } +} + +func checkCanChangeOrgUserStatus(ctx *context.APIContext, targetUser *user_model.User) { + // allow user themselves to change their status, and allow admins to change any user + if targetUser.ID == ctx.Doer.ID || ctx.Doer.IsAdmin { + return + } + // allow org owners to change status of members + isOwner, err := ctx.Org.Organization.IsOwnedBy(ctx, ctx.Doer.ID) + if err != nil { + ctx.APIError(http.StatusInternalServerError, err) + } else if !isOwner { + ctx.APIError(http.StatusForbidden, "Cannot change member visibility") } } @@ -225,7 +240,7 @@ func PublicizeMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the user + // description: username of the user whose membership is to be publicized // type: string // required: true // responses: @@ -240,13 +255,13 @@ func PublicizeMember(ctx *context.APIContext) { if ctx.Written() { return } - if userToPublicize.ID != ctx.Doer.ID { - ctx.Error(http.StatusForbidden, "", "Cannot publicize another member") + checkCanChangeOrgUserStatus(ctx, userToPublicize) + if ctx.Written() { return } err := organization.ChangeOrgUserStatus(ctx, ctx.Org.Organization.ID, userToPublicize.ID, true) if err != nil { - ctx.Error(http.StatusInternalServerError, "ChangeOrgUserStatus", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) @@ -267,7 +282,7 @@ func ConcealMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the user + // description: username of the user whose membership is to be concealed // type: string // required: true // responses: @@ -282,13 +297,13 @@ func ConcealMember(ctx *context.APIContext) { if ctx.Written() { return } - if userToConceal.ID != ctx.Doer.ID { - ctx.Error(http.StatusForbidden, "", "Cannot conceal another member") + checkCanChangeOrgUserStatus(ctx, userToConceal) + if ctx.Written() { return } err := organization.ChangeOrgUserStatus(ctx, ctx.Org.Organization.ID, userToConceal.ID, false) if err != nil { - ctx.Error(http.StatusInternalServerError, "ChangeOrgUserStatus", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) @@ -309,7 +324,7 @@ func DeleteMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the user + // description: username of the user to remove from the organization // type: string // required: true // responses: @@ -323,7 +338,7 @@ func DeleteMember(ctx *context.APIContext) { return } if err := org_service.RemoveOrgUser(ctx, ctx.Org.Organization, member); err != nil { - ctx.Error(http.StatusInternalServerError, "RemoveOrgUser", err) + ctx.APIErrorInternal(err) } ctx.Status(http.StatusNoContent) } diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index d65f922434..05744ba155 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -26,16 +26,14 @@ import ( func listUserOrgs(ctx *context.APIContext, u *user_model.User) { listOptions := utils.GetListOptions(ctx) - showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == u.ID) - opts := organization.FindOrgOptions{ - ListOptions: listOptions, - UserID: u.ID, - IncludePrivate: showPrivate, + ListOptions: listOptions, + UserID: u.ID, + IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, u), } orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "db.FindAndCount[organization.Organization]", err) + ctx.APIErrorInternal(err) return } @@ -84,7 +82,7 @@ func ListUserOrgs(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose organizations are to be listed // type: string // required: true // - name: page @@ -114,7 +112,7 @@ func GetUserOrgsPermissions(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose permissions are to be obtained // type: string // required: true // - name: org @@ -138,14 +136,14 @@ func GetUserOrgsPermissions(ctx *context.APIContext) { op := api.OrganizationPermissions{} if !organization.HasOrgOrUserVisible(ctx, o, ctx.ContextUser) { - ctx.NotFound("HasOrgOrUserVisible", nil) + ctx.APIErrorNotFound("HasOrgOrUserVisible", nil) return } org := organization.OrgFromUser(o) authorizeLevel, err := org.GetOrgUserMaxAuthorizeLevel(ctx, ctx.ContextUser.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetOrgUserAuthorizeLevel", err) + ctx.APIErrorInternal(err) return } @@ -164,7 +162,7 @@ func GetUserOrgsPermissions(ctx *context.APIContext) { op.CanCreateRepository, err = org.CanCreateOrgRepo(ctx, ctx.ContextUser.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "CanCreateOrgRepo", err) + ctx.APIErrorInternal(err) return } @@ -201,7 +199,7 @@ func GetAll(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) - publicOrgs, maxResults, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ + publicOrgs, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, ListOptions: listOptions, Type: user_model.UserTypeOrganization, @@ -209,7 +207,7 @@ func GetAll(ctx *context.APIContext) { Visible: vMode, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "SearchOrganizations", err) + ctx.APIErrorInternal(err) return } orgs := make([]*api.Organization, len(publicOrgs)) @@ -245,7 +243,7 @@ func Create(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.CreateOrgOption) if !ctx.Doer.CanCreateOrganization() { - ctx.Error(http.StatusForbidden, "Create organization not allowed", nil) + ctx.APIError(http.StatusForbidden, nil) return } @@ -271,9 +269,9 @@ func Create(ctx *context.APIContext) { db.IsErrNameReserved(err) || db.IsErrNameCharsNotAllowed(err) || db.IsErrNamePatternNotAllowed(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "CreateOrganization", err) + ctx.APIErrorInternal(err) } return } @@ -301,7 +299,7 @@ func Get(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) { - ctx.NotFound("HasOrgOrUserVisible", nil) + ctx.APIErrorNotFound("HasOrgOrUserVisible", nil) return } @@ -315,6 +313,44 @@ func Get(ctx *context.APIContext) { ctx.JSON(http.StatusOK, org) } +func Rename(ctx *context.APIContext) { + // swagger:operation POST /orgs/{org}/rename organization renameOrg + // --- + // summary: Rename an organization + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: existing org name + // type: string + // required: true + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/RenameOrgOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "422": + // "$ref": "#/responses/validationError" + + form := web.GetForm(ctx).(*api.RenameOrgOption) + orgUser := ctx.Org.Organization.AsUser() + if err := user_service.RenameUser(ctx, orgUser, form.NewName); err != nil { + if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + } else { + ctx.APIErrorInternal(err) + } + return + } + ctx.Status(http.StatusNoContent) +} + // Edit change an organization's information func Edit(ctx *context.APIContext) { // swagger:operation PATCH /orgs/{org} organization orgEdit @@ -345,7 +381,7 @@ func Edit(ctx *context.APIContext) { if form.Email != "" { if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil { - ctx.Error(http.StatusInternalServerError, "ReplacePrimaryEmailAddress", err) + ctx.APIErrorInternal(err) return } } @@ -359,7 +395,7 @@ func Edit(ctx *context.APIContext) { RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess), } if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateUser", err) + ctx.APIErrorInternal(err) return } @@ -386,7 +422,7 @@ func Delete(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if err := org.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteOrganization", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) @@ -431,7 +467,7 @@ func ListOrgActivityFeeds(ctx *context.APIContext) { org := organization.OrgFromUser(ctx.ContextUser) isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "IsOrgMember", err) + ctx.APIErrorInternal(err) return } includePrivate = isMember @@ -450,7 +486,7 @@ func ListOrgActivityFeeds(ctx *context.APIContext) { feeds, count, err := feed_service.GetFeeds(ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetFeeds", err) + ctx.APIErrorInternal(err) return } ctx.SetTotalCountHeader(count) diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 7f44f6ed95..1a1710750a 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -59,13 +59,13 @@ func ListTeams(ctx *context.APIContext) { OrgID: ctx.Org.Organization.ID, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "LoadTeams", err) + ctx.APIErrorInternal(err) return } apiTeams, err := convert.ToTeams(ctx, teams, false) if err != nil { - ctx.Error(http.StatusInternalServerError, "ConvertToTeams", err) + ctx.APIErrorInternal(err) return } @@ -98,13 +98,13 @@ func ListUserTeams(ctx *context.APIContext) { UserID: ctx.Doer.ID, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserTeams", err) + ctx.APIErrorInternal(err) return } apiTeams, err := convert.ToTeams(ctx, teams, true) if err != nil { - ctx.Error(http.StatusInternalServerError, "ConvertToTeams", err) + ctx.APIErrorInternal(err) return } @@ -134,33 +134,25 @@ func GetTeam(ctx *context.APIContext) { apiTeam, err := convert.ToTeam(ctx, ctx.Org.Team, true) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, apiTeam) } -func attachTeamUnits(team *organization.Team, units []string) { +func attachTeamUnits(team *organization.Team, defaultAccessMode perm.AccessMode, units []string) { unitTypes, _ := unit_model.FindUnitTypes(units...) team.Units = make([]*organization.TeamUnit, 0, len(units)) for _, tp := range unitTypes { team.Units = append(team.Units, &organization.TeamUnit{ OrgID: team.OrgID, Type: tp, - AccessMode: team.AccessMode, + AccessMode: defaultAccessMode, }) } } -func convertUnitsMap(unitsMap map[string]string) map[unit_model.Type]perm.AccessMode { - res := make(map[unit_model.Type]perm.AccessMode, len(unitsMap)) - for unitKey, p := range unitsMap { - res[unit_model.TypeFromKey(unitKey)] = perm.ParseAccessMode(p) - } - return res -} - func attachTeamUnitsMap(team *organization.Team, unitsMap map[string]string) { team.Units = make([]*organization.TeamUnit, 0, len(unitsMap)) for unitKey, p := range unitsMap { @@ -214,26 +206,24 @@ func CreateTeam(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.CreateTeamOption) - p := perm.ParseAccessMode(form.Permission) - if p < perm.AccessModeAdmin && len(form.UnitsMap) > 0 { - p = unit_model.MinUnitAccessMode(convertUnitsMap(form.UnitsMap)) - } + teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin) team := &organization.Team{ OrgID: ctx.Org.Organization.ID, Name: form.Name, Description: form.Description, IncludesAllRepositories: form.IncludesAllRepositories, CanCreateOrgRepo: form.CanCreateOrgRepo, - AccessMode: p, + AccessMode: teamPermission, } if team.AccessMode < perm.AccessModeAdmin { if len(form.UnitsMap) > 0 { attachTeamUnitsMap(team, form.UnitsMap) } else if len(form.Units) > 0 { - attachTeamUnits(team, form.Units) + unitPerm := perm.ParseAccessMode(form.Permission, perm.AccessModeRead, perm.AccessModeWrite) + attachTeamUnits(team, unitPerm, form.Units) } else { - ctx.Error(http.StatusInternalServerError, "getTeamUnits", errors.New("units permission should not be empty")) + ctx.APIErrorInternal(errors.New("units permission should not be empty")) return } } else { @@ -242,16 +232,16 @@ func CreateTeam(ctx *context.APIContext) { if err := org_service.NewTeam(ctx, team); err != nil { if organization.IsErrTeamAlreadyExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "NewTeam", err) + ctx.APIErrorInternal(err) } return } apiTeam, err := convert.ToTeam(ctx, team, true) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusCreated, apiTeam) @@ -285,7 +275,7 @@ func EditTeam(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.EditTeamOption) team := ctx.Org.Team if err := team.LoadUnits(ctx); err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -304,15 +294,10 @@ func EditTeam(ctx *context.APIContext) { isAuthChanged := false isIncludeAllChanged := false if !team.IsOwnerTeam() && len(form.Permission) != 0 { - // Validate permission level. - p := perm.ParseAccessMode(form.Permission) - if p < perm.AccessModeAdmin && len(form.UnitsMap) > 0 { - p = unit_model.MinUnitAccessMode(convertUnitsMap(form.UnitsMap)) - } - - if team.AccessMode != p { + teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin) + if team.AccessMode != teamPermission { isAuthChanged = true - team.AccessMode = p + team.AccessMode = teamPermission } if form.IncludesAllRepositories != nil { @@ -325,20 +310,21 @@ func EditTeam(ctx *context.APIContext) { if len(form.UnitsMap) > 0 { attachTeamUnitsMap(team, form.UnitsMap) } else if len(form.Units) > 0 { - attachTeamUnits(team, form.Units) + unitPerm := perm.ParseAccessMode(form.Permission, perm.AccessModeRead, perm.AccessModeWrite) + attachTeamUnits(team, unitPerm, form.Units) } } else { attachAdminTeamUnits(team) } if err := org_service.UpdateTeam(ctx, team, isAuthChanged, isIncludeAllChanged); err != nil { - ctx.Error(http.StatusInternalServerError, "EditTeam", err) + ctx.APIErrorInternal(err) return } apiTeam, err := convert.ToTeam(ctx, team) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, apiTeam) @@ -363,7 +349,7 @@ func DeleteTeam(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if err := org_service.DeleteTeam(ctx, ctx.Org.Team); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteTeam", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) @@ -399,10 +385,10 @@ func GetTeamMembers(ctx *context.APIContext) { isMember, err := organization.IsOrganizationMember(ctx, ctx.Org.Team.OrgID, ctx.Doer.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "IsOrganizationMember", err) + ctx.APIErrorInternal(err) return } else if !isMember && !ctx.Doer.IsAdmin { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -411,7 +397,7 @@ func GetTeamMembers(ctx *context.APIContext) { TeamID: ctx.Org.Team.ID, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetTeamMembers", err) + ctx.APIErrorInternal(err) return } @@ -440,7 +426,7 @@ func GetTeamMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the member to list + // description: username of the user whose data is to be listed // type: string // required: true // responses: @@ -456,10 +442,10 @@ func GetTeamMember(ctx *context.APIContext) { teamID := ctx.PathParamInt64("teamid") isTeamMember, err := organization.IsUserInTeams(ctx, u.ID, []int64{teamID}) if err != nil { - ctx.Error(http.StatusInternalServerError, "IsUserInTeams", err) + ctx.APIErrorInternal(err) return } else if !isTeamMember { - ctx.NotFound() + ctx.APIErrorNotFound() return } ctx.JSON(http.StatusOK, convert.ToUser(ctx, u, ctx.Doer)) @@ -481,7 +467,7 @@ func AddTeamMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the user to add + // description: username of the user to add to a team // type: string // required: true // responses: @@ -498,9 +484,9 @@ 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.Error(http.StatusForbidden, "AddTeamMember", err) + ctx.APIError(http.StatusForbidden, err) } else { - ctx.Error(http.StatusInternalServerError, "AddTeamMember", err) + ctx.APIErrorInternal(err) } return } @@ -523,7 +509,7 @@ func RemoveTeamMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the user to remove + // description: username of the user to remove from a team // type: string // required: true // responses: @@ -538,7 +524,7 @@ func RemoveTeamMember(ctx *context.APIContext) { } if err := org_service.RemoveTeamMember(ctx, ctx.Org.Team, u); err != nil { - ctx.Error(http.StatusInternalServerError, "RemoveTeamMember", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) @@ -578,14 +564,14 @@ func GetTeamRepos(ctx *context.APIContext) { TeamID: team.ID, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetTeamRepositories", err) + ctx.APIErrorInternal(err) return } repos := make([]*api.Repository, len(teamRepos)) for i, repo := range teamRepos { permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + ctx.APIErrorInternal(err) return } repos[i] = convert.ToRepo(ctx, repo, permission) @@ -630,13 +616,13 @@ func GetTeamRepo(ctx *context.APIContext) { } if !organization.HasTeamRepo(ctx, ctx.Org.Team.OrgID, ctx.Org.Team.ID, repo.ID) { - ctx.NotFound() + ctx.APIErrorNotFound() return } permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetTeamRepos", err) + ctx.APIErrorInternal(err) return } @@ -648,9 +634,9 @@ func getRepositoryByParams(ctx *context.APIContext) *repo_model.Repository { repo, err := repo_model.GetRepositoryByName(ctx, ctx.Org.Team.OrgID, ctx.PathParam("reponame")) if err != nil { if repo_model.IsErrRepoNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetRepositoryByName", err) + ctx.APIErrorInternal(err) } return nil } @@ -694,14 +680,14 @@ func AddTeamRepository(ctx *context.APIContext) { return } if access, err := access_model.AccessLevel(ctx, ctx.Doer, repo); err != nil { - ctx.Error(http.StatusInternalServerError, "AccessLevel", err) + ctx.APIErrorInternal(err) return } else if access < perm.AccessModeAdmin { - ctx.Error(http.StatusForbidden, "", "Must have admin-level access to the repository") + ctx.APIError(http.StatusForbidden, "Must have admin-level access to the repository") return } if err := repo_service.TeamAddRepository(ctx, ctx.Org.Team, repo); err != nil { - ctx.Error(http.StatusInternalServerError, "TeamAddRepository", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) @@ -746,14 +732,14 @@ func RemoveTeamRepository(ctx *context.APIContext) { return } if access, err := access_model.AccessLevel(ctx, ctx.Doer, repo); err != nil { - ctx.Error(http.StatusInternalServerError, "AccessLevel", err) + ctx.APIErrorInternal(err) return } else if access < perm.AccessModeAdmin { - ctx.Error(http.StatusForbidden, "", "Must have admin-level access to the repository") + ctx.APIError(http.StatusForbidden, "Must have admin-level access to the repository") return } if err := repo_service.RemoveRepositoryFromTeam(ctx, ctx.Org.Team, repo.ID); err != nil { - ctx.Error(http.StatusInternalServerError, "RemoveRepository", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) @@ -829,7 +815,7 @@ func SearchTeam(ctx *context.APIContext) { apiTeams, err := convert.ToTeams(ctx, teams, false) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -885,7 +871,7 @@ func ListTeamActivityFeeds(ctx *context.APIContext) { feeds, count, err := feed_service.GetFeeds(ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetFeeds", err) + ctx.APIErrorInternal(err) return } ctx.SetTotalCountHeader(count) diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index b38aa13167..41b7f2a43f 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -4,11 +4,14 @@ package packages 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" @@ -53,37 +56,18 @@ func ListPackages(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) - packageType := ctx.FormTrim("type") - query := ctx.FormTrim("q") - - pvs, count, err := packages.SearchVersions(ctx, &packages.PackageSearchOptions{ + apiPackages, count, err := searchPackages(ctx, &packages.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID, - Type: packages.Type(packageType), - Name: packages.SearchValue{Value: query}, + Type: packages.Type(ctx.FormTrim("type")), + Name: packages.SearchValue{Value: ctx.FormTrim("q")}, IsInternal: optional.Some(false), Paginator: &listOptions, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "SearchVersions", err) + ctx.APIErrorInternal(err) return } - pds, err := packages.GetPackageDescriptors(ctx, pvs) - if err != nil { - ctx.Error(http.StatusInternalServerError, "GetPackageDescriptors", err) - return - } - - apiPackages := make([]*api.Package, 0, len(pds)) - for _, pd := range pds { - apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer) - if err != nil { - ctx.Error(http.StatusInternalServerError, "Error converting package for api", err) - return - } - apiPackages = append(apiPackages, apiPackage) - } - ctx.SetLinkHeader(int(count), listOptions.PageSize) ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, apiPackages) @@ -125,7 +109,7 @@ func GetPackage(ctx *context.APIContext) { apiPackage, err := convert.ToPackage(ctx, ctx.Package.Descriptor, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "Error converting package for api", err) + ctx.APIErrorInternal(err) return } @@ -166,7 +150,7 @@ func DeletePackage(ctx *context.APIContext) { err := packages_service.RemovePackageVersion(ctx, ctx.Doer, ctx.Package.Descriptor.Version) if err != nil { - ctx.Error(http.StatusInternalServerError, "RemovePackageVersion", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) @@ -213,3 +197,260 @@ func ListPackageFiles(ctx *context.APIContext) { ctx.JSON(http.StatusOK, apiPackageFiles) } + +// ListPackageVersions gets all versions of a package +func ListPackageVersions(ctx *context.APIContext) { + // swagger:operation GET /packages/{owner}/{type}/{name} package listPackageVersions + // --- + // summary: Gets all versions of a package + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the package + // type: string + // required: true + // - name: type + // in: path + // description: type of the package + // type: string + // required: true + // - name: name + // in: path + // description: name of the package + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/PackageList" + // "404": + // "$ref": "#/responses/notFound" + + listOptions := utils.GetListOptions(ctx) + + apiPackages, count, err := searchPackages(ctx, &packages.PackageSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + Type: packages.Type(ctx.PathParam("type")), + Name: packages.SearchValue{Value: ctx.PathParam("name"), ExactMatch: true}, + IsInternal: optional.Some(false), + Paginator: &listOptions, + }) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + ctx.SetLinkHeader(int(count), listOptions.PageSize) + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, apiPackages) +} + +// GetLatestPackageVersion gets the latest version of a package +func GetLatestPackageVersion(ctx *context.APIContext) { + // swagger:operation GET /packages/{owner}/{type}/{name}/-/latest package getLatestPackageVersion + // --- + // summary: Gets the latest version of a package + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the package + // type: string + // required: true + // - name: type + // in: path + // description: type of the package + // type: string + // required: true + // - name: name + // in: path + // description: name of the package + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/Package" + // "404": + // "$ref": "#/responses/notFound" + + pvs, _, err := packages.SearchLatestVersions(ctx, &packages.PackageSearchOptions{ + OwnerID: ctx.Package.Owner.ID, + Type: packages.Type(ctx.PathParam("type")), + Name: packages.SearchValue{Value: ctx.PathParam("name"), ExactMatch: true}, + IsInternal: optional.Some(false), + }) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if len(pvs) == 0 { + ctx.APIError(http.StatusNotFound, err) + return + } + + pd, err := packages.GetPackageDescriptor(ctx, pvs[0]) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + ctx.JSON(http.StatusOK, apiPackage) +} + +// LinkPackage sets a repository link for a package +func LinkPackage(ctx *context.APIContext) { + // swagger:operation POST /packages/{owner}/{type}/{name}/-/link/{repo_name} package linkPackage + // --- + // summary: Link a package to a repository + // parameters: + // - name: owner + // in: path + // description: owner of the package + // type: string + // required: true + // - name: type + // in: path + // description: type of the package + // type: string + // required: true + // - name: name + // in: path + // description: name of the package + // type: string + // required: true + // - name: repo_name + // in: path + // description: name of the repository to link. + // type: string + // required: true + // responses: + // "201": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name")) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusNotFound, err) + } else { + ctx.APIErrorInternal(err) + } + return + } + + repo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ctx.PathParam("repo_name")) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusNotFound, err) + } else { + ctx.APIErrorInternal(err) + } + return + } + + err = packages_service.LinkToRepository(ctx, pkg, repo, ctx.Doer) + if err != nil { + switch { + case errors.Is(err, util.ErrInvalidArgument): + ctx.APIError(http.StatusBadRequest, err) + case errors.Is(err, util.ErrPermissionDenied): + ctx.APIError(http.StatusForbidden, err) + default: + ctx.APIErrorInternal(err) + } + return + } + ctx.Status(http.StatusCreated) +} + +// UnlinkPackage sets a repository link for a package +func UnlinkPackage(ctx *context.APIContext) { + // swagger:operation POST /packages/{owner}/{type}/{name}/-/unlink package unlinkPackage + // --- + // summary: Unlink a package from a repository + // parameters: + // - name: owner + // in: path + // description: owner of the package + // type: string + // required: true + // - name: type + // in: path + // description: type of the package + // type: string + // required: true + // - name: name + // in: path + // description: name of the package + // type: string + // required: true + // responses: + // "201": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name")) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusNotFound, err) + } else { + ctx.APIErrorInternal(err) + } + return + } + + err = packages_service.UnlinkFromRepository(ctx, pkg, ctx.Doer) + if err != nil { + switch { + case errors.Is(err, util.ErrPermissionDenied): + ctx.APIError(http.StatusForbidden, err) + case errors.Is(err, util.ErrInvalidArgument): + ctx.APIError(http.StatusBadRequest, err) + default: + ctx.APIErrorInternal(err) + } + return + } + ctx.Status(http.StatusNoContent) +} + +func searchPackages(ctx *context.APIContext, opts *packages.PackageSearchOptions) ([]*api.Package, int64, error) { + pvs, count, err := packages.SearchVersions(ctx, opts) + if err != nil { + return nil, 0, err + } + + pds, err := packages.GetPackageDescriptors(ctx, pvs) + if err != nil { + return nil, 0, err + } + + apiPackages := make([]*api.Package, 0, len(pds)) + for _, pd := range pds { + apiPackage, err := convert.ToPackage(ctx, pd, ctx.Doer) + if err != nil { + return nil, 0, err + } + apiPackages = append(apiPackages, apiPackage) + } + + return apiPackages, count, nil +} diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index d27e8d2427..a57db015f0 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -4,12 +4,25 @@ package repo import ( + go_context "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" "errors" + "fmt" "net/http" + "net/url" + "strconv" + "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" + "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" @@ -19,6 +32,8 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" secret_service "code.gitea.io/gitea/services/secrets" + + "github.com/nektos/act/pkg/model" ) // ListActionsSecrets list an repo's actions secrets @@ -62,15 +77,16 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) { secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } apiSecrets := make([]*api.Secret, len(secrets)) for k, v := range secrets { apiSecrets[k] = &api.Secret{ - Name: v.Name, - Created: v.CreatedUnix.AsTime(), + Name: v.Name, + Description: v.Description, + Created: v.CreatedUnix.AsTime(), } } @@ -121,14 +137,14 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) { opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption) - _, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, repo.ID, ctx.PathParam("secretname"), opt.Data) + _, 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.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err) + ctx.APIError(http.StatusBadRequest, err) } else if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "CreateOrUpdateSecret", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err) + ctx.APIErrorInternal(err) } return } @@ -167,7 +183,7 @@ func (Action) DeleteSecret(ctx *context.APIContext) { // required: true // responses: // "204": - // description: delete one secret of the organization + // description: delete one secret of the repository // "400": // "$ref": "#/responses/error" // "404": @@ -178,11 +194,11 @@ 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.Error(http.StatusBadRequest, "DeleteSecret", err) + ctx.APIError(http.StatusBadRequest, err) } else if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "DeleteSecret", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "DeleteSecret", err) + ctx.APIErrorInternal(err) } return } @@ -226,18 +242,19 @@ func (Action) GetVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "GetVariable", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "GetVariable", err) + ctx.APIErrorInternal(err) } return } variable := &api.ActionVariable{ - OwnerID: v.OwnerID, - RepoID: v.RepoID, - Name: v.Name, - Data: v.Data, + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + Description: v.Description, } ctx.JSON(http.StatusOK, variable) @@ -280,11 +297,11 @@ 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.Error(http.StatusBadRequest, "DeleteVariableByName", err) + ctx.APIError(http.StatusBadRequest, err) } else if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "DeleteVariableByName", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err) + ctx.APIErrorInternal(err) } return } @@ -322,12 +339,12 @@ func (Action) CreateVariable(ctx *context.APIContext) { // responses: // "201": // description: response when creating a repo-level variable - // "204": - // description: response when creating a repo-level variable // "400": // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/notFound" + // "409": + // description: variable name already exists. + // "500": + // "$ref": "#/responses/error" opt := web.GetForm(ctx).(*api.CreateVariableOption) @@ -339,24 +356,24 @@ func (Action) CreateVariable(ctx *context.APIContext) { Name: variableName, }) if err != nil && !errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusInternalServerError, "GetVariable", err) + ctx.APIErrorInternal(err) return } if v != nil && v.ID > 0 { - ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + ctx.APIError(http.StatusConflict, util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) return } - if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value); err != nil { + if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value, opt.Description); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.Error(http.StatusBadRequest, "CreateVariable", err) + ctx.APIError(http.StatusBadRequest, err) } else { - ctx.Error(http.StatusInternalServerError, "CreateVariable", err) + ctx.APIErrorInternal(err) } return } - ctx.Status(http.StatusNoContent) + ctx.Status(http.StatusCreated) } // UpdateVariable update a repo-level variable @@ -404,9 +421,9 @@ func (Action) UpdateVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "GetVariable", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "GetVariable", err) + ctx.APIErrorInternal(err) } return } @@ -414,11 +431,16 @@ func (Action) UpdateVariable(ctx *context.APIContext) { if opt.Name == "" { opt.Name = ctx.PathParam("variablename") } - if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil { + + v.Name = opt.Name + v.Data = opt.Value + v.Description = opt.Description + + if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.Error(http.StatusBadRequest, "UpdateVariable", err) + ctx.APIError(http.StatusBadRequest, err) } else { - ctx.Error(http.StatusInternalServerError, "UpdateVariable", err) + ctx.APIErrorInternal(err) } return } @@ -465,16 +487,18 @@ func (Action) ListVariables(ctx *context.APIContext) { ListOptions: utils.GetListOptions(ctx), }) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindVariables", err) + ctx.APIErrorInternal(err) return } variables := make([]*api.ActionVariable, len(vars)) for i, v := range vars { variables[i] = &api.ActionVariable{ - OwnerID: v.OwnerID, - RepoID: v.RepoID, - Name: v.Name, + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + Description: v.Description, } } @@ -507,6 +531,233 @@ func (Action) GetRegistrationToken(ctx *context.APIContext) { shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID) } +// CreateRegistrationToken returns the token to register repo runners +func (Action) CreateRegistrationToken(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/actions/runners/registration-token repository repoCreateRunnerRegistrationToken + // --- + // summary: Get a repository's actions runner registration token + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/RegistrationToken" + + shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID) +} + +// ListRunners get repo-level runners +func (Action) ListRunners(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runners repository getRepoRunners + // --- + // summary: Get repo-level runners + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/definitions/ActionRunnersResponse" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.ListRunners(ctx, 0, ctx.Repo.Repository.ID) +} + +// GetRunner get an repo-level runner +func (Action) GetRunner(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runners/{runner_id} repository getRepoRunner + // --- + // summary: Get an repo-level runner + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/definitions/ActionRunner" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.GetRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id")) +} + +// DeleteRunner delete an repo-level runner +func (Action) DeleteRunner(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/actions/runners/{runner_id} repository deleteRepoRunner + // --- + // summary: Delete an repo-level runner + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "204": + // description: runner has been deleted + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id")) +} + +// GetWorkflowRunJobs Lists all jobs for a workflow run. +func (Action) ListWorkflowJobs(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/jobs repository listWorkflowJobs + // --- + // summary: Lists all jobs for a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WorkflowJobsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + repoID := ctx.Repo.Repository.ID + + shared.ListJobs(ctx, 0, repoID, 0) +} + +// ListWorkflowRuns Lists all runs for a repository run. +func (Action) ListWorkflowRuns(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs repository getWorkflowRuns + // --- + // summary: Lists all runs for a repository run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: event + // in: query + // description: workflow event name + // type: string + // required: false + // - name: branch + // in: query + // description: workflow branch + // type: string + // required: false + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: actor + // in: query + // description: triggered by user + // type: string + // required: false + // - name: head_sha + // in: query + // description: triggering sha of the workflow run + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/ArtifactsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + repoID := ctx.Repo.Repository.ID + + shared.ListRuns(ctx, 0, repoID) +} + var _ actions_service.API = new(Action) // Action implements actions_service.API @@ -562,7 +813,7 @@ func ListActionTasks(ctx *context.APIContext) { RepoID: ctx.Repo.Repository.ID, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "ListActionTasks", err) + ctx.APIErrorInternal(err) return } @@ -573,7 +824,7 @@ func ListActionTasks(ctx *context.APIContext) { for i := range tasks { convertedTask, err := convert.ToActionTask(ctx, tasks[i]) if err != nil { - ctx.Error(http.StatusInternalServerError, "ToActionTask", err) + ctx.APIErrorInternal(err) return } res.Entries[i] = convertedTask @@ -581,3 +832,852 @@ func ListActionTasks(ctx *context.APIContext) { ctx.JSON(http.StatusOK, &res) } + +func ActionsListRepositoryWorkflows(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/workflows repository ActionsListRepositoryWorkflows + // --- + // summary: List repository workflows + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/ActionWorkflowList" + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + // "500": + // "$ref": "#/responses/error" + + workflows, err := convert.ListActionWorkflows(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + ctx.JSON(http.StatusOK, &api.ActionWorkflowResponse{Workflows: workflows, TotalCount: int64(len(workflows))}) +} + +func ActionsGetWorkflow(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/workflows/{workflow_id} repository ActionsGetWorkflow + // --- + // summary: Get 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 + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/ActionWorkflow" + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + // "500": + // "$ref": "#/responses/error" + + workflowID := ctx.PathParam("workflow_id") + 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) + } else { + ctx.APIErrorInternal(err) + } + return + } + + ctx.JSON(http.StatusOK, workflow) +} + +func ActionsDisableWorkflow(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/disable repository ActionsDisableWorkflow + // --- + // summary: Disable 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 + // type: string + // required: true + // responses: + // "204": + // description: No Content + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + workflowID := ctx.PathParam("workflow_id") + err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, false) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusNotFound, err) + } else { + ctx.APIErrorInternal(err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +func ActionsDispatchWorkflow(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches repository ActionsDispatchWorkflow + // --- + // summary: Create a workflow dispatch event + // produces: + // - application/json + // parameters: + // - name: owner + // in: 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 + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateActionWorkflowDispatch" + // responses: + // "204": + // description: No Content + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + workflowID := ctx.PathParam("workflow_id") + opt := web.GetForm(ctx).(*api.CreateActionWorkflowDispatch) + if opt.Ref == "" { + ctx.APIError(http.StatusUnprocessableEntity, util.NewInvalidArgumentErrorf("ref is required parameter")) + return + } + + err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error { + 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 + for name, config := range workflowDispatch.Inputs { + value := ctx.FormString("inputs["+name+"]", config.Default) + inputs[name] = value + } + } else { + for name, config := range workflowDispatch.Inputs { + value, ok := opt.Inputs[name] + if ok { + inputs[name] = value + } else { + inputs[name] = config.Default + } + } + } + return nil + }) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusNotFound, err) + } else if errors.Is(err, util.ErrPermissionDenied) { + ctx.APIError(http.StatusForbidden, err) + } else { + ctx.APIErrorInternal(err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +func ActionsEnableWorkflow(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/enable repository ActionsEnableWorkflow + // --- + // summary: Enable 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 + // type: string + // required: true + // responses: + // "204": + // description: No Content + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "409": + // "$ref": "#/responses/conflict" + // "422": + // "$ref": "#/responses/validationError" + + workflowID := ctx.PathParam("workflow_id") + err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, true) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusNotFound, err) + } else { + ctx.APIErrorInternal(err) + } + return + } + + ctx.Status(http.StatusNoContent) +} + +// GetWorkflowRun Gets a specific workflow run. +func GetWorkflowRun(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun + // --- + // summary: Gets a specific workflow run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // 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: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/WorkflowRun" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + runID := ctx.PathParamInt64("run") + job, _, err := db.GetByID[actions_model.ActionRun](ctx, runID) + + if err != nil || job.RepoID != ctx.Repo.Repository.ID { + ctx.APIError(http.StatusNotFound, util.ErrNotExist) + } + + convertedArtifact, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, job) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.JSON(http.StatusOK, convertedArtifact) +} + +// ListWorkflowRunJobs Lists all jobs for a workflow run. +func ListWorkflowRunJobs(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs repository listWorkflowRunJobs + // --- + // summary: Lists all jobs for a workflow run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run + // in: path + // description: runid of the workflow 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" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + repoID := ctx.Repo.Repository.ID + + runID := 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")) + 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) +} + +// GetWorkflowJob Gets a specific workflow job for a workflow run. +func GetWorkflowJob(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id} repository getWorkflowJob + // --- + // summary: Gets a specific workflow job for a workflow run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: job_id + // in: path + // description: id of the job + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/WorkflowJob" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + jobID := ctx.PathParamInt64("job_id") + job, _, err := db.GetByID[actions_model.ActionRunJob](ctx, jobID) + + if err != nil || job.RepoID != ctx.Repo.Repository.ID { + ctx.APIError(http.StatusNotFound, util.ErrNotExist) + } + + convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, job) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.JSON(http.StatusOK, convertedWorkflowJob) +} + +// GetArtifacts Lists all artifacts for a repository. +func GetArtifactsOfRun(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/artifacts repository getArtifactsOfRun + // --- + // summary: Lists all artifacts for a repository run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run + // in: path + // description: runid of the workflow run + // type: integer + // required: true + // - name: name + // in: query + // description: name of the artifact + // type: string + // required: false + // responses: + // "200": + // "$ref": "#/responses/ArtifactsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + repoID := ctx.Repo.Repository.ID + artifactName := ctx.Req.URL.Query().Get("name") + + runID := ctx.PathParamInt64("run") + + artifacts, total, err := db.FindAndCount[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{ + RepoID: repoID, + RunID: runID, + ArtifactName: artifactName, + FinalizedArtifactsV4: true, + ListOptions: utils.GetListOptions(ctx), + }) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + res := new(api.ActionArtifactsResponse) + res.TotalCount = total + + res.Entries = make([]*api.ActionArtifact, len(artifacts)) + for i := range artifacts { + convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, artifacts[i]) + if err != nil { + ctx.APIErrorInternal(err) + return + } + res.Entries[i] = convertedArtifact + } + + ctx.JSON(http.StatusOK, &res) +} + +// DeleteActionRun Delete a workflow run +func DeleteActionRun(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/actions/runs/{run} repository deleteActionRun + // --- + // summary: Delete a workflow run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run + // in: path + // description: runid of the workflow run + // type: integer + // required: true + // responses: + // "204": + // description: "No Content" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + runID := ctx.PathParamInt64("run") + run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID) + if errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusNotFound, err) + return + } else if err != nil { + ctx.APIErrorInternal(err) + return + } + if !run.Status.IsDone() { + ctx.APIError(http.StatusBadRequest, "this workflow run is not done") + return + } + + if err := actions_service.DeleteRun(ctx, run); err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.Status(http.StatusNoContent) +} + +// GetArtifacts Lists all artifacts for a repository. +func GetArtifacts(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/artifacts repository getArtifacts + // --- + // summary: Lists all artifacts for a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: name + // in: query + // description: name of the artifact + // type: string + // required: false + // responses: + // "200": + // "$ref": "#/responses/ArtifactsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + repoID := ctx.Repo.Repository.ID + artifactName := ctx.Req.URL.Query().Get("name") + + artifacts, total, err := db.FindAndCount[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{ + RepoID: repoID, + ArtifactName: artifactName, + FinalizedArtifactsV4: true, + ListOptions: utils.GetListOptions(ctx), + }) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + res := new(api.ActionArtifactsResponse) + res.TotalCount = total + + res.Entries = make([]*api.ActionArtifact, len(artifacts)) + for i := range artifacts { + convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, artifacts[i]) + if err != nil { + ctx.APIErrorInternal(err) + return + } + res.Entries[i] = convertedArtifact + } + + ctx.JSON(http.StatusOK, &res) +} + +// GetArtifact Gets a specific artifact for a workflow run. +func GetArtifact(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id} repository getArtifact + // --- + // summary: Gets a specific artifact for a workflow run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: artifact_id + // in: path + // description: id of the artifact + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/Artifact" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + art := getArtifactByPathParam(ctx, ctx.Repo.Repository) + if ctx.Written() { + return + } + + if actions.IsArtifactV4(art) { + convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, art) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.JSON(http.StatusOK, convertedArtifact) + return + } + // v3 not supported due to not having one unique id + ctx.APIError(http.StatusNotFound, "Artifact not found") +} + +// DeleteArtifact Deletes a specific artifact for a workflow run. +func DeleteArtifact(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/actions/artifacts/{artifact_id} repository deleteArtifact + // --- + // summary: Deletes a specific artifact for a workflow run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: artifact_id + // in: path + // description: id of the artifact + // type: string + // required: true + // responses: + // "204": + // description: "No Content" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + art := getArtifactByPathParam(ctx, ctx.Repo.Repository) + if ctx.Written() { + return + } + + if actions.IsArtifactV4(art) { + if err := actions_model.SetArtifactNeedDelete(ctx, art.RunID, art.ArtifactName); err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.Status(http.StatusNoContent) + return + } + // v3 not supported due to not having one unique id + ctx.APIError(http.StatusNotFound, "Artifact not found") +} + +func buildSignature(endp string, expires, artifactID int64) []byte { + mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret()) + mac.Write([]byte(endp)) + fmt.Fprint(mac, expires) + fmt.Fprint(mac, artifactID) + return mac.Sum(nil) +} + +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 buildSigURL(ctx go_context.Context, endPoint string, artifactID int64) string { + // endPoint is a path like "api/v1/repos/owner/repo/actions/artifacts/1/zip/raw" + expires := time.Now().Add(60 * time.Minute).Unix() + uploadURL := httplib.GuessCurrentAppURL(ctx) + endPoint + "?sig=" + base64.URLEncoding.EncodeToString(buildSignature(endPoint, expires, artifactID)) + "&expires=" + strconv.FormatInt(expires, 10) + return uploadURL +} + +// DownloadArtifact Downloads a specific artifact for a workflow run redirects to blob url. +func DownloadArtifact(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/zip repository downloadArtifact + // --- + // summary: Downloads a specific artifact for a workflow run redirects to blob url + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: artifact_id + // in: path + // description: id of the artifact + // type: string + // required: true + // responses: + // "302": + // description: redirect to the blob download + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + art := getArtifactByPathParam(ctx, ctx.Repo.Repository) + if ctx.Written() { + 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 + } + ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName)) + + if actions.IsArtifactV4(art) { + ok, err := actions.DownloadArtifactV4ServeDirectOnly(ctx.Base, art) + if ok { + return + } + if err != nil { + ctx.APIErrorInternal(err) + return + } + + redirectURL := buildSigURL(ctx, buildDownloadRawEndpoint(ctx.Repo.Repository, art.ID), art.ID) + ctx.Redirect(redirectURL, http.StatusFound) + return + } + // v3 not supported due to not having one unique id + ctx.APIError(http.StatusNotFound, "Artifact not found") +} + +// 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")) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.APIErrorNotFound() + } else { + ctx.APIErrorInternal(err) + } + return + } + art := getArtifactByPathParam(ctx, repo) + if ctx.Written() { + return + } + + sigStr := ctx.Req.URL.Query().Get("sig") + expiresStr := ctx.Req.URL.Query().Get("expires") + sigBytes, _ := base64.URLEncoding.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 + } + ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(art.ArtifactName), art.ArtifactName)) + + if actions.IsArtifactV4(art) { + err := actions.DownloadArtifactV4(ctx.Base, art) + if err != nil { + ctx.APIErrorInternal(err) + return + } + return + } + // v3 not supported due to not having one unique id + ctx.APIError(http.StatusNotFound, "artifact not found") +} + +// Try to get the artifact by ID and check access +func getArtifactByPathParam(ctx *context.APIContext, repo *repo_model.Repository) *actions_model.ActionArtifact { + artifactID := ctx.PathParamInt64("artifact_id") + + art, ok, err := db.GetByID[actions_model.ActionArtifact](ctx, artifactID) + if err != nil { + ctx.APIErrorInternal(err) + return nil + } + // if artifacts status is not uploaded-confirmed, treat it as not found + // only check RepoID here, because the repository owner may change over the time + if !ok || + art.RepoID != repo.ID || + art.Status != actions_model.ArtifactStatusUploadConfirmed && art.Status != actions_model.ArtifactStatusExpired { + ctx.APIError(http.StatusNotFound, "artifact not found") + return nil + } + return art +} diff --git a/routers/api/v1/repo/actions_run.go b/routers/api/v1/repo/actions_run.go new file mode 100644 index 0000000000..c6d18af6aa --- /dev/null +++ b/routers/api/v1/repo/actions_run.go @@ -0,0 +1,64 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +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" +) + +func DownloadActionsRunJobLogs(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs repository downloadActionsRunJobLogs + // --- + // summary: Downloads the job logs for a workflow run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: job_id + // in: path + // description: id of the job + // type: integer + // required: true + // responses: + // "200": + // description: output blob content + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + jobID := ctx.PathParamInt64("job_id") + curJob, err := actions_model.GetRunJobByID(ctx, jobID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if err = curJob.LoadRepo(ctx); err != nil { + ctx.APIErrorInternal(err) + return + } + + err = common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, curJob) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.APIErrorNotFound(err) + } else { + ctx.APIErrorInternal(err) + } + } +} diff --git a/routers/api/v1/repo/avatar.go b/routers/api/v1/repo/avatar.go index 698337ffd2..593460586f 100644 --- a/routers/api/v1/repo/avatar.go +++ b/routers/api/v1/repo/avatar.go @@ -44,13 +44,13 @@ func UpdateAvatar(ctx *context.APIContext) { content, err := base64.StdEncoding.DecodeString(form.Image) if err != nil { - ctx.Error(http.StatusBadRequest, "DecodeImage", err) + ctx.APIError(http.StatusBadRequest, err) return } err = repo_service.UploadAvatar(ctx, ctx.Repo.Repository, content) if err != nil { - ctx.Error(http.StatusInternalServerError, "UploadAvatar", err) + ctx.APIErrorInternal(err) } ctx.Status(http.StatusNoContent) @@ -81,7 +81,7 @@ func DeleteAvatar(ctx *context.APIContext) { // "$ref": "#/responses/notFound" err := repo_service.DeleteAvatar(ctx, ctx.Repo.Repository) if err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteAvatar", err) + ctx.APIErrorInternal(err) } ctx.Status(http.StatusNoContent) diff --git a/routers/api/v1/repo/blob.go b/routers/api/v1/repo/blob.go index f38086954b..9a17fc1bbf 100644 --- a/routers/api/v1/repo/blob.go +++ b/routers/api/v1/repo/blob.go @@ -43,12 +43,12 @@ func GetBlob(ctx *context.APIContext) { sha := ctx.PathParam("sha") if len(sha) == 0 { - ctx.Error(http.StatusBadRequest, "", "sha not provided") + ctx.APIError(http.StatusBadRequest, "sha not provided") return } - if blob, err := files_service.GetBlobBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil { - ctx.Error(http.StatusBadRequest, "", err) + if blob, err := files_service.GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil { + ctx.APIError(http.StatusBadRequest, err) } else { ctx.JSON(http.StatusOK, blob) } diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index a5ea752cf1..9af958a5b7 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -6,7 +6,6 @@ package repo import ( "errors" - "fmt" "net/http" "code.gitea.io/gitea/models/db" @@ -60,31 +59,30 @@ func GetBranch(ctx *context.APIContext) { branchName := ctx.PathParam("*") - branch, err := ctx.Repo.GitRepo.GetBranch(branchName) + exist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, branchName) if err != nil { - if git.IsErrBranchNotExist(err) { - ctx.NotFound(err) - } else { - ctx.Error(http.StatusInternalServerError, "GetBranch", err) - } + ctx.APIErrorInternal(err) + return + } else if !exist { + ctx.APIErrorNotFound(err) return } - c, err := branch.GetCommit() + c, err := ctx.Repo.GitRepo.GetBranchCommit(branchName) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetCommit", err) + ctx.APIErrorInternal(err) return } branchProtection, err := git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, branchName) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err) + ctx.APIErrorInternal(err) return } - br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch.Name, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) + br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branchName, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) if err != nil { - ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err) + ctx.APIErrorInternal(err) return } @@ -124,12 +122,12 @@ func DeleteBranch(ctx *context.APIContext) { // "423": // "$ref": "#/responses/repoArchivedError" if ctx.Repo.Repository.IsEmpty { - ctx.Error(http.StatusNotFound, "", "Git Repository is empty.") + ctx.APIError(http.StatusNotFound, "Git Repository is empty.") return } if ctx.Repo.Repository.IsMirror { - ctx.Error(http.StatusForbidden, "", "Git Repository is a mirror.") + ctx.APIError(http.StatusForbidden, "Git Repository is a mirror.") return } @@ -141,13 +139,13 @@ func DeleteBranch(ctx *context.APIContext) { IsDeletedBranch: optional.Some(false), }) if err != nil { - ctx.Error(http.StatusInternalServerError, "CountBranches", err) + ctx.APIErrorInternal(err) return } if totalNumOfBranches == 0 { // sync branches immediately because non-empty repository should have at least 1 branch _, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0) if err != nil { - ctx.ServerError("SyncRepoBranches", err) + ctx.APIErrorInternal(err) return } } @@ -155,13 +153,13 @@ func DeleteBranch(ctx *context.APIContext) { if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName, nil); err != nil { switch { case git.IsErrBranchNotExist(err): - ctx.NotFound(err) + ctx.APIErrorNotFound(err) case errors.Is(err, repo_service.ErrBranchIsDefault): - ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch")) + ctx.APIError(http.StatusForbidden, errors.New("can not delete default branch")) case errors.Is(err, git_model.ErrBranchIsProtected): - ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected")) + ctx.APIError(http.StatusForbidden, errors.New("branch protected")) default: - ctx.Error(http.StatusInternalServerError, "DeleteBranch", err) + ctx.APIErrorInternal(err) } return } @@ -206,12 +204,12 @@ func CreateBranch(ctx *context.APIContext) { // "$ref": "#/responses/repoArchivedError" if ctx.Repo.Repository.IsEmpty { - ctx.Error(http.StatusNotFound, "", "Git Repository is empty.") + ctx.APIError(http.StatusNotFound, "Git Repository is empty.") return } if ctx.Repo.Repository.IsMirror { - ctx.Error(http.StatusForbidden, "", "Git Repository is a mirror.") + ctx.APIError(http.StatusForbidden, "Git Repository is a mirror.") return } @@ -223,24 +221,24 @@ func CreateBranch(ctx *context.APIContext) { if len(opt.OldRefName) > 0 { oldCommit, err = ctx.Repo.GitRepo.GetCommit(opt.OldRefName) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetCommit", err) + ctx.APIErrorInternal(err) return } - } else if len(opt.OldBranchName) > 0 { //nolint - if ctx.Repo.GitRepo.IsBranchExist(opt.OldBranchName) { //nolint - oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(opt.OldBranchName) //nolint + } else if len(opt.OldBranchName) > 0 { //nolint:staticcheck // deprecated field + if gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, opt.OldBranchName) { //nolint:staticcheck // deprecated field + oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(opt.OldBranchName) //nolint:staticcheck // deprecated field if err != nil { - ctx.Error(http.StatusInternalServerError, "GetBranchCommit", err) + ctx.APIErrorInternal(err) return } } else { - ctx.Error(http.StatusNotFound, "", "The old branch does not exist") + ctx.APIError(http.StatusNotFound, "The old branch does not exist") return } } else { oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetBranchCommit", err) + ctx.APIErrorInternal(err) return } } @@ -248,40 +246,34 @@ func CreateBranch(ctx *context.APIContext) { err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, oldCommit.ID.String(), opt.BranchName) if err != nil { if git_model.IsErrBranchNotExist(err) { - ctx.Error(http.StatusNotFound, "", "The old branch does not exist") + ctx.APIError(http.StatusNotFound, "The old branch does not exist") } else if release_service.IsErrTagAlreadyExists(err) { - ctx.Error(http.StatusConflict, "", "The branch with the same tag already exists.") + ctx.APIError(http.StatusConflict, "The branch with the same tag already exists.") } else if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) { - ctx.Error(http.StatusConflict, "", "The branch already exists.") + ctx.APIError(http.StatusConflict, "The branch already exists.") } else if git_model.IsErrBranchNameConflict(err) { - ctx.Error(http.StatusConflict, "", "The branch with the same name already exists.") + ctx.APIError(http.StatusConflict, "The branch with the same name already exists.") } else { - ctx.Error(http.StatusInternalServerError, "CreateNewBranchFromCommit", err) + ctx.APIErrorInternal(err) } return } - branch, err := ctx.Repo.GitRepo.GetBranch(opt.BranchName) + commit, err := ctx.Repo.GitRepo.GetBranchCommit(opt.BranchName) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetBranch", err) + ctx.APIErrorInternal(err) return } - commit, err := branch.GetCommit() + branchProtection, err := git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, opt.BranchName) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetCommit", err) + ctx.APIErrorInternal(err) return } - branchProtection, err := git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, branch.Name) + br, err := convert.ToBranch(ctx, ctx.Repo.Repository, opt.BranchName, commit, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err) - return - } - - br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch.Name, commit, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) - if err != nil { - ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err) + ctx.APIErrorInternal(err) return } @@ -325,7 +317,7 @@ func ListBranches(ctx *context.APIContext) { if !ctx.Repo.Repository.IsEmpty { if ctx.Repo.GitRepo == nil { - ctx.Error(http.StatusInternalServerError, "Load git repository failed", nil) + ctx.APIErrorInternal(nil) return } @@ -337,26 +329,26 @@ func ListBranches(ctx *context.APIContext) { var err error totalNumOfBranches, err = db.Count[git_model.Branch](ctx, branchOpts) if err != nil { - ctx.Error(http.StatusInternalServerError, "CountBranches", err) + ctx.APIErrorInternal(err) return } if totalNumOfBranches == 0 { // sync branches immediately because non-empty repository should have at least 1 branch totalNumOfBranches, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0) if err != nil { - ctx.ServerError("SyncRepoBranches", err) + ctx.APIErrorInternal(err) return } } rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindMatchedProtectedBranchRules", err) + ctx.APIErrorInternal(err) return } branches, err := db.Find[git_model.Branch](ctx, branchOpts) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetBranches", err) + ctx.APIErrorInternal(err) return } @@ -369,14 +361,14 @@ func ListBranches(ctx *context.APIContext) { totalNumOfBranches-- continue } - ctx.Error(http.StatusInternalServerError, "GetCommit", err) + ctx.APIErrorInternal(err) return } branchProtection := rules.GetFirstMatched(branches[i].Name) apiBranch, err := convert.ToBranch(ctx, ctx.Repo.Repository, branches[i].Name, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin()) if err != nil { - ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err) + ctx.APIErrorInternal(err) return } apiBranches = append(apiBranches, apiBranch) @@ -433,12 +425,12 @@ func UpdateBranch(ctx *context.APIContext) { repo := ctx.Repo.Repository if repo.IsEmpty { - ctx.Error(http.StatusNotFound, "", "Git Repository is empty.") + ctx.APIError(http.StatusNotFound, "Git Repository is empty.") return } if repo.IsMirror { - ctx.Error(http.StatusForbidden, "", "Git Repository is a mirror.") + ctx.APIError(http.StatusForbidden, "Git Repository is a mirror.") return } @@ -446,20 +438,20 @@ func UpdateBranch(ctx *context.APIContext) { if err != nil { switch { case repo_model.IsErrUserDoesNotHaveAccessToRepo(err): - ctx.Error(http.StatusForbidden, "", "User must be a repo or site admin to rename default or protected branches.") + ctx.APIError(http.StatusForbidden, "User must be a repo or site admin to rename default or protected branches.") case errors.Is(err, git_model.ErrBranchIsProtected): - ctx.Error(http.StatusForbidden, "", "Branch is protected by glob-based protection rules.") + ctx.APIError(http.StatusForbidden, "Branch is protected by glob-based protection rules.") default: - ctx.Error(http.StatusInternalServerError, "RenameBranch", err) + ctx.APIErrorInternal(err) } return } if msg == "target_exist" { - ctx.Error(http.StatusUnprocessableEntity, "", "Cannot rename a branch using the same name or rename to a branch that already exists.") + ctx.APIError(http.StatusUnprocessableEntity, "Cannot rename a branch using the same name or rename to a branch that already exists.") return } if msg == "from_not_exist" { - ctx.Error(http.StatusNotFound, "", "Branch doesn't exist.") + ctx.APIError(http.StatusNotFound, "Branch doesn't exist.") return } @@ -499,11 +491,11 @@ func GetBranchProtection(ctx *context.APIContext) { bpName := ctx.PathParam("name") bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) + ctx.APIErrorInternal(err) return } if bp == nil || bp.RepoID != repo.ID { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -535,7 +527,7 @@ func ListBranchProtections(ctx *context.APIContext) { repo := ctx.Repo.Repository bps, err := git_model.FindRepoProtectedBranchRules(ctx, repo.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetProtectedBranches", err) + ctx.APIErrorInternal(err) return } apiBps := make([]*api.BranchProtection, len(bps)) @@ -587,25 +579,19 @@ func CreateBranchProtection(ctx *context.APIContext) { ruleName := form.RuleName if ruleName == "" { - ruleName = form.BranchName //nolint + ruleName = form.BranchName //nolint:staticcheck // deprecated field } if len(ruleName) == 0 { - ctx.Error(http.StatusBadRequest, "both rule_name and branch_name are empty", "both rule_name and branch_name are empty") + ctx.APIError(http.StatusBadRequest, "both rule_name and branch_name are empty") return } - isPlainRule := !git_model.IsRuleNameSpecial(ruleName) - var isBranchExist bool - if isPlainRule { - isBranchExist = git.IsBranchExist(ctx.Req.Context(), ctx.Repo.Repository.RepoPath(), ruleName) - } - protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, ruleName) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetProtectBranchOfRepoByName", err) + ctx.APIErrorInternal(err) return } else if protectBranch != nil { - ctx.Error(http.StatusForbidden, "Create branch protection", "Branch protection already exist") + ctx.APIError(http.StatusForbidden, "Branch protection already exist") return } @@ -617,37 +603,37 @@ func CreateBranchProtection(ctx *context.APIContext) { whitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + ctx.APIErrorInternal(err) return } forcePushAllowlistUsers, err := user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + ctx.APIErrorInternal(err) return } mergeWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + ctx.APIErrorInternal(err) return } approvalsWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + ctx.APIErrorInternal(err) return } var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64 @@ -655,37 +641,37 @@ func CreateBranchProtection(ctx *context.APIContext) { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + ctx.APIErrorInternal(err) return } forcePushAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushAllowlistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + ctx.APIErrorInternal(err) return } mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + ctx.APIErrorInternal(err) return } approvalsWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ApprovalsWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + ctx.APIErrorInternal(err) return } } @@ -716,7 +702,7 @@ func CreateBranchProtection(ctx *context.APIContext) { BlockAdminMergeOverride: form.BlockAdminMergeOverride, } - if err := git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{ + if err := pull_service.CreateOrUpdateProtectedBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{ UserIDs: whitelistUsers, TeamIDs: whitelistTeams, ForcePushUserIDs: forcePushAllowlistUsers, @@ -726,48 +712,18 @@ func CreateBranchProtection(ctx *context.APIContext) { ApprovalsUserIDs: approvalsWhitelistUsers, ApprovalsTeamIDs: approvalsWhitelistTeams, }); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateProtectBranch", err) + ctx.APIErrorInternal(err) return } - if isBranchExist { - if err := pull_service.CheckPRsForBaseBranch(ctx, ctx.Repo.Repository, ruleName); err != nil { - ctx.Error(http.StatusInternalServerError, "CheckPRsForBaseBranch", err) - return - } - } else { - if !isPlainRule { - if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) - if err != nil { - ctx.Error(http.StatusInternalServerError, "OpenRepository", err) - return - } - } - // FIXME: since we only need to recheck files protected rules, we could improve this - matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, ruleName) - if err != nil { - ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err) - return - } - - for _, branchName := range matchedBranches { - if err = pull_service.CheckPRsForBaseBranch(ctx, ctx.Repo.Repository, branchName); err != nil { - ctx.Error(http.StatusInternalServerError, "CheckPRsForBaseBranch", err) - return - } - } - } - } - // Reload from db to get all whitelists bp, err := git_model.GetProtectedBranchRuleByName(ctx, ctx.Repo.Repository.ID, ruleName) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) + ctx.APIErrorInternal(err) return } if bp == nil || bp.RepoID != ctx.Repo.Repository.ID { - ctx.Error(http.StatusInternalServerError, "New branch protection not found", err) + ctx.APIErrorInternal(err) return } @@ -817,11 +773,11 @@ func EditBranchProtection(ctx *context.APIContext) { bpName := ctx.PathParam("name") protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) + ctx.APIErrorInternal(err) return } if protectBranch == nil || protectBranch.RepoID != repo.ID { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -932,10 +888,10 @@ func EditBranchProtection(ctx *context.APIContext) { whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + ctx.APIErrorInternal(err) return } } else { @@ -945,10 +901,10 @@ func EditBranchProtection(ctx *context.APIContext) { forcePushAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + ctx.APIErrorInternal(err) return } } else { @@ -958,10 +914,10 @@ func EditBranchProtection(ctx *context.APIContext) { mergeWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + ctx.APIErrorInternal(err) return } } else { @@ -971,10 +927,10 @@ func EditBranchProtection(ctx *context.APIContext) { approvalsWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + ctx.APIErrorInternal(err) return } } else { @@ -987,10 +943,10 @@ func EditBranchProtection(ctx *context.APIContext) { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + ctx.APIErrorInternal(err) return } } else { @@ -1000,10 +956,10 @@ func EditBranchProtection(ctx *context.APIContext) { forcePushAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushAllowlistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + ctx.APIErrorInternal(err) return } } else { @@ -1013,10 +969,10 @@ func EditBranchProtection(ctx *context.APIContext) { mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + ctx.APIErrorInternal(err) return } } else { @@ -1026,10 +982,10 @@ func EditBranchProtection(ctx *context.APIContext) { approvalsWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ApprovalsWhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + ctx.APIErrorInternal(err) return } } else { @@ -1048,19 +1004,19 @@ func EditBranchProtection(ctx *context.APIContext) { ApprovalsTeamIDs: approvalsWhitelistTeams, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateProtectBranch", err) + ctx.APIErrorInternal(err) return } isPlainRule := !git_model.IsRuleNameSpecial(bpName) var isBranchExist bool if isPlainRule { - isBranchExist = git.IsBranchExist(ctx.Req.Context(), ctx.Repo.Repository.RepoPath(), bpName) + isBranchExist = gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, bpName) } if isBranchExist { if err = pull_service.CheckPRsForBaseBranch(ctx, ctx.Repo.Repository, bpName); err != nil { - ctx.Error(http.StatusInternalServerError, "CheckPrsForBaseBranch", err) + ctx.APIErrorInternal(err) return } } else { @@ -1068,7 +1024,7 @@ func EditBranchProtection(ctx *context.APIContext) { if ctx.Repo.GitRepo == nil { ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { - ctx.Error(http.StatusInternalServerError, "OpenRepository", err) + ctx.APIErrorInternal(err) return } } @@ -1076,13 +1032,13 @@ func EditBranchProtection(ctx *context.APIContext) { // FIXME: since we only need to recheck files protected rules, we could improve this matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, protectBranch.RuleName) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err) + ctx.APIErrorInternal(err) return } for _, branchName := range matchedBranches { if err = pull_service.CheckPRsForBaseBranch(ctx, ctx.Repo.Repository, branchName); err != nil { - ctx.Error(http.StatusInternalServerError, "CheckPrsForBaseBranch", err) + ctx.APIErrorInternal(err) return } } @@ -1092,11 +1048,11 @@ func EditBranchProtection(ctx *context.APIContext) { // Reload from db to ensure get all whitelists bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetProtectedBranchBy", err) + ctx.APIErrorInternal(err) return } if bp == nil || bp.RepoID != ctx.Repo.Repository.ID { - ctx.Error(http.StatusInternalServerError, "New branch protection not found", err) + ctx.APIErrorInternal(err) return } @@ -1136,16 +1092,16 @@ func DeleteBranchProtection(ctx *context.APIContext) { bpName := ctx.PathParam("name") bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err) + ctx.APIErrorInternal(err) return } if bp == nil || bp.RepoID != repo.ID { - ctx.NotFound() + ctx.APIErrorNotFound() return } if err := git_model.DeleteProtectedBranch(ctx, ctx.Repo.Repository, bp.ID); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteProtectedBranch", err) + ctx.APIErrorInternal(err) return } @@ -1189,7 +1145,7 @@ func UpdateBranchProtectionPriories(ctx *context.APIContext) { repo := ctx.Repo.Repository if err := git_model.UpdateProtectBranchPriorities(ctx, repo, form.IDs); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateProtectBranchPriorities", err) + ctx.APIErrorInternal(err) return } @@ -1225,16 +1181,16 @@ func MergeUpstream(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.MergeUpstreamRequest) - mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch) + 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.Error(http.StatusBadRequest, "MergeUpstream", err) + ctx.APIError(http.StatusBadRequest, err) return } else if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "MergeUpstream", err) + ctx.APIError(http.StatusNotFound, err) return } - ctx.Error(http.StatusInternalServerError, "MergeUpstream", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, &api.MergeUpstreamResponse{MergeStyle: mergeStyle}) diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index da3ee54e69..c2c10cc695 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -7,6 +7,7 @@ package repo import ( "errors" "net/http" + "strings" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" @@ -59,7 +60,7 @@ func ListCollaborators(ctx *context.APIContext) { RepoID: ctx.Repo.Repository.ID, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "ListCollaborators", err) + ctx.APIErrorInternal(err) return } @@ -92,7 +93,7 @@ func IsCollaborator(ctx *context.APIContext) { // required: true // - name: collaborator // in: path - // description: username of the collaborator + // description: username of the user to check for being a collaborator // type: string // required: true // responses: @@ -106,21 +107,21 @@ func IsCollaborator(ctx *context.APIContext) { user, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) } return } isColab, err := repo_model.IsCollaborator(ctx, ctx.Repo.Repository.ID, user.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "IsCollaborator", err) + ctx.APIErrorInternal(err) return } if isColab { ctx.Status(http.StatusNoContent) } else { - ctx.NotFound() + ctx.APIErrorNotFound() } } @@ -144,7 +145,7 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) { // required: true // - name: collaborator // in: path - // description: username of the collaborator to add + // description: username of the user to add or update as a collaborator // type: string // required: true // - name: body @@ -166,28 +167,28 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) { collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) } return } if !collaborator.IsActive { - ctx.Error(http.StatusInternalServerError, "InactiveCollaborator", errors.New("collaborator's account is inactive")) + ctx.APIErrorInternal(errors.New("collaborator's account is inactive")) return } p := perm.AccessModeWrite if form.Permission != nil { - p = perm.ParseAccessMode(*form.Permission) + p = perm.ParseAccessMode(*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.Error(http.StatusForbidden, "AddOrUpdateCollaborator", err) + ctx.APIError(http.StatusForbidden, err) } else { - ctx.Error(http.StatusInternalServerError, "AddOrUpdateCollaborator", err) + ctx.APIErrorInternal(err) } return } @@ -229,15 +230,15 @@ func DeleteCollaborator(ctx *context.APIContext) { collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) } return } if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) @@ -263,7 +264,7 @@ func GetRepoPermissions(ctx *context.APIContext) { // required: true // - name: collaborator // in: path - // description: username of the collaborator + // description: username of the collaborator whose permissions are to be obtained // type: string // required: true // responses: @@ -274,24 +275,25 @@ func GetRepoPermissions(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" - if !ctx.Doer.IsAdmin && ctx.Doer.LoginName != ctx.PathParam("collaborator") && !ctx.IsUserRepoAdmin() { - ctx.Error(http.StatusForbidden, "User", "Only admins can query all permissions, repo admins can query all repo permissions, collaborators can query only their own") + collaboratorUsername := ctx.PathParam("collaborator") + if !ctx.Doer.IsAdmin && ctx.Doer.LowerName != strings.ToLower(collaboratorUsername) && !ctx.IsUserRepoAdmin() { + ctx.APIError(http.StatusForbidden, "Only admins can query all permissions, repo admins can query all repo permissions, collaborators can query only their own") return } - collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator")) + collaborator, err := user_model.GetUserByName(ctx, collaboratorUsername) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusNotFound, "GetUserByName", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) } return } permission, err := access_model.GetUserRepoPermission(ctx, ctx.Repo.Repository, collaborator) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + ctx.APIErrorInternal(err) return } @@ -324,13 +326,13 @@ func GetReviewers(ctx *context.APIContext) { canChooseReviewer := issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, ctx.Repo.Repository, 0) if !canChooseReviewer { - ctx.Error(http.StatusForbidden, "GetReviewers", errors.New("doer has no permission to get reviewers")) + ctx.APIError(http.StatusForbidden, errors.New("doer has no permission to get reviewers")) return } reviewers, err := pull_service.GetReviewers(ctx, ctx.Repo.Repository, ctx.Doer.ID, 0) if err != nil { - ctx.Error(http.StatusInternalServerError, "ListCollaborators", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, convert.ToUsers(ctx, ctx.Doer, reviewers)) @@ -362,7 +364,7 @@ func GetAssignees(ctx *context.APIContext) { assignees, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository) if err != nil { - ctx.Error(http.StatusInternalServerError, "ListCollaborators", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, convert.ToUsers(ctx, ctx.Doer, assignees)) diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index 3b144d0c43..6a93be624f 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -5,10 +5,10 @@ package repo import ( - "fmt" "math" "net/http" "strconv" + "time" issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" @@ -65,7 +65,7 @@ func GetSingleCommit(ctx *context.APIContext) { sha := ctx.PathParam("sha") if !git.IsValidRefPattern(sha) { - ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha)) + ctx.APIError(http.StatusUnprocessableEntity, "no valid ref or sha: "+sha) return } @@ -76,16 +76,16 @@ func getCommit(ctx *context.APIContext, identifier string, toCommitOpts convert. commit, err := ctx.Repo.GitRepo.GetCommit(identifier) if err != nil { if git.IsErrNotExist(err) { - ctx.NotFound(identifier) + ctx.APIErrorNotFound("commit doesn't exist: " + identifier) return } - ctx.Error(http.StatusInternalServerError, "gitRepo.GetCommit", err) + ctx.APIErrorInternal(err) return } json, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, commit, nil, toCommitOpts) if err != nil { - ctx.Error(http.StatusInternalServerError, "toCommit", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, json) @@ -117,6 +117,16 @@ func GetAllCommits(ctx *context.APIContext) { // in: query // description: filepath of a file/dir // type: string + // - name: since + // in: query + // description: Only commits after this date will be returned (ISO 8601 format) + // type: string + // format: date-time + // - name: until + // in: query + // description: Only commits before this date will be returned (ISO 8601 format) + // type: string + // format: date-time // - name: stat // in: query // description: include diff stats for every commit (disable for speedup, default 'true') @@ -149,6 +159,23 @@ func GetAllCommits(ctx *context.APIContext) { // "409": // "$ref": "#/responses/EmptyRepository" + since := ctx.FormString("since") + until := ctx.FormString("until") + + // Validate since/until as ISO 8601 (RFC3339) + if since != "" { + if _, err := time.Parse(time.RFC3339, since); err != nil { + ctx.APIError(http.StatusUnprocessableEntity, "invalid 'since' format, expected ISO 8601 (RFC3339)") + return + } + } + if until != "" { + if _, err := time.Parse(time.RFC3339, until); err != nil { + ctx.APIError(http.StatusUnprocessableEntity, "invalid 'until' format, expected ISO 8601 (RFC3339)") + return + } + } + if ctx.Repo.Repository.IsEmpty { ctx.JSON(http.StatusConflict, api.APIError{ Message: "Git Repository is empty.", @@ -180,22 +207,16 @@ func GetAllCommits(ctx *context.APIContext) { var baseCommit *git.Commit if len(sha) == 0 { // no sha supplied - use default branch - head, err := ctx.Repo.GitRepo.GetHEADBranch() - if err != nil { - ctx.Error(http.StatusInternalServerError, "GetHEADBranch", err) - return - } - - baseCommit, err = ctx.Repo.GitRepo.GetBranchCommit(head.Name) + baseCommit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetCommit", err) + ctx.APIErrorInternal(err) return } } else { // get commit specified by sha baseCommit, err = ctx.Repo.GitRepo.GetCommit(sha) if err != nil { - ctx.NotFoundOrServerError("GetCommit", git.IsErrNotExist, err) + ctx.NotFoundOrServerError(err) return } } @@ -205,16 +226,18 @@ func GetAllCommits(ctx *context.APIContext) { RepoPath: ctx.Repo.GitRepo.Path, Not: not, Revision: []string{baseCommit.ID.String()}, + Since: since, + Until: until, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetCommitsCount", err) + ctx.APIErrorInternal(err) return } // Query commits - commits, err = baseCommit.CommitsByRange(listOptions.Page, listOptions.PageSize, not) + commits, err = baseCommit.CommitsByRange(listOptions.Page, listOptions.PageSize, not, since, until) if err != nil { - ctx.Error(http.StatusInternalServerError, "CommitsByRange", err) + ctx.APIErrorInternal(err) return } } else { @@ -228,13 +251,15 @@ func GetAllCommits(ctx *context.APIContext) { Not: not, Revision: []string{sha}, RelPath: []string{path}, + Since: since, + Until: until, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "FileCommitsCount", err) + ctx.APIErrorInternal(err) return } else if commitsCountTotal == 0 { - ctx.NotFound("FileCommitsCount", nil) + ctx.APIErrorNotFound("FileCommitsCount", nil) return } @@ -244,9 +269,11 @@ func GetAllCommits(ctx *context.APIContext) { File: path, Not: not, Page: listOptions.Page, + Since: since, + Until: until, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "CommitsByFileAndRange", err) + ctx.APIErrorInternal(err) return } } @@ -259,7 +286,7 @@ func GetAllCommits(ctx *context.APIContext) { // Create json struct apiCommits[i], err = convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, commit, userCache, convert.ParseCommitOptions(ctx)) if err != nil { - ctx.Error(http.StatusInternalServerError, "toCommit", err) + ctx.APIErrorInternal(err) return } } @@ -317,10 +344,10 @@ func DownloadCommitDiffOrPatch(ctx *context.APIContext) { if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, diffType, ctx.Resp); err != nil { if git.IsErrNotExist(err) { - ctx.NotFound(sha) + ctx.APIErrorNotFound("commit doesn't exist: " + sha) return } - ctx.Error(http.StatusInternalServerError, "DownloadCommitDiffOrPatch", err) + ctx.APIErrorInternal(err) return } } @@ -357,19 +384,19 @@ 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.Error(http.StatusNotFound, "GetPullRequestByMergedCommit", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + ctx.APIErrorInternal(err) } return } if err = pr.LoadBaseRepo(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err) + ctx.APIErrorInternal(err) return } if err = pr.LoadHeadRepo(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer)) diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index a1813a8a76..6d427c8073 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -47,7 +47,7 @@ func CompareDiff(ctx *context.APIContext) { var err error ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { - ctx.Error(http.StatusInternalServerError, "OpenRepository", err) + ctx.APIErrorInternal(err) return } } @@ -82,7 +82,7 @@ func CompareDiff(ctx *context.APIContext) { Files: files, }) if err != nil { - ctx.ServerError("toCommit", err) + ctx.APIErrorInternal(err) return } apiCommits = append(apiCommits, apiCommit) diff --git a/routers/api/v1/repo/download.go b/routers/api/v1/repo/download.go index e6296c9fe7..acd93ecf2e 100644 --- a/routers/api/v1/repo/download.go +++ b/routers/api/v1/repo/download.go @@ -4,7 +4,6 @@ package repo import ( - "fmt" "net/http" "code.gitea.io/gitea/modules/git" @@ -23,7 +22,7 @@ func DownloadArchive(ctx *context.APIContext) { case "bundle": tp = git.ArchiveBundle default: - ctx.Error(http.StatusBadRequest, "", fmt.Sprintf("Unknown archive type: %s", ballType)) + ctx.APIError(http.StatusBadRequest, "Unknown archive type: "+ballType) return } @@ -31,20 +30,20 @@ func DownloadArchive(ctx *context.APIContext) { var err error ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { - ctx.Error(http.StatusInternalServerError, "OpenRepository", err) + ctx.APIErrorInternal(err) return } } r, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*")+"."+tp.String()) if err != nil { - ctx.ServerError("NewRequest", err) + ctx.APIErrorInternal(err) return } archive, err := r.Await(ctx) if err != nil { - ctx.ServerError("archive.Await", err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 3eefd2ae29..69b5996222 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -16,16 +16,18 @@ import ( git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/httpcache" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" api "code.gitea.io/gitea/modules/structs" + "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" @@ -60,7 +62,7 @@ func GetRawFile(ctx *context.APIContext) { // required: true // - name: ref // in: query - // description: "The name of the commit/branch/tag. Default the repository’s default branch" + // description: "The name of the commit/branch/tag. Default to the repository’s default branch" // type: string // required: false // responses: @@ -72,7 +74,7 @@ func GetRawFile(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if ctx.Repo.Repository.IsEmpty { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -83,8 +85,8 @@ func GetRawFile(ctx *context.APIContext) { ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry))) - if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil { - ctx.Error(http.StatusInternalServerError, "ServeBlob", err) + if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil { + ctx.APIErrorInternal(err) } } @@ -113,7 +115,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { // required: true // - name: ref // in: query - // description: "The name of the commit/branch/tag. Default the repository’s default branch" + // description: "The name of the commit/branch/tag. Default to the repository’s default branch" // type: string // required: false // responses: @@ -125,7 +127,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if ctx.Repo.Repository.IsEmpty { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -137,31 +139,31 @@ func GetRawFileOrLFS(ctx *context.APIContext) { ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry))) // LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file - if blob.Size() > 1024 { + if blob.Size() > lfs.MetaFileMaxSize { // First handle caching for the blob if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { return } - // OK not cached - serve! - if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil { - ctx.ServerError("ServeBlob", err) + // If not cached - serve! + if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil { + ctx.APIErrorInternal(err) } return } - // OK, now the blob is known to have at most 1024 bytes we can simply read this in one go (This saves reading it twice) + // OK, now the blob is known to have at most 1024 (lfs pointer max size) bytes, + // we can simply read this in one go (This saves reading it twice) dataRc, err := blob.DataAsync() if err != nil { - ctx.ServerError("DataAsync", err) + ctx.APIErrorInternal(err) return } - // FIXME: code from #19689, what if the file is large ... OOM ... buf, err := io.ReadAll(dataRc) if err != nil { _ = dataRc.Close() - ctx.ServerError("DataAsync", err) + ctx.APIErrorInternal(err) return } @@ -179,7 +181,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { return } - // OK not cached - serve! + // If not cached - serve! common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)) return } @@ -197,7 +199,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)) return } else if err != nil { - ctx.ServerError("GetLFSMetaObjectByOid", err) + ctx.APIErrorInternal(err) return } @@ -217,7 +219,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer) if err != nil { - ctx.ServerError("ReadMetaObject", err) + ctx.APIErrorInternal(err) return } defer lfsDataRc.Close() @@ -229,21 +231,21 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEn entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) if err != nil { if git.IsErrNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetTreeEntryByPath", err) + ctx.APIErrorInternal(err) } return nil, nil, nil } if entry.IsDir() || entry.IsSubModule() { - ctx.NotFound("getBlobForEntry", nil) + ctx.APIErrorNotFound("getBlobForEntry", nil) return nil, nil, nil } latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetTreePathLatestCommit", err) + ctx.APIErrorInternal(err) return nil, nil, nil } when := &latestCommit.Committer.When @@ -284,7 +286,7 @@ func GetArchive(ctx *context.APIContext) { var err error ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { - ctx.Error(http.StatusInternalServerError, "OpenRepository", err) + ctx.APIErrorInternal(err) return } } @@ -296,18 +298,18 @@ func archiveDownload(ctx *context.APIContext) { aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*")) if err != nil { if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) { - ctx.Error(http.StatusBadRequest, "unknown archive format", err) + ctx.APIError(http.StatusBadRequest, err) } else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) { - ctx.Error(http.StatusNotFound, "unrecognized reference", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.ServerError("archiver_service.NewRequest", err) + ctx.APIErrorInternal(err) } return } archiver, err := aReq.Await(ctx) if err != nil { - ctx.ServerError("archiver.Await", err) + ctx.APIErrorInternal(err) return } @@ -339,7 +341,7 @@ func download(ctx *context.APIContext, archiveName string, archiver *repo_model. // If we have matched and access to release or issue fr, err := storage.RepoArchives.Open(rPath) if err != nil { - ctx.ServerError("Open", err) + ctx.APIErrorInternal(err) return } defer fr.Close() @@ -375,7 +377,7 @@ func GetEditorconfig(ctx *context.APIContext) { // required: true // - name: ref // in: query - // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)" + // description: "The name of the commit/branch/tag. Default to the repository’s default branch." // type: string // required: false // responses: @@ -387,9 +389,9 @@ func GetEditorconfig(ctx *context.APIContext) { ec, _, err := ctx.Repo.GetEditorconfig(ctx.Repo.Commit) if err != nil { if git.IsErrNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) } else { - ctx.Error(http.StatusInternalServerError, "GetEditorconfig", err) + ctx.APIErrorInternal(err) } return } @@ -397,24 +399,12 @@ func GetEditorconfig(ctx *context.APIContext) { fileName := ctx.PathParam("filename") def, err := ec.GetDefinitionForFilename(fileName) if def == nil { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) return } ctx.JSON(http.StatusOK, def) } -// canWriteFiles returns true if repository is editable and user has proper access level. -func canWriteFiles(ctx *context.APIContext, branch string) bool { - return ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, branch) && - !ctx.Repo.Repository.IsMirror && - !ctx.Repo.Repository.IsArchived -} - -// canReadFiles returns true if repository is readable and user has proper access level. -func canReadFiles(r *context.Repository) bool { - return r.Permission.CanRead(unit.TypeCode) -} - func base64Reader(s string) (io.ReadSeeker, error) { b, err := base64.StdEncoding.DecodeString(s) if err != nil { @@ -423,6 +413,45 @@ func base64Reader(s string) (io.ReadSeeker, error) { return bytes.NewReader(b), nil } +func ReqChangeRepoFileOptionsAndCheck(ctx *context.APIContext) { + commonOpts := web.GetForm(ctx).(api.FileOptionsInterface).GetFileOptions() + commonOpts.BranchName = util.IfZero(commonOpts.BranchName, ctx.Repo.Repository.DefaultBranch) + commonOpts.NewBranchName = util.IfZero(commonOpts.NewBranchName, commonOpts.BranchName) + if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, commonOpts.NewBranchName) && !ctx.IsUserSiteAdmin() { + ctx.APIError(http.StatusForbidden, "user should have a permission to write to the target branch") + return + } + changeFileOpts := &files_service.ChangeRepoFilesOptions{ + Message: commonOpts.Message, + OldBranch: commonOpts.BranchName, + NewBranch: commonOpts.NewBranchName, + Committer: &files_service.IdentityOptions{ + GitUserName: commonOpts.Committer.Name, + GitUserEmail: commonOpts.Committer.Email, + }, + Author: &files_service.IdentityOptions{ + GitUserName: commonOpts.Author.Name, + GitUserEmail: commonOpts.Author.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: commonOpts.Dates.Author, + Committer: commonOpts.Dates.Committer, + }, + Signoff: commonOpts.Signoff, + } + if commonOpts.Dates.Author.IsZero() { + commonOpts.Dates.Author = time.Now() + } + if commonOpts.Dates.Committer.IsZero() { + commonOpts.Dates.Committer = time.Now() + } + ctx.Data["__APIChangeRepoFilesOptions"] = changeFileOpts +} + +func getAPIChangeRepoFileOptions[T api.FileOptionsInterface](ctx *context.APIContext) (apiOpts T, opts *files_service.ChangeRepoFilesOptions) { + return web.GetForm(ctx).(T), ctx.Data["__APIChangeRepoFilesOptions"].(*files_service.ChangeRepoFilesOptions) +} + // ChangeFiles handles API call for modifying multiple files func ChangeFiles(ctx *context.APIContext) { // swagger:operation POST /repos/{owner}/{repo}/contents repository repoChangeFiles @@ -459,20 +488,18 @@ func ChangeFiles(ctx *context.APIContext) { // "$ref": "#/responses/error" // "423": // "$ref": "#/responses/repoArchivedError" - - apiOpts := web.GetForm(ctx).(*api.ChangeFilesOptions) - - if apiOpts.BranchName == "" { - apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch + apiOpts, opts := getAPIChangeRepoFileOptions[*api.ChangeFilesOptions](ctx) + if ctx.Written() { + return } - - var files []*files_service.ChangeRepoFile for _, file := range apiOpts.Files { contentReader, err := base64Reader(file.ContentBase64) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, "Invalid base64 content", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } + // FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options + // But the LastCommitID is not provided in the API options, need to fully fix them in API changeRepoFile := &files_service.ChangeRepoFile{ Operation: file.Operation, TreePath: file.Path, @@ -480,41 +507,15 @@ func ChangeFiles(ctx *context.APIContext) { ContentReader: contentReader, SHA: file.SHA, } - files = append(files, changeRepoFile) - } - - opts := &files_service.ChangeRepoFilesOptions{ - Files: files, - Message: apiOpts.Message, - OldBranch: apiOpts.BranchName, - NewBranch: apiOpts.NewBranchName, - Committer: &files_service.IdentityOptions{ - Name: apiOpts.Committer.Name, - Email: apiOpts.Committer.Email, - }, - Author: &files_service.IdentityOptions{ - Name: apiOpts.Author.Name, - Email: apiOpts.Author.Email, - }, - Dates: &files_service.CommitDateOptions{ - Author: apiOpts.Dates.Author, - Committer: apiOpts.Dates.Committer, - }, - Signoff: apiOpts.Signoff, - } - if opts.Dates.Author.IsZero() { - opts.Dates.Author = time.Now() - } - if opts.Dates.Committer.IsZero() { - opts.Dates.Committer = time.Now() + opts.Files = append(opts.Files, changeRepoFile) } if opts.Message == "" { - opts.Message = changeFilesCommitMessage(ctx, files) + opts.Message = changeFilesCommitMessage(ctx, opts.Files) } - if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil { - handleCreateOrUpdateFileError(ctx, err) + if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { + handleChangeRepoFilesError(ctx, err) } else { ctx.JSON(http.StatusCreated, filesResponse) } @@ -562,56 +563,27 @@ func CreateFile(ctx *context.APIContext) { // "423": // "$ref": "#/responses/repoArchivedError" - apiOpts := web.GetForm(ctx).(*api.CreateFileOptions) - - if apiOpts.BranchName == "" { - apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch + apiOpts, opts := getAPIChangeRepoFileOptions[*api.CreateFileOptions](ctx) + if ctx.Written() { + return } - contentReader, err := base64Reader(apiOpts.ContentBase64) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, "Invalid base64 content", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - opts := &files_service.ChangeRepoFilesOptions{ - Files: []*files_service.ChangeRepoFile{ - { - Operation: "create", - TreePath: ctx.PathParam("*"), - ContentReader: contentReader, - }, - }, - Message: apiOpts.Message, - OldBranch: apiOpts.BranchName, - NewBranch: apiOpts.NewBranchName, - Committer: &files_service.IdentityOptions{ - Name: apiOpts.Committer.Name, - Email: apiOpts.Committer.Email, - }, - Author: &files_service.IdentityOptions{ - Name: apiOpts.Author.Name, - Email: apiOpts.Author.Email, - }, - Dates: &files_service.CommitDateOptions{ - Author: apiOpts.Dates.Author, - Committer: apiOpts.Dates.Committer, - }, - Signoff: apiOpts.Signoff, - } - if opts.Dates.Author.IsZero() { - opts.Dates.Author = time.Now() - } - if opts.Dates.Committer.IsZero() { - opts.Dates.Committer = time.Now() - } - + opts.Files = append(opts.Files, &files_service.ChangeRepoFile{ + Operation: "create", + TreePath: ctx.PathParam("*"), + ContentReader: contentReader, + }) if opts.Message == "" { opts.Message = changeFilesCommitMessage(ctx, opts.Files) } - if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil { - handleCreateOrUpdateFileError(ctx, err) + if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { + handleChangeRepoFilesError(ctx, err) } else { fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0) ctx.JSON(http.StatusCreated, fileResponse) @@ -659,96 +631,55 @@ func UpdateFile(ctx *context.APIContext) { // "$ref": "#/responses/error" // "423": // "$ref": "#/responses/repoArchivedError" - apiOpts := web.GetForm(ctx).(*api.UpdateFileOptions) - if ctx.Repo.Repository.IsEmpty { - ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty")) - return - } - if apiOpts.BranchName == "" { - apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch + apiOpts, opts := getAPIChangeRepoFileOptions[*api.UpdateFileOptions](ctx) + if ctx.Written() { + return } - contentReader, err := base64Reader(apiOpts.ContentBase64) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, "Invalid base64 content", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - - opts := &files_service.ChangeRepoFilesOptions{ - Files: []*files_service.ChangeRepoFile{ - { - Operation: "update", - ContentReader: contentReader, - SHA: apiOpts.SHA, - FromTreePath: apiOpts.FromPath, - TreePath: ctx.PathParam("*"), - }, - }, - Message: apiOpts.Message, - OldBranch: apiOpts.BranchName, - NewBranch: apiOpts.NewBranchName, - Committer: &files_service.IdentityOptions{ - Name: apiOpts.Committer.Name, - Email: apiOpts.Committer.Email, - }, - Author: &files_service.IdentityOptions{ - Name: apiOpts.Author.Name, - Email: apiOpts.Author.Email, - }, - Dates: &files_service.CommitDateOptions{ - Author: apiOpts.Dates.Author, - Committer: apiOpts.Dates.Committer, - }, - Signoff: apiOpts.Signoff, - } - if opts.Dates.Author.IsZero() { - opts.Dates.Author = time.Now() - } - if opts.Dates.Committer.IsZero() { - opts.Dates.Committer = time.Now() - } - + opts.Files = append(opts.Files, &files_service.ChangeRepoFile{ + Operation: "update", + ContentReader: contentReader, + SHA: apiOpts.SHA, + FromTreePath: apiOpts.FromPath, + TreePath: ctx.PathParam("*"), + }) if opts.Message == "" { opts.Message = changeFilesCommitMessage(ctx, opts.Files) } - if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil { - handleCreateOrUpdateFileError(ctx, err) + if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { + handleChangeRepoFilesError(ctx, err) } else { fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0) ctx.JSON(http.StatusOK, fileResponse) } } -func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) { +func handleChangeRepoFilesError(ctx *context.APIContext, err error) { if files_service.IsErrUserCannotCommit(err) || pull_service.IsErrFilePathProtected(err) { - ctx.Error(http.StatusForbidden, "Access", err) + ctx.APIError(http.StatusForbidden, err) return } if git_model.IsErrBranchAlreadyExists(err) || files_service.IsErrFilenameInvalid(err) || pull_service.IsErrSHADoesNotMatch(err) || - files_service.IsErrFilePathInvalid(err) || files_service.IsErrRepoFileAlreadyExists(err) { - ctx.Error(http.StatusUnprocessableEntity, "Invalid", err) + files_service.IsErrFilePathInvalid(err) || files_service.IsErrRepoFileAlreadyExists(err) || + files_service.IsErrCommitIDDoesNotMatch(err) || files_service.IsErrSHAOrCommitIDNotProvided(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) return } - if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) { - ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err) + if git.IsErrBranchNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) { + ctx.APIError(http.StatusNotFound, err) return } - - ctx.Error(http.StatusInternalServerError, "UpdateFile", err) -} - -// Called from both CreateFile or UpdateFile to handle both -func createOrUpdateFiles(ctx *context.APIContext, opts *files_service.ChangeRepoFilesOptions) (*api.FilesResponse, error) { - if !canWriteFiles(ctx, opts.OldBranch) { - return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{ - UserID: ctx.Doer.ID, - RepoName: ctx.Repo.Repository.LowerName, - } + if errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusNotFound, err) + return } - - return files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts) + ctx.APIErrorInternal(err) } // format commit message if empty @@ -762,7 +693,7 @@ func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.Ch switch file.Operation { case "create": createFiles = append(createFiles, file.TreePath) - case "update": + case "update", "upload", "rename": // upload and rename works like "update", there is no translation for them at the moment updateFiles = append(updateFiles, file.TreePath) case "delete": deleteFiles = append(deleteFiles, file.TreePath) @@ -820,85 +751,119 @@ func DeleteFile(ctx *context.APIContext) { // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/error" + // "422": + // "$ref": "#/responses/error" // "423": // "$ref": "#/responses/repoArchivedError" - apiOpts := web.GetForm(ctx).(*api.DeleteFileOptions) - if !canWriteFiles(ctx, apiOpts.BranchName) { - ctx.Error(http.StatusForbidden, "DeleteFile", repo_model.ErrUserDoesNotHaveAccessToRepo{ - UserID: ctx.Doer.ID, - RepoName: ctx.Repo.Repository.LowerName, - }) + apiOpts, opts := getAPIChangeRepoFileOptions[*api.DeleteFileOptions](ctx) + if ctx.Written() { return } - if apiOpts.BranchName == "" { - apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch - } - - opts := &files_service.ChangeRepoFilesOptions{ - Files: []*files_service.ChangeRepoFile{ - { - Operation: "delete", - SHA: apiOpts.SHA, - TreePath: ctx.PathParam("*"), - }, - }, - Message: apiOpts.Message, - OldBranch: apiOpts.BranchName, - NewBranch: apiOpts.NewBranchName, - Committer: &files_service.IdentityOptions{ - Name: apiOpts.Committer.Name, - Email: apiOpts.Committer.Email, - }, - Author: &files_service.IdentityOptions{ - Name: apiOpts.Author.Name, - Email: apiOpts.Author.Email, - }, - Dates: &files_service.CommitDateOptions{ - Author: apiOpts.Dates.Author, - Committer: apiOpts.Dates.Committer, - }, - Signoff: apiOpts.Signoff, - } - if opts.Dates.Author.IsZero() { - opts.Dates.Author = time.Now() - } - if opts.Dates.Committer.IsZero() { - opts.Dates.Committer = time.Now() - } - + opts.Files = append(opts.Files, &files_service.ChangeRepoFile{ + Operation: "delete", + SHA: apiOpts.SHA, + TreePath: ctx.PathParam("*"), + }) if opts.Message == "" { opts.Message = changeFilesCommitMessage(ctx, opts.Files) } if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { - if git.IsErrBranchNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) { - ctx.Error(http.StatusNotFound, "DeleteFile", err) - return - } else if git_model.IsErrBranchAlreadyExists(err) || - files_service.IsErrFilenameInvalid(err) || - pull_service.IsErrSHADoesNotMatch(err) || - files_service.IsErrCommitIDDoesNotMatch(err) || - files_service.IsErrSHAOrCommitIDNotProvided(err) { - ctx.Error(http.StatusBadRequest, "DeleteFile", err) - return - } else if files_service.IsErrUserCannotCommit(err) { - ctx.Error(http.StatusForbidden, "DeleteFile", err) - return - } - ctx.Error(http.StatusInternalServerError, "DeleteFile", err) + handleChangeRepoFilesError(ctx, err) } else { fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0) ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent } } -// GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir +func resolveRefCommit(ctx *context.APIContext, ref string, minCommitIDLen ...int) *utils.RefCommit { + ref = util.IfZero(ref, ctx.Repo.Repository.DefaultBranch) + refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ref, minCommitIDLen...) + if errors.Is(err, util.ErrNotExist) { + ctx.APIErrorNotFound(err) + } else if err != nil { + ctx.APIErrorInternal(err) + } + return refCommit +} + +func GetContentsExt(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/contents-ext/{filepath} repository repoGetContentsExt + // --- + // summary: The extended "contents" API, to get file metadata and/or content, or list a directory. + // description: It guarantees that only one of the response fields is set if the request succeeds. + // Users can pass "includes=file_content" or "includes=lfs_metadata" to retrieve more fields. + // "includes=file_content" only works for single file, if you need to retrieve file contents in batch, + // use "file-contents" API after listing the directory. + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: filepath + // in: path + // description: path of the dir, file, symlink or submodule in the repo. Swagger requires path parameter to be "required", + // you can leave it empty or pass a single dot (".") to get the root directory. + // type: string + // required: true + // - name: ref + // in: query + // description: the name of the commit/branch/tag, default to the repository’s default branch. + // type: string + // required: false + // - name: includes + // in: query + // description: By default this API's response only contains file's metadata. Use comma-separated "includes" options to retrieve more fields. + // Option "file_content" will try to retrieve the file content, "lfs_metadata" will try to retrieve LFS metadata, + // "commit_metadata" will try to retrieve commit metadata, and "commit_message" will try to retrieve commit message. + // type: string + // required: false + // responses: + // "200": + // "$ref": "#/responses/ContentsExtResponse" + // "404": + // "$ref": "#/responses/notFound" + + if treePath := ctx.PathParam("*"); treePath == "." || treePath == "/" { + ctx.SetPathParam("*", "") // workaround for swagger, it requires path parameter to be "required", but we need to list root directory + } + opts := files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*")} + for includeOpt := range strings.SplitSeq(ctx.FormString("includes"), ",") { + if includeOpt == "" { + continue + } + switch includeOpt { + case "file_content": + opts.IncludeSingleFileContent = true + case "lfs_metadata": + opts.IncludeLfsMetadata = true + case "commit_metadata": + opts.IncludeCommitMetadata = true + case "commit_message": + opts.IncludeCommitMessage = true + default: + ctx.APIError(http.StatusBadRequest, fmt.Sprintf("unknown include option %q", includeOpt)) + return + } + } + ctx.JSON(http.StatusOK, getRepoContents(ctx, opts)) +} + func GetContents(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents // --- - // summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir + // summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir. + // description: This API follows GitHub's design, and it is not easy to use. Recommend users to use the "contents-ext" API instead. // produces: // - application/json // parameters: @@ -919,7 +884,7 @@ func GetContents(ctx *context.APIContext) { // required: true // - name: ref // in: query - // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)" + // description: "The name of the commit/branch/tag. Default to the repository’s default branch." // type: string // required: false // responses: @@ -927,34 +892,38 @@ func GetContents(ctx *context.APIContext) { // "$ref": "#/responses/ContentsResponse" // "404": // "$ref": "#/responses/notFound" - - if !canReadFiles(ctx.Repo) { - ctx.Error(http.StatusInternalServerError, "GetContentsOrList", repo_model.ErrUserDoesNotHaveAccessToRepo{ - UserID: ctx.Doer.ID, - RepoName: ctx.Repo.Repository.LowerName, - }) + ret := getRepoContents(ctx, files_service.GetContentsOrListOptions{ + TreePath: ctx.PathParam("*"), + IncludeSingleFileContent: true, + IncludeCommitMetadata: true, + }) + if ctx.Written() { return } + ctx.JSON(http.StatusOK, util.Iif[any](ret.FileContents != nil, ret.FileContents, ret.DirContents)) +} - treePath := ctx.PathParam("*") - ref := ctx.FormTrim("ref") - - if fileList, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, treePath, ref); err != nil { +func getRepoContents(ctx *context.APIContext, opts files_service.GetContentsOrListOptions) *api.ContentsExtResponse { + refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref")) + if ctx.Written() { + return nil + } + ret, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts) + if err != nil { if git.IsErrNotExist(err) { - ctx.NotFound("GetContentsOrList", err) - return + ctx.APIErrorNotFound("GetContentsOrList", err) + return nil } - ctx.Error(http.StatusInternalServerError, "GetContentsOrList", err) - } else { - ctx.JSON(http.StatusOK, fileList) + ctx.APIErrorInternal(err) } + return &ret } -// GetContentsList Get the metadata of all the entries of the root dir func GetContentsList(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList // --- - // summary: Gets the metadata of all the entries of the root dir + // summary: Gets the metadata of all the entries of the root dir. + // description: This API follows GitHub's design, and it is not easy to use. Recommend users to use our "contents-ext" API instead. // produces: // - application/json // parameters: @@ -970,7 +939,7 @@ func GetContentsList(ctx *context.APIContext) { // required: true // - name: ref // in: query - // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)" + // description: "The name of the commit/branch/tag. Default to the repository’s default branch." // type: string // required: false // responses: @@ -982,3 +951,102 @@ func GetContentsList(ctx *context.APIContext) { // same as GetContents(), this function is here because swagger fails if path is empty in GetContents() interface GetContents(ctx) } + +func GetFileContentsGet(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/file-contents repository repoGetFileContents + // --- + // summary: Get the metadata and contents of requested files + // description: See the POST method. This GET method supports using JSON encoded request body in query parameter. + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: ref + // in: query + // description: "The name of the commit/branch/tag. Default to the repository’s default branch." + // type: string + // required: false + // - name: body + // in: query + // description: "The JSON encoded body (see the POST request): {\"files\": [\"filename1\", \"filename2\"]}" + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/ContentsListResponse" + // "404": + // "$ref": "#/responses/notFound" + + // The POST method requires "write" permission, so we also support this "GET" method + handleGetFileContents(ctx) +} + +func GetFileContentsPost(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/file-contents repository repoGetFileContentsPost + // --- + // summary: Get the metadata and contents of requested files + // description: Uses automatic pagination based on default page size and + // max response size and returns the maximum allowed number of files. + // Files which could not be retrieved are null. Files which are too large + // are being returned with `encoding == null`, `content == null` and `size > 0`, + // they can be requested separately by using the `download_url`. + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: ref + // in: query + // description: "The name of the commit/branch/tag. Default to the repository’s default branch." + // type: string + // required: false + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/GetFilesOptions" + // responses: + // "200": + // "$ref": "#/responses/ContentsListResponse" + // "404": + // "$ref": "#/responses/notFound" + + // This is actually a "read" request, but we need to accept a "files" list, then POST method seems easy to use. + // But the permission system requires that the caller must have "write" permission to use POST method. + // At the moment, there is no other way to get around the permission check, so there is a "GET" workaround method above. + handleGetFileContents(ctx) +} + +func handleGetFileContents(ctx *context.APIContext) { + opts, ok := web.GetForm(ctx).(*api.GetFilesOptions) + if !ok { + err := json.Unmarshal(util.UnsafeStringToBytes(ctx.FormString("body")), &opts) + if err != nil { + ctx.APIError(http.StatusBadRequest, "invalid body parameter") + return + } + } + refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref")) + if ctx.Written() { + return + } + filesResponse := files_service.GetContentsListFromTreePaths(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts.Files) + ctx.JSON(http.StatusOK, util.SliceNilAsEmpty(filesResponse)) +} diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index 14a1a8d1c4..58f66954e1 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -57,15 +57,15 @@ func ListForks(ctx *context.APIContext) { forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx)) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindForks", err) + ctx.APIErrorInternal(err) return } if err := repo_model.RepositoryList(forks).LoadOwners(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadOwners", err) + ctx.APIErrorInternal(err) return } if err := repo_model.RepositoryList(forks).LoadUnits(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadUnits", err) + ctx.APIErrorInternal(err) return } @@ -73,7 +73,7 @@ func ListForks(ctx *context.APIContext) { for i, fork := range forks { permission, err := access_model.GetUserRepoPermission(ctx, fork, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + ctx.APIErrorInternal(err) return } apiForks[i] = convert.ToRepo(ctx, fork, permission) @@ -126,19 +126,21 @@ func CreateFork(ctx *context.APIContext) { org, err := organization.GetOrgByName(ctx, *form.Organization) if err != nil { if organization.IsErrOrgNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "GetOrgByName", err) + ctx.APIErrorInternal(err) } return } - isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID) - if err != nil { - ctx.Error(http.StatusInternalServerError, "IsOrgMember", err) - return - } else if !isMember { - ctx.Error(http.StatusForbidden, "isMemberNot", fmt.Sprintf("User is no Member of Organisation '%s'", org.Name)) - return + if !ctx.Doer.IsAdmin { + isMember, err := org.IsOrgMember(ctx, ctx.Doer.ID) + if err != nil { + ctx.APIErrorInternal(err) + return + } else if !isMember { + ctx.APIError(http.StatusForbidden, fmt.Sprintf("User is no Member of Organisation '%s'", org.Name)) + return + } } forker = org.AsUser() } @@ -157,11 +159,11 @@ func CreateFork(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) { - ctx.Error(http.StatusConflict, "ForkRepository", err) + ctx.APIError(http.StatusConflict, err) } else if errors.Is(err, user_model.ErrBlockedUser) { - ctx.Error(http.StatusForbidden, "ForkRepository", err) + ctx.APIError(http.StatusForbidden, err) } else { - ctx.Error(http.StatusInternalServerError, "ForkRepository", err) + ctx.APIErrorInternal(err) } return } diff --git a/routers/api/v1/repo/git_hook.go b/routers/api/v1/repo/git_hook.go index 868acf3d85..487c74e183 100644 --- a/routers/api/v1/repo/git_hook.go +++ b/routers/api/v1/repo/git_hook.go @@ -40,7 +40,7 @@ func ListGitHooks(ctx *context.APIContext) { hooks, err := ctx.Repo.GitRepo.Hooks() if err != nil { - ctx.Error(http.StatusInternalServerError, "Hooks", err) + ctx.APIErrorInternal(err) return } @@ -84,9 +84,9 @@ func GetGitHook(ctx *context.APIContext) { hook, err := ctx.Repo.GitRepo.GetHook(hookID) if err != nil { if errors.Is(err, git.ErrNotValidHook) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetHook", err) + ctx.APIErrorInternal(err) } return } @@ -131,16 +131,16 @@ func EditGitHook(ctx *context.APIContext) { hook, err := ctx.Repo.GitRepo.GetHook(hookID) if err != nil { if errors.Is(err, git.ErrNotValidHook) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetHook", err) + ctx.APIErrorInternal(err) } return } hook.Content = form.Content if err = hook.Update(); err != nil { - ctx.Error(http.StatusInternalServerError, "hook.Update", err) + ctx.APIErrorInternal(err) return } @@ -180,16 +180,16 @@ func DeleteGitHook(ctx *context.APIContext) { hook, err := ctx.Repo.GitRepo.GetHook(hookID) if err != nil { if errors.Is(err, git.ErrNotValidHook) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetHook", err) + ctx.APIErrorInternal(err) } return } hook.Content = "" if err = hook.Update(); err != nil { - ctx.Error(http.StatusInternalServerError, "hook.Update", err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/repo/git_ref.go b/routers/api/v1/repo/git_ref.go index 1743c0fc20..f042e9e344 100644 --- a/routers/api/v1/repo/git_ref.go +++ b/routers/api/v1/repo/git_ref.go @@ -4,6 +4,7 @@ package repo import ( + "fmt" "net/http" "net/url" @@ -77,12 +78,12 @@ func GetGitRefs(ctx *context.APIContext) { func getGitRefsInternal(ctx *context.APIContext, filter string) { refs, lastMethodName, err := utils.GetGitRefs(ctx, filter) if err != nil { - ctx.Error(http.StatusInternalServerError, lastMethodName, err) + ctx.APIErrorInternal(fmt.Errorf("%s: %w", lastMethodName, err)) return } if len(refs) == 0 { - ctx.NotFound() + ctx.APIErrorNotFound() return } diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go index 03143c8f99..ac47e15d64 100644 --- a/routers/api/v1/repo/hook.go +++ b/routers/api/v1/repo/hook.go @@ -61,7 +61,7 @@ func ListHooks(ctx *context.APIContext) { hooks, count, err := db.FindAndCount[webhook.Webhook](ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -69,7 +69,7 @@ func ListHooks(ctx *context.APIContext) { for i := range hooks { apiHooks[i], err = webhook_service.ToHook(ctx.Repo.RepoLink, hooks[i]) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } } @@ -116,7 +116,7 @@ func GetHook(ctx *context.APIContext) { } apiHook, err := webhook_service.ToHook(repo.RepoLink, hook) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, apiHook) @@ -189,7 +189,7 @@ func TestHook(ctx *context.APIContext) { Pusher: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone), Sender: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone), }); err != nil { - ctx.Error(http.StatusInternalServerError, "PrepareWebhook: ", err) + ctx.APIErrorInternal(err) return } @@ -298,9 +298,9 @@ func DeleteHook(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if err := webhook.DeleteWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")); err != nil { if webhook.IsErrWebhookNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "DeleteWebhookByRepoID", err) + ctx.APIErrorInternal(err) } return } diff --git a/routers/api/v1/repo/hook_test.go b/routers/api/v1/repo/hook_test.go index c659a16f54..f8d61ccf00 100644 --- a/routers/api/v1/repo/hook_test.go +++ b/routers/api/v1/repo/hook_test.go @@ -23,7 +23,7 @@ func TestTestHook(t *testing.T) { contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) TestHook(ctx) - assert.EqualValues(t, http.StatusNoContent, ctx.Resp.Status()) + assert.Equal(t, http.StatusNoContent, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &webhook.HookTask{ HookID: 1, diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 86dbcee5f7..d4a5872fd1 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -132,7 +132,7 @@ func SearchIssues(ctx *context.APIContext) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } @@ -152,7 +152,7 @@ func SearchIssues(ctx *context.APIContext) { ) { // find repos user can access (for issue search) - opts := &repo_model.SearchRepoOptions{ + opts := repo_model.SearchRepoOptions{ Private: false, AllPublic: true, TopicOnly: false, @@ -170,9 +170,9 @@ func SearchIssues(ctx *context.APIContext) { owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner")) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusBadRequest, "Owner not found", err) + ctx.APIError(http.StatusBadRequest, err) } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) } return } @@ -183,15 +183,15 @@ func SearchIssues(ctx *context.APIContext) { } if ctx.FormString("team") != "" { if ctx.FormString("owner") == "" { - ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team") + ctx.APIError(http.StatusBadRequest, "Owner organisation is required for filtering on team") return } team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team")) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.Error(http.StatusBadRequest, "Team not found", err) + ctx.APIError(http.StatusBadRequest, err) } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) } return } @@ -204,7 +204,7 @@ func SearchIssues(ctx *context.APIContext) { } repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "SearchRepositoryIDs", err) + ctx.APIErrorInternal(err) return } if len(repoIDs) == 0 { @@ -237,7 +237,7 @@ func SearchIssues(ctx *context.APIContext) { } includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLabelIDsByNames", err) + ctx.APIErrorInternal(err) return } } @@ -251,7 +251,7 @@ func SearchIssues(ctx *context.APIContext) { } includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetMilestoneIDsByNames", err) + ctx.APIErrorInternal(err) return } } @@ -290,10 +290,10 @@ func SearchIssues(ctx *context.APIContext) { if ctx.IsSigned { ctxUserID := ctx.Doer.ID if ctx.FormBool("created") { - searchOpt.PosterID = optional.Some(ctxUserID) + searchOpt.PosterID = strconv.FormatInt(ctxUserID, 10) } if ctx.FormBool("assigned") { - searchOpt.AssigneeID = optional.Some(ctxUserID) + searchOpt.AssigneeID = strconv.FormatInt(ctxUserID, 10) } if ctx.FormBool("mentioned") { searchOpt.MentionID = optional.Some(ctxUserID) @@ -312,12 +312,12 @@ func SearchIssues(ctx *context.APIContext) { ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt) if err != nil { - ctx.Error(http.StatusInternalServerError, "SearchIssues", err) + ctx.APIErrorInternal(err) return } issues, err := issues_model.GetIssuesByIDs(ctx, ids, true) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err) + ctx.APIErrorInternal(err) return } @@ -405,7 +405,7 @@ func ListIssues(ctx *context.APIContext) { // "$ref": "#/responses/notFound" before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } @@ -428,7 +428,7 @@ func ListIssues(ctx *context.APIContext) { if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 { labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, splitted) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err) + ctx.APIErrorInternal(err) return } } @@ -444,7 +444,7 @@ func ListIssues(ctx *context.APIContext) { continue } if !issues_model.IsErrMilestoneNotExist(err) { - ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoIDANDName", err) + ctx.APIErrorInternal(err) return } id, err := strconv.ParseInt(part[i], 10, 64) @@ -459,7 +459,7 @@ func ListIssues(ctx *context.APIContext) { if issues_model.IsErrMilestoneNotExist(err) { continue } - ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err) + ctx.APIErrorInternal(err) } } @@ -474,7 +474,7 @@ func ListIssues(ctx *context.APIContext) { } if isPull.Has() && !ctx.Repo.CanReadIssuesOrPulls(isPull.Value()) { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -482,7 +482,7 @@ func ListIssues(ctx *context.APIContext) { canReadIssues := ctx.Repo.CanRead(unit.TypeIssues) canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests) if !canReadIssues && !canReadPulls { - ctx.NotFound() + ctx.APIErrorNotFound() return } else if !canReadIssues { isPull = optional.Some(true) @@ -538,10 +538,10 @@ func ListIssues(ctx *context.APIContext) { } if createdByID > 0 { - searchOpt.PosterID = optional.Some(createdByID) + searchOpt.PosterID = strconv.FormatInt(createdByID, 10) } if assignedByID > 0 { - searchOpt.AssigneeID = optional.Some(assignedByID) + searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 10) } if mentionedByID > 0 { searchOpt.MentionID = optional.Some(mentionedByID) @@ -549,12 +549,12 @@ func ListIssues(ctx *context.APIContext) { ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt) if err != nil { - ctx.Error(http.StatusInternalServerError, "SearchIssues", err) + ctx.APIErrorInternal(err) return } issues, err := issues_model.GetIssuesByIDs(ctx, ids, true) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err) + ctx.APIErrorInternal(err) return } @@ -571,12 +571,12 @@ func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 { user, err := user_model.GetUserByName(ctx, userName) if user_model.IsErrUserNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) return 0 } if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return 0 } @@ -616,14 +616,14 @@ func GetIssue(ctx *context.APIContext) { issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { - ctx.NotFound() + ctx.APIErrorNotFound() return } ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue)) @@ -691,9 +691,9 @@ func CreateIssue(ctx *context.APIContext) { assigneeIDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: [name: %s]", err)) } else { - ctx.Error(http.StatusInternalServerError, "AddAssigneeByName", err) + ctx.APIErrorInternal(err) } return } @@ -702,17 +702,17 @@ func CreateIssue(ctx *context.APIContext) { for _, aID := range assigneeIDs { assignee, err := user_model.GetUserByID(ctx, aID) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserByID", err) + ctx.APIErrorInternal(err) return } valid, err := access_model.CanBeAssigned(ctx, assignee, ctx.Repo.Repository, false) if err != nil { - ctx.Error(http.StatusInternalServerError, "canBeAssigned", err) + ctx.APIErrorInternal(err) return } if !valid { - ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name}) + ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name}) return } } @@ -723,11 +723,11 @@ func CreateIssue(ctx *context.APIContext) { if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { - ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err) + ctx.APIError(http.StatusBadRequest, err) } else if errors.Is(err, user_model.ErrBlockedUser) { - ctx.Error(http.StatusForbidden, "NewIssue", err) + ctx.APIError(http.StatusForbidden, err) } else { - ctx.Error(http.StatusInternalServerError, "NewIssue", err) + ctx.APIErrorInternal(err) } return } @@ -735,10 +735,10 @@ func CreateIssue(ctx *context.APIContext) { if form.Closed { if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { if issues_model.IsErrDependenciesLeft(err) { - ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies") + ctx.APIError(http.StatusPreconditionFailed, "cannot close this issue because it still has open dependencies") return } - ctx.Error(http.StatusInternalServerError, "ChangeStatus", err) + ctx.APIErrorInternal(err) return } } @@ -746,7 +746,7 @@ func CreateIssue(ctx *context.APIContext) { // Refetch from database to assign some automatic values issue, err = issues_model.GetIssueByID(ctx, issue.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetIssueByID", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, issue)) @@ -796,9 +796,9 @@ func EditIssue(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } @@ -807,7 +807,7 @@ func EditIssue(ctx *context.APIContext) { err = issue.LoadAttributes(ctx) if err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } @@ -819,7 +819,7 @@ func EditIssue(ctx *context.APIContext) { if len(form.Title) > 0 { err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title) if err != nil { - ctx.Error(http.StatusInternalServerError, "ChangeTitle", err) + ctx.APIErrorInternal(err) return } } @@ -827,18 +827,18 @@ func EditIssue(ctx *context.APIContext) { err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion) if err != nil { if errors.Is(err, issues_model.ErrIssueAlreadyChanged) { - ctx.Error(http.StatusBadRequest, "ChangeContent", err) + ctx.APIError(http.StatusBadRequest, err) return } - ctx.Error(http.StatusInternalServerError, "ChangeContent", err) + ctx.APIErrorInternal(err) return } } if form.Ref != nil { err = issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, *form.Ref) if err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateRef", err) + ctx.APIErrorInternal(err) return } } @@ -849,7 +849,7 @@ func EditIssue(ctx *context.APIContext) { if form.RemoveDeadline == nil || !*form.RemoveDeadline { if form.Deadline == nil { - ctx.Error(http.StatusBadRequest, "", "The due_date cannot be empty") + ctx.APIError(http.StatusBadRequest, "The due_date cannot be empty") return } if !form.Deadline.IsZero() { @@ -860,7 +860,7 @@ func EditIssue(ctx *context.APIContext) { } if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err) + ctx.APIErrorInternal(err) return } issue.DeadlineUnix = deadlineUnix @@ -883,9 +883,9 @@ 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.Error(http.StatusForbidden, "UpdateAssignees", err) + ctx.APIError(http.StatusForbidden, err) } else { - ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err) + ctx.APIErrorInternal(err) } return } @@ -895,19 +895,28 @@ func EditIssue(ctx *context.APIContext) { issue.MilestoneID != *form.Milestone { oldMilestoneID := issue.MilestoneID issue.MilestoneID = *form.Milestone + if issue.MilestoneID > 0 { + issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, *form.Milestone) + if err != nil { + ctx.APIErrorInternal(err) + return + } + } else { + issue.Milestone = nil + } if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil { - ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err) + ctx.APIErrorInternal(err) return } } if form.State != nil { if issue.IsPull { if err := issue.LoadPullRequest(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "GetPullRequest", err) + ctx.APIErrorInternal(err) return } if issue.PullRequest.HasMerged { - ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged") + ctx.APIError(http.StatusPreconditionFailed, "cannot change state of this pull request, it was already merged") return } } @@ -922,11 +931,11 @@ func EditIssue(ctx *context.APIContext) { // Refetch from database to assign some automatic values issue, err = issues_model.GetIssueByID(ctx, issue.ID) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } if err = issue.LoadMilestone(ctx); err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, issue)) @@ -963,15 +972,15 @@ func DeleteIssue(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByID", err) + ctx.APIErrorInternal(err) } return } if err = issue_service.DeleteIssue(ctx, ctx.Doer, ctx.Repo.GitRepo, issue); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteIssueByID", err) + ctx.APIErrorInternal(err) return } @@ -1019,21 +1028,21 @@ func UpdateIssueDeadline(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { - ctx.Error(http.StatusForbidden, "", "Not repo writer") + ctx.APIError(http.StatusForbidden, "Not repo writer") return } deadlineUnix, _ := common.ParseAPIDeadlineToEndOfDay(form.Deadline) if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err) + ctx.APIErrorInternal(err) return } @@ -1042,22 +1051,22 @@ func UpdateIssueDeadline(ctx *context.APIContext) { func closeOrReopenIssue(ctx *context.APIContext, issue *issues_model.Issue, state api.StateType) { if state != api.StateOpen && state != api.StateClosed { - ctx.Error(http.StatusPreconditionFailed, "UnknownIssueStateError", fmt.Sprintf("unknown state: %s", state)) + ctx.APIError(http.StatusPreconditionFailed, fmt.Sprintf("unknown state: %s", state)) return } if state == api.StateClosed && !issue.IsClosed { if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { if issues_model.IsErrDependenciesLeft(err) { - ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue or pull request because it still has open dependencies") + ctx.APIError(http.StatusPreconditionFailed, "cannot close this issue or pull request because it still has open dependencies") return } - ctx.Error(http.StatusInternalServerError, "CloseIssue", err) + ctx.APIErrorInternal(err) return } } else if state == api.StateOpen && issue.IsClosed { if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil { - ctx.Error(http.StatusInternalServerError, "ReopenIssue", err) + ctx.APIErrorInternal(err) return } } diff --git a/routers/api/v1/repo/issue_attachment.go b/routers/api/v1/repo/issue_attachment.go index d0bcadde37..3f751a295c 100644 --- a/routers/api/v1/repo/issue_attachment.go +++ b/routers/api/v1/repo/issue_attachment.go @@ -104,7 +104,7 @@ func ListIssueAttachments(ctx *context.APIContext) { } if err := issue.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } @@ -171,7 +171,7 @@ func CreateIssueAttachment(ctx *context.APIContext) { // Get uploaded file from request file, header, err := ctx.Req.FormFile("attachment") if err != nil { - ctx.Error(http.StatusInternalServerError, "FormFile", err) + ctx.APIErrorInternal(err) return } defer file.Close() @@ -189,9 +189,9 @@ func CreateIssueAttachment(ctx *context.APIContext) { }) if err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "UploadAttachment", err) + ctx.APIErrorInternal(err) } return } @@ -199,7 +199,7 @@ func CreateIssueAttachment(ctx *context.APIContext) { issue.Attachments = append(issue.Attachments, attachment) if err := issue_service.ChangeContent(ctx, issue, ctx.Doer, issue.Content, issue.ContentVersion); err != nil { - ctx.Error(http.StatusInternalServerError, "ChangeContent", err) + ctx.APIErrorInternal(err) return } @@ -265,10 +265,10 @@ func EditIssueAttachment(ctx *context.APIContext) { if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attachment); err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "UpdateAttachment", err) + ctx.APIErrorInternal(err) return } @@ -319,7 +319,7 @@ func DeleteIssueAttachment(ctx *context.APIContext) { } if err := repo_model.DeleteAttachment(ctx, attachment, true); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteAttachment", err) + ctx.APIErrorInternal(err) return } @@ -329,7 +329,7 @@ func DeleteIssueAttachment(ctx *context.APIContext) { func getIssueFromContext(ctx *context.APIContext) *issues_model.Issue { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err) + ctx.NotFoundOrServerError(err) return nil } @@ -354,7 +354,7 @@ func getIssueAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment func getIssueAttachmentSafeRead(ctx *context.APIContext, issue *issues_model.Issue) *repo_model.Attachment { attachment, err := repo_model.GetAttachmentByID(ctx, ctx.PathParamInt64("attachment_id")) if err != nil { - ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err) + ctx.NotFoundOrServerError(err) return nil } if !attachmentBelongsToRepoOrIssue(ctx, attachment, issue) { @@ -366,7 +366,7 @@ func getIssueAttachmentSafeRead(ctx *context.APIContext, issue *issues_model.Iss func canUserWriteIssueAttachment(ctx *context.APIContext, issue *issues_model.Issue) bool { canEditIssue := ctx.IsSigned && (ctx.Doer.ID == issue.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) if !canEditIssue { - ctx.Error(http.StatusForbidden, "", "user should have permission to write issue") + ctx.APIError(http.StatusForbidden, "user should have permission to write issue") return false } @@ -376,16 +376,16 @@ func canUserWriteIssueAttachment(ctx *context.APIContext, issue *issues_model.Is func attachmentBelongsToRepoOrIssue(ctx *context.APIContext, attachment *repo_model.Attachment, issue *issues_model.Issue) bool { if attachment.RepoID != ctx.Repo.Repository.ID { log.Debug("Requested attachment[%d] does not belong to repo[%-v].", attachment.ID, ctx.Repo.Repository) - ctx.NotFound("no such attachment in repo") + ctx.APIErrorNotFound("no such attachment in repo") return false } if attachment.IssueID == 0 { log.Debug("Requested attachment[%d] is not in an issue.", attachment.ID) - ctx.NotFound("no such attachment in issue") + ctx.APIErrorNotFound("no such attachment in issue") return false } else if issue != nil && attachment.IssueID != issue.ID { log.Debug("Requested attachment[%d] does not belong to issue[%d, #%d].", attachment.ID, issue.ID, issue.Index) - ctx.NotFound("no such attachment in issue") + ctx.APIErrorNotFound("no such attachment in issue") return false } return true diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go index 96a61a527e..cc342a9313 100644 --- a/routers/api/v1/repo/issue_comment.go +++ b/routers/api/v1/repo/issue_comment.go @@ -65,16 +65,16 @@ func ListIssueComments(ctx *context.APIContext) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err) + ctx.APIErrorInternal(err) return } if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -89,23 +89,23 @@ func ListIssueComments(ctx *context.APIContext) { comments, err := issues_model.FindComments(ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindComments", err) + ctx.APIErrorInternal(err) return } totalCount, err := issues_model.CountComments(ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } if err := comments.LoadPosters(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadPosters", err) + ctx.APIErrorInternal(err) return } if err := comments.LoadAttachments(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttachments", err) + ctx.APIErrorInternal(err) return } @@ -169,12 +169,12 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err) + ctx.APIErrorInternal(err) return } issue.Repo = ctx.Repo.Repository @@ -189,12 +189,12 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) { comments, err := issues_model.FindComments(ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindComments", err) + ctx.APIErrorInternal(err) return } if err := comments.LoadPosters(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadPosters", err) + ctx.APIErrorInternal(err) return } @@ -274,7 +274,7 @@ func ListRepoIssueComments(ctx *context.APIContext) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } @@ -288,7 +288,7 @@ func ListRepoIssueComments(ctx *context.APIContext) { } else if canReadPull { isPull = optional.Some(true) } else { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -303,32 +303,32 @@ func ListRepoIssueComments(ctx *context.APIContext) { comments, err := issues_model.FindComments(ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindComments", err) + ctx.APIErrorInternal(err) return } totalCount, err := issues_model.CountComments(ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } if err = comments.LoadPosters(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadPosters", err) + ctx.APIErrorInternal(err) return } apiComments := make([]*api.Comment, len(comments)) if err := comments.LoadIssues(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadIssues", err) + ctx.APIErrorInternal(err) return } if err := comments.LoadAttachments(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttachments", err) + ctx.APIErrorInternal(err) return } if _, err := comments.Issues().LoadRepositories(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadRepositories", err) + ctx.APIErrorInternal(err) return } for i := range comments { @@ -382,26 +382,26 @@ func CreateIssueComment(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateIssueCommentOption) issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) return } if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { - ctx.NotFound() + ctx.APIErrorNotFound() return } if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { - ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked"))) + ctx.APIError(http.StatusForbidden, errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked"))) return } comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil) if err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.Error(http.StatusForbidden, "CreateIssueComment", err) + ctx.APIError(http.StatusForbidden, err) } else { - ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err) + ctx.APIErrorInternal(err) } return } @@ -448,15 +448,15 @@ func GetIssueComment(ctx *context.APIContext) { comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrCommentNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) } else { - ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) + ctx.APIErrorInternal(err) } return } if err = comment.LoadIssue(ctx); err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } if comment.Issue.RepoID != ctx.Repo.Repository.ID { @@ -465,7 +465,7 @@ func GetIssueComment(ctx *context.APIContext) { } if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -475,7 +475,7 @@ func GetIssueComment(ctx *context.APIContext) { } if err := comment.LoadPoster(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "comment.LoadPoster", err) + ctx.APIErrorInternal(err) return } @@ -582,15 +582,15 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrCommentNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) } else { - ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) + ctx.APIErrorInternal(err) } return } if err := comment.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadIssue", err) + ctx.APIErrorInternal(err) return } @@ -609,15 +609,17 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) return } - oldContent := comment.Content - comment.Content = form.Body - if err := issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, oldContent); err != nil { - if errors.Is(err, user_model.ErrBlockedUser) { - ctx.Error(http.StatusForbidden, "UpdateComment", err) - } else { - ctx.Error(http.StatusInternalServerError, "UpdateComment", err) + if form.Body != comment.Content { + oldContent := comment.Content + comment.Content = form.Body + if err := issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, oldContent); err != nil { + if errors.Is(err, user_model.ErrBlockedUser) { + ctx.APIError(http.StatusForbidden, err) + } else { + ctx.APIErrorInternal(err) + } + return } - return } ctx.JSON(http.StatusOK, convert.ToAPIComment(ctx, ctx.Repo.Repository, comment)) @@ -699,15 +701,15 @@ func deleteIssueComment(ctx *context.APIContext) { comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrCommentNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) } else { - ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) + ctx.APIErrorInternal(err) } return } if err := comment.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadIssue", err) + ctx.APIErrorInternal(err) return } @@ -725,7 +727,7 @@ func deleteIssueComment(ctx *context.APIContext) { } if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/repo/issue_comment_attachment.go b/routers/api/v1/repo/issue_comment_attachment.go index a556a803e5..5f660c5750 100644 --- a/routers/api/v1/repo/issue_comment_attachment.go +++ b/routers/api/v1/repo/issue_comment_attachment.go @@ -67,7 +67,7 @@ func GetIssueCommentAttachment(ctx *context.APIContext) { } if attachment.CommentID != comment.ID { log.Debug("User requested attachment[%d] is not in comment[%d].", attachment.ID, comment.ID) - ctx.NotFound("attachment not in comment") + ctx.APIErrorNotFound("attachment not in comment") return } @@ -109,7 +109,7 @@ func ListIssueCommentAttachments(ctx *context.APIContext) { } if err := comment.LoadAttachments(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttachments", err) + ctx.APIErrorInternal(err) return } @@ -179,7 +179,7 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) { // Get uploaded file from request file, header, err := ctx.Req.FormFile("attachment") if err != nil { - ctx.Error(http.StatusInternalServerError, "FormFile", err) + ctx.APIErrorInternal(err) return } defer file.Close() @@ -198,23 +198,23 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) { }) if err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "UploadAttachment", err) + ctx.APIErrorInternal(err) } return } if err := comment.LoadAttachments(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttachments", err) + ctx.APIErrorInternal(err) return } if err = issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, comment.Content); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.Error(http.StatusForbidden, "UpdateComment", err) + ctx.APIError(http.StatusForbidden, err) } else { - ctx.ServerError("UpdateComment", err) + ctx.APIErrorInternal(err) } return } @@ -279,10 +279,10 @@ func EditIssueCommentAttachment(ctx *context.APIContext) { if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attach); err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach)) @@ -331,7 +331,7 @@ func DeleteIssueCommentAttachment(ctx *context.APIContext) { } if err := repo_model.DeleteAttachment(ctx, attach, true); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteAttachment", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) @@ -340,15 +340,15 @@ func DeleteIssueCommentAttachment(ctx *context.APIContext) { func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment { comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { - ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err) + ctx.NotFoundOrServerError(err) return nil } if err := comment.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err) + ctx.APIErrorInternal(err) return nil } if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID { - ctx.Error(http.StatusNotFound, "", "no matching issue comment found") + ctx.APIError(http.StatusNotFound, "no matching issue comment found") return nil } @@ -375,7 +375,7 @@ func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Att func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool { canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) if !canEditComment { - ctx.Error(http.StatusForbidden, "", "user should have permission to edit comment") + ctx.APIError(http.StatusForbidden, "user should have permission to edit comment") return false } @@ -385,7 +385,7 @@ func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_model.Comment) *repo_model.Attachment { attachment, err := repo_model.GetAttachmentByID(ctx, ctx.PathParamInt64("attachment_id")) if err != nil { - ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err) + ctx.NotFoundOrServerError(err) return nil } if !attachmentBelongsToRepoOrComment(ctx, attachment, comment) { @@ -397,17 +397,17 @@ func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_ func attachmentBelongsToRepoOrComment(ctx *context.APIContext, attachment *repo_model.Attachment, comment *issues_model.Comment) bool { if attachment.RepoID != ctx.Repo.Repository.ID { log.Debug("Requested attachment[%d] does not belong to repo[%-v].", attachment.ID, ctx.Repo.Repository) - ctx.NotFound("no such attachment in repo") + ctx.APIErrorNotFound("no such attachment in repo") return false } if attachment.IssueID == 0 || attachment.CommentID == 0 { log.Debug("Requested attachment[%d] is not in a comment.", attachment.ID) - ctx.NotFound("no such attachment in comment") + ctx.APIErrorNotFound("no such attachment in comment") return false } if comment != nil && attachment.CommentID != comment.ID { log.Debug("Requested attachment[%d] does not belong to comment[%d].", attachment.ID, comment.ID) - ctx.NotFound("no such attachment in comment") + ctx.APIErrorNotFound("no such attachment in comment") return false } return true diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go index 19dcf999b8..1b58beb7b6 100644 --- a/routers/api/v1/repo/issue_dependency.go +++ b/routers/api/v1/repo/issue_dependency.go @@ -57,30 +57,27 @@ func GetIssueDependencies(ctx *context.APIContext) { // If this issue's repository does not enable dependencies then there can be no dependencies by default if !ctx.Repo.Repository.IsDependenciesEnabled(ctx) { - ctx.NotFound() + ctx.APIErrorNotFound() return } issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound("IsErrIssueNotExist", err) + ctx.APIErrorNotFound("IsErrIssueNotExist", err) } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } // 1. We must be able to read this issue if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) { - ctx.NotFound() + ctx.APIErrorNotFound() return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) limit := ctx.FormInt("limit") if limit == 0 { limit = setting.API.DefaultPagingNum @@ -98,7 +95,7 @@ func GetIssueDependencies(ctx *context.APIContext) { PageSize: limit, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "BlockedByDependencies", err) + ctx.APIErrorInternal(err) return } @@ -116,7 +113,7 @@ func GetIssueDependencies(ctx *context.APIContext) { var err error perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer) if err != nil { - ctx.ServerError("GetUserRepoPermission", err) + ctx.APIErrorInternal(err) return } repoPerms[blocker.RepoID] = perm @@ -324,14 +321,11 @@ func GetIssueBlocks(ctx *context.APIContext) { } if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) { - ctx.NotFound() + ctx.APIErrorNotFound() return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) limit := ctx.FormInt("limit") if limit <= 1 { limit = setting.API.DefaultPagingNum @@ -342,7 +336,7 @@ func GetIssueBlocks(ctx *context.APIContext) { deps, err := issue.BlockingDependencies(ctx) if err != nil { - ctx.Error(http.StatusInternalServerError, "BlockingDependencies", err) + ctx.APIErrorInternal(err) return } @@ -367,7 +361,7 @@ func GetIssueBlocks(ctx *context.APIContext) { var err error perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer) if err != nil { - ctx.ServerError("GetUserRepoPermission", err) + ctx.APIErrorInternal(err) return } repoPerms[depMeta.RepoID] = perm @@ -502,9 +496,9 @@ func getParamsIssue(ctx *context.APIContext) *issues_model.Issue { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound("IsErrIssueNotExist", err) + ctx.APIErrorNotFound("IsErrIssueNotExist", err) } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return nil } @@ -523,9 +517,9 @@ func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Is repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, form.Owner, form.Name) if err != nil { if repo_model.IsErrRepoNotExist(err) { - ctx.NotFound("IsErrRepoNotExist", err) + ctx.APIErrorNotFound("IsErrRepoNotExist", err) } else { - ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerAndName", err) + ctx.APIErrorInternal(err) } return nil } @@ -536,9 +530,9 @@ func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Is issue, err := issues_model.GetIssueByIndex(ctx, repo.ID, form.Index) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound("IsErrIssueNotExist", err) + ctx.APIErrorNotFound("IsErrIssueNotExist", err) } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return nil } @@ -553,7 +547,7 @@ func getPermissionForRepo(ctx *context.APIContext, repo *repo_model.Repository) perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + ctx.APIErrorInternal(err) return nil } @@ -563,25 +557,25 @@ func getPermissionForRepo(ctx *context.APIContext, repo *repo_model.Repository) func createIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) { if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) { // The target's repository doesn't have dependencies enabled - ctx.NotFound() + ctx.APIErrorNotFound() return } if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) { // We can't write to the target - ctx.NotFound() + ctx.APIErrorNotFound() return } if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) { // We can't read the dependency - ctx.NotFound() + ctx.APIErrorNotFound() return } err := issues_model.CreateIssueDependency(ctx, ctx.Doer, target, dependency) if err != nil { - ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err) + ctx.APIErrorInternal(err) return } } @@ -589,25 +583,25 @@ func createIssueDependency(ctx *context.APIContext, target, dependency *issues_m func removeIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) { if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) { // The target's repository doesn't have dependencies enabled - ctx.NotFound() + ctx.APIErrorNotFound() return } if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) { // We can't write to the target - ctx.NotFound() + ctx.APIErrorNotFound() return } if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) { // We can't read the dependency - ctx.NotFound() + ctx.APIErrorNotFound() return } err := issues_model.RemoveIssueDependency(ctx, ctx.Doer, target, dependency, issues_model.DependencyTypeBlockedBy) if err != nil { - ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err) + ctx.APIErrorInternal(err) return } } diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go index ee1a842bc6..d5eee2d469 100644 --- a/routers/api/v1/repo/issue_label.go +++ b/routers/api/v1/repo/issue_label.go @@ -5,7 +5,7 @@ package repo import ( - "fmt" + "errors" "net/http" "reflect" @@ -50,15 +50,15 @@ func ListIssueLabels(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } if err := issue.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } @@ -110,13 +110,13 @@ func AddIssueLabels(ctx *context.APIContext) { } if err = issue_service.AddLabels(ctx, issue, ctx.Doer, labels); err != nil { - ctx.Error(http.StatusInternalServerError, "AddLabels", err) + ctx.APIErrorInternal(err) return } labels, err = issues_model.GetLabelsByIssueID(ctx, issue.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLabelsByIssueID", err) + ctx.APIErrorInternal(err) return } @@ -166,9 +166,9 @@ func DeleteIssueLabel(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } @@ -181,15 +181,15 @@ func DeleteIssueLabel(ctx *context.APIContext) { label, err := issues_model.GetLabelByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrLabelNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "GetLabelByID", err) + ctx.APIErrorInternal(err) } return } if err := issue_service.RemoveLabel(ctx, issue, ctx.Doer, label); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteIssueLabel", err) + ctx.APIErrorInternal(err) return } @@ -240,13 +240,13 @@ func ReplaceIssueLabels(ctx *context.APIContext) { } if err := issue_service.ReplaceLabels(ctx, issue, ctx.Doer, labels); err != nil { - ctx.Error(http.StatusInternalServerError, "ReplaceLabels", err) + ctx.APIErrorInternal(err) return } labels, err = issues_model.GetLabelsByIssueID(ctx, issue.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLabelsByIssueID", err) + ctx.APIErrorInternal(err) return } @@ -288,9 +288,9 @@ func ClearIssueLabels(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } @@ -301,7 +301,7 @@ func ClearIssueLabels(ctx *context.APIContext) { } if err := issue_service.ClearLabels(ctx, issue, ctx.Doer); err != nil { - ctx.Error(http.StatusInternalServerError, "ClearLabels", err) + ctx.APIErrorInternal(err) return } @@ -312,16 +312,16 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return nil, nil, err } if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { - ctx.Error(http.StatusForbidden, "CanWriteIssuesOrPulls", "write permission is required") - return nil, nil, fmt.Errorf("permission denied") + ctx.APIError(http.StatusForbidden, "write permission is required") + return nil, nil, errors.New("permission denied") } var ( @@ -336,25 +336,25 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) case reflect.String: labelNames = append(labelNames, rv.String()) default: - ctx.Error(http.StatusBadRequest, "InvalidLabel", "a label must be an integer or a string") - return nil, nil, fmt.Errorf("invalid label") + ctx.APIError(http.StatusBadRequest, "a label must be an integer or a string") + return nil, nil, errors.New("invalid label") } } if len(labelIDs) > 0 && len(labelNames) > 0 { - ctx.Error(http.StatusBadRequest, "InvalidLabels", "labels should be an array of strings or integers") - return nil, nil, fmt.Errorf("invalid labels") + ctx.APIError(http.StatusBadRequest, "labels should be an array of strings or integers") + return nil, nil, errors.New("invalid labels") } if len(labelNames) > 0 { repoLabelIDs, err := issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err) + ctx.APIErrorInternal(err) return nil, nil, err } labelIDs = append(labelIDs, repoLabelIDs...) if ctx.Repo.Owner.IsOrganization() { orgLabelIDs, err := issues_model.GetLabelIDsInOrgByNames(ctx, ctx.Repo.Owner.ID, labelNames) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLabelIDsInOrgByNames", err) + ctx.APIErrorInternal(err) return nil, nil, err } labelIDs = append(labelIDs, orgLabelIDs...) @@ -363,7 +363,7 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) labels, err := issues_model.GetLabelsByIDs(ctx, labelIDs, "id", "repo_id", "org_id", "name", "exclusive") if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLabelsByIDs", err) + ctx.APIErrorInternal(err) return nil, nil, err } diff --git a/routers/api/v1/repo/issue_lock.go b/routers/api/v1/repo/issue_lock.go new file mode 100644 index 0000000000..b9e5bcf6eb --- /dev/null +++ b/routers/api/v1/repo/issue_lock.go @@ -0,0 +1,152 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "errors" + "net/http" + + issues_model "code.gitea.io/gitea/models/issues" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" +) + +// LockIssue lock an issue +func LockIssue(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{repo}/issues/{index}/lock issue issueLockIssue + // --- + // summary: Lock an issue + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the issue + // type: integer + // format: int64 + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/LockIssueOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + reason := web.GetForm(ctx).(*api.LockIssueOption).Reason + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + if err != nil { + if issues_model.IsErrIssueNotExist(err) { + ctx.APIErrorNotFound(err) + } else { + ctx.APIErrorInternal(err) + } + return + } + + if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + ctx.APIError(http.StatusForbidden, errors.New("no permission to lock this issue")) + return + } + + if !issue.IsLocked { + opt := &issues_model.IssueLockOptions{ + Doer: ctx.ContextUser, + Issue: issue, + Reason: reason, + } + + issue.Repo = ctx.Repo.Repository + err = issues_model.LockIssue(ctx, opt) + if err != nil { + ctx.APIErrorInternal(err) + return + } + } + + ctx.Status(http.StatusNoContent) +} + +// UnlockIssue unlock an issue +func UnlockIssue(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/lock issue issueUnlockIssue + // --- + // summary: Unlock an issue + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the issue + // type: integer + // format: int64 + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) + if err != nil { + if issues_model.IsErrIssueNotExist(err) { + ctx.APIErrorNotFound(err) + } else { + ctx.APIErrorInternal(err) + } + return + } + + if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + ctx.APIError(http.StatusForbidden, errors.New("no permission to unlock this issue")) + return + } + + if issue.IsLocked { + opt := &issues_model.IssueLockOptions{ + Doer: ctx.ContextUser, + Issue: issue, + } + + issue.Repo = ctx.Repo.Repository + err = issues_model.UnlockIssue(ctx, opt) + if err != nil { + ctx.APIErrorInternal(err) + return + } + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/repo/issue_pin.go b/routers/api/v1/repo/issue_pin.go index 388d4a3e99..71985ac765 100644 --- a/routers/api/v1/repo/issue_pin.go +++ b/routers/api/v1/repo/issue_pin.go @@ -44,11 +44,11 @@ func PinIssue(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else if issues_model.IsErrIssueMaxPinReached(err) { - ctx.Error(http.StatusBadRequest, "MaxPinReached", err) + ctx.APIError(http.StatusBadRequest, err) } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } @@ -56,13 +56,13 @@ func PinIssue(ctx *context.APIContext) { // If we don't do this, it will crash when trying to add the pin event to the comment history err = issue.LoadRepo(ctx) if err != nil { - ctx.Error(http.StatusInternalServerError, "LoadRepo", err) + ctx.APIErrorInternal(err) return } - err = issue.Pin(ctx, ctx.Doer) + err = issues_model.PinIssue(ctx, issue, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "PinIssue", err) + ctx.APIErrorInternal(err) return } @@ -101,9 +101,9 @@ func UnpinIssue(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } @@ -111,13 +111,13 @@ func UnpinIssue(ctx *context.APIContext) { // If we don't do this, it will crash when trying to add the unpin event to the comment history err = issue.LoadRepo(ctx) if err != nil { - ctx.Error(http.StatusInternalServerError, "LoadRepo", err) + ctx.APIErrorInternal(err) return } - err = issue.Unpin(ctx, ctx.Doer) + err = issues_model.UnpinIssue(ctx, issue, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "UnpinIssue", err) + ctx.APIErrorInternal(err) return } @@ -162,16 +162,16 @@ func MoveIssuePin(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } - err = issue.MovePin(ctx, int(ctx.PathParamInt64("position"))) + err = issues_model.MovePin(ctx, issue, int(ctx.PathParamInt64("position"))) if err != nil { - ctx.Error(http.StatusInternalServerError, "MovePin", err) + ctx.APIErrorInternal(err) return } @@ -203,7 +203,7 @@ func ListPinnedIssues(ctx *context.APIContext) { // "$ref": "#/responses/notFound" issues, err := issues_model.GetPinnedIssues(ctx, ctx.Repo.Repository.ID, false) if err != nil { - ctx.Error(http.StatusInternalServerError, "LoadPinnedIssues", err) + ctx.APIErrorInternal(err) return } @@ -235,29 +235,29 @@ func ListPinnedPullRequests(ctx *context.APIContext) { // "$ref": "#/responses/notFound" issues, err := issues_model.GetPinnedIssues(ctx, ctx.Repo.Repository.ID, true) if err != nil { - ctx.Error(http.StatusInternalServerError, "LoadPinnedPullRequests", err) + ctx.APIErrorInternal(err) return } apiPrs := make([]*api.PullRequest, len(issues)) if err := issues.LoadPullRequests(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadPullRequests", err) + ctx.APIErrorInternal(err) return } for i, currentIssue := range issues { pr := currentIssue.PullRequest if err = pr.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } if err = pr.LoadBaseRepo(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err) + ctx.APIErrorInternal(err) return } if err = pr.LoadHeadRepo(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err) + ctx.APIErrorInternal(err) return } @@ -295,13 +295,13 @@ func AreNewIssuePinsAllowed(ctx *context.APIContext) { pinsAllowed.Issues, err = issues_model.IsNewPinAllowed(ctx, ctx.Repo.Repository.ID, false) if err != nil { - ctx.Error(http.StatusInternalServerError, "IsNewIssuePinAllowed", err) + ctx.APIErrorInternal(err) return } pinsAllowed.PullRequests, err = issues_model.IsNewPinAllowed(ctx, ctx.Repo.Repository.ID, true) if err != nil { - ctx.Error(http.StatusInternalServerError, "IsNewPullRequestPinAllowed", err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/repo/issue_reaction.go b/routers/api/v1/repo/issue_reaction.go index ead86a717f..e535b5e009 100644 --- a/routers/api/v1/repo/issue_reaction.go +++ b/routers/api/v1/repo/issue_reaction.go @@ -54,36 +54,36 @@ func GetIssueCommentReactions(ctx *context.APIContext) { comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrCommentNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) } else { - ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) + ctx.APIErrorInternal(err) } return } if err := comment.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err) + ctx.APIErrorInternal(err) return } if comment.Issue.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound() + ctx.APIErrorNotFound() return } if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { - ctx.Error(http.StatusForbidden, "GetIssueCommentReactions", errors.New("no permission to get reactions")) + ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions")) return } reactions, _, err := issues_model.FindCommentReactions(ctx, comment.IssueID, comment.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindCommentReactions", err) + ctx.APIErrorInternal(err) return } _, err = reactions.LoadUsers(ctx, ctx.Repo.Repository) if err != nil { - ctx.Error(http.StatusInternalServerError, "ReactionList.LoadUsers()", err) + ctx.APIErrorInternal(err) return } @@ -191,30 +191,30 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrCommentNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) } else { - ctx.Error(http.StatusInternalServerError, "GetCommentByID", err) + ctx.APIErrorInternal(err) } return } if err = comment.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "comment.LoadIssue() failed", err) + ctx.APIErrorInternal(err) return } if comment.Issue.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound() + ctx.APIErrorNotFound() return } if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) { - ctx.NotFound() + ctx.APIErrorNotFound() return } if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) { - ctx.Error(http.StatusForbidden, "ChangeIssueCommentReaction", errors.New("no permission to change reaction")) + ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction")) return } @@ -223,7 +223,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction) if err != nil { if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) { - ctx.Error(http.StatusForbidden, err.Error(), err) + ctx.APIError(http.StatusForbidden, err) } else if issues_model.IsErrReactionAlreadyExist(err) { ctx.JSON(http.StatusOK, api.Reaction{ User: convert.ToUser(ctx, ctx.Doer, ctx.Doer), @@ -231,7 +231,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp Created: reaction.CreatedUnix.AsTime(), }) } else { - ctx.Error(http.StatusInternalServerError, "CreateCommentReaction", err) + ctx.APIErrorInternal(err) } return } @@ -245,7 +245,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp // DeleteIssueCommentReaction part err = issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction) if err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteCommentReaction", err) + ctx.APIErrorInternal(err) return } // ToDo respond 204 @@ -298,26 +298,26 @@ func GetIssueReactions(ctx *context.APIContext) { issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) { - ctx.Error(http.StatusForbidden, "GetIssueReactions", errors.New("no permission to get reactions")) + ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions")) return } reactions, count, err := issues_model.FindIssueReactions(ctx, issue.ID, utils.GetListOptions(ctx)) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindIssueReactions", err) + ctx.APIErrorInternal(err) return } _, err = reactions.LoadUsers(ctx, ctx.Repo.Repository) if err != nil { - ctx.Error(http.StatusInternalServerError, "ReactionList.LoadUsers()", err) + ctx.APIErrorInternal(err) return } @@ -422,15 +422,15 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { - ctx.Error(http.StatusForbidden, "ChangeIssueCommentReaction", errors.New("no permission to change reaction")) + ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction")) return } @@ -439,7 +439,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction) if err != nil { if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) { - ctx.Error(http.StatusForbidden, err.Error(), err) + ctx.APIError(http.StatusForbidden, err) } else if issues_model.IsErrReactionAlreadyExist(err) { ctx.JSON(http.StatusOK, api.Reaction{ User: convert.ToUser(ctx, ctx.Doer, ctx.Doer), @@ -447,7 +447,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i Created: reaction.CreatedUnix.AsTime(), }) } else { - ctx.Error(http.StatusInternalServerError, "CreateIssueReaction", err) + ctx.APIErrorInternal(err) } return } @@ -461,7 +461,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i // DeleteIssueReaction part err = issues_model.DeleteIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Reaction) if err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteIssueReaction", err) + ctx.APIErrorInternal(err) return } // ToDo respond 204 diff --git a/routers/api/v1/repo/issue_stopwatch.go b/routers/api/v1/repo/issue_stopwatch.go index e7fba6d0ed..0f28b9757d 100644 --- a/routers/api/v1/repo/issue_stopwatch.go +++ b/routers/api/v1/repo/issue_stopwatch.go @@ -4,7 +4,6 @@ package repo import ( - "errors" "net/http" issues_model "code.gitea.io/gitea/models/issues" @@ -49,13 +48,16 @@ func StartIssueStopwatch(ctx *context.APIContext) { // "409": // description: Cannot start a stopwatch again if it already exists - issue, err := prepareIssueStopwatch(ctx, false) - if err != nil { + issue := prepareIssueForStopwatch(ctx) + if ctx.Written() { return } - if err := issues_model.CreateIssueStopwatch(ctx, ctx.Doer, issue); err != nil { - ctx.Error(http.StatusInternalServerError, "CreateOrStopIssueStopwatch", err) + if ok, err := issues_model.CreateIssueStopwatch(ctx, ctx.Doer, issue); err != nil { + ctx.APIErrorInternal(err) + return + } else if !ok { + ctx.APIError(http.StatusConflict, "cannot start a stopwatch again if it already exists") return } @@ -96,18 +98,20 @@ func StopIssueStopwatch(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" // "409": - // description: Cannot stop a non existent stopwatch + // description: Cannot stop a non-existent stopwatch - issue, err := prepareIssueStopwatch(ctx, true) - if err != nil { + issue := prepareIssueForStopwatch(ctx) + if ctx.Written() { return } - if err := issues_model.FinishIssueStopwatch(ctx, ctx.Doer, issue); err != nil { - ctx.Error(http.StatusInternalServerError, "CreateOrStopIssueStopwatch", err) + if ok, err := issues_model.FinishIssueStopwatch(ctx, ctx.Doer, issue); err != nil { + ctx.APIErrorInternal(err) + return + } else if !ok { + ctx.APIError(http.StatusConflict, "cannot stop a non-existent stopwatch") return } - ctx.Status(http.StatusCreated) } @@ -145,55 +149,45 @@ func DeleteIssueStopwatch(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" // "409": - // description: Cannot cancel a non existent stopwatch + // description: Cannot cancel a non-existent stopwatch - issue, err := prepareIssueStopwatch(ctx, true) - if err != nil { + issue := prepareIssueForStopwatch(ctx) + if ctx.Written() { return } - if err := issues_model.CancelStopwatch(ctx, ctx.Doer, issue); err != nil { - ctx.Error(http.StatusInternalServerError, "CancelStopwatch", err) + if ok, err := issues_model.CancelStopwatch(ctx, ctx.Doer, issue); err != nil { + ctx.APIErrorInternal(err) + return + } else if !ok { + ctx.APIError(http.StatusConflict, "cannot cancel a non-existent stopwatch") return } ctx.Status(http.StatusNoContent) } -func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*issues_model.Issue, error) { +func prepareIssueForStopwatch(ctx *context.APIContext) *issues_model.Issue { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } - - return nil, err + return nil } if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { ctx.Status(http.StatusForbidden) - return nil, errors.New("Unable to write to PRs") + return nil } if !ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) { ctx.Status(http.StatusForbidden) - return nil, errors.New("Cannot use time tracker") - } - - if issues_model.StopwatchExists(ctx, ctx.Doer.ID, issue.ID) != shouldExist { - if shouldExist { - ctx.Error(http.StatusConflict, "StopwatchExists", "cannot stop/cancel a non existent stopwatch") - err = errors.New("cannot stop/cancel a non existent stopwatch") - } else { - ctx.Error(http.StatusConflict, "StopwatchExists", "cannot start a stopwatch again if it already exists") - err = errors.New("cannot start a stopwatch again if it already exists") - } - return nil, err + return nil } - - return issue, nil + return issue } // GetStopwatches get all stopwatches @@ -220,19 +214,19 @@ func GetStopwatches(ctx *context.APIContext) { sws, err := issues_model.GetUserStopwatches(ctx, ctx.Doer.ID, utils.GetListOptions(ctx)) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserStopwatches", err) + ctx.APIErrorInternal(err) return } count, err := issues_model.CountUserStopwatches(ctx, ctx.Doer.ID) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } apiSWs, err := convert.ToStopWatches(ctx, sws) if err != nil { - ctx.Error(http.StatusInternalServerError, "APIFormat", err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/repo/issue_subscription.go b/routers/api/v1/repo/issue_subscription.go index 4fb80b1ec4..c89f228a06 100644 --- a/routers/api/v1/repo/issue_subscription.go +++ b/routers/api/v1/repo/issue_subscription.go @@ -43,7 +43,7 @@ func AddIssueSubscription(ctx *context.APIContext) { // required: true // - name: user // in: path - // description: user to subscribe + // description: username of the user to subscribe the issue to // type: string // required: true // responses: @@ -87,7 +87,7 @@ func DelIssueSubscription(ctx *context.APIContext) { // required: true // - name: user // in: path - // description: user witch unsubscribe + // description: username of the user to unsubscribe from an issue // type: string // required: true // responses: @@ -107,9 +107,9 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return @@ -118,9 +118,9 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) { user, err := user_model.GetUserByName(ctx, ctx.PathParam("user")) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) } return @@ -128,13 +128,13 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) { // only admin and user for itself can change subscription if user.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin { - ctx.Error(http.StatusForbidden, "User", fmt.Errorf("%s is not permitted to change subscriptions for %s", ctx.Doer.Name, user.Name)) + ctx.APIError(http.StatusForbidden, fmt.Errorf("%s is not permitted to change subscriptions for %s", ctx.Doer.Name, user.Name)) return } current, err := issues_model.CheckIssueWatch(ctx, user, issue) if err != nil { - ctx.Error(http.StatusInternalServerError, "CheckIssueWatch", err) + ctx.APIErrorInternal(err) return } @@ -146,7 +146,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) { // Update watch state if err := issues_model.CreateOrUpdateIssueWatch(ctx, user.ID, issue.ID, watch); err != nil { - ctx.Error(http.StatusInternalServerError, "CreateOrUpdateIssueWatch", err) + ctx.APIErrorInternal(err) return } @@ -188,9 +188,9 @@ func CheckIssueSubscription(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return @@ -198,7 +198,7 @@ func CheckIssueSubscription(ctx *context.APIContext) { watching, err := issues_model.CheckIssueWatch(ctx, ctx.Doer, issue) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, api.WatchInfo{ @@ -254,9 +254,9 @@ func GetIssueSubscribers(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return @@ -264,7 +264,7 @@ func GetIssueSubscribers(ctx *context.APIContext) { iwl, err := issues_model.GetIssueWatchers(ctx, issue.ID, utils.GetListOptions(ctx)) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetIssueWatchers", err) + ctx.APIErrorInternal(err) return } @@ -275,7 +275,7 @@ func GetIssueSubscribers(ctx *context.APIContext) { users, err := user_model.GetUsersByIDs(ctx, userIDs) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUsersByIDs", err) + ctx.APIErrorInternal(err) return } apiUsers := make([]*api.User, 0, len(users)) @@ -285,7 +285,7 @@ func GetIssueSubscribers(ctx *context.APIContext) { count, err := issues_model.CountIssueWatchers(ctx, issue.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "CountIssueWatchers", err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index 57961b0660..171da272cc 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -4,7 +4,7 @@ package repo import ( - "fmt" + "errors" "net/http" "time" @@ -72,15 +72,15 @@ func ListTrackedTimes(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) { - ctx.NotFound("Timetracker is disabled") + ctx.APIErrorNotFound("Timetracker is disabled") return } issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } @@ -95,16 +95,16 @@ func ListTrackedTimes(ctx *context.APIContext) { if qUser != "" { user, err := user_model.GetUserByName(ctx, qUser) if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusNotFound, "User does not exist", err) + ctx.APIError(http.StatusNotFound, err) } else if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) return } opts.UserID = user.ID } if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil { - ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } @@ -116,24 +116,24 @@ func ListTrackedTimes(ctx *context.APIContext) { if opts.UserID == 0 { opts.UserID = ctx.Doer.ID } else { - ctx.Error(http.StatusForbidden, "", fmt.Errorf("query by user not allowed; not enough rights")) + ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights")) return } } count, err := issues_model.CountTrackedTimes(ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err) + ctx.APIErrorInternal(err) return } if err = trackedTimes.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } @@ -184,16 +184,16 @@ func AddTime(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } if !ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) { if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) { - ctx.Error(http.StatusBadRequest, "", "time tracking disabled") + ctx.APIError(http.StatusBadRequest, "time tracking disabled") return } ctx.Status(http.StatusForbidden) @@ -206,7 +206,7 @@ func AddTime(ctx *context.APIContext) { // allow only RepoAdmin, Admin and User to add time user, err = user_model.GetUserByName(ctx, form.User) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) } } } @@ -218,11 +218,11 @@ func AddTime(ctx *context.APIContext) { trackedTime, err := issues_model.AddTime(ctx, user, issue, form.Time, created) if err != nil { - ctx.Error(http.StatusInternalServerError, "AddTime", err) + ctx.APIErrorInternal(err) return } if err = trackedTime.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, convert.ToTrackedTime(ctx, user, trackedTime)) @@ -267,9 +267,9 @@ func ResetIssueTime(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } @@ -286,9 +286,9 @@ func ResetIssueTime(ctx *context.APIContext) { err = issues_model.DeleteIssueUserTimes(ctx, issue, ctx.Doer) if err != nil { if db.IsErrNotExist(err) { - ctx.Error(http.StatusNotFound, "DeleteIssueUserTimes", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "DeleteIssueUserTimes", err) + ctx.APIErrorInternal(err) } return } @@ -340,9 +340,9 @@ func DeleteTime(ctx *context.APIContext) { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) } else { - ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) + ctx.APIErrorInternal(err) } return } @@ -359,14 +359,14 @@ func DeleteTime(ctx *context.APIContext) { time, err := issues_model.GetTrackedTimeByID(ctx, ctx.PathParamInt64("id")) if err != nil { if db.IsErrNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) return } - ctx.Error(http.StatusInternalServerError, "GetTrackedTimeByID", err) + ctx.APIErrorInternal(err) return } if time.Deleted { - ctx.NotFound(fmt.Errorf("tracked time [%d] already deleted", time.ID)) + ctx.APIErrorNotFound("tracked time was already deleted") return } @@ -378,7 +378,7 @@ func DeleteTime(ctx *context.APIContext) { err = issues_model.DeleteTime(ctx, time) if err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteTime", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) @@ -405,7 +405,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) { // required: true // - name: user // in: path - // description: username of user + // description: username of the user whose tracked times are to be listed // type: string // required: true // responses: @@ -419,25 +419,25 @@ func ListTrackedTimesByUser(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) { - ctx.Error(http.StatusBadRequest, "", "time tracking disabled") + ctx.APIError(http.StatusBadRequest, "time tracking disabled") return } user, err := user_model.GetUserByName(ctx, ctx.PathParam("timetrackingusername")) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) } return } if user == nil { - ctx.NotFound() + ctx.APIErrorNotFound() return } if !ctx.IsUserRepoAdmin() && !ctx.Doer.IsAdmin && ctx.Doer.ID != user.ID { - ctx.Error(http.StatusForbidden, "", fmt.Errorf("query by user not allowed; not enough rights")) + ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights")) return } @@ -448,11 +448,11 @@ func ListTrackedTimesByUser(ctx *context.APIContext) { trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err) + ctx.APIErrorInternal(err) return } if err = trackedTimes.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes)) @@ -509,7 +509,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) { - ctx.Error(http.StatusBadRequest, "", "time tracking disabled") + ctx.APIError(http.StatusBadRequest, "time tracking disabled") return } @@ -523,9 +523,9 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { if qUser != "" { user, err := user_model.GetUserByName(ctx, qUser) if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusNotFound, "User does not exist", err) + ctx.APIError(http.StatusNotFound, err) } else if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) return } opts.UserID = user.ID @@ -533,7 +533,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { var err error if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil { - ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } @@ -545,24 +545,24 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { if opts.UserID == 0 { opts.UserID = ctx.Doer.ID } else { - ctx.Error(http.StatusForbidden, "", fmt.Errorf("query by user not allowed; not enough rights")) + ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights")) return } } count, err := issues_model.CountTrackedTimes(ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err) + ctx.APIErrorInternal(err) return } if err = trackedTimes.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } @@ -607,24 +607,24 @@ func ListMyTrackedTimes(ctx *context.APIContext) { var err error if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil { - ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } count, err := issues_model.CountTrackedTimes(ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetTrackedTimesByUser", err) + ctx.APIErrorInternal(err) return } if err = trackedTimes.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go index 23cc922628..8cb61e9e0c 100644 --- a/routers/api/v1/repo/key.go +++ b/routers/api/v1/repo/key.go @@ -92,7 +92,7 @@ func ListDeployKeys(ctx *context.APIContext) { keys, count, err := db.FindAndCount[asymkey_model.DeployKey](ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -100,7 +100,7 @@ func ListDeployKeys(ctx *context.APIContext) { apiKeys := make([]*api.DeployKey, len(keys)) for i := range keys { if err := keys[i].GetContent(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "GetContent", err) + ctx.APIErrorInternal(err) return } apiKeys[i] = convert.ToDeployKey(apiLink, keys[i]) @@ -146,21 +146,21 @@ func GetDeployKey(ctx *context.APIContext) { key, err := asymkey_model.GetDeployKeyByID(ctx, ctx.PathParamInt64("id")) if err != nil { if asymkey_model.IsErrDeployKeyNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetDeployKeyByID", err) + ctx.APIErrorInternal(err) } return } // this check make it more consistent if key.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound() + ctx.APIErrorNotFound() return } if err = key.GetContent(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "GetContent", err) + ctx.APIErrorInternal(err) return } @@ -175,11 +175,11 @@ func GetDeployKey(ctx *context.APIContext) { // HandleCheckKeyStringError handle check key error func HandleCheckKeyStringError(ctx *context.APIContext, err error) { if db.IsErrSSHDisabled(err) { - ctx.Error(http.StatusUnprocessableEntity, "", "SSH is disabled") + ctx.APIError(http.StatusUnprocessableEntity, "SSH is disabled") } else if asymkey_model.IsErrKeyUnableVerify(err) { - ctx.Error(http.StatusUnprocessableEntity, "", "Unable to verify key content") + ctx.APIError(http.StatusUnprocessableEntity, "Unable to verify key content") } else { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid key content: %w", err)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid key content: %w", err)) } } @@ -187,15 +187,15 @@ func HandleCheckKeyStringError(ctx *context.APIContext, err error) { func HandleAddKeyError(ctx *context.APIContext, err error) { switch { case asymkey_model.IsErrDeployKeyAlreadyExist(err): - ctx.Error(http.StatusUnprocessableEntity, "", "This key has already been added to this repository") + ctx.APIError(http.StatusUnprocessableEntity, "This key has already been added to this repository") case asymkey_model.IsErrKeyAlreadyExist(err): - ctx.Error(http.StatusUnprocessableEntity, "", "Key content has been used as non-deploy key") + ctx.APIError(http.StatusUnprocessableEntity, "Key content has been used as non-deploy key") case asymkey_model.IsErrKeyNameAlreadyUsed(err): - ctx.Error(http.StatusUnprocessableEntity, "", "Key title has been used") + ctx.APIError(http.StatusUnprocessableEntity, "Key title has been used") case asymkey_model.IsErrDeployKeyNameAlreadyUsed(err): - ctx.Error(http.StatusUnprocessableEntity, "", "A key with the same name already exists") + ctx.APIError(http.StatusUnprocessableEntity, "A key with the same name already exists") default: - ctx.Error(http.StatusInternalServerError, "AddKey", err) + ctx.APIErrorInternal(err) } } @@ -281,9 +281,9 @@ func DeleteDeploykey(ctx *context.APIContext) { if err := asymkey_service.DeleteDeployKey(ctx, ctx.Repo.Repository, ctx.PathParamInt64("id")); err != nil { if asymkey_model.IsErrKeyAccessDenied(err) { - ctx.Error(http.StatusForbidden, "", "You do not have access to this key") + ctx.APIError(http.StatusForbidden, "You do not have access to this key") } else { - ctx.Error(http.StatusInternalServerError, "DeleteDeployKey", err) + ctx.APIErrorInternal(err) } return } diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go index 1ece2521e0..4f79d42595 100644 --- a/routers/api/v1/repo/label.go +++ b/routers/api/v1/repo/label.go @@ -51,13 +51,13 @@ func ListLabels(ctx *context.APIContext) { labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), utils.GetListOptions(ctx)) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLabelsByRepoID", err) + ctx.APIErrorInternal(err) return } count, err := issues_model.CountLabelsByRepoID(ctx, ctx.Repo.Repository.ID) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -107,9 +107,9 @@ func GetLabel(ctx *context.APIContext) { } if err != nil { if issues_model.IsErrRepoLabelNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err) + ctx.APIErrorInternal(err) } return } @@ -153,7 +153,7 @@ func CreateLabel(ctx *context.APIContext) { color, err := label.NormalizeColor(form.Color) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, "StringToColor", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } form.Color = color @@ -166,7 +166,7 @@ func CreateLabel(ctx *context.APIContext) { } l.SetArchived(form.IsArchived) if err := issues_model.NewLabel(ctx, l); err != nil { - ctx.Error(http.StatusInternalServerError, "NewLabel", err) + ctx.APIErrorInternal(err) return } @@ -215,9 +215,9 @@ func EditLabel(ctx *context.APIContext) { l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrRepoLabelNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetLabelByRepoID", err) + ctx.APIErrorInternal(err) } return } @@ -231,7 +231,7 @@ func EditLabel(ctx *context.APIContext) { if form.Color != nil { color, err := label.NormalizeColor(*form.Color) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, "StringToColor", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } l.Color = color @@ -241,7 +241,7 @@ func EditLabel(ctx *context.APIContext) { } l.SetArchived(form.IsArchived != nil && *form.IsArchived) if err := issues_model.UpdateLabel(ctx, l); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateLabel", err) + ctx.APIErrorInternal(err) return } @@ -277,7 +277,7 @@ func DeleteLabel(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if err := issues_model.DeleteLabel(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteLabel", err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/repo/language.go b/routers/api/v1/repo/language.go index f1d5bbe45f..00789983ce 100644 --- a/routers/api/v1/repo/language.go +++ b/routers/api/v1/repo/language.go @@ -70,7 +70,7 @@ func GetLanguages(ctx *context.APIContext) { langs, err := repo_model.GetLanguageStats(ctx, ctx.Repo.Repository) if err != nil { log.Error("GetLanguageStats failed: %v", err) - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/repo/license.go b/routers/api/v1/repo/license.go index 8a6bdfd42f..3040815e8a 100644 --- a/routers/api/v1/repo/license.go +++ b/routers/api/v1/repo/license.go @@ -38,7 +38,7 @@ func GetLicenses(ctx *context.APIContext) { licenses, err := repo_model.GetRepoLicenses(ctx, ctx.Repo.Repository) if err != nil { log.Error("GetRepoLicenses failed: %v", err) - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index 452825c0a3..c1e0b47d33 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -72,21 +72,21 @@ func Migrate(ctx *context.APIContext) { } if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "GetUser", err) + ctx.APIErrorInternal(err) } return } if ctx.HasAPIError() { - ctx.Error(http.StatusUnprocessableEntity, "", ctx.GetErrMsg()) + ctx.APIError(http.StatusUnprocessableEntity, ctx.GetErrMsg()) return } if !ctx.Doer.IsAdmin { if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID { - ctx.Error(http.StatusForbidden, "", "Given user is not an organization.") + ctx.APIError(http.StatusForbidden, "Given user is not an organization.") return } @@ -94,10 +94,10 @@ func Migrate(ctx *context.APIContext) { // Check ownership of organization. isOwner, err := organization.OrgFromUser(repoOwner).IsOwnedBy(ctx, ctx.Doer.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "IsOwnedBy", err) + ctx.APIErrorInternal(err) return } else if !isOwner { - ctx.Error(http.StatusForbidden, "", "Given user is not owner of organization.") + ctx.APIError(http.StatusForbidden, "Given user is not owner of organization.") return } } @@ -115,12 +115,12 @@ func Migrate(ctx *context.APIContext) { gitServiceType := convert.ToGitServiceType(form.Service) if form.Mirror && setting.Mirror.DisableNewPull { - ctx.Error(http.StatusForbidden, "MirrorsGlobalDisabled", fmt.Errorf("the site administrator has disabled the creation of new pull mirrors")) + ctx.APIError(http.StatusForbidden, errors.New("the site administrator has disabled the creation of new pull mirrors")) return } if setting.Repository.DisableMigrations { - ctx.Error(http.StatusForbidden, "MigrationsGlobalDisabled", fmt.Errorf("the site administrator has disabled migrations")) + ctx.APIError(http.StatusForbidden, errors.New("the site administrator has disabled migrations")) return } @@ -129,7 +129,7 @@ func Migrate(ctx *context.APIContext) { if form.LFS && len(form.LFSEndpoint) > 0 { ep := lfs.DetermineEndpoint("", form.LFSEndpoint) if ep == nil { - ctx.Error(http.StatusInternalServerError, "", ctx.Tr("repo.migrate.invalid_lfs_endpoint")) + ctx.APIErrorInternal(errors.New("the LFS endpoint is not valid")) return } err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer) @@ -181,7 +181,7 @@ func Migrate(ctx *context.APIContext) { IsPrivate: opts.Private || setting.Repository.ForcePrivate, IsMirror: opts.Mirror, Status: repo_model.RepositoryBeingMigrated, - }) + }, false) if err != nil { handleMigrateError(ctx, repoOwner, err) return @@ -203,7 +203,7 @@ func Migrate(ctx *context.APIContext) { } if repo != nil { - if errDelete := repo_service.DeleteRepositoryDirectly(ctx, ctx.Doer, repo.ID); errDelete != nil { + if errDelete := repo_service.DeleteRepositoryDirectly(ctx, repo.ID); errDelete != nil { log.Error("DeleteRepository: %v", errDelete) } } @@ -221,35 +221,35 @@ func Migrate(ctx *context.APIContext) { func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, err error) { switch { case repo_model.IsErrRepoAlreadyExist(err): - ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.") + ctx.APIError(http.StatusConflict, "The repository with the same name already exists.") case repo_model.IsErrRepoFilesAlreadyExist(err): - ctx.Error(http.StatusConflict, "", "Files already exist for this repository. Adopt them or delete them.") + ctx.APIError(http.StatusConflict, "Files already exist for this repository. Adopt them or delete them.") case migrations.IsRateLimitError(err): - ctx.Error(http.StatusUnprocessableEntity, "", "Remote visit addressed rate limitation.") + ctx.APIError(http.StatusUnprocessableEntity, "Remote visit addressed rate limitation.") case migrations.IsTwoFactorAuthError(err): - ctx.Error(http.StatusUnprocessableEntity, "", "Remote visit required two factors authentication.") + ctx.APIError(http.StatusUnprocessableEntity, "Remote visit required two factors authentication.") case repo_model.IsErrReachLimitOfRepo(err): - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("You have already reached your limit of %d repositories.", repoOwner.MaxCreationLimit())) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("You have already reached your limit of %d repositories.", repoOwner.MaxCreationLimit())) case db.IsErrNameReserved(err): - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' is reserved.", err.(db.ErrNameReserved).Name)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("The username '%s' is reserved.", err.(db.ErrNameReserved).Name)) case db.IsErrNameCharsNotAllowed(err): - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' contains invalid characters.", err.(db.ErrNameCharsNotAllowed).Name)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("The username '%s' contains invalid characters.", err.(db.ErrNameCharsNotAllowed).Name)) case db.IsErrNamePatternNotAllowed(err): - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(db.ErrNamePatternNotAllowed).Pattern)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(db.ErrNamePatternNotAllowed).Pattern)) case git.IsErrInvalidCloneAddr(err): - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) case base.IsErrNotSupported(err): - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) default: err = util.SanitizeErrorCredentialURLs(err) if strings.Contains(err.Error(), "Authentication failed") || strings.Contains(err.Error(), "Bad credentials") || strings.Contains(err.Error(), "could not read Username") { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Authentication failed: %v.", err)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Authentication failed: %v.", err)) } else if strings.Contains(err.Error(), "fatal:") { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Migration failed: %v.", err)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Migration failed: %v.", err)) } else { - ctx.Error(http.StatusInternalServerError, "MigrateRepository", err) + ctx.APIErrorInternal(err) } } } @@ -259,19 +259,19 @@ func handleRemoteAddrError(ctx *context.APIContext, err error) { addrErr := err.(*git.ErrInvalidCloneAddr) switch { case addrErr.IsURLError: - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) case addrErr.IsPermissionDenied: if addrErr.LocalPath { - ctx.Error(http.StatusUnprocessableEntity, "", "You are not allowed to import local repositories.") + ctx.APIError(http.StatusUnprocessableEntity, "You are not allowed to import local repositories.") } else { - ctx.Error(http.StatusUnprocessableEntity, "", "You can not import from disallowed hosts.") + ctx.APIError(http.StatusUnprocessableEntity, "You can not import from disallowed hosts.") } case addrErr.IsInvalidPath: - ctx.Error(http.StatusUnprocessableEntity, "", "Invalid local path, it does not exist or not a directory.") + ctx.APIError(http.StatusUnprocessableEntity, "Invalid local path, it does not exist or not a directory.") default: - ctx.Error(http.StatusInternalServerError, "ParseRemoteAddr", "Unknown error type (ErrInvalidCloneAddr): "+err.Error()) + ctx.APIErrorInternal(fmt.Errorf("unknown error type (ErrInvalidCloneAddr): %w", err)) } } else { - ctx.Error(http.StatusInternalServerError, "ParseRemoteAddr", err) + ctx.APIErrorInternal(err) } } diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go index 8d7516491e..33fa7c4b16 100644 --- a/routers/api/v1/repo/milestone.go +++ b/routers/api/v1/repo/milestone.go @@ -74,7 +74,7 @@ func ListMilestones(ctx *context.APIContext) { Name: ctx.FormString("name"), }) if err != nil { - ctx.Error(http.StatusInternalServerError, "db.FindAndCount[issues_model.Milestone]", err) + ctx.APIErrorInternal(err) return } @@ -173,7 +173,7 @@ func CreateMilestone(ctx *context.APIContext) { } if err := issues_model.NewMilestone(ctx, milestone); err != nil { - ctx.Error(http.StatusInternalServerError, "NewMilestone", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusCreated, convert.ToAPIMilestone(milestone)) @@ -233,7 +233,7 @@ func EditMilestone(ctx *context.APIContext) { } if err := issues_model.UpdateMilestone(ctx, milestone, oldIsClosed); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateMilestone", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, convert.ToAPIMilestone(milestone)) @@ -272,7 +272,7 @@ func DeleteMilestone(ctx *context.APIContext) { } if err := issues_model.DeleteMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, m.ID); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteMilestoneByRepoID", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) @@ -288,7 +288,7 @@ func getMilestoneByIDOrName(ctx *context.APIContext) *issues_model.Milestone { if err == nil { return milestone } else if !issues_model.IsErrMilestoneNotExist(err) { - ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err) + ctx.APIErrorInternal(err) return nil } } @@ -296,10 +296,10 @@ func getMilestoneByIDOrName(ctx *context.APIContext) *issues_model.Milestone { milestone, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, mile) if err != nil { if issues_model.IsErrMilestoneNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() return nil } - ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err) + ctx.APIErrorInternal(err) return nil } diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go index c911f6830c..f11a1603c4 100644 --- a/routers/api/v1/repo/mirror.go +++ b/routers/api/v1/repo/mirror.go @@ -5,7 +5,6 @@ package repo import ( "errors" - "fmt" "net/http" "time" @@ -53,20 +52,20 @@ func MirrorSync(ctx *context.APIContext) { repo := ctx.Repo.Repository if !ctx.Repo.CanWrite(unit.TypeCode) { - ctx.Error(http.StatusForbidden, "MirrorSync", "Must have write access") + ctx.APIError(http.StatusForbidden, "Must have write access") } if !setting.Mirror.Enabled { - ctx.Error(http.StatusBadRequest, "MirrorSync", "Mirror feature is disabled") + ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled") return } if _, err := repo_model.GetMirrorByRepoID(ctx, repo.ID); err != nil { if errors.Is(err, repo_model.ErrMirrorNotExist) { - ctx.Error(http.StatusBadRequest, "MirrorSync", "Repository is not a mirror") + ctx.APIError(http.StatusBadRequest, "Repository is not a mirror") return } - ctx.Error(http.StatusInternalServerError, "MirrorSync", err) + ctx.APIErrorInternal(err) return } @@ -104,19 +103,19 @@ func PushMirrorSync(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if !setting.Mirror.Enabled { - ctx.Error(http.StatusBadRequest, "PushMirrorSync", "Mirror feature is disabled") + ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled") return } // Get All push mirrors of a specific repo pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, ctx.Repo.Repository.ID, db.ListOptions{}) if err != nil { - ctx.Error(http.StatusNotFound, "PushMirrorSync", err) + ctx.APIError(http.StatusNotFound, err) return } for _, mirror := range pushMirrors { ok := mirror_service.SyncPushMirror(ctx, mirror.ID) if !ok { - ctx.Error(http.StatusInternalServerError, "PushMirrorSync", "error occurred when syncing push mirror "+mirror.RemoteName) + ctx.APIErrorInternal(errors.New("error occurred when syncing push mirror " + mirror.RemoteName)) return } } @@ -161,7 +160,7 @@ func ListPushMirrors(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if !setting.Mirror.Enabled { - ctx.Error(http.StatusBadRequest, "GetPushMirrorsByRepoID", "Mirror feature is disabled") + ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled") return } @@ -169,7 +168,7 @@ func ListPushMirrors(ctx *context.APIContext) { // Get all push mirrors for the specified repository. pushMirrors, count, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, utils.GetListOptions(ctx)) if err != nil { - ctx.Error(http.StatusNotFound, "GetPushMirrorsByRepoID", err) + ctx.APIError(http.StatusNotFound, err) return } @@ -219,7 +218,7 @@ func GetPushMirrorByName(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if !setting.Mirror.Enabled { - ctx.Error(http.StatusBadRequest, "GetPushMirrorByRemoteName", "Mirror feature is disabled") + ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled") return } @@ -230,16 +229,16 @@ func GetPushMirrorByName(ctx *context.APIContext) { RemoteName: mirrorName, }.ToConds()) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetPushMirrors", err) + ctx.APIErrorInternal(err) return } else if !exist { - ctx.Error(http.StatusNotFound, "GetPushMirrors", nil) + ctx.APIError(http.StatusNotFound, nil) return } m, err := convert.ToPushMirror(ctx, pushMirror) if err != nil { - ctx.ServerError("GetPushMirrorByRemoteName", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, m) @@ -280,7 +279,7 @@ func AddPushMirror(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if !setting.Mirror.Enabled { - ctx.Error(http.StatusBadRequest, "AddPushMirror", "Mirror feature is disabled") + ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled") return } @@ -320,7 +319,7 @@ func DeletePushMirrorByRemoteName(ctx *context.APIContext) { // "$ref": "#/responses/error" if !setting.Mirror.Enabled { - ctx.Error(http.StatusBadRequest, "DeletePushMirrorByName", "Mirror feature is disabled") + ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled") return } @@ -328,7 +327,7 @@ func DeletePushMirrorByRemoteName(ctx *context.APIContext) { // Delete push mirror on repo by name. err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: remoteName}) if err != nil { - ctx.Error(http.StatusNotFound, "DeletePushMirrors", err) + ctx.APIError(http.StatusNotFound, err) return } ctx.Status(http.StatusNoContent) @@ -339,7 +338,7 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro interval, err := time.ParseDuration(mirrorOption.Interval) if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { - ctx.Error(http.StatusBadRequest, "CreatePushMirror", err) + ctx.APIError(http.StatusBadRequest, err) return } @@ -354,42 +353,42 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro remoteSuffix, err := util.CryptoRandomString(10) if err != nil { - ctx.ServerError("CryptoRandomString", err) + ctx.APIErrorInternal(err) return } remoteAddress, err := util.SanitizeURL(mirrorOption.RemoteAddress) if err != nil { - ctx.ServerError("SanitizeURL", err) + ctx.APIErrorInternal(err) return } pushMirror := &repo_model.PushMirror{ RepoID: repo.ID, Repo: repo, - RemoteName: fmt.Sprintf("remote_mirror_%s", remoteSuffix), + RemoteName: "remote_mirror_" + remoteSuffix, Interval: interval, SyncOnCommit: mirrorOption.SyncOnCommit, RemoteAddress: remoteAddress, } if err = db.Insert(ctx, pushMirror); err != nil { - ctx.ServerError("InsertPushMirror", err) + ctx.APIErrorInternal(err) return } // if the registration of the push mirrorOption fails remove it from the database if err = mirror_service.AddPushMirrorRemote(ctx, pushMirror, address); err != nil { if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: pushMirror.ID, RepoID: pushMirror.RepoID}); err != nil { - ctx.ServerError("DeletePushMirrors", err) + ctx.APIErrorInternal(err) return } - ctx.ServerError("AddPushMirrorRemote", err) + ctx.APIErrorInternal(err) return } m, err := convert.ToPushMirror(ctx, pushMirror) if err != nil { - ctx.ServerError("ToPushMirror", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, m) @@ -400,13 +399,13 @@ func HandleRemoteAddressError(ctx *context.APIContext, err error) { addrErr := err.(*git.ErrInvalidCloneAddr) switch { case addrErr.IsProtocolInvalid: - ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Invalid mirror protocol") + ctx.APIError(http.StatusBadRequest, "Invalid mirror protocol") case addrErr.IsURLError: - ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Invalid Url ") + ctx.APIError(http.StatusBadRequest, "Invalid Url ") case addrErr.IsPermissionDenied: - ctx.Error(http.StatusUnauthorized, "CreatePushMirror", "Permission denied") + ctx.APIError(http.StatusUnauthorized, "Permission denied") default: - ctx.Error(http.StatusBadRequest, "CreatePushMirror", "Unknown error") + ctx.APIError(http.StatusBadRequest, "Unknown error") } return } diff --git a/routers/api/v1/repo/notes.go b/routers/api/v1/repo/notes.go index 8fec844cc4..87efb1caf2 100644 --- a/routers/api/v1/repo/notes.go +++ b/routers/api/v1/repo/notes.go @@ -4,7 +4,7 @@ package repo import ( - "fmt" + "errors" "net/http" "code.gitea.io/gitea/modules/git" @@ -54,7 +54,7 @@ func GetNote(ctx *context.APIContext) { sha := ctx.PathParam("sha") if !git.IsValidRefPattern(sha) { - ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha)) + ctx.APIError(http.StatusUnprocessableEntity, "no valid ref or sha: "+sha) return } getNote(ctx, sha) @@ -62,16 +62,16 @@ func GetNote(ctx *context.APIContext) { func getNote(ctx *context.APIContext, identifier string) { if ctx.Repo.GitRepo == nil { - ctx.InternalServerError(fmt.Errorf("no open git repo")) + ctx.APIErrorInternal(errors.New("no open git repo")) return } commitID, err := ctx.Repo.GitRepo.ConvertToGitID(identifier) if err != nil { if git.IsErrNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) } else { - ctx.Error(http.StatusInternalServerError, "ConvertToSHA1", err) + ctx.APIErrorInternal(err) } return } @@ -79,10 +79,10 @@ func getNote(ctx *context.APIContext, identifier string) { var note git.Note if err := git.GetNote(ctx, ctx.Repo.GitRepo, commitID.String(), ¬e); err != nil { if git.IsErrNotExist(err) { - ctx.NotFound(identifier) + ctx.APIErrorNotFound("commit doesn't exist: " + identifier) return } - ctx.Error(http.StatusInternalServerError, "GetNote", err) + ctx.APIErrorInternal(err) return } @@ -96,7 +96,7 @@ func getNote(ctx *context.APIContext, identifier string) { Files: files, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "ToCommit", err) + ctx.APIErrorInternal(err) return } apiNote := api.Note{Message: string(note.Message), Commit: cmt} diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go index 5e24dcf891..e9f5cf5d90 100644 --- a/routers/api/v1/repo/patch.go +++ b/routers/api/v1/repo/patch.go @@ -5,15 +5,10 @@ package repo import ( "net/http" - "time" - git_model "code.gitea.io/gitea/models/git" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" - pull_service "code.gitea.io/gitea/services/pull" "code.gitea.io/gitea/services/repository/files" ) @@ -49,63 +44,22 @@ func ApplyDiffPatch(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // "423": // "$ref": "#/responses/repoArchivedError" - apiOpts := web.GetForm(ctx).(*api.ApplyDiffPatchFileOptions) - + apiOpts, changeRepoFileOpts := getAPIChangeRepoFileOptions[*api.ApplyDiffPatchFileOptions](ctx) opts := &files.ApplyDiffPatchOptions{ - Content: apiOpts.Content, - SHA: apiOpts.SHA, - Message: apiOpts.Message, - OldBranch: apiOpts.BranchName, - NewBranch: apiOpts.NewBranchName, - Committer: &files.IdentityOptions{ - Name: apiOpts.Committer.Name, - Email: apiOpts.Committer.Email, - }, - Author: &files.IdentityOptions{ - Name: apiOpts.Author.Name, - Email: apiOpts.Author.Email, - }, - Dates: &files.CommitDateOptions{ - Author: apiOpts.Dates.Author, - Committer: apiOpts.Dates.Committer, - }, - Signoff: apiOpts.Signoff, - } - if opts.Dates.Author.IsZero() { - opts.Dates.Author = time.Now() - } - if opts.Dates.Committer.IsZero() { - opts.Dates.Committer = time.Now() - } - - if opts.Message == "" { - opts.Message = "apply-patch" - } + Content: apiOpts.Content, + Message: util.IfZero(apiOpts.Message, "apply-patch"), - if !canWriteFiles(ctx, apiOpts.BranchName) { - ctx.Error(http.StatusInternalServerError, "ApplyPatch", repo_model.ErrUserDoesNotHaveAccessToRepo{ - UserID: ctx.Doer.ID, - RepoName: ctx.Repo.Repository.LowerName, - }) - return + OldBranch: changeRepoFileOpts.OldBranch, + NewBranch: changeRepoFileOpts.NewBranch, + Committer: changeRepoFileOpts.Committer, + Author: changeRepoFileOpts.Author, + Dates: changeRepoFileOpts.Dates, + Signoff: changeRepoFileOpts.Signoff, } fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts) if err != nil { - if files.IsErrUserCannotCommit(err) || pull_service.IsErrFilePathProtected(err) { - ctx.Error(http.StatusForbidden, "Access", err) - return - } - if git_model.IsErrBranchAlreadyExists(err) || files.IsErrFilenameInvalid(err) || pull_service.IsErrSHADoesNotMatch(err) || - files.IsErrFilePathInvalid(err) || files.IsErrRepoFileAlreadyExists(err) { - ctx.Error(http.StatusUnprocessableEntity, "Invalid", err) - return - } - if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) { - ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err) - return - } - ctx.Error(http.StatusInternalServerError, "ApplyPatch", err) + handleChangeRepoFilesError(ctx, err) } else { ctx.JSON(http.StatusCreated, fileResponse) } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index f7fdc93f81..2c194f9253 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" @@ -59,6 +60,10 @@ func ListPullRequests(ctx *context.APIContext) { // description: Name of the repo // type: string // required: true + // - name: base_branch + // in: query + // description: Filter by target base branch of the pull request + // type: string // - name: state // in: query // description: State of pull request @@ -69,7 +74,7 @@ func ListPullRequests(ctx *context.APIContext) { // in: query // description: Type of sort // type: string - // enum: [oldest, recentupdate, leastupdate, mostcomment, leastcomment, priority] + // enum: [oldest, recentupdate, recentclose, leastupdate, mostcomment, leastcomment, priority] // - name: milestone // in: query // description: ID of the milestone @@ -108,7 +113,7 @@ func ListPullRequests(ctx *context.APIContext) { labelIDs, err := base.StringsToInt64s(ctx.FormStrings("labels")) if err != nil { - ctx.Error(http.StatusInternalServerError, "PullRequests", err) + ctx.APIErrorInternal(err) return } var posterID int64 @@ -116,9 +121,9 @@ func ListPullRequests(ctx *context.APIContext) { poster, err := user_model.GetUserByName(ctx, posterStr) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusBadRequest, "Poster not found", err) + ctx.APIError(http.StatusBadRequest, err) } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) } return } @@ -132,15 +137,16 @@ func ListPullRequests(ctx *context.APIContext) { Labels: labelIDs, MilestoneID: ctx.FormInt64("milestone"), PosterID: posterID, + BaseBranch: ctx.FormTrim("base_branch"), }) if err != nil { - ctx.Error(http.StatusInternalServerError, "PullRequests", err) + ctx.APIErrorInternal(err) return } apiPrs, err := convert.ToAPIPullRequests(ctx, ctx.Repo.Repository, prs, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "ToAPIPullRequests", err) + ctx.APIErrorInternal(err) return } @@ -182,21 +188,25 @@ func GetPullRequest(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + ctx.APIErrorInternal(err) } return } if err = pr.LoadBaseRepo(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err) + ctx.APIErrorInternal(err) return } if err = pr.LoadHeadRepo(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err) + ctx.APIErrorInternal(err) return } + + // Consider API access a view for delayed checking. + pull_service.StartPullRequestCheckOnView(ctx, pr) + ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer)) } @@ -252,9 +262,9 @@ func GetPullRequestByBaseHead(ctx *context.APIContext) { repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, name) if err != nil { if repo_model.IsErrRepoNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerName", err) + ctx.APIErrorInternal(err) } return } @@ -267,21 +277,25 @@ func GetPullRequestByBaseHead(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByBaseHeadInfo(ctx, ctx.Repo.Repository.ID, headRepoID, ctx.PathParam("base"), headBranch) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetPullRequestByBaseHeadInfo", err) + ctx.APIErrorInternal(err) } return } if err = pr.LoadBaseRepo(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err) + ctx.APIErrorInternal(err) return } if err = pr.LoadHeadRepo(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err) + ctx.APIErrorInternal(err) return } + + // Consider API access a view for delayed checking. + pull_service.StartPullRequestCheckOnView(ctx, pr) + ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer)) } @@ -327,9 +341,9 @@ func DownloadPullDiffOrPatch(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) } return } @@ -343,7 +357,7 @@ func DownloadPullDiffOrPatch(ctx *context.APIContext) { binary := ctx.FormBool("binary") if err := pull_service.DownloadDiffOrPatch(ctx, pr, ctx, patch, binary); err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } } @@ -388,7 +402,7 @@ func CreatePullRequest(ctx *context.APIContext) { form := *web.GetForm(ctx).(*api.CreatePullRequestOption) if form.Head == form.Base { - ctx.Error(http.StatusUnprocessableEntity, "BaseHeadSame", "Invalid PullRequest: There are no changes between the head and the base") + ctx.APIError(http.StatusUnprocessableEntity, "Invalid PullRequest: There are no changes between the head and the base") return } @@ -406,7 +420,7 @@ func CreatePullRequest(ctx *context.APIContext) { defer closer() if !compareResult.baseRef.IsBranch() || !compareResult.headRef.IsBranch() { - ctx.Error(http.StatusUnprocessableEntity, "BaseHeadInvalidRefType", "Invalid PullRequest: base and head must be branches") + ctx.APIError(http.StatusUnprocessableEntity, "Invalid PullRequest: base and head must be branches") return } @@ -417,7 +431,7 @@ func CreatePullRequest(ctx *context.APIContext) { ) if err != nil { if !issues_model.IsErrPullRequestNotExist(err) { - ctx.Error(http.StatusInternalServerError, "GetUnmergedPullRequest", err) + ctx.APIErrorInternal(err) return } } else { @@ -429,14 +443,14 @@ func CreatePullRequest(ctx *context.APIContext) { HeadBranch: existingPr.HeadBranch, BaseBranch: existingPr.BaseBranch, } - ctx.Error(http.StatusConflict, "GetUnmergedPullRequest", err) + ctx.APIError(http.StatusConflict, err) return } if len(form.Labels) > 0 { labels, err := issues_model.GetLabelsInRepoByIDs(ctx, ctx.Repo.Repository.ID, form.Labels) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err) + ctx.APIErrorInternal(err) return } @@ -448,7 +462,7 @@ func CreatePullRequest(ctx *context.APIContext) { if ctx.Repo.Owner.IsOrganization() { orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx, ctx.Repo.Owner.ID, form.Labels) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLabelsInOrgByIDs", err) + ctx.APIErrorInternal(err) return } @@ -464,9 +478,9 @@ func CreatePullRequest(ctx *context.APIContext) { milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone) if err != nil { if issues_model.IsErrMilestoneNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err) + ctx.APIErrorInternal(fmt.Errorf("GetMilestoneByRepoID: %w", err)) } return } @@ -504,9 +518,9 @@ func CreatePullRequest(ctx *context.APIContext) { assigneeIDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: [name: %s]", err)) } else { - ctx.Error(http.StatusInternalServerError, "AddAssigneeByName", err) + ctx.APIErrorInternal(err) } return } @@ -514,17 +528,17 @@ func CreatePullRequest(ctx *context.APIContext) { for _, aID := range assigneeIDs { assignee, err := user_model.GetUserByID(ctx, aID) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserByID", err) + ctx.APIErrorInternal(err) return } valid, err := access_model.CanBeAssigned(ctx, assignee, repo, true) if err != nil { - ctx.Error(http.StatusInternalServerError, "canBeAssigned", err) + ctx.APIErrorInternal(err) return } if !valid { - ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}) + ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}) return } } @@ -543,13 +557,13 @@ func CreatePullRequest(ctx *context.APIContext) { if err := pull_service.NewPullRequest(ctx, prOpts); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { - ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err) + ctx.APIError(http.StatusBadRequest, err) } else if errors.Is(err, user_model.ErrBlockedUser) { - ctx.Error(http.StatusForbidden, "BlockedUser", err) + ctx.APIError(http.StatusForbidden, err) } else if errors.Is(err, issues_model.ErrMustCollaborator) { - ctx.Error(http.StatusForbidden, "MustCollaborator", err) + ctx.APIError(http.StatusForbidden, err) } else { - ctx.Error(http.StatusInternalServerError, "NewPullRequest", err) + ctx.APIErrorInternal(err) } return } @@ -606,23 +620,23 @@ func EditPullRequest(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + ctx.APIErrorInternal(err) } return } err = pr.LoadIssue(ctx) if err != nil { - ctx.Error(http.StatusInternalServerError, "LoadIssue", err) + ctx.APIErrorInternal(err) return } issue := pr.Issue issue.Repo = ctx.Repo.Repository if err := issue.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } @@ -634,7 +648,7 @@ func EditPullRequest(ctx *context.APIContext) { if len(form.Title) > 0 { err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title) if err != nil { - ctx.Error(http.StatusInternalServerError, "ChangeTitle", err) + ctx.APIErrorInternal(err) return } } @@ -642,11 +656,11 @@ func EditPullRequest(ctx *context.APIContext) { err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, issue.ContentVersion) if err != nil { if errors.Is(err, issues_model.ErrIssueAlreadyChanged) { - ctx.Error(http.StatusBadRequest, "ChangeContent", err) + ctx.APIError(http.StatusBadRequest, err) return } - ctx.Error(http.StatusInternalServerError, "ChangeContent", err) + ctx.APIErrorInternal(err) return } } @@ -661,7 +675,7 @@ func EditPullRequest(ctx *context.APIContext) { } if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err) + ctx.APIErrorInternal(err) return } issue.DeadlineUnix = deadlineUnix @@ -679,11 +693,11 @@ func EditPullRequest(ctx *context.APIContext) { err = issue_service.UpdateAssignees(ctx, issue, form.Assignee, form.Assignees, ctx.Doer) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: [name: %s]", err)) } else if errors.Is(err, user_model.ErrBlockedUser) { - ctx.Error(http.StatusForbidden, "UpdateAssignees", err) + ctx.APIError(http.StatusForbidden, err) } else { - ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err) + ctx.APIErrorInternal(err) } return } @@ -693,8 +707,13 @@ func EditPullRequest(ctx *context.APIContext) { issue.MilestoneID != form.Milestone { oldMilestoneID := issue.MilestoneID issue.MilestoneID = form.Milestone + issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, form.Milestone) + if err != nil { + ctx.APIErrorInternal(err) + return + } if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil { - ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err) + ctx.APIErrorInternal(err) return } } @@ -702,14 +721,14 @@ func EditPullRequest(ctx *context.APIContext) { if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Labels != nil { labels, err := issues_model.GetLabelsInRepoByIDs(ctx, ctx.Repo.Repository.ID, form.Labels) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDsError", err) + ctx.APIErrorInternal(err) return } if ctx.Repo.Owner.IsOrganization() { orgLabels, err := issues_model.GetLabelsInOrgByIDs(ctx, ctx.Repo.Owner.ID, form.Labels) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLabelsInOrgByIDs", err) + ctx.APIErrorInternal(err) return } @@ -717,14 +736,14 @@ func EditPullRequest(ctx *context.APIContext) { } if err = issues_model.ReplaceIssueLabels(ctx, issue, labels, ctx.Doer); err != nil { - ctx.Error(http.StatusInternalServerError, "ReplaceLabelsError", err) + ctx.APIErrorInternal(err) return } } if form.State != nil { if pr.HasMerged { - ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged") + ctx.APIError(http.StatusPreconditionFailed, "cannot change state of this pull request, it was already merged") return } @@ -737,22 +756,22 @@ func EditPullRequest(ctx *context.APIContext) { // change pull target branch if !pr.HasMerged && len(form.Base) != 0 && form.Base != pr.BaseBranch { - if !ctx.Repo.GitRepo.IsBranchExist(form.Base) { - ctx.Error(http.StatusNotFound, "NewBaseBranchNotExist", fmt.Errorf("new base '%s' not exist", form.Base)) + if !gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, form.Base) { + ctx.APIError(http.StatusNotFound, fmt.Errorf("new base '%s' not exist", form.Base)) return } if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, form.Base); err != nil { if issues_model.IsErrPullRequestAlreadyExists(err) { - ctx.Error(http.StatusConflict, "IsErrPullRequestAlreadyExists", err) + ctx.APIError(http.StatusConflict, err) return } else if issues_model.IsErrIssueIsClosed(err) { - ctx.Error(http.StatusUnprocessableEntity, "IsErrIssueIsClosed", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } else if pull_service.IsErrPullRequestHasMerged(err) { - ctx.Error(http.StatusConflict, "IsErrPullRequestHasMerged", err) + ctx.APIError(http.StatusConflict, err) return } - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } notify_service.PullRequestChangeTargetBranch(ctx, ctx.Doer, pr, form.Base) @@ -762,10 +781,10 @@ func EditPullRequest(ctx *context.APIContext) { if form.AllowMaintainerEdit != nil { if err := pull_service.SetAllowEdits(ctx, ctx.Doer, pr, *form.AllowMaintainerEdit); err != nil { if errors.Is(err, pull_service.ErrUserHasNoPermissionForAction) { - ctx.Error(http.StatusForbidden, "SetAllowEdits", fmt.Sprintf("SetAllowEdits: %s", err)) + ctx.APIError(http.StatusForbidden, fmt.Sprintf("SetAllowEdits: %s", err)) return } - ctx.ServerError("SetAllowEdits", err) + ctx.APIErrorInternal(err) return } } @@ -774,9 +793,9 @@ func EditPullRequest(ctx *context.APIContext) { pr, err = issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pr.Index) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + ctx.APIErrorInternal(err) } return } @@ -818,9 +837,9 @@ func IsPullRequestMerged(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + ctx.APIErrorInternal(err) } return } @@ -828,7 +847,7 @@ func IsPullRequestMerged(ctx *context.APIContext) { if pr.HasMerged { ctx.Status(http.StatusNoContent) } - ctx.NotFound() + ctx.APIErrorNotFound() } // MergePullRequest merges a PR given an index @@ -876,20 +895,20 @@ func MergePullRequest(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.NotFound("GetPullRequestByIndex", err) + ctx.APIErrorNotFound("GetPullRequestByIndex", err) } else { - ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + ctx.APIErrorInternal(err) } return } if err := pr.LoadHeadRepo(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err) + ctx.APIErrorInternal(err) return } if err := pr.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadIssue", err) + ctx.APIErrorInternal(err) return } pr.Issue.Repo = ctx.Repo.Repository @@ -897,7 +916,7 @@ func MergePullRequest(ctx *context.APIContext) { if ctx.IsSigned { // Update issue-user. if err = activities_model.SetIssueReadBy(ctx, pr.Issue.ID, ctx.Doer.ID); err != nil { - ctx.Error(http.StatusInternalServerError, "ReadBy", err) + ctx.APIErrorInternal(err) return } } @@ -915,21 +934,21 @@ func MergePullRequest(ctx *context.APIContext) { // start with merging by checking if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil { if errors.Is(err, pull_service.ErrIsClosed) { - ctx.NotFound() - } else if errors.Is(err, pull_service.ErrUserNotAllowedToMerge) { - ctx.Error(http.StatusMethodNotAllowed, "Merge", "User not allowed to merge PR") + ctx.APIErrorNotFound() + } else if errors.Is(err, pull_service.ErrNoPermissionToMerge) { + ctx.APIError(http.StatusMethodNotAllowed, "User not allowed to merge PR") } else if errors.Is(err, pull_service.ErrHasMerged) { - ctx.Error(http.StatusMethodNotAllowed, "PR already merged", "") + ctx.APIError(http.StatusMethodNotAllowed, "") } else if errors.Is(err, pull_service.ErrIsWorkInProgress) { - ctx.Error(http.StatusMethodNotAllowed, "PR is a work in progress", "Work in progress PRs cannot be merged") + ctx.APIError(http.StatusMethodNotAllowed, "Work in progress PRs cannot be merged") } else if errors.Is(err, pull_service.ErrNotMergeableState) { - ctx.Error(http.StatusMethodNotAllowed, "PR not in mergeable state", "Please try again later") - } else if pull_service.IsErrDisallowedToMerge(err) { - ctx.Error(http.StatusMethodNotAllowed, "PR is not ready to be merged", err) + ctx.APIError(http.StatusMethodNotAllowed, "Please try again later") + } else if errors.Is(err, pull_service.ErrNotReadyToMerge) { + ctx.APIError(http.StatusMethodNotAllowed, err) } else if asymkey_service.IsErrWontSign(err) { - ctx.Error(http.StatusMethodNotAllowed, fmt.Sprintf("Protected branch %s requires signed commits but this merge would not be signed", pr.BaseBranch), err) + ctx.APIError(http.StatusMethodNotAllowed, err) } else { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) } return } @@ -938,14 +957,14 @@ func MergePullRequest(ctx *context.APIContext) { if manuallyMerged { if err := pull_service.MergedManually(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, form.MergeCommitID); err != nil { if pull_service.IsErrInvalidMergeStyle(err) { - ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) + ctx.APIError(http.StatusMethodNotAllowed, fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) return } if strings.Contains(err.Error(), "Wrong commit ID") { ctx.JSON(http.StatusConflict, err) return } - ctx.Error(http.StatusInternalServerError, "Manually-Merged", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusOK) @@ -960,7 +979,7 @@ func MergePullRequest(ctx *context.APIContext) { if len(message) == 0 { message, _, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do)) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetDefaultMergeMessage", err) + ctx.APIErrorInternal(err) return } } @@ -974,10 +993,10 @@ func MergePullRequest(ctx *context.APIContext) { scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, form.DeleteBranchAfterMerge) if err != nil { if pull_model.IsErrAlreadyScheduledToAutoMerge(err) { - ctx.Error(http.StatusConflict, "ScheduleAutoMerge", err) + ctx.APIError(http.StatusConflict, err) return } - ctx.Error(http.StatusInternalServerError, "ScheduleAutoMerge", err) + ctx.APIErrorInternal(err) return } else if scheduled { // nothing more to do ... @@ -988,7 +1007,7 @@ func MergePullRequest(ctx *context.APIContext) { if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil { if pull_service.IsErrInvalidMergeStyle(err) { - ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) + ctx.APIError(http.StatusMethodNotAllowed, fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) } else if pull_service.IsErrMergeConflicts(err) { conflictError := err.(pull_service.ErrMergeConflicts) ctx.JSON(http.StatusConflict, conflictError) @@ -999,18 +1018,18 @@ func MergePullRequest(ctx *context.APIContext) { conflictError := err.(pull_service.ErrMergeUnrelatedHistories) ctx.JSON(http.StatusConflict, conflictError) } else if git.IsErrPushOutOfDate(err) { - ctx.Error(http.StatusConflict, "Merge", "merge push out of date") + ctx.APIError(http.StatusConflict, "merge push out of date") } else if pull_service.IsErrSHADoesNotMatch(err) { - ctx.Error(http.StatusConflict, "Merge", "head out of date") + ctx.APIError(http.StatusConflict, "head out of date") } else if git.IsErrPushRejected(err) { errPushRej := err.(*git.ErrPushRejected) if len(errPushRej.Message) == 0 { - ctx.Error(http.StatusConflict, "Merge", "PushRejected without remote error message") + ctx.APIError(http.StatusConflict, "PushRejected without remote error message") } else { - ctx.Error(http.StatusConflict, "Merge", "PushRejected with remote message: "+errPushRej.Message) + ctx.APIError(http.StatusConflict, "PushRejected with remote message: "+errPushRej.Message) } } else { - ctx.Error(http.StatusInternalServerError, "Merge", err) + ctx.APIErrorInternal(err) } return } @@ -1024,7 +1043,7 @@ func MergePullRequest(ctx *context.APIContext) { // Don't cleanup when there are other PR's that use this branch as head branch. exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch) if err != nil { - ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err) + ctx.APIErrorInternal(err) return } if exist { @@ -1038,7 +1057,7 @@ func MergePullRequest(ctx *context.APIContext) { } else { headRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo) if err != nil { - ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.FullName()), err) + ctx.APIErrorInternal(err) return } defer headRepo.Close() @@ -1047,13 +1066,13 @@ func MergePullRequest(ctx *context.APIContext) { if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, headRepo, pr.HeadBranch, pr); err != nil { switch { case git.IsErrBranchNotExist(err): - ctx.NotFound(err) + ctx.APIErrorNotFound(err) case errors.Is(err, repo_service.ErrBranchIsDefault): - ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch")) + ctx.APIError(http.StatusForbidden, errors.New("can not delete default branch")) case errors.Is(err, git_model.ErrBranchIsProtected): - ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected")) + ctx.APIError(http.StatusForbidden, errors.New("branch protected")) default: - ctx.Error(http.StatusInternalServerError, "DeleteBranch", err) + ctx.APIErrorInternal(err) } return } @@ -1092,14 +1111,14 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) headUser, err = user_model.GetUserByName(ctx, headInfos[0]) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.NotFound("GetUserByName") + ctx.APIErrorNotFound("GetUserByName") } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) } return nil, nil } } else { - ctx.NotFound() + ctx.APIErrorNotFound() return nil, nil } @@ -1110,14 +1129,14 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) if headRepo == nil && !isSameRepo { err = baseRepo.GetBaseRepo(ctx) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetBaseRepo", err) + ctx.APIErrorInternal(err) return nil, nil } // Check if baseRepo's base repository is the same as headUser's repository. if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID { log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID) - ctx.NotFound("GetBaseRepo") + ctx.APIErrorNotFound("GetBaseRepo") return nil, nil } // Assign headRepo so it can be used below. @@ -1132,7 +1151,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) } else { headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo) if err != nil { - ctx.Error(http.StatusInternalServerError, "OpenRepository", err) + ctx.APIErrorInternal(err) return nil, nil } closer = func() { _ = headGitRepo.Close() } @@ -1146,13 +1165,13 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) // user should have permission to read baseRepo's codes and pulls, NOT headRepo's permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + ctx.APIErrorInternal(err) return nil, nil } if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) { log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase) - ctx.NotFound("Can't read pulls or can't read UnitTypeCode") + ctx.APIErrorNotFound("Can't read pulls or can't read UnitTypeCode") return nil, nil } @@ -1160,12 +1179,12 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) // TODO: could the logic be simplified if the headRepo is the same as the baseRepo? Need to think more about it. permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + ctx.APIErrorInternal(err) return nil, nil } if !permHead.CanRead(unit.TypeCode) { log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", ctx.Doer, headRepo, permHead) - ctx.NotFound("Can't read headRepo UnitTypeCode") + ctx.APIErrorNotFound("Can't read headRepo UnitTypeCode") return nil, nil } @@ -1178,13 +1197,13 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) headRefValid := headRef.IsBranch() || headRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(headRepo.ObjectFormatName), headRef.ShortName()) // Check if base&head ref are valid. if !baseRefValid || !headRefValid { - ctx.NotFound() + ctx.APIErrorNotFound() return nil, nil } compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseRef.ShortName(), headRef.ShortName(), false, false) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err) + ctx.APIErrorInternal(err) return nil, nil } @@ -1236,34 +1255,34 @@ func UpdatePullRequest(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + ctx.APIErrorInternal(err) } return } if pr.HasMerged { - ctx.Error(http.StatusUnprocessableEntity, "UpdatePullRequest", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } if err = pr.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadIssue", err) + ctx.APIErrorInternal(err) return } if pr.Issue.IsClosed { - ctx.Error(http.StatusUnprocessableEntity, "UpdatePullRequest", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } if err = pr.LoadBaseRepo(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err) + ctx.APIErrorInternal(err) return } if err = pr.LoadHeadRepo(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err) + ctx.APIErrorInternal(err) return } @@ -1271,7 +1290,7 @@ func UpdatePullRequest(ctx *context.APIContext) { allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, pr, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "IsUserAllowedToMerge", err) + ctx.APIErrorInternal(err) return } @@ -1283,15 +1302,15 @@ func UpdatePullRequest(ctx *context.APIContext) { // default merge commit message message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch) - if err = pull_service.Update(ctx, pr, ctx.Doer, message, rebase); err != nil { + if err = pull_service.Update(graceful.GetManager().ShutdownContext(), pr, ctx.Doer, message, rebase); err != nil { if pull_service.IsErrMergeConflicts(err) { - ctx.Error(http.StatusConflict, "Update", "merge failed because of conflict") + ctx.APIError(http.StatusConflict, "merge failed because of conflict") return } else if pull_service.IsErrRebaseConflicts(err) { - ctx.Error(http.StatusConflict, "Update", "rebase failed because of conflict") + ctx.APIError(http.StatusConflict, "rebase failed because of conflict") return } - ctx.Error(http.StatusInternalServerError, "pull_service.Update", err) + ctx.APIErrorInternal(err) return } @@ -1336,37 +1355,37 @@ func CancelScheduledAutoMerge(ctx *context.APIContext) { pull, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, pullIndex) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() return } - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } exist, autoMerge, err := pull_model.GetScheduledMergeByPullID(ctx, pull.ID) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } if !exist { - ctx.NotFound() + ctx.APIErrorNotFound() return } if ctx.Doer.ID != autoMerge.DoerID { allowed, err := access_model.IsUserRepoAdmin(ctx, ctx.Repo.Repository, ctx.Doer) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } if !allowed { - ctx.Error(http.StatusForbidden, "No permission to cancel", "user has no permission to cancel the scheduled auto merge") + ctx.APIError(http.StatusForbidden, "user has no permission to cancel the scheduled auto merge") return } } if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, pull); err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) } else { ctx.Status(http.StatusNoContent) } @@ -1421,22 +1440,22 @@ func GetPullRequestCommits(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + ctx.APIErrorInternal(err) } return } if err := pr.LoadBaseRepo(ctx); err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } var prInfo *git.CompareInfo baseGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo) if err != nil { - ctx.ServerError("OpenRepository", err) + ctx.APIErrorInternal(err) return } defer closer.Close() @@ -1447,7 +1466,7 @@ func GetPullRequestCommits(ctx *context.APIContext) { prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName(), false, false) } if err != nil { - ctx.ServerError("GetCompareInfo", err) + ctx.APIErrorInternal(err) return } commits := prInfo.Commits @@ -1476,7 +1495,7 @@ func GetPullRequestCommits(ctx *context.APIContext) { Files: files, }) if err != nil { - ctx.ServerError("toCommit", err) + ctx.APIErrorInternal(err) return } apiCommits = append(apiCommits, apiCommit) @@ -1544,20 +1563,20 @@ func GetPullRequestFiles(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + ctx.APIErrorInternal(err) } return } if err := pr.LoadBaseRepo(ctx); err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } if err := pr.LoadHeadRepo(ctx); err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -1570,13 +1589,13 @@ func GetPullRequestFiles(ctx *context.APIContext) { prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName(), true, false) } if err != nil { - ctx.ServerError("GetCompareInfo", err) + ctx.APIErrorInternal(err) return } headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName()) if err != nil { - ctx.ServerError("GetRefCommitID", err) + ctx.APIErrorInternal(err) return } @@ -1586,7 +1605,7 @@ func GetPullRequestFiles(ctx *context.APIContext) { maxLines := setting.Git.MaxGitDiffLines // FIXME: If there are too many files in the repo, may cause some unpredictable issues. - diff, err := gitdiff.GetDiff(ctx, baseGitRepo, + diff, err := gitdiff.GetDiffForAPI(ctx, baseGitRepo, &gitdiff.DiffOptions{ BeforeCommitID: startCommitID, AfterCommitID: endCommitID, @@ -1597,13 +1616,18 @@ func GetPullRequestFiles(ctx *context.APIContext) { WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.FormString("whitespace")), }) if err != nil { - ctx.ServerError("GetDiff", err) + ctx.APIErrorInternal(err) return } + diffShortStat, err := gitdiff.GetDiffShortStat(baseGitRepo, startCommitID, endCommitID) + if err != nil { + ctx.APIErrorInternal(err) + return + } listOptions := utils.GetListOptions(ctx) - totalNumberOfFiles := diff.NumFiles + totalNumberOfFiles := diffShortStat.NumFiles totalNumberOfPages := int(math.Ceil(float64(totalNumberOfFiles) / float64(listOptions.PageSize))) start, limit := listOptions.GetSkipTake() @@ -1614,7 +1638,9 @@ func GetPullRequestFiles(ctx *context.APIContext) { apiFiles := make([]*api.ChangedFile, 0, limit) for i := start; i < start+limit; i++ { - apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.HeadRepo, endCommitID)) + // refs/pull/1/head stores the HEAD commit ID, allowing all related commits to be found in the base repository. + // The head repository might have been deleted, so we should not rely on it here. + apiFiles = append(apiFiles, convert.ToChangedFile(diff.Files[i], pr.BaseRepo, endCommitID)) } ctx.SetLinkHeader(totalNumberOfFiles, listOptions.PageSize) diff --git a/routers/api/v1/repo/pull_review.go b/routers/api/v1/repo/pull_review.go index 6d7a326370..9421a052db 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -64,20 +64,20 @@ func ListPullReviews(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.NotFound("GetPullRequestByIndex", err) + ctx.APIErrorNotFound("GetPullRequestByIndex", err) } else { - ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + ctx.APIErrorInternal(err) } return } if err = pr.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadIssue", err) + ctx.APIErrorInternal(err) return } if err = pr.Issue.LoadRepo(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadRepo", err) + ctx.APIErrorInternal(err) return } @@ -88,19 +88,19 @@ func ListPullReviews(ctx *context.APIContext) { allReviews, err := issues_model.FindReviews(ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } count, err := issues_model.CountReviews(ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } apiReviews, err := convert.ToPullReviewList(ctx, allReviews, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "convertToPullReviewList", err) + ctx.APIErrorInternal(err) return } @@ -151,7 +151,7 @@ func GetPullReview(ctx *context.APIContext) { apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "convertToPullReview", err) + ctx.APIErrorInternal(err) return } @@ -201,7 +201,7 @@ func GetPullReviewComments(ctx *context.APIContext) { apiComments, err := convert.ToPullReviewCommentList(ctx, review, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "convertToPullReviewCommentList", err) + ctx.APIErrorInternal(err) return } @@ -252,16 +252,16 @@ func DeletePullReview(ctx *context.APIContext) { } if ctx.Doer == nil { - ctx.NotFound() + ctx.APIErrorNotFound() return } if !ctx.Doer.IsAdmin && ctx.Doer.ID != review.ReviewerID { - ctx.Error(http.StatusForbidden, "only admin and user itself can delete a review", nil) + ctx.APIError(http.StatusForbidden, nil) return } if err := issues_model.DeleteReview(ctx, review); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteReview", fmt.Errorf("can not delete ReviewID: %d", review.ID)) + ctx.APIErrorInternal(fmt.Errorf("can not delete ReviewID: %d", review.ID)) return } @@ -309,9 +309,9 @@ func CreatePullReview(ctx *context.APIContext) { pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.NotFound("GetPullRequestByIndex", err) + ctx.APIErrorNotFound("GetPullRequestByIndex", err) } else { - ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + ctx.APIErrorInternal(err) } return } @@ -323,7 +323,7 @@ func CreatePullReview(ctx *context.APIContext) { } if err := pr.Issue.LoadRepo(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "pr.Issue.LoadRepo", err) + ctx.APIErrorInternal(err) return } @@ -331,14 +331,14 @@ func CreatePullReview(ctx *context.APIContext) { if opts.CommitID == "" { gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.Issue.Repo) if err != nil { - ctx.Error(http.StatusInternalServerError, "git.OpenRepository", err) + ctx.APIErrorInternal(err) return } defer closer.Close() headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName()) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetRefCommitID", err) + ctx.APIErrorInternal(err) return } @@ -364,7 +364,7 @@ func CreatePullReview(ctx *context.APIContext) { opts.CommitID, nil, ); err != nil { - ctx.Error(http.StatusInternalServerError, "CreateCodeComment", err) + ctx.APIErrorInternal(err) return } } @@ -373,9 +373,9 @@ func CreatePullReview(ctx *context.APIContext) { review, _, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil) if err != nil { if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "SubmitReview", err) + ctx.APIErrorInternal(err) } return } @@ -383,7 +383,7 @@ func CreatePullReview(ctx *context.APIContext) { // convert response apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "convertToPullReview", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, apiReview) @@ -439,7 +439,7 @@ func SubmitPullReview(ctx *context.APIContext) { } if review.Type != issues_model.ReviewTypePending { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("only a pending review can be submitted")) + ctx.APIError(http.StatusUnprocessableEntity, errors.New("only a pending review can be submitted")) return } @@ -451,13 +451,13 @@ func SubmitPullReview(ctx *context.APIContext) { // if review stay pending return if reviewType == issues_model.ReviewTypePending { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review stay pending")) + ctx.APIError(http.StatusUnprocessableEntity, errors.New("review stay pending")) return } headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pr.GetGitRefName()) if err != nil { - ctx.Error(http.StatusInternalServerError, "GitRepo: GetRefCommitID", err) + ctx.APIErrorInternal(err) return } @@ -465,9 +465,9 @@ func SubmitPullReview(ctx *context.APIContext) { review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil) if err != nil { if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "SubmitReview", err) + ctx.APIErrorInternal(err) } return } @@ -475,7 +475,7 @@ func SubmitPullReview(ctx *context.APIContext) { // convert response apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "convertToPullReview", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, apiReview) @@ -484,7 +484,7 @@ func SubmitPullReview(ctx *context.APIContext) { // preparePullReviewType return ReviewType and false or nil and true if an error happen func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest, event api.ReviewStateType, body string, hasComments bool) (issues_model.ReviewType, bool) { if err := pr.LoadIssue(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadIssue", err) + ctx.APIErrorInternal(err) return -1, true } @@ -496,7 +496,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest case api.ReviewStateApproved: // can not approve your own PR if pr.Issue.IsPoster(ctx.Doer.ID) { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("approve your own pull is not allowed")) + ctx.APIError(http.StatusUnprocessableEntity, errors.New("approve your own pull is not allowed")) return -1, true } reviewType = issues_model.ReviewTypeApprove @@ -505,7 +505,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest case api.ReviewStateRequestChanges: // can not reject your own PR if pr.Issue.IsPoster(ctx.Doer.ID) { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("reject your own pull is not allowed")) + ctx.APIError(http.StatusUnprocessableEntity, errors.New("reject your own pull is not allowed")) return -1, true } reviewType = issues_model.ReviewTypeReject @@ -515,7 +515,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest needsBody = false // if there is no body we need to ensure that there are comments if !hasBody && !hasComments { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s requires a body or a comment", event)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("review event %s requires a body or a comment", event)) return -1, true } default: @@ -524,7 +524,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest // reject reviews with empty body if a body is required for this call if needsBody && !hasBody { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("review event %s requires a body", event)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("review event %s requires a body", event)) return -1, true } @@ -536,9 +536,9 @@ func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.NotFound("GetPullRequestByIndex", err) + ctx.APIErrorNotFound("GetPullRequestByIndex", err) } else { - ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + ctx.APIErrorInternal(err) } return nil, nil, true } @@ -546,27 +546,27 @@ func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues review, err := issues_model.GetReviewByID(ctx, ctx.PathParamInt64("id")) if err != nil { if issues_model.IsErrReviewNotExist(err) { - ctx.NotFound("GetReviewByID", err) + ctx.APIErrorNotFound("GetReviewByID", err) } else { - ctx.Error(http.StatusInternalServerError, "GetReviewByID", err) + ctx.APIErrorInternal(err) } return nil, nil, true } // validate the review is for the given PR if review.IssueID != pr.IssueID { - ctx.NotFound("ReviewNotInPR") + ctx.APIErrorNotFound("ReviewNotInPR") return nil, nil, true } // make sure that the user has access to this review if it is pending if review.Type == issues_model.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin { - ctx.NotFound("GetReviewByID") + ctx.APIErrorNotFound("GetReviewByID") return nil, nil, true } if err := review.LoadAttributes(ctx); err != nil && !user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusInternalServerError, "ReviewLoadAttributes", err) + ctx.APIErrorInternal(err) return nil, nil, true } @@ -668,10 +668,10 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.NotFound("UserNotExist", fmt.Sprintf("User '%s' not exist", r)) + ctx.APIErrorNotFound("UserNotExist", fmt.Sprintf("User '%s' not exist", r)) return nil, nil } - ctx.Error(http.StatusInternalServerError, "GetUser", err) + ctx.APIErrorInternal(err) return nil, nil } @@ -684,10 +684,10 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN teamReviewer, err = organization.GetTeam(ctx, ctx.Repo.Owner.ID, t) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.NotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t)) + ctx.APIErrorNotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t)) return nil, nil } - ctx.Error(http.StatusInternalServerError, "ReviewRequest", err) + ctx.APIErrorInternal(err) return nil, nil } @@ -701,21 +701,21 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrPullRequestNotExist(err) { - ctx.NotFound("GetPullRequestByIndex", err) + ctx.APIErrorNotFound("GetPullRequestByIndex", err) } else { - ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err) + ctx.APIErrorInternal(err) } return } if err := pr.Issue.LoadRepo(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "pr.Issue.LoadRepo", err) + ctx.APIErrorInternal(err) return } permDoer, err := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + ctx.APIErrorInternal(err) return } @@ -733,20 +733,20 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, &permDoer, reviewer, isAdd) if err != nil { if issues_model.IsErrReviewRequestOnClosedPR(err) { - ctx.Error(http.StatusForbidden, "", err) + ctx.APIError(http.StatusForbidden, err) return } if issues_model.IsErrNotValidReviewRequest(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "ReviewRequest", err) + ctx.APIErrorInternal(err) return } if comment != nil && isAdd { if err = comment.LoadReview(ctx); err != nil { - ctx.ServerError("ReviewRequest", err) + ctx.APIErrorInternal(err) return } reviews = append(reviews, comment.Review) @@ -758,20 +758,20 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions comment, err := issue_service.TeamReviewRequest(ctx, pr.Issue, ctx.Doer, teamReviewer, isAdd) if err != nil { if issues_model.IsErrReviewRequestOnClosedPR(err) { - ctx.Error(http.StatusForbidden, "", err) + ctx.APIError(http.StatusForbidden, err) return } if issues_model.IsErrNotValidReviewRequest(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.ServerError("TeamReviewRequest", err) + ctx.APIErrorInternal(err) return } if comment != nil && isAdd { if err = comment.LoadReview(ctx); err != nil { - ctx.ServerError("ReviewRequest", err) + ctx.APIErrorInternal(err) return } reviews = append(reviews, comment.Review) @@ -782,7 +782,7 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions if isAdd { apiReviews, err := convert.ToPullReviewList(ctx, reviews, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "convertToPullReviewList", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusCreated, apiReviews) @@ -884,7 +884,7 @@ func UnDismissPullReview(ctx *context.APIContext) { func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors bool) { if !ctx.Repo.IsAdmin() { - ctx.Error(http.StatusForbidden, "", "Must be repo admin") + ctx.APIError(http.StatusForbidden, "Must be repo admin") return } review, _, isWrong := prepareSingleReview(ctx) @@ -893,29 +893,29 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors } if review.Type != issues_model.ReviewTypeApprove && review.Type != issues_model.ReviewTypeReject { - ctx.Error(http.StatusForbidden, "", "not need to dismiss this review because it's type is not Approve or change request") + ctx.APIError(http.StatusForbidden, "not need to dismiss this review because it's type is not Approve or change request") return } _, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss, dismissPriors) if err != nil { if pull_service.IsErrDismissRequestOnClosedPR(err) { - ctx.Error(http.StatusForbidden, "", err) + ctx.APIError(http.StatusForbidden, err) return } - ctx.Error(http.StatusInternalServerError, "pull_service.DismissReview", err) + ctx.APIErrorInternal(err) return } if review, err = issues_model.GetReviewByID(ctx, review.ID); err != nil { - ctx.Error(http.StatusInternalServerError, "GetReviewByID", err) + ctx.APIErrorInternal(err) return } // convert response apiReview, err := convert.ToPullReview(ctx, review, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "convertToPullReview", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, apiReview) diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index 076f00f1d1..272b395dfb 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -4,6 +4,7 @@ package repo import ( + "errors" "fmt" "net/http" @@ -53,16 +54,16 @@ func GetRelease(ctx *context.APIContext) { id := ctx.PathParamInt64("id") release, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id) if err != nil && !repo_model.IsErrReleaseNotExist(err) { - ctx.Error(http.StatusInternalServerError, "GetReleaseForRepoByID", err) + ctx.APIErrorInternal(err) return } if err != nil && repo_model.IsErrReleaseNotExist(err) || release.IsTag { - ctx.NotFound() + ctx.APIErrorNotFound() return } if err := release.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, release)) @@ -93,17 +94,17 @@ func GetLatestRelease(ctx *context.APIContext) { // "$ref": "#/responses/notFound" release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID) if err != nil && !repo_model.IsErrReleaseNotExist(err) { - ctx.Error(http.StatusInternalServerError, "GetLatestRelease", err) + ctx.APIErrorInternal(err) return } if err != nil && repo_model.IsErrReleaseNotExist(err) || release.IsTag || release.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound() + ctx.APIErrorNotFound() return } if err := release.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, release)) @@ -161,13 +162,13 @@ func ListReleases(ctx *context.APIContext) { releases, err := db.Find[repo_model.Release](ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetReleasesByRepoID", err) + ctx.APIErrorInternal(err) return } rels := make([]*api.Release, len(releases)) for i, release := range releases { if err := release.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } rels[i] = convert.ToAPIRelease(ctx, ctx.Repo.Repository, release) @@ -175,7 +176,7 @@ func ListReleases(ctx *context.APIContext) { filteredCount, err := db.Count[repo_model.Release](ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -220,13 +221,13 @@ func CreateRelease(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateReleaseOption) if ctx.Repo.Repository.IsEmpty { - ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty")) + ctx.APIError(http.StatusUnprocessableEntity, errors.New("repo is empty")) return } rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName) if err != nil { if !repo_model.IsErrReleaseNotExist(err) { - ctx.Error(http.StatusInternalServerError, "GetRelease", err) + ctx.APIErrorInternal(err) return } // If target is not provided use default branch @@ -246,21 +247,23 @@ func CreateRelease(ctx *context.APIContext) { IsTag: false, Repo: ctx.Repo.Repository, } - if err := release_service.CreateRelease(ctx.Repo.GitRepo, rel, nil, ""); err != nil { + // GitHub doesn't have "tag_message", GitLab has: https://docs.gitlab.com/api/releases/#create-a-release + // It doesn't need to be the same as the "release note" + if err := release_service.CreateRelease(ctx.Repo.GitRepo, rel, nil, form.TagMessage); err != nil { if repo_model.IsErrReleaseAlreadyExist(err) { - ctx.Error(http.StatusConflict, "ReleaseAlreadyExist", err) + ctx.APIError(http.StatusConflict, err) } else if release_service.IsErrProtectedTagName(err) { - ctx.Error(http.StatusUnprocessableEntity, "ProtectedTagName", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else if git.IsErrNotExist(err) { - ctx.Error(http.StatusNotFound, "ErrNotExist", fmt.Errorf("target \"%v\" not found: %w", rel.Target, err)) + ctx.APIError(http.StatusNotFound, fmt.Errorf("target \"%v\" not found: %w", rel.Target, err)) } else { - ctx.Error(http.StatusInternalServerError, "CreateRelease", err) + ctx.APIErrorInternal(err) } return } } else { if !rel.IsTag { - ctx.Error(http.StatusConflict, "GetRelease", "Release is has no Tag") + ctx.APIError(http.StatusConflict, "Release is has no Tag") return } @@ -275,7 +278,7 @@ func CreateRelease(ctx *context.APIContext) { rel.Target = form.Target if err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateRelease", err) + ctx.APIErrorInternal(err) return } } @@ -322,11 +325,11 @@ func EditRelease(ctx *context.APIContext) { id := ctx.PathParamInt64("id") rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id) if err != nil && !repo_model.IsErrReleaseNotExist(err) { - ctx.Error(http.StatusInternalServerError, "GetReleaseForRepoByID", err) + ctx.APIErrorInternal(err) return } if err != nil && repo_model.IsErrReleaseNotExist(err) || rel.IsTag { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -349,18 +352,18 @@ func EditRelease(ctx *context.APIContext) { rel.IsPrerelease = *form.IsPrerelease } if err := release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateRelease", err) + ctx.APIErrorInternal(err) return } // reload data from database rel, err = repo_model.GetReleaseByID(ctx, id) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err) + ctx.APIErrorInternal(err) return } if err := rel.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, rel)) @@ -399,19 +402,19 @@ func DeleteRelease(ctx *context.APIContext) { id := ctx.PathParamInt64("id") rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, id) if err != nil && !repo_model.IsErrReleaseNotExist(err) { - ctx.Error(http.StatusInternalServerError, "GetReleaseForRepoByID", err) + ctx.APIErrorInternal(err) return } if err != nil && repo_model.IsErrReleaseNotExist(err) || rel.IsTag { - ctx.NotFound() + ctx.APIErrorNotFound() return } if err := release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, rel, ctx.Doer, false); err != nil { if release_service.IsErrProtectedTagName(err) { - ctx.Error(http.StatusUnprocessableEntity, "delTag", "user not allowed to delete protected tag") + ctx.APIError(http.StatusUnprocessableEntity, "user not allowed to delete protected tag") return } - ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) diff --git a/routers/api/v1/repo/release_attachment.go b/routers/api/v1/repo/release_attachment.go index 54ca1fc843..defde81a1d 100644 --- a/routers/api/v1/repo/release_attachment.go +++ b/routers/api/v1/repo/release_attachment.go @@ -23,14 +23,14 @@ func checkReleaseMatchRepo(ctx *context.APIContext, releaseID int64) bool { release, err := repo_model.GetReleaseByID(ctx, releaseID) if err != nil { if repo_model.IsErrReleaseNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() return false } - ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err) + ctx.APIErrorInternal(err) return false } if release.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound() + ctx.APIErrorNotFound() return false } return true @@ -81,15 +81,15 @@ func GetReleaseAttachment(ctx *context.APIContext) { attach, err := repo_model.GetAttachmentByID(ctx, attachID) if err != nil { if repo_model.IsErrAttachmentNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() return } - ctx.Error(http.StatusInternalServerError, "GetAttachmentByID", err) + ctx.APIErrorInternal(err) return } if attach.ReleaseID != releaseID { log.Info("User requested attachment is not in release, release_id %v, attachment_id: %v", releaseID, attachID) - ctx.NotFound() + ctx.APIErrorNotFound() return } // FIXME Should prove the existence of the given repo, but results in unnecessary database requests @@ -130,18 +130,18 @@ func ListReleaseAttachments(ctx *context.APIContext) { release, err := repo_model.GetReleaseByID(ctx, releaseID) if err != nil { if repo_model.IsErrReleaseNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() return } - ctx.Error(http.StatusInternalServerError, "GetReleaseByID", err) + ctx.APIErrorInternal(err) return } if release.RepoID != ctx.Repo.Repository.ID { - ctx.NotFound() + ctx.APIErrorNotFound() return } if err := release.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, release).Attachments) @@ -194,7 +194,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { // Check if attachments are enabled if !setting.Attachment.Enabled { - ctx.NotFound("Attachment is not enabled") + ctx.APIErrorNotFound("Attachment is not enabled") return } @@ -212,7 +212,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { if strings.HasPrefix(strings.ToLower(ctx.Req.Header.Get("Content-Type")), "multipart/form-data") { file, header, err := ctx.Req.FormFile("attachment") if err != nil { - ctx.Error(http.StatusInternalServerError, "GetFile", err) + ctx.APIErrorInternal(err) return } defer file.Close() @@ -229,7 +229,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) { } if filename == "" { - ctx.Error(http.StatusBadRequest, "CreateReleaseAttachment", "Could not determine name of attachment.") + ctx.APIError(http.StatusBadRequest, "Could not determine name of attachment.") return } @@ -242,10 +242,10 @@ func CreateReleaseAttachment(ctx *context.APIContext) { }) if err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.Error(http.StatusBadRequest, "DetectContentType", err) + ctx.APIError(http.StatusBadRequest, err) return } - ctx.Error(http.StatusInternalServerError, "NewAttachment", err) + ctx.APIErrorInternal(err) return } @@ -308,15 +308,15 @@ func EditReleaseAttachment(ctx *context.APIContext) { attach, err := repo_model.GetAttachmentByID(ctx, attachID) if err != nil { if repo_model.IsErrAttachmentNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() return } - ctx.Error(http.StatusInternalServerError, "GetAttachmentByID", err) + ctx.APIErrorInternal(err) return } if attach.ReleaseID != releaseID { log.Info("User requested attachment is not in release, release_id %v, attachment_id: %v", releaseID, attachID) - ctx.NotFound() + ctx.APIErrorNotFound() return } // FIXME Should prove the existence of the given repo, but results in unnecessary database requests @@ -326,10 +326,10 @@ func EditReleaseAttachment(ctx *context.APIContext) { if err := attachment_service.UpdateAttachment(ctx, setting.Repository.Release.AllowedTypes, attach); err != nil { if upload.IsErrFileTypeForbidden(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach)) @@ -381,21 +381,21 @@ func DeleteReleaseAttachment(ctx *context.APIContext) { attach, err := repo_model.GetAttachmentByID(ctx, attachID) if err != nil { if repo_model.IsErrAttachmentNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() return } - ctx.Error(http.StatusInternalServerError, "GetAttachmentByID", err) + ctx.APIErrorInternal(err) return } if attach.ReleaseID != releaseID { log.Info("User requested attachment is not in release, release_id %v, attachment_id: %v", releaseID, attachID) - ctx.NotFound() + ctx.APIErrorNotFound() return } // FIXME Should prove the existence of the given repo, but results in unnecessary database requests if err := repo_model.DeleteAttachment(ctx, attach, true); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteAttachment", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) diff --git a/routers/api/v1/repo/release_tags.go b/routers/api/v1/repo/release_tags.go index 7380c5231c..b5e7d83b2a 100644 --- a/routers/api/v1/repo/release_tags.go +++ b/routers/api/v1/repo/release_tags.go @@ -46,20 +46,20 @@ func GetReleaseByTag(ctx *context.APIContext) { release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tag) if err != nil { if repo_model.IsErrReleaseNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() return } - ctx.Error(http.StatusInternalServerError, "GetRelease", err) + ctx.APIErrorInternal(err) return } if release.IsTag { - ctx.NotFound() + ctx.APIErrorNotFound() return } if err = release.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, convert.ToAPIRelease(ctx, ctx.Repo.Repository, release)) @@ -99,24 +99,24 @@ func DeleteReleaseByTag(ctx *context.APIContext) { release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tag) if err != nil { if repo_model.IsErrReleaseNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() return } - ctx.Error(http.StatusInternalServerError, "GetRelease", err) + ctx.APIErrorInternal(err) return } if release.IsTag { - ctx.NotFound() + ctx.APIErrorNotFound() return } if err = release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, release, ctx.Doer, false); err != nil { if release_service.IsErrProtectedTagName(err) { - ctx.Error(http.StatusUnprocessableEntity, "delTag", "user not allowed to delete protected tag") + ctx.APIError(http.StatusUnprocessableEntity, "user not allowed to delete protected tag") return } - ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index ce09e7fc0f..8acc912796 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -5,6 +5,7 @@ package repo import ( + "errors" "fmt" "net/http" "slices" @@ -12,7 +13,6 @@ import ( "strings" "time" - actions_model "code.gitea.io/gitea/models/actions" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" @@ -134,7 +134,7 @@ func Search(ctx *context.APIContext) { private = false } - opts := &repo_model.SearchRepoOptions{ + opts := repo_model.SearchRepoOptions{ ListOptions: utils.GetListOptions(ctx), Actor: ctx.Doer, Keyword: ctx.FormTrim("q"), @@ -171,7 +171,7 @@ func Search(ctx *context.APIContext) { opts.Collaborate = optional.Some(true) case "": default: - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid search mode: \"%s\"", mode)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid search mode: \"%s\"", mode)) return } @@ -193,11 +193,11 @@ func Search(ctx *context.APIContext) { if orderBy, ok := searchModeMap[sortMode]; ok { opts.OrderBy = orderBy } else { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort mode: \"%s\"", sortMode)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: \"%s\"", sortMode)) return } } else { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid sort order: \"%s\"", sortOrder)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: \"%s\"", sortOrder)) return } } @@ -245,7 +245,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre // If the readme template does not exist, a 400 will be returned. if opt.AutoInit && len(opt.Readme) > 0 && !slices.Contains(repo_module.Readmes, opt.Readme) { - ctx.Error(http.StatusBadRequest, "", fmt.Errorf("readme template does not exist, available templates: %v", repo_module.Readmes)) + ctx.APIError(http.StatusBadRequest, fmt.Errorf("readme template does not exist, available templates: %v", repo_module.Readmes)) return } @@ -265,13 +265,13 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre }) if err != nil { if repo_model.IsErrRepoAlreadyExist(err) { - ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.") + ctx.APIError(http.StatusConflict, "The repository with the same name already exists.") } else if db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || label.IsErrTemplateLoad(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "CreateRepository", err) + ctx.APIErrorInternal(err) } return } @@ -279,7 +279,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre // reload repo from db to get a real state after creation repo, err = repo_model.GetRepositoryByID(ctx, repo.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetRepositoryByID", err) + ctx.APIErrorInternal(err) } ctx.JSON(http.StatusCreated, convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner})) @@ -311,7 +311,7 @@ func Create(ctx *context.APIContext) { opt := web.GetForm(ctx).(*api.CreateRepoOption) if ctx.Doer.IsOrganization() { // Shouldn't reach this condition, but just in case. - ctx.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization") + ctx.APIError(http.StatusUnprocessableEntity, "not allowed creating repository for organization") return } CreateUserRepo(ctx, ctx.Doer, *opt) @@ -355,12 +355,12 @@ func Generate(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.GenerateRepoOption) if !ctx.Repo.Repository.IsTemplate { - ctx.Error(http.StatusUnprocessableEntity, "", "this is not a template repo") + ctx.APIError(http.StatusUnprocessableEntity, "this is not a template repo") return } if ctx.Doer.IsOrganization() { - ctx.Error(http.StatusUnprocessableEntity, "", "not allowed creating repository for organization") + ctx.APIError(http.StatusUnprocessableEntity, "not allowed creating repository for organization") return } @@ -379,7 +379,7 @@ func Generate(ctx *context.APIContext) { } if !opts.IsValid() { - ctx.Error(http.StatusUnprocessableEntity, "", "must select at least one template item") + ctx.APIError(http.StatusUnprocessableEntity, "must select at least one template item") return } @@ -395,22 +395,22 @@ func Generate(ctx *context.APIContext) { return } - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) return } if !ctx.Doer.IsAdmin && !ctxUser.IsOrganization() { - ctx.Error(http.StatusForbidden, "", "Only admin can generate repository for other user.") + ctx.APIError(http.StatusForbidden, "Only admin can generate repository for other user.") return } if !ctx.Doer.IsAdmin { canCreate, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID) if err != nil { - ctx.ServerError("CanCreateOrgRepo", err) + ctx.APIErrorInternal(err) return } else if !canCreate { - ctx.Error(http.StatusForbidden, "", "Given user is not allowed to create repository in organization.") + ctx.APIError(http.StatusForbidden, "Given user is not allowed to create repository in organization.") return } } @@ -419,12 +419,12 @@ func Generate(ctx *context.APIContext) { repo, err := repo_service.GenerateRepository(ctx, ctx.Doer, ctxUser, ctx.Repo.Repository, opts) if err != nil { if repo_model.IsErrRepoAlreadyExist(err) { - ctx.Error(http.StatusConflict, "", "The repository with the same name already exists.") + ctx.APIError(http.StatusConflict, "The repository with the same name already exists.") } else if db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "CreateRepository", err) + ctx.APIErrorInternal(err) } return } @@ -498,25 +498,25 @@ func CreateOrgRepo(ctx *context.APIContext) { org, err := organization.GetOrgByName(ctx, ctx.PathParam("org")) if err != nil { if organization.IsErrOrgNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "", err) + ctx.APIError(http.StatusUnprocessableEntity, err) } else { - ctx.Error(http.StatusInternalServerError, "GetOrgByName", err) + ctx.APIErrorInternal(err) } return } if !organization.HasOrgOrUserVisible(ctx, org.AsUser(), ctx.Doer) { - ctx.NotFound("HasOrgOrUserVisible", nil) + ctx.APIErrorNotFound("HasOrgOrUserVisible", nil) return } if !ctx.Doer.IsAdmin { canCreate, err := org.CanCreateOrgRepo(ctx, ctx.Doer.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "CanCreateOrgRepo", err) + ctx.APIErrorInternal(err) return } else if !canCreate { - ctx.Error(http.StatusForbidden, "", "Given user is not allowed to create repository in organization.") + ctx.APIError(http.StatusForbidden, "Given user is not allowed to create repository in organization.") return } } @@ -548,7 +548,7 @@ func Get(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if err := ctx.Repo.Repository.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "Repository.LoadAttributes", err) + ctx.APIErrorInternal(err) return } @@ -578,19 +578,19 @@ func GetByID(ctx *context.APIContext) { repo, err := repo_model.GetRepositoryByID(ctx, ctx.PathParamInt64("id")) if err != nil { if repo_model.IsErrRepoNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetRepositoryByID", err) + ctx.APIErrorInternal(err) } return } permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + ctx.APIErrorInternal(err) return } else if !permission.HasAnyUnitAccess() { - ctx.NotFound() + ctx.APIErrorNotFound() return } ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, permission)) @@ -653,7 +653,7 @@ func Edit(ctx *context.APIContext) { repo, err := repo_model.GetRepositoryByID(ctx, ctx.Repo.Repository.ID) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -673,13 +673,13 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil { switch { case repo_model.IsErrRepoAlreadyExist(err): - ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is already taken [name: %s]", newRepoName), err) + ctx.APIError(http.StatusUnprocessableEntity, err) case db.IsErrNameReserved(err): - ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is reserved [name: %s]", newRepoName), err) + ctx.APIError(http.StatusUnprocessableEntity, err) case db.IsErrNamePatternNotAllowed(err): - ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name's pattern is not allowed [name: %s, pattern: %s]", newRepoName, err.(db.ErrNamePatternNotAllowed).Pattern), err) + ctx.APIError(http.StatusUnprocessableEntity, err) default: - ctx.Error(http.StatusUnprocessableEntity, "ChangeRepositoryName", err) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("ChangeRepositoryName: %w", err)) } return err } @@ -703,7 +703,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err // Visibility of forked repository is forced sync with base repository. if repo.IsFork { if err := repo.GetBaseRepo(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "Unable to load base repository", err) + ctx.APIErrorInternal(err) return err } *opts.Private = repo.BaseRepo.IsPrivate @@ -712,8 +712,8 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err visibilityChanged = repo.IsPrivate != *opts.Private // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.Doer.IsAdmin { - err := fmt.Errorf("cannot change private repository to public") - ctx.Error(http.StatusUnprocessableEntity, "Force Private enabled", err) + err := errors.New("cannot change private repository to public") + ctx.APIError(http.StatusUnprocessableEntity, err) return err } @@ -728,17 +728,17 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err var err error ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) if err != nil { - ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err) + ctx.APIErrorInternal(err) return err } } // Default branch only updated if changed and exist or the repository is empty updateRepoLicense := false - if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || ctx.Repo.GitRepo.IsBranchExist(*opts.DefaultBranch)) { + if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, *opts.DefaultBranch)) { if !repo.IsEmpty { if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, *opts.DefaultBranch); err != nil { - ctx.Error(http.StatusInternalServerError, "SetDefaultBranch", err) + ctx.APIErrorInternal(err) return err } updateRepoLicense = true @@ -747,7 +747,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err } if err := repo_service.UpdateRepository(ctx, repo, visibilityChanged); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateRepository", err) + ctx.APIErrorInternal(err) return err } @@ -755,7 +755,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err if err := repo_service.AddRepoToLicenseUpdaterQueue(&repo_service.LicenseUpdaterOptions{ RepoID: ctx.Repo.Repository.ID, }); err != nil { - ctx.Error(http.StatusInternalServerError, "AddRepoToLicenseUpdaterQueue", err) + ctx.APIErrorInternal(err) return err } } @@ -781,13 +781,13 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { if newHasIssues && opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { // Check that values are valid if !validation.IsValidExternalURL(opts.ExternalTracker.ExternalTrackerURL) { - err := fmt.Errorf("External tracker URL not valid") - ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL", err) + err := errors.New("External tracker URL not valid") + ctx.APIError(http.StatusUnprocessableEntity, err) return err } if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) { - err := fmt.Errorf("External tracker URL format not valid") - ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL format", err) + err := errors.New("External tracker URL format not valid") + ctx.APIError(http.StatusUnprocessableEntity, err) return err } @@ -848,8 +848,8 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { if newHasWiki && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() { // Check that values are valid if !validation.IsValidExternalURL(opts.ExternalWiki.ExternalWikiURL) { - err := fmt.Errorf("External wiki URL not valid") - ctx.Error(http.StatusUnprocessableEntity, "", "Invalid external wiki URL") + err := errors.New("External wiki URL not valid") + ctx.APIError(http.StatusUnprocessableEntity, "Invalid external wiki URL") return err } @@ -1024,7 +1024,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { if len(units)+len(deleteUnitTypes) > 0 { if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err) + ctx.APIErrorInternal(err) return err } } @@ -1039,24 +1039,24 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e // archive / un-archive if opts.Archived != nil { if repo.IsMirror { - err := fmt.Errorf("repo is a mirror, cannot archive/un-archive") - ctx.Error(http.StatusUnprocessableEntity, err.Error(), err) + err := errors.New("repo is a mirror, cannot archive/un-archive") + ctx.APIError(http.StatusUnprocessableEntity, err) return err } if *opts.Archived { if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil { log.Error("Tried to archive a repo: %s", err) - ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err) + ctx.APIErrorInternal(err) return err } - if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil { + if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil { log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err) } log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) } else { if err := repo_model.SetArchiveRepoState(ctx, repo, *opts.Archived); err != nil { log.Error("Tried to un-archive a repo: %s", err) - ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err) + ctx.APIErrorInternal(err) return err } if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) { @@ -1084,7 +1084,7 @@ func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error { mirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID) if err != nil { log.Error("Failed to get mirror: %s", err) - ctx.Error(http.StatusInternalServerError, "MirrorInterval", err) + ctx.APIErrorInternal(err) return err } @@ -1094,14 +1094,14 @@ func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error { interval, err := time.ParseDuration(*opts.MirrorInterval) if err != nil { log.Error("Wrong format for MirrorInternal Sent: %s", err) - ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return err } // Ensure the provided duration is not too short if interval != 0 && interval < setting.Mirror.MinInterval { err := fmt.Errorf("invalid mirror interval: %s is below minimum interval: %s", interval, setting.Mirror.MinInterval) - ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return err } @@ -1120,7 +1120,7 @@ func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error { // finally update the mirror in the DB if err := repo_model.UpdateMirror(ctx, mirror); err != nil { log.Error("Failed to Set Mirror Interval: %s", err) - ctx.Error(http.StatusUnprocessableEntity, "MirrorInterval", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return err } @@ -1158,10 +1158,10 @@ func Delete(ctx *context.APIContext) { canDelete, err := repo_module.CanUserDelete(ctx, repo, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "CanUserDelete", err) + ctx.APIErrorInternal(err) return } else if !canDelete { - ctx.Error(http.StatusForbidden, "", "Given user is not owner of organization.") + ctx.APIError(http.StatusForbidden, "Given user is not owner of organization.") return } @@ -1170,7 +1170,7 @@ func Delete(ctx *context.APIContext) { } if err := repo_service.DeleteRepository(ctx, ctx.Doer, repo, true); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteRepository", err) + ctx.APIErrorInternal(err) return } @@ -1315,7 +1315,7 @@ func ListRepoActivityFeeds(ctx *context.APIContext) { feeds, count, err := feed_service.GetFeeds(ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetFeeds", err) + ctx.APIErrorInternal(err) return } ctx.SetTotalCountHeader(count) diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go index 8d6ca9e3b5..97233f85dc 100644 --- a/routers/api/v1/repo/repo_test.go +++ b/routers/api/v1/repo/repo_test.go @@ -58,7 +58,7 @@ func TestRepoEdit(t *testing.T) { web.SetForm(ctx, &opts) Edit(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ ID: 1, }, unittest.Cond("name = ? AND is_archived = 1", *opts.Name)) @@ -78,7 +78,7 @@ func TestRepoEditNameChange(t *testing.T) { web.SetForm(ctx, &opts) Edit(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ ID: 1, diff --git a/routers/api/v1/repo/star.go b/routers/api/v1/repo/star.go index 99676de119..46218e0e28 100644 --- a/routers/api/v1/repo/star.go +++ b/routers/api/v1/repo/star.go @@ -44,10 +44,12 @@ func ListStargazers(ctx *context.APIContext) { // "$ref": "#/responses/UserList" // "404": // "$ref": "#/responses/notFound" + // "403": + // "$ref": "#/responses/forbidden" stargazers, err := repo_model.GetStargazers(ctx, ctx.Repo.Repository, utils.GetListOptions(ctx)) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetStargazers", err) + ctx.APIErrorInternal(err) return } users := make([]*api.User, len(stargazers)) diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go index 8c910a68f9..40007ea1e5 100644 --- a/routers/api/v1/repo/status.go +++ b/routers/api/v1/repo/status.go @@ -55,7 +55,7 @@ func NewCommitStatus(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateStatusOption) sha := ctx.PathParam("sha") if len(sha) == 0 { - ctx.Error(http.StatusBadRequest, "sha not given", nil) + ctx.APIError(http.StatusBadRequest, nil) return } status := &git_model.CommitStatus{ @@ -65,7 +65,7 @@ func NewCommitStatus(ctx *context.APIContext) { Context: form.Context, } if err := commitstatus_service.CreateCommitStatus(ctx, ctx.Repo.Repository, ctx.Doer, sha, status); err != nil { - ctx.Error(http.StatusInternalServerError, "CreateCommitStatus", err) + ctx.APIErrorInternal(err) return } @@ -177,20 +177,14 @@ func GetCommitStatusesByRef(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - filter := utils.ResolveRefOrSha(ctx, ctx.PathParam("ref")) + refCommit := resolveRefCommit(ctx, ctx.PathParam("ref"), 7) if ctx.Written() { return } - - getCommitStatuses(ctx, filter) // By default filter is maybe the raw SHA + getCommitStatuses(ctx, refCommit.CommitID) } -func getCommitStatuses(ctx *context.APIContext, sha string) { - if len(sha) == 0 { - ctx.Error(http.StatusBadRequest, "ref/sha not given", nil) - return - } - sha = utils.MustConvertToSHA1(ctx.Base, ctx.Repo, sha) +func getCommitStatuses(ctx *context.APIContext, commitID string) { repo := ctx.Repo.Repository listOptions := utils.GetListOptions(ctx) @@ -198,12 +192,12 @@ func getCommitStatuses(ctx *context.APIContext, sha string) { statuses, maxResults, err := db.FindAndCount[git_model.CommitStatus](ctx, &git_model.CommitStatusOptions{ ListOptions: listOptions, RepoID: repo.ID, - SHA: sha, + SHA: commitID, SortType: ctx.FormTrim("sort"), State: ctx.FormTrim("state"), }) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetCommitStatuses", fmt.Errorf("GetCommitStatuses[%s, %s, %d]: %w", repo.FullName(), sha, ctx.FormInt("page"), err)) + ctx.APIErrorInternal(fmt.Errorf("GetCommitStatuses[%s, %s, %d]: %w", repo.FullName(), commitID, ctx.FormInt("page"), err)) return } @@ -257,26 +251,31 @@ func GetCombinedCommitStatusByRef(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - sha := utils.ResolveRefOrSha(ctx, ctx.PathParam("ref")) + refCommit := resolveRefCommit(ctx, ctx.PathParam("ref"), 7) if ctx.Written() { return } repo := ctx.Repo.Repository - statuses, count, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, utils.GetListOptions(ctx)) + statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, refCommit.Commit.ID.String(), utils.GetListOptions(ctx)) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetLatestCommitStatus", fmt.Errorf("GetLatestCommitStatus[%s, %s]: %w", repo.FullName(), sha, err)) + ctx.APIErrorInternal(fmt.Errorf("GetLatestCommitStatus[%s, %s]: %w", repo.FullName(), refCommit.CommitID, err)) return } + count, err := git_model.CountLatestCommitStatus(ctx, repo.ID, refCommit.Commit.ID.String()) + if err != nil { + ctx.APIErrorInternal(fmt.Errorf("CountLatestCommitStatus[%s, %s]: %w", repo.FullName(), refCommit.CommitID, err)) + return + } + ctx.SetTotalCountHeader(count) + if len(statuses) == 0 { ctx.JSON(http.StatusOK, &api.CombinedStatus{}) return } combiStatus := convert.ToCombinedStatus(ctx, statuses, convert.ToRepo(ctx, repo, ctx.Repo.Permission)) - - ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, combiStatus) } diff --git a/routers/api/v1/repo/subscriber.go b/routers/api/v1/repo/subscriber.go index 8584182857..14f296a83d 100644 --- a/routers/api/v1/repo/subscriber.go +++ b/routers/api/v1/repo/subscriber.go @@ -47,7 +47,7 @@ func ListSubscribers(ctx *context.APIContext) { subscribers, err := repo_model.GetRepoWatchers(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx)) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetRepoWatchers", err) + ctx.APIErrorInternal(err) return } users := make([]*api.User, len(subscribers)) diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go index 8447a8f1f2..9e77637282 100644 --- a/routers/api/v1/repo/tag.go +++ b/routers/api/v1/repo/tag.go @@ -57,7 +57,7 @@ func ListTags(ctx *context.APIContext) { tags, total, err := ctx.Repo.GitRepo.GetTagInfos(listOpts.Page, listOpts.PageSize) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetTags", err) + ctx.APIErrorInternal(err) return } @@ -103,16 +103,16 @@ func GetAnnotatedTag(ctx *context.APIContext) { sha := ctx.PathParam("sha") if len(sha) == 0 { - ctx.Error(http.StatusBadRequest, "", "SHA not provided") + ctx.APIError(http.StatusBadRequest, "SHA not provided") return } if tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha); err != nil { - ctx.Error(http.StatusBadRequest, "GetAnnotatedTag", err) + ctx.APIError(http.StatusBadRequest, err) } else { - commit, err := tag.Commit(ctx.Repo.GitRepo) + commit, err := ctx.Repo.GitRepo.GetTagCommit(tag.Name) if err != nil { - ctx.Error(http.StatusBadRequest, "GetAnnotatedTag", err) + ctx.APIError(http.StatusBadRequest, err) } ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit)) } @@ -150,7 +150,7 @@ func GetTag(ctx *context.APIContext) { tag, err := ctx.Repo.GitRepo.GetTag(tagName) if err != nil { - ctx.NotFound(tagName) + ctx.APIErrorNotFound("tag doesn't exist: " + tagName) return } ctx.JSON(http.StatusOK, convert.ToTag(ctx.Repo.Repository, tag)) @@ -200,27 +200,27 @@ func CreateTag(ctx *context.APIContext) { commit, err := ctx.Repo.GitRepo.GetCommit(form.Target) if err != nil { - ctx.Error(http.StatusNotFound, "target not found", fmt.Errorf("target not found: %w", err)) + ctx.APIError(http.StatusNotFound, fmt.Errorf("target not found: %w", err)) return } if err := release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, commit.ID.String(), form.TagName, form.Message); err != nil { if release_service.IsErrTagAlreadyExists(err) { - ctx.Error(http.StatusConflict, "tag exist", err) + ctx.APIError(http.StatusConflict, err) return } if release_service.IsErrProtectedTagName(err) { - ctx.Error(http.StatusUnprocessableEntity, "CreateNewTag", "user not allowed to create protected tag") + ctx.APIError(http.StatusUnprocessableEntity, "user not allowed to create protected tag") return } - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } tag, err := ctx.Repo.GitRepo.GetTag(form.TagName) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusCreated, convert.ToTag(ctx.Repo.Repository, tag)) @@ -267,24 +267,24 @@ func DeleteTag(ctx *context.APIContext) { tag, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tagName) if err != nil { if repo_model.IsErrReleaseNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() return } - ctx.Error(http.StatusInternalServerError, "GetRelease", err) + ctx.APIErrorInternal(err) return } if !tag.IsTag { - ctx.Error(http.StatusConflict, "IsTag", errors.New("a tag attached to a release cannot be deleted directly")) + ctx.APIError(http.StatusConflict, errors.New("a tag attached to a release cannot be deleted directly")) return } if err = release_service.DeleteReleaseByID(ctx, ctx.Repo.Repository, tag, ctx.Doer, true); err != nil { if release_service.IsErrProtectedTagName(err) { - ctx.Error(http.StatusUnprocessableEntity, "delTag", "user not allowed to delete protected tag") + ctx.APIError(http.StatusUnprocessableEntity, "user not allowed to delete protected tag") return } - ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err) + ctx.APIErrorInternal(err) return } @@ -316,7 +316,7 @@ func ListTagProtection(ctx *context.APIContext) { repo := ctx.Repo.Repository pts, err := git_model.GetProtectedTags(ctx, repo.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetProtectedTags", err) + ctx.APIErrorInternal(err) return } apiPts := make([]*api.TagProtection, len(pts)) @@ -360,12 +360,12 @@ func GetTagProtection(ctx *context.APIContext) { id := ctx.PathParamInt64("id") pt, err := git_model.GetProtectedTagByID(ctx, id) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) + ctx.APIErrorInternal(err) return } if pt == nil || repo.ID != pt.RepoID { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -413,21 +413,21 @@ func CreateTagProtection(ctx *context.APIContext) { namePattern := strings.TrimSpace(form.NamePattern) if namePattern == "" { - ctx.Error(http.StatusBadRequest, "name_pattern are empty", "name_pattern are empty") + ctx.APIError(http.StatusBadRequest, "name_pattern are empty") return } if len(form.WhitelistUsernames) == 0 && len(form.WhitelistTeams) == 0 { - ctx.Error(http.StatusBadRequest, "both whitelist_usernames and whitelist_teams are empty", "both whitelist_usernames and whitelist_teams are empty") + ctx.APIError(http.StatusBadRequest, "both whitelist_usernames and whitelist_teams are empty") return } pt, err := git_model.GetProtectedTagByNamePattern(ctx, repo.ID, namePattern) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetProtectTagOfRepo", err) + ctx.APIErrorInternal(err) return } else if pt != nil { - ctx.Error(http.StatusForbidden, "Create tag protection", "Tag protection already exist") + ctx.APIError(http.StatusForbidden, "Tag protection already exist") return } @@ -435,10 +435,10 @@ func CreateTagProtection(ctx *context.APIContext) { whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + ctx.APIErrorInternal(err) return } @@ -446,10 +446,10 @@ func CreateTagProtection(ctx *context.APIContext) { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + ctx.APIErrorInternal(err) return } } @@ -461,18 +461,18 @@ func CreateTagProtection(ctx *context.APIContext) { AllowlistTeamIDs: whitelistTeams, } if err := git_model.InsertProtectedTag(ctx, protectTag); err != nil { - ctx.Error(http.StatusInternalServerError, "InsertProtectedTag", err) + ctx.APIErrorInternal(err) return } pt, err = git_model.GetProtectedTagByID(ctx, protectTag.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) + ctx.APIErrorInternal(err) return } if pt == nil || pt.RepoID != repo.ID { - ctx.Error(http.StatusInternalServerError, "New tag protection not found", err) + ctx.APIErrorInternal(err) return } @@ -524,12 +524,12 @@ func EditTagProtection(ctx *context.APIContext) { id := ctx.PathParamInt64("id") pt, err := git_model.GetProtectedTagByID(ctx, id) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) + ctx.APIErrorInternal(err) return } if pt == nil || pt.RepoID != repo.ID { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -543,10 +543,10 @@ func EditTagProtection(ctx *context.APIContext) { whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err) + ctx.APIErrorInternal(err) return } } @@ -557,10 +557,10 @@ func EditTagProtection(ctx *context.APIContext) { whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err) + ctx.APIError(http.StatusUnprocessableEntity, err) return } - ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err) + ctx.APIErrorInternal(err) return } pt.AllowlistUserIDs = whitelistUsers @@ -568,18 +568,18 @@ func EditTagProtection(ctx *context.APIContext) { err = git_model.UpdateProtectedTag(ctx, pt) if err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateProtectedTag", err) + ctx.APIErrorInternal(err) return } pt, err = git_model.GetProtectedTagByID(ctx, id) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) + ctx.APIErrorInternal(err) return } if pt == nil || pt.RepoID != repo.ID { - ctx.Error(http.StatusInternalServerError, "New tag protection not found", "New tag protection not found") + ctx.APIErrorInternal(errors.New("new tag protection not found")) return } @@ -619,18 +619,18 @@ func DeleteTagProtection(ctx *context.APIContext) { id := ctx.PathParamInt64("id") pt, err := git_model.GetProtectedTagByID(ctx, id) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err) + ctx.APIErrorInternal(err) return } if pt == nil || pt.RepoID != repo.ID { - ctx.NotFound() + ctx.APIErrorNotFound() return } err = git_model.DeleteProtectedTag(ctx, pt) if err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteProtectedTag", err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/repo/teams.go b/routers/api/v1/repo/teams.go index e5a2d5c320..739a9e3892 100644 --- a/routers/api/v1/repo/teams.go +++ b/routers/api/v1/repo/teams.go @@ -38,19 +38,19 @@ func ListTeams(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if !ctx.Repo.Owner.IsOrganization() { - ctx.Error(http.StatusMethodNotAllowed, "noOrg", "repo is not owned by an organization") + ctx.APIError(http.StatusMethodNotAllowed, "repo is not owned by an organization") return } teams, err := organization.GetRepoTeams(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } apiTeams, err := convert.ToTeams(ctx, teams, false) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -89,7 +89,7 @@ func IsTeam(ctx *context.APIContext) { // "$ref": "#/responses/error" if !ctx.Repo.Owner.IsOrganization() { - ctx.Error(http.StatusMethodNotAllowed, "noOrg", "repo is not owned by an organization") + ctx.APIError(http.StatusMethodNotAllowed, "repo is not owned by an organization") return } @@ -101,14 +101,14 @@ func IsTeam(ctx *context.APIContext) { if repo_service.HasRepository(ctx, team, ctx.Repo.Repository.ID) { apiTeam, err := convert.ToTeam(ctx, team) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, apiTeam) return } - ctx.NotFound() + ctx.APIErrorNotFound() } // AddTeam add a team to a repository @@ -185,10 +185,10 @@ func DeleteTeam(ctx *context.APIContext) { func changeRepoTeam(ctx *context.APIContext, add bool) { if !ctx.Repo.Owner.IsOrganization() { - ctx.Error(http.StatusMethodNotAllowed, "noOrg", "repo is not owned by an organization") + ctx.APIError(http.StatusMethodNotAllowed, "repo is not owned by an organization") } if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() { - ctx.Error(http.StatusForbidden, "noAdmin", "user is nor repo admin nor owner") + ctx.APIError(http.StatusForbidden, "user is nor repo admin nor owner") return } @@ -201,19 +201,19 @@ func changeRepoTeam(ctx *context.APIContext, add bool) { var err error if add { if repoHasTeam { - ctx.Error(http.StatusUnprocessableEntity, "alreadyAdded", fmt.Errorf("team '%s' is already added to repo", team.Name)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team '%s' is already added to repo", team.Name)) return } err = repo_service.TeamAddRepository(ctx, team, ctx.Repo.Repository) } else { if !repoHasTeam { - ctx.Error(http.StatusUnprocessableEntity, "notAdded", fmt.Errorf("team '%s' was not added to repo", team.Name)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team '%s' was not added to repo", team.Name)) return } err = repo_service.RemoveRepositoryFromTeam(ctx, team, ctx.Repo.Repository.ID) } if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -224,10 +224,10 @@ func getTeamByParam(ctx *context.APIContext) *organization.Team { team, err := organization.GetTeam(ctx, ctx.Repo.Owner.ID, ctx.PathParam("team")) if err != nil { if organization.IsErrTeamNotExist(err) { - ctx.Error(http.StatusNotFound, "TeamNotExit", err) + ctx.APIError(http.StatusNotFound, err) return nil } - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return nil } return team diff --git a/routers/api/v1/repo/topic.go b/routers/api/v1/repo/topic.go index a1a15e7f46..9c4c22e039 100644 --- a/routers/api/v1/repo/topic.go +++ b/routers/api/v1/repo/topic.go @@ -56,7 +56,7 @@ func ListTopics(ctx *context.APIContext) { topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -124,7 +124,7 @@ func UpdateTopics(ctx *context.APIContext) { err := repo_model.SaveTopics(ctx, ctx.Repo.Repository.ID, validTopics...) if err != nil { log.Error("SaveTopics failed: %v", err) - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -178,7 +178,7 @@ func AddTopic(ctx *context.APIContext) { }) if err != nil { log.Error("CountTopics failed: %v", err) - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } if count >= 25 { @@ -191,7 +191,7 @@ func AddTopic(ctx *context.APIContext) { _, err = repo_model.AddTopic(ctx, ctx.Repo.Repository.ID, topicName) if err != nil { log.Error("AddTopic failed: %v", err) - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -242,12 +242,12 @@ func DeleteTopic(ctx *context.APIContext) { topic, err := repo_model.DeleteTopic(ctx, ctx.Repo.Repository.ID, topicName) if err != nil { log.Error("DeleteTopic failed: %v", err) - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } if topic == nil { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -290,7 +290,7 @@ func TopicSearch(ctx *context.APIContext) { topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go index b2090cac41..cbf3d10c39 100644 --- a/routers/api/v1/repo/transfer.go +++ b/routers/api/v1/repo/transfer.go @@ -15,6 +15,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" @@ -60,17 +61,17 @@ func Transfer(ctx *context.APIContext) { newOwner, err := user_model.GetUserByName(ctx, opts.NewOwner) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.Error(http.StatusNotFound, "", "The new owner does not exist or cannot be found") + ctx.APIError(http.StatusNotFound, "The new owner does not exist or cannot be found") return } - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } if newOwner.Type == user_model.UserTypeOrganization { if !ctx.Doer.IsAdmin && newOwner.Visibility == api.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) { // The user shouldn't know about this organization - ctx.Error(http.StatusNotFound, "", "The new owner does not exist or cannot be found") + ctx.APIError(http.StatusNotFound, "The new owner does not exist or cannot be found") return } } @@ -78,7 +79,7 @@ func Transfer(ctx *context.APIContext) { var teams []*organization.Team if opts.TeamIDs != nil { if !newOwner.IsOrganization() { - ctx.Error(http.StatusUnprocessableEntity, "repoTransfer", "Teams can only be added to organization-owned repositories") + ctx.APIError(http.StatusUnprocessableEntity, "Teams can only be added to organization-owned repositories") return } @@ -86,12 +87,12 @@ func Transfer(ctx *context.APIContext) { for _, tID := range *opts.TeamIDs { team, err := organization.GetTeamByID(ctx, tID) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, "team", fmt.Errorf("team %d not found", tID)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team %d not found", tID)) return } if team.OrgID != org.ID { - ctx.Error(http.StatusForbidden, "team", fmt.Errorf("team %d belongs not to org %d", tID, org.ID)) + ctx.APIError(http.StatusForbidden, fmt.Errorf("team %d belongs not to org %d", tID, org.ID)) return } @@ -107,20 +108,17 @@ func Transfer(ctx *context.APIContext) { oldFullname := ctx.Repo.Repository.FullName() if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil { - if repo_model.IsErrRepoTransferInProgress(err) { - ctx.Error(http.StatusConflict, "StartRepositoryTransfer", err) - return - } - - if repo_model.IsErrRepoAlreadyExist(err) { - ctx.Error(http.StatusUnprocessableEntity, "StartRepositoryTransfer", err) - return - } - - if errors.Is(err, user_model.ErrBlockedUser) { - ctx.Error(http.StatusForbidden, "BlockedUser", err) - } else { - ctx.InternalServerError(err) + switch { + case repo_model.IsErrRepoTransferInProgress(err): + ctx.APIError(http.StatusConflict, err) + case repo_model.IsErrRepoAlreadyExist(err): + ctx.APIError(http.StatusUnprocessableEntity, err) + case repo_service.IsRepositoryLimitReached(err): + ctx.APIError(http.StatusForbidden, err) + case errors.Is(err, user_model.ErrBlockedUser): + ctx.APIError(http.StatusForbidden, err) + default: + ctx.APIErrorInternal(err) } return } @@ -161,12 +159,18 @@ func AcceptTransfer(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - err := acceptOrRejectRepoTransfer(ctx, true) - if ctx.Written() { - return - } + err := repo_service.AcceptTransferOwnership(ctx, ctx.Repo.Repository, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "acceptOrRejectRepoTransfer", err) + switch { + case repo_model.IsErrNoPendingTransfer(err): + ctx.APIError(http.StatusNotFound, err) + case errors.Is(err, util.ErrPermissionDenied): + ctx.APIError(http.StatusForbidden, err) + case repo_service.IsRepositoryLimitReached(err): + ctx.APIError(http.StatusForbidden, err) + default: + ctx.APIErrorInternal(err) + } return } @@ -199,40 +203,18 @@ func RejectTransfer(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - err := acceptOrRejectRepoTransfer(ctx, false) - if ctx.Written() { - return - } + err := repo_service.RejectRepositoryTransfer(ctx, ctx.Repo.Repository, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "acceptOrRejectRepoTransfer", err) + switch { + case repo_model.IsErrNoPendingTransfer(err): + ctx.APIError(http.StatusNotFound, err) + case errors.Is(err, util.ErrPermissionDenied): + ctx.APIError(http.StatusForbidden, err) + default: + ctx.APIErrorInternal(err) + } return } ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission)) } - -func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error { - repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository) - if err != nil { - if repo_model.IsErrNoPendingTransfer(err) { - ctx.NotFound() - return nil - } - return err - } - - if err := repoTransfer.LoadAttributes(ctx); err != nil { - return err - } - - if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) { - ctx.Error(http.StatusForbidden, "CanUserAcceptTransfer", nil) - return fmt.Errorf("user does not have permissions to do this") - } - - if accept { - return repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams) - } - - return repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository) -} diff --git a/routers/api/v1/repo/tree.go b/routers/api/v1/repo/tree.go index 768e5d41c1..dfd69600fb 100644 --- a/routers/api/v1/repo/tree.go +++ b/routers/api/v1/repo/tree.go @@ -58,11 +58,11 @@ func GetTree(ctx *context.APIContext) { sha := ctx.PathParam("sha") if len(sha) == 0 { - ctx.Error(http.StatusBadRequest, "", "sha not provided") + ctx.APIError(http.StatusBadRequest, "sha not provided") return } if tree, err := files_service.GetTreeBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, sha, ctx.FormInt("page"), ctx.FormInt("per_page"), ctx.FormBool("recursive")); err != nil { - ctx.Error(http.StatusBadRequest, "", err.Error()) + ctx.APIError(http.StatusBadRequest, err.Error()) } else { ctx.SetTotalCountHeader(int64(tree.TotalCount)) ctx.JSON(http.StatusOK, tree) diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index 352d8f48fc..8e24ffa465 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -59,7 +59,7 @@ func NewWikiPage(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateWikiPageOptions) if util.IsEmptyString(form.Title) { - ctx.Error(http.StatusBadRequest, "emptyTitle", nil) + ctx.APIError(http.StatusBadRequest, nil) return } @@ -71,18 +71,18 @@ func NewWikiPage(ctx *context.APIContext) { content, err := base64.StdEncoding.DecodeString(form.ContentBase64) if err != nil { - ctx.Error(http.StatusBadRequest, "invalid base64 encoding of content", err) + ctx.APIError(http.StatusBadRequest, err) return } form.ContentBase64 = string(content) if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.ContentBase64, form.Message); err != nil { if repo_model.IsErrWikiReservedName(err) { - ctx.Error(http.StatusBadRequest, "IsErrWikiReservedName", err) + ctx.APIError(http.StatusBadRequest, err) } else if repo_model.IsErrWikiAlreadyExist(err) { - ctx.Error(http.StatusBadRequest, "IsErrWikiAlreadyExists", err) + ctx.APIError(http.StatusBadRequest, err) } else { - ctx.Error(http.StatusInternalServerError, "AddWikiPage", err) + ctx.APIErrorInternal(err) } return } @@ -149,13 +149,13 @@ func EditWikiPage(ctx *context.APIContext) { content, err := base64.StdEncoding.DecodeString(form.ContentBase64) if err != nil { - ctx.Error(http.StatusBadRequest, "invalid base64 encoding of content", err) + ctx.APIError(http.StatusBadRequest, err) return } form.ContentBase64 = string(content) if err := wiki_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, oldWikiName, newWikiName, form.ContentBase64, form.Message); err != nil { - ctx.Error(http.StatusInternalServerError, "EditWikiPage", err) + ctx.APIErrorInternal(err) return } @@ -193,12 +193,12 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi } // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) + commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) // Get last change information. lastCommit, err := wikiRepo.GetCommitByPath(pageFilename) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetCommitByPath", err) + ctx.APIErrorInternal(err) return nil } @@ -246,10 +246,10 @@ func DeleteWikiPage(ctx *context.APIContext) { if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil { if err.Error() == "file does not exist" { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) return } - ctx.Error(http.StatusInternalServerError, "DeleteWikiPage", err) + ctx.APIErrorInternal(err) return } @@ -298,10 +298,7 @@ func ListWikiPages(ctx *context.APIContext) { return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) limit := ctx.FormInt("limit") if limit <= 1 { limit = setting.API.DefaultPagingNum @@ -312,7 +309,7 @@ func ListWikiPages(ctx *context.APIContext) { entries, err := commit.ListEntries() if err != nil { - ctx.ServerError("ListEntries", err) + ctx.APIErrorInternal(err) return } pages := make([]*api.WikiPageMetaData, 0, len(entries)) @@ -322,7 +319,7 @@ func ListWikiPages(ctx *context.APIContext) { } c, err := wikiRepo.GetCommitByPath(entry.Name()) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetCommit", err) + ctx.APIErrorInternal(err) return } wikiName, err := wiki_service.GitPathToWebPath(entry.Name()) @@ -330,7 +327,7 @@ func ListWikiPages(ctx *context.APIContext) { if repo_model.IsErrWikiInvalidFileName(err) { continue } - ctx.Error(http.StatusInternalServerError, "WikiFilenameToName", err) + ctx.APIErrorInternal(err) return } pages = append(pages, wiki_service.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository)) @@ -432,22 +429,19 @@ func ListPageRevisions(ctx *context.APIContext) { } // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) + commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) // get Commit Count commitsHistory, err := wikiRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ - Revision: "master", + Revision: ctx.Repo.Repository.DefaultWikiBranch, File: pageFilename, Page: page, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "CommitsByFileAndRange", err) + ctx.APIErrorInternal(err) return } @@ -476,22 +470,22 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) // findWikiRepoCommit opens the wiki repo and returns the latest commit, writing to context on error. // The caller is responsible for closing the returned repo again func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) { - wikiRepo, err := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) + wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) if err != nil { if git.IsErrNotExist(err) || err.Error() == "no such file or directory" { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) } else { - ctx.Error(http.StatusInternalServerError, "OpenRepository", err) + ctx.APIErrorInternal(err) } return nil, nil } - commit, err := wikiRepo.GetBranchCommit("master") + commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch) if err != nil { if git.IsErrNotExist(err) { - ctx.NotFound(err) + ctx.APIErrorNotFound(err) } else { - ctx.Error(http.StatusInternalServerError, "GetBranchCommit", err) + ctx.APIErrorInternal(err) } return wikiRepo, nil } @@ -505,9 +499,9 @@ func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string { if blob.Size() > setting.API.DefaultMaxBlobSize { return "" } - content, err := blob.GetBlobContentBase64() + content, err := blob.GetBlobContentBase64(nil) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetBlobContentBase64", err) + ctx.APIErrorInternal(err) return "" } return content @@ -521,10 +515,10 @@ func wikiContentsByName(ctx *context.APIContext, commit *git.Commit, wikiName wi if err != nil { if git.IsErrNotExist(err) { if !isSidebarOrFooter { - ctx.NotFound() + ctx.APIErrorNotFound() } } else { - ctx.ServerError("findEntryForFile", err) + ctx.APIErrorInternal(err) } return "", "" } diff --git a/routers/api/v1/settings/settings.go b/routers/api/v1/settings/settings.go index 0ee81b96d5..94fbadeab0 100644 --- a/routers/api/v1/settings/settings.go +++ b/routers/api/v1/settings/settings.go @@ -43,6 +43,7 @@ func GetGeneralAPISettings(ctx *context.APIContext) { DefaultPagingNum: setting.API.DefaultPagingNum, DefaultGitTreesPerPage: setting.API.DefaultGitTreesPerPage, DefaultMaxBlobSize: setting.API.DefaultMaxBlobSize, + DefaultMaxResponseSize: setting.API.DefaultMaxResponseSize, }) } diff --git a/routers/api/v1/shared/action.go b/routers/api/v1/shared/action.go new file mode 100644 index 0000000000..c97e9419fd --- /dev/null +++ b/routers/api/v1/shared/action.go @@ -0,0 +1,187 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package shared + +import ( + "fmt" + "net/http" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + 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/setting" + api "code.gitea.io/gitea/modules/structs" + "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" +) + +// ListJobs lists jobs for api route validated ownerID and repoID +// ownerID == 0 and repoID == 0 means all jobs +// ownerID == 0 and repoID != 0 means all jobs for the given repo +// ownerID != 0 and repoID == 0 means all jobs for the given user/org +// ownerID != 0 and repoID != 0 undefined behavior +// runID == 0 means all jobs +// runID is used as an additional filter together with ownerID and repoID to only return jobs for the given run +// Access rights are checked at the API route level +func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) { + if ownerID != 0 && repoID != 0 { + setting.PanicInDevOrTesting("ownerID and repoID should not be both set") + } + opts := actions_model.FindRunJobOptions{ + OwnerID: ownerID, + RepoID: repoID, + RunID: runID, + ListOptions: utils.GetListOptions(ctx), + } + for _, status := range ctx.FormStrings("status") { + values, err := convertToInternal(status) + if err != nil { + ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status)) + return + } + opts.Statuses = append(opts.Statuses, values...) + } + + jobs, total, err := db.FindAndCount[actions_model.ActionRunJob](ctx, opts) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + res := new(api.ActionWorkflowJobsResponse) + res.TotalCount = total + + res.Entries = make([]*api.ActionWorkflowJob, len(jobs)) + + isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID + for i := range jobs { + var repository *repo_model.Repository + if isRepoLevel { + repository = ctx.Repo.Repository + } else { + repository, err = repo_model.GetRepositoryByID(ctx, jobs[i].RepoID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + } + + convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, repository, nil, jobs[i]) + if err != nil { + ctx.APIErrorInternal(err) + return + } + res.Entries[i] = convertedWorkflowJob + } + + ctx.JSON(http.StatusOK, &res) +} + +func convertToInternal(s string) ([]actions_model.Status, error) { + switch s { + case "pending", "waiting", "requested", "action_required": + return []actions_model.Status{actions_model.StatusBlocked}, nil + case "queued": + return []actions_model.Status{actions_model.StatusWaiting}, nil + case "in_progress": + return []actions_model.Status{actions_model.StatusRunning}, nil + case "completed": + return []actions_model.Status{ + actions_model.StatusSuccess, + actions_model.StatusFailure, + actions_model.StatusSkipped, + actions_model.StatusCancelled, + }, nil + case "failure": + return []actions_model.Status{actions_model.StatusFailure}, nil + case "success": + return []actions_model.Status{actions_model.StatusSuccess}, nil + case "skipped", "neutral": + return []actions_model.Status{actions_model.StatusSkipped}, nil + case "cancelled", "timed_out": + return []actions_model.Status{actions_model.StatusCancelled}, nil + default: + return nil, fmt.Errorf("invalid status %s", s) + } +} + +// ListRuns lists jobs for api route validated ownerID and repoID +// ownerID == 0 and repoID == 0 means all runs +// ownerID == 0 and repoID != 0 means all runs for the given repo +// ownerID != 0 and repoID == 0 means all runs for the given user/org +// ownerID != 0 and repoID != 0 undefined behavior +// Access rights are checked at the API route level +func ListRuns(ctx *context.APIContext, ownerID, repoID int64) { + if ownerID != 0 && repoID != 0 { + setting.PanicInDevOrTesting("ownerID and repoID should not be both set") + } + opts := actions_model.FindRunOptions{ + OwnerID: ownerID, + RepoID: repoID, + ListOptions: utils.GetListOptions(ctx), + } + + if event := ctx.FormString("event"); event != "" { + opts.TriggerEvent = webhook.HookEventType(event) + } + if branch := ctx.FormString("branch"); branch != "" { + opts.Ref = string(git.RefNameFromBranch(branch)) + } + for _, status := range ctx.FormStrings("status") { + values, err := convertToInternal(status) + if err != nil { + ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status)) + return + } + opts.Status = append(opts.Status, values...) + } + if actor := ctx.FormString("actor"); actor != "" { + user, err := user_model.GetUserByName(ctx, actor) + if err != nil { + ctx.APIErrorInternal(err) + return + } + opts.TriggerUserID = user.ID + } + if headSHA := ctx.FormString("head_sha"); headSHA != "" { + opts.CommitSHA = headSHA + } + + runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + res := new(api.ActionWorkflowRunsResponse) + res.TotalCount = total + + res.Entries = make([]*api.ActionWorkflowRun, len(runs)) + isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID + for i := range runs { + var repository *repo_model.Repository + if isRepoLevel { + repository = ctx.Repo.Repository + } else { + repository, err = repo_model.GetRepositoryByID(ctx, runs[i].RepoID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + } + + convertedRun, err := convert.ToActionWorkflowRun(ctx, repository, runs[i]) + if err != nil { + ctx.APIErrorInternal(err) + return + } + res.Entries[i] = convertedRun + } + + ctx.JSON(http.StatusOK, &res) +} diff --git a/routers/api/v1/shared/block.go b/routers/api/v1/shared/block.go index 490a48f81c..b22f8a74fd 100644 --- a/routers/api/v1/shared/block.go +++ b/routers/api/v1/shared/block.go @@ -21,12 +21,12 @@ func ListBlocks(ctx *context.APIContext, blocker *user_model.User) { BlockerID: blocker.ID, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindBlockings", err) + ctx.APIErrorInternal(err) return } if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) + ctx.APIErrorInternal(err) return } @@ -42,14 +42,14 @@ func ListBlocks(ctx *context.APIContext, blocker *user_model.User) { func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) { blockee, err := user_model.GetUserByName(ctx, ctx.PathParam("username")) if err != nil { - ctx.NotFound("GetUserByName", err) + ctx.APIErrorNotFound("GetUserByName", err) return } status := http.StatusNotFound blocking, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetBlocking", err) + ctx.APIErrorInternal(err) return } if blocking != nil { @@ -62,15 +62,15 @@ func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) { func BlockUser(ctx *context.APIContext, blocker *user_model.User) { blockee, err := user_model.GetUserByName(ctx, ctx.PathParam("username")) if err != nil { - ctx.NotFound("GetUserByName", err) + ctx.APIErrorNotFound("GetUserByName", err) return } if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, ctx.FormString("note")); err != nil { if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) { - ctx.Error(http.StatusBadRequest, "BlockUser", err) + ctx.APIError(http.StatusBadRequest, err) } else { - ctx.Error(http.StatusInternalServerError, "BlockUser", err) + ctx.APIErrorInternal(err) } return } @@ -81,15 +81,15 @@ func BlockUser(ctx *context.APIContext, blocker *user_model.User) { func UnblockUser(ctx *context.APIContext, doer, blocker *user_model.User) { blockee, err := user_model.GetUserByName(ctx, ctx.PathParam("username")) if err != nil { - ctx.NotFound("GetUserByName", err) + ctx.APIErrorNotFound("GetUserByName", err) return } if err := user_service.UnblockUser(ctx, doer, blocker, blockee); err != nil { if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) { - ctx.Error(http.StatusBadRequest, "UnblockUser", err) + ctx.APIError(http.StatusBadRequest, err) } else { - ctx.Error(http.StatusInternalServerError, "UnblockUser", err) + ctx.APIErrorInternal(err) } return } diff --git a/routers/api/v1/shared/runners.go b/routers/api/v1/shared/runners.go index f088e9a2d4..e9834aff9f 100644 --- a/routers/api/v1/shared/runners.go +++ b/routers/api/v1/shared/runners.go @@ -8,8 +8,13 @@ import ( "net/http" actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/setting" + 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" ) // RegistrationToken is response related to registration token @@ -24,9 +29,99 @@ func GetRegistrationToken(ctx *context.APIContext, ownerID, repoID int64) { token, err = actions_model.NewRunnerToken(ctx, ownerID, repoID) } if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, RegistrationToken{Token: token.Token}) } + +// ListRunners lists runners for api route validated ownerID and repoID +// ownerID == 0 and repoID == 0 means all runners including global runners, does not appear in sql where clause +// ownerID == 0 and repoID != 0 means all runners for the given repo +// ownerID != 0 and repoID == 0 means all runners for the given user/org +// ownerID != 0 and repoID != 0 undefined behavior +// Access rights are checked at the API route level +func ListRunners(ctx *context.APIContext, ownerID, repoID int64) { + if ownerID != 0 && repoID != 0 { + setting.PanicInDevOrTesting("ownerID and repoID should not be both set") + } + runners, total, err := db.FindAndCount[actions_model.ActionRunner](ctx, &actions_model.FindRunnerOptions{ + OwnerID: ownerID, + RepoID: repoID, + ListOptions: utils.GetListOptions(ctx), + }) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + res := new(api.ActionRunnersResponse) + res.TotalCount = total + + res.Entries = make([]*api.ActionRunner, len(runners)) + for i, runner := range runners { + res.Entries[i] = convert.ToActionRunner(ctx, runner) + } + + ctx.JSON(http.StatusOK, &res) +} + +func getRunnerByID(ctx *context.APIContext, ownerID, repoID, runnerID int64) (*actions_model.ActionRunner, bool) { + if ownerID != 0 && repoID != 0 { + setting.PanicInDevOrTesting("ownerID and repoID should not be both set") + } + + runner, err := actions_model.GetRunnerByID(ctx, runnerID) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.APIErrorNotFound("Runner not found") + } else { + ctx.APIErrorInternal(err) + } + return nil, false + } + + if !runner.EditableInContext(ownerID, repoID) { + ctx.APIErrorNotFound("No permission to access this runner") + return nil, false + } + return runner, true +} + +// GetRunner get the runner for api route validated ownerID and repoID +// ownerID == 0 and repoID == 0 means any runner including global runners +// ownerID == 0 and repoID != 0 means any runner for the given repo +// ownerID != 0 and repoID == 0 means any runner for the given user/org +// ownerID != 0 and repoID != 0 undefined behavior +// Access rights are checked at the API route level +func GetRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) { + if ownerID != 0 && repoID != 0 { + setting.PanicInDevOrTesting("ownerID and repoID should not be both set") + } + runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID) + if !ok { + return + } + ctx.JSON(http.StatusOK, convert.ToActionRunner(ctx, runner)) +} + +// DeleteRunner deletes the runner for api route validated ownerID and repoID +// ownerID == 0 and repoID == 0 means any runner including global runners +// ownerID == 0 and repoID != 0 means any runner for the given repo +// ownerID != 0 and repoID == 0 means any runner for the given user/org +// ownerID != 0 and repoID != 0 undefined behavior +// Access rights are checked at the API route level +func DeleteRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) { + runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID) + if !ok { + return + } + + err := actions_model.DeleteRunner(ctx, runner.ID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/swagger/action.go b/routers/api/v1/swagger/action.go index 665f4d0b85..16a250184a 100644 --- a/routers/api/v1/swagger/action.go +++ b/routers/api/v1/swagger/action.go @@ -32,3 +32,17 @@ type swaggerResponseVariableList struct { // in:body Body []api.ActionVariable `json:"body"` } + +// ActionWorkflow +// swagger:response ActionWorkflow +type swaggerResponseActionWorkflow struct { + // in:body + Body api.ActionWorkflow `json:"body"` +} + +// ActionWorkflowList +// swagger:response ActionWorkflowList +type swaggerResponseActionWorkflowList struct { + // in:body + Body []api.ActionWorkflow `json:"body"` +} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 125605d98f..bafd5e04a2 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -119,6 +119,9 @@ type swaggerParameterBodies struct { EditAttachmentOptions api.EditAttachmentOptions // in:body + GetFilesOptions api.GetFilesOptions + + // in:body ChangeFilesOptions api.ChangeFilesOptions // in:body @@ -209,5 +212,14 @@ type swaggerParameterBodies struct { CreateVariableOption api.CreateVariableOption // in:body + RenameOrgOption api.RenameOrgOption + + // in:body + CreateActionWorkflowDispatch api.CreateActionWorkflowDispatch + + // in:body UpdateVariableOption api.UpdateVariableOption + + // in:body + LockIssueOption api.LockIssueOption } diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index f754c80a5b..9e20c0533b 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -331,6 +331,12 @@ type swaggerContentsListResponse struct { Body []api.ContentsResponse `json:"body"` } +// swagger:response ContentsExtResponse +type swaggerContentsExtResponse struct { + // in:body + Body api.ContentsExtResponse `json:"body"` +} + // FileDeleteResponse // swagger:response FileDeleteResponse type swaggerFileDeleteResponse struct { @@ -443,6 +449,62 @@ type swaggerRepoTasksList struct { Body api.ActionTaskResponse `json:"body"` } +// WorkflowRunsList +// swagger:response WorkflowRunsList +type swaggerActionWorkflowRunsResponse struct { + // in:body + Body api.ActionWorkflowRunsResponse `json:"body"` +} + +// WorkflowRun +// swagger:response WorkflowRun +type swaggerWorkflowRun struct { + // in:body + Body api.ActionWorkflowRun `json:"body"` +} + +// WorkflowJobsList +// swagger:response WorkflowJobsList +type swaggerActionWorkflowJobsResponse struct { + // in:body + Body api.ActionWorkflowJobsResponse `json:"body"` +} + +// WorkflowJob +// swagger:response WorkflowJob +type swaggerWorkflowJob struct { + // in:body + Body api.ActionWorkflowJob `json:"body"` +} + +// ArtifactsList +// swagger:response ArtifactsList +type swaggerRepoArtifactsList struct { + // in:body + Body api.ActionArtifactsResponse `json:"body"` +} + +// Artifact +// swagger:response Artifact +type swaggerRepoArtifact struct { + // in:body + Body api.ActionArtifact `json:"body"` +} + +// RunnerList +// swagger:response RunnerList +type swaggerRunnerList struct { + // in:body + Body api.ActionRunnersResponse `json:"body"` +} + +// Runner +// swagger:response Runner +type swaggerRunner struct { + // in:body + Body api.ActionRunner `json:"body"` +} + // swagger:response Compare type swaggerCompare struct { // in:body diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index 22707196f4..e934d02aa7 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -12,6 +12,7 @@ import ( 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" @@ -49,14 +50,14 @@ func CreateOrUpdateSecret(ctx *context.APIContext) { opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption) - _, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname"), opt.Data) + _, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err) + ctx.APIError(http.StatusBadRequest, err) } else if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "CreateOrUpdateSecret", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err) + ctx.APIErrorInternal(err) } return } @@ -94,11 +95,11 @@ func DeleteSecret(ctx *context.APIContext) { err := secret_service.DeleteSecretByName(ctx, ctx.Doer.ID, 0, ctx.PathParam("secretname")) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.Error(http.StatusBadRequest, "DeleteSecret", err) + ctx.APIError(http.StatusBadRequest, err) } else if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "DeleteSecret", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "DeleteSecret", err) + ctx.APIErrorInternal(err) } return } @@ -127,13 +128,11 @@ func CreateVariable(ctx *context.APIContext) { // "$ref": "#/definitions/CreateVariableOption" // responses: // "201": - // description: response when creating a variable - // "204": - // description: response when creating a variable + // description: successfully created the user-level variable // "400": // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/notFound" + // "409": + // description: variable name already exists. opt := web.GetForm(ctx).(*api.CreateVariableOption) @@ -145,24 +144,24 @@ func CreateVariable(ctx *context.APIContext) { Name: variableName, }) if err != nil && !errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusInternalServerError, "GetVariable", err) + ctx.APIErrorInternal(err) return } if v != nil && v.ID > 0 { - ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) + ctx.APIError(http.StatusConflict, util.NewAlreadyExistErrorf("variable name %s already exists", variableName)) return } - if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil { + if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value, opt.Description); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.Error(http.StatusBadRequest, "CreateVariable", err) + ctx.APIError(http.StatusBadRequest, err) } else { - ctx.Error(http.StatusInternalServerError, "CreateVariable", err) + ctx.APIErrorInternal(err) } return } - ctx.Status(http.StatusNoContent) + ctx.Status(http.StatusCreated) } // UpdateVariable update a user-level variable which is created by current doer @@ -202,9 +201,9 @@ func UpdateVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "GetVariable", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "GetVariable", err) + ctx.APIErrorInternal(err) } return } @@ -212,11 +211,16 @@ func UpdateVariable(ctx *context.APIContext) { if opt.Name == "" { opt.Name = ctx.PathParam("variablename") } - if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil { + + v.Name = opt.Name + v.Data = opt.Value + v.Description = opt.Description + + if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.Error(http.StatusBadRequest, "UpdateVariable", err) + ctx.APIError(http.StatusBadRequest, err) } else { - ctx.Error(http.StatusInternalServerError, "UpdateVariable", err) + ctx.APIErrorInternal(err) } return } @@ -249,11 +253,11 @@ func DeleteVariable(ctx *context.APIContext) { if err := actions_service.DeleteVariableByName(ctx, ctx.Doer.ID, 0, ctx.PathParam("variablename")); err != nil { if errors.Is(err, util.ErrInvalidArgument) { - ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err) + ctx.APIError(http.StatusBadRequest, err) } else if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "DeleteVariableByName", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err) + ctx.APIErrorInternal(err) } return } @@ -288,18 +292,19 @@ func GetVariable(ctx *context.APIContext) { }) if err != nil { if errors.Is(err, util.ErrNotExist) { - ctx.Error(http.StatusNotFound, "GetVariable", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "GetVariable", err) + ctx.APIErrorInternal(err) } return } variable := &api.ActionVariable{ - OwnerID: v.OwnerID, - RepoID: v.RepoID, - Name: v.Name, - Data: v.Data, + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + Description: v.Description, } ctx.JSON(http.StatusOK, variable) @@ -334,20 +339,104 @@ func ListVariables(ctx *context.APIContext) { ListOptions: utils.GetListOptions(ctx), }) if err != nil { - ctx.Error(http.StatusInternalServerError, "FindVariables", err) + ctx.APIErrorInternal(err) return } variables := make([]*api.ActionVariable, len(vars)) for i, v := range vars { variables[i] = &api.ActionVariable{ - OwnerID: v.OwnerID, - RepoID: v.RepoID, - Name: v.Name, - Data: v.Data, + OwnerID: v.OwnerID, + RepoID: v.RepoID, + Name: v.Name, + Data: v.Data, + Description: v.Description, } } ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, variables) } + +// ListWorkflowRuns lists workflow runs +func ListWorkflowRuns(ctx *context.APIContext) { + // swagger:operation GET /user/actions/runs user getUserWorkflowRuns + // --- + // summary: Get workflow runs + // parameters: + // - name: event + // in: query + // description: workflow event name + // type: string + // required: false + // - name: branch + // in: query + // description: workflow branch + // type: string + // required: false + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: actor + // in: query + // description: triggered by user + // type: string + // required: false + // - name: head_sha + // in: query + // description: triggering sha of the workflow run + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/WorkflowRunsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.ListRuns(ctx, ctx.Doer.ID, 0) +} + +// ListWorkflowJobs lists workflow jobs +func ListWorkflowJobs(ctx *context.APIContext) { + // swagger:operation GET /user/actions/jobs user getUserWorkflowJobs + // --- + // summary: Get workflow jobs + // parameters: + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/WorkflowJobsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + shared.ListJobs(ctx, ctx.Doer.ID, 0, 0) +} diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index bfbc2ba622..6f1053e7ac 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -30,7 +30,7 @@ func ListAccessTokens(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of to user whose access tokens are to be listed // type: string // required: true // - name: page @@ -51,7 +51,7 @@ func ListAccessTokens(ctx *context.APIContext) { tokens, count, err := db.FindAndCount[auth_model.AccessToken](ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -62,6 +62,8 @@ func ListAccessTokens(ctx *context.APIContext) { Name: tokens[i].Name, TokenLastEight: tokens[i].TokenLastEight, Scopes: tokens[i].Scope.StringSlice(), + Created: tokens[i].CreatedUnix.AsTime(), + Updated: tokens[i].UpdatedUnix.AsTime(), } } @@ -81,7 +83,7 @@ func CreateAccessToken(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose token is to be created // required: true // type: string // - name: body @@ -105,27 +107,27 @@ func CreateAccessToken(ctx *context.APIContext) { exist, err := auth_model.AccessTokenByNameExists(ctx, t) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } if exist { - ctx.Error(http.StatusBadRequest, "AccessTokenByNameExists", errors.New("access token name has been used already")) + ctx.APIError(http.StatusBadRequest, errors.New("access token name has been used already")) return } scope, err := auth_model.AccessTokenScope(strings.Join(form.Scopes, ",")).Normalize() if err != nil { - ctx.Error(http.StatusBadRequest, "AccessTokenScope.Normalize", fmt.Errorf("invalid access token scope provided: %w", err)) + ctx.APIError(http.StatusBadRequest, fmt.Errorf("invalid access token scope provided: %w", err)) return } if scope == "" { - ctx.Error(http.StatusBadRequest, "AccessTokenScope", "access token must have a scope") + ctx.APIError(http.StatusBadRequest, "access token must have a scope") return } t.Scope = scope if err := auth_model.NewAccessToken(ctx, t); err != nil { - ctx.Error(http.StatusInternalServerError, "NewAccessToken", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusCreated, &api.AccessToken{ @@ -147,7 +149,7 @@ func DeleteAccessToken(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose token is to be deleted // type: string // required: true // - name: token @@ -174,31 +176,31 @@ func DeleteAccessToken(ctx *context.APIContext) { UserID: ctx.ContextUser.ID, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "ListAccessTokens", err) + ctx.APIErrorInternal(err) return } switch len(tokens) { case 0: - ctx.NotFound() + ctx.APIErrorNotFound() return case 1: tokenID = tokens[0].ID default: - ctx.Error(http.StatusUnprocessableEntity, "DeleteAccessTokenByID", fmt.Errorf("multiple matches for token name '%s'", token)) + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("multiple matches for token name '%s'", token)) return } } if tokenID == 0 { - ctx.Error(http.StatusInternalServerError, "Invalid TokenID", nil) + ctx.APIErrorInternal(nil) return } if err := auth_model.DeleteAccessTokenByID(ctx, tokenID, ctx.ContextUser.ID); err != nil { if auth_model.IsErrAccessTokenNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "DeleteAccessTokenByID", err) + ctx.APIErrorInternal(err) } return } @@ -235,12 +237,12 @@ func CreateOauth2Application(ctx *context.APIContext) { SkipSecondaryAuthorization: data.SkipSecondaryAuthorization, }) if err != nil { - ctx.Error(http.StatusBadRequest, "", "error creating oauth2 application") + ctx.APIError(http.StatusBadRequest, "error creating oauth2 application") return } secret, err := app.GenerateClientSecret(ctx) if err != nil { - ctx.Error(http.StatusBadRequest, "", "error creating application secret") + ctx.APIError(http.StatusBadRequest, "error creating application secret") return } app.ClientSecret = secret @@ -273,7 +275,7 @@ func ListOauth2Applications(ctx *context.APIContext) { OwnerID: ctx.Doer.ID, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "ListOAuth2Applications", err) + ctx.APIErrorInternal(err) return } @@ -309,9 +311,9 @@ func DeleteOauth2Application(ctx *context.APIContext) { appID := ctx.PathParamInt64("id") if err := auth_model.DeleteOAuth2Application(ctx, appID, ctx.Doer.ID); err != nil { if auth_model.IsErrOAuthApplicationNotFound(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "DeleteOauth2ApplicationByID", err) + ctx.APIErrorInternal(err) } return } @@ -342,14 +344,14 @@ func GetOauth2Application(ctx *context.APIContext) { app, err := auth_model.GetOAuth2ApplicationByID(ctx, appID) if err != nil { if auth_model.IsErrOauthClientIDInvalid(err) || auth_model.IsErrOAuthApplicationNotFound(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetOauth2ApplicationByID", err) + ctx.APIErrorInternal(err) } return } if app.UID != ctx.Doer.ID { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -396,15 +398,15 @@ func UpdateOauth2Application(ctx *context.APIContext) { }) if err != nil { if auth_model.IsErrOauthClientIDInvalid(err) || auth_model.IsErrOAuthApplicationNotFound(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "UpdateOauth2ApplicationByID", err) + ctx.APIErrorInternal(err) } return } app.ClientSecret, err = app.GenerateClientSecret(ctx) if err != nil { - ctx.Error(http.StatusBadRequest, "", "error updating application secret") + ctx.APIError(http.StatusBadRequest, "error updating application secret") return } diff --git a/routers/api/v1/user/avatar.go b/routers/api/v1/user/avatar.go index 30ccb63587..9c7bd57bc0 100644 --- a/routers/api/v1/user/avatar.go +++ b/routers/api/v1/user/avatar.go @@ -32,13 +32,13 @@ func UpdateAvatar(ctx *context.APIContext) { content, err := base64.StdEncoding.DecodeString(form.Image) if err != nil { - ctx.Error(http.StatusBadRequest, "DecodeImage", err) + ctx.APIError(http.StatusBadRequest, err) return } err = user_service.UploadAvatar(ctx, ctx.Doer, content) if err != nil { - ctx.Error(http.StatusInternalServerError, "UploadAvatar", err) + ctx.APIErrorInternal(err) return } @@ -57,7 +57,7 @@ func DeleteAvatar(ctx *context.APIContext) { // "$ref": "#/responses/empty" err := user_service.DeleteAvatar(ctx, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteAvatar", err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/user/block.go b/routers/api/v1/user/block.go index 7231e9add7..8365188f60 100644 --- a/routers/api/v1/user/block.go +++ b/routers/api/v1/user/block.go @@ -37,7 +37,7 @@ func CheckUserBlock(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: user to check + // description: username of the user to check // type: string // required: true // responses: @@ -56,7 +56,7 @@ func BlockUser(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: user to block + // description: username of the user to block // type: string // required: true // - name: note @@ -81,7 +81,7 @@ func UnblockUser(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: user to unblock + // description: username of the user to unblock // type: string // required: true // responses: diff --git a/routers/api/v1/user/email.go b/routers/api/v1/user/email.go index 33aa851a80..055e5ea419 100644 --- a/routers/api/v1/user/email.go +++ b/routers/api/v1/user/email.go @@ -29,7 +29,7 @@ func ListEmails(ctx *context.APIContext) { emails, err := user_model.GetEmailAddresses(ctx, ctx.Doer.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetEmailAddresses", err) + ctx.APIErrorInternal(err) return } apiEmails := make([]*api.Email, len(emails)) @@ -59,13 +59,13 @@ func AddEmail(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateEmailOption) if len(form.Emails) == 0 { - ctx.Error(http.StatusUnprocessableEntity, "", "Email list empty") + ctx.APIError(http.StatusUnprocessableEntity, "Email list empty") return } if err := user_service.AddEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil { if user_model.IsErrEmailAlreadyUsed(err) { - ctx.Error(http.StatusUnprocessableEntity, "", "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email) + ctx.APIError(http.StatusUnprocessableEntity, "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email) } else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) { email := "" if typedError, ok := err.(user_model.ErrEmailInvalid); ok { @@ -76,16 +76,16 @@ func AddEmail(ctx *context.APIContext) { } errMsg := fmt.Sprintf("Email address %q invalid", email) - ctx.Error(http.StatusUnprocessableEntity, "", errMsg) + ctx.APIError(http.StatusUnprocessableEntity, errMsg) } else { - ctx.Error(http.StatusInternalServerError, "AddEmailAddresses", err) + ctx.APIErrorInternal(err) } return } emails, err := user_model.GetEmailAddresses(ctx, ctx.Doer.ID) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetEmailAddresses", err) + ctx.APIErrorInternal(err) return } @@ -122,9 +122,9 @@ func DeleteEmail(ctx *context.APIContext) { if err := user_service.DeleteEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil { if user_model.IsErrEmailAddressNotExist(err) { - ctx.Error(http.StatusNotFound, "DeleteEmailAddresses", err) + ctx.APIError(http.StatusNotFound, err) } else { - ctx.Error(http.StatusInternalServerError, "DeleteEmailAddresses", err) + ctx.APIErrorInternal(err) } return } diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go index 8f46808f9e..339b994af4 100644 --- a/routers/api/v1/user/follower.go +++ b/routers/api/v1/user/follower.go @@ -26,7 +26,7 @@ func responseAPIUsers(ctx *context.APIContext, users []*user_model.User) { func listUserFollowers(ctx *context.APIContext, u *user_model.User) { users, count, err := user_model.GetUserFollowers(ctx, u, ctx.Doer, utils.GetListOptions(ctx)) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserFollowers", err) + ctx.APIErrorInternal(err) return } @@ -67,7 +67,7 @@ func ListFollowers(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose followers are to be listed // type: string // required: true // - name: page @@ -90,7 +90,7 @@ func ListFollowers(ctx *context.APIContext) { func listUserFollowing(ctx *context.APIContext, u *user_model.User) { users, count, err := user_model.GetUserFollowing(ctx, u, ctx.Doer, utils.GetListOptions(ctx)) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserFollowing", err) + ctx.APIErrorInternal(err) return } @@ -131,7 +131,7 @@ func ListFollowing(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose followed users are to be listed // type: string // required: true // - name: page @@ -155,7 +155,7 @@ func checkUserFollowing(ctx *context.APIContext, u *user_model.User, followID in if user_model.IsFollowing(ctx, u.ID, followID) { ctx.Status(http.StatusNoContent) } else { - ctx.NotFound() + ctx.APIErrorNotFound() } } @@ -167,7 +167,7 @@ func CheckMyFollowing(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of followed user + // description: username of the user to check for authenticated followers // type: string // required: true // responses: @@ -187,12 +187,12 @@ func CheckFollowing(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of following user + // description: username of the following user // type: string // required: true // - name: target // in: path - // description: username of followed user + // description: username of the followed user // type: string // required: true // responses: @@ -216,7 +216,7 @@ func Follow(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user to follow + // description: username of the user to follow // type: string // required: true // responses: @@ -229,9 +229,9 @@ func Follow(ctx *context.APIContext) { if err := user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.Error(http.StatusForbidden, "FollowUser", err) + ctx.APIError(http.StatusForbidden, err) } else { - ctx.Error(http.StatusInternalServerError, "FollowUser", err) + ctx.APIErrorInternal(err) } return } @@ -246,7 +246,7 @@ func Unfollow(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user to unfollow + // description: username of the user to unfollow // type: string // required: true // responses: @@ -256,7 +256,7 @@ func Unfollow(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if err := user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil { - ctx.Error(http.StatusInternalServerError, "UnfollowUser", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index ef667a1883..9ec4d2c938 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -4,7 +4,7 @@ package user import ( - "fmt" + "errors" "net/http" "strings" @@ -25,12 +25,12 @@ func listGPGKeys(ctx *context.APIContext, uid int64, listOptions db.ListOptions) OwnerID: uid, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "ListGPGKeys", err) + ctx.APIErrorInternal(err) return } if err := asymkey_model.GPGKeyList(keys).LoadSubKeys(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "ListGPGKeys", err) + ctx.APIErrorInternal(err) return } @@ -53,7 +53,7 @@ func ListGPGKeys(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose GPG key list is to be obtained // type: string // required: true // - name: page @@ -119,14 +119,14 @@ func GetGPGKey(ctx *context.APIContext) { key, err := asymkey_model.GetGPGKeyForUserByID(ctx, ctx.Doer.ID, ctx.PathParamInt64("id")) if err != nil { if asymkey_model.IsErrGPGKeyNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetGPGKeyByID", err) + ctx.APIErrorInternal(err) } return } if err := key.LoadSubKeys(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadSubKeys", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, convert.ToGPGKey(key)) @@ -135,7 +135,7 @@ func GetGPGKey(ctx *context.APIContext) { // CreateUserGPGKey creates new GPG key to given user by ID. func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) { if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { - ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) + ctx.APIErrorNotFound("Not Found", errors.New("gpg keys setting is not allowed to be visited")) return } @@ -194,7 +194,7 @@ func VerifyUserGPGKey(ctx *context.APIContext) { form.KeyID = strings.TrimLeft(form.KeyID, "0") if form.KeyID == "" { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -205,10 +205,10 @@ func VerifyUserGPGKey(ctx *context.APIContext) { if err != nil { if asymkey_model.IsErrGPGInvalidTokenSignature(err) { - ctx.Error(http.StatusUnprocessableEntity, "GPGInvalidSignature", fmt.Sprintf("The provided GPG key, signature and token do not match or token is out of date. Provide a valid signature for the token: %s", token)) + ctx.APIError(http.StatusUnprocessableEntity, "The provided GPG key, signature and token do not match or token is out of date. Provide a valid signature for the token: "+token) return } - ctx.Error(http.StatusInternalServerError, "VerifyUserGPGKey", err) + ctx.APIErrorInternal(err) } keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{ @@ -217,9 +217,9 @@ func VerifyUserGPGKey(ctx *context.APIContext) { }) if err != nil { if asymkey_model.IsErrGPGKeyNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetGPGKeysByKeyID", err) + ctx.APIErrorInternal(err) } return } @@ -276,15 +276,15 @@ func DeleteGPGKey(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) { - ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited")) + ctx.APIErrorNotFound("Not Found", errors.New("gpg keys setting is not allowed to be visited")) return } if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.PathParamInt64("id")); err != nil { if asymkey_model.IsErrGPGKeyAccessDenied(err) { - ctx.Error(http.StatusForbidden, "", "You do not have access to this key") + ctx.APIError(http.StatusForbidden, "You do not have access to this key") } else { - ctx.Error(http.StatusInternalServerError, "DeleteGPGKey", err) + ctx.APIErrorInternal(err) } return } @@ -296,16 +296,16 @@ func DeleteGPGKey(ctx *context.APIContext) { func HandleAddGPGKeyError(ctx *context.APIContext, err error, token string) { switch { case asymkey_model.IsErrGPGKeyAccessDenied(err): - ctx.Error(http.StatusUnprocessableEntity, "GPGKeyAccessDenied", "You do not have access to this GPG key") + ctx.APIError(http.StatusUnprocessableEntity, "You do not have access to this GPG key") case asymkey_model.IsErrGPGKeyIDAlreadyUsed(err): - ctx.Error(http.StatusUnprocessableEntity, "GPGKeyIDAlreadyUsed", "A key with the same id already exists") + ctx.APIError(http.StatusUnprocessableEntity, "A key with the same id already exists") case asymkey_model.IsErrGPGKeyParsing(err): - ctx.Error(http.StatusUnprocessableEntity, "GPGKeyParsing", err) + ctx.APIError(http.StatusUnprocessableEntity, err) case asymkey_model.IsErrGPGNoEmailFound(err): - ctx.Error(http.StatusNotFound, "GPGNoEmailFound", fmt.Sprintf("None of the emails attached to the GPG key could be found. It may still be added if you provide a valid signature for the token: %s", token)) + ctx.APIError(http.StatusNotFound, "None of the emails attached to the GPG key could be found. It may still be added if you provide a valid signature for the token: "+token) case asymkey_model.IsErrGPGInvalidTokenSignature(err): - ctx.Error(http.StatusUnprocessableEntity, "GPGInvalidSignature", fmt.Sprintf("The provided GPG key, signature and token do not match or token is out of date. Provide a valid signature for the token: %s", token)) + ctx.APIError(http.StatusUnprocessableEntity, "The provided GPG key, signature and token do not match or token is out of date. Provide a valid signature for the token: "+token) default: - ctx.Error(http.StatusInternalServerError, "AddGPGKey", err) + ctx.APIErrorInternal(err) } } diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go index 9a6f305700..f49bbbd6db 100644 --- a/routers/api/v1/user/helper.go +++ b/routers/api/v1/user/helper.go @@ -4,8 +4,6 @@ package user import ( - "net/http" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/services/context" ) @@ -20,10 +18,10 @@ func GetUserByPathParam(ctx *context.APIContext, name string) *user_model.User { if redirectUserID, err2 := user_model.LookupUserRedirect(ctx, username); err2 == nil { context.RedirectToUser(ctx.Base, username, redirectUserID) } else { - ctx.NotFound("GetUserByName", err) + ctx.APIErrorNotFound("GetUserByName", err) } } else { - ctx.Error(http.StatusInternalServerError, "GetUserByName", err) + ctx.APIErrorInternal(err) } return nil } diff --git a/routers/api/v1/user/hook.go b/routers/api/v1/user/hook.go index b4605c8a29..73c98ce746 100644 --- a/routers/api/v1/user/hook.go +++ b/routers/api/v1/user/hook.go @@ -63,13 +63,13 @@ func GetHook(ctx *context.APIContext) { } if !ctx.Doer.IsAdmin && hook.OwnerID != ctx.Doer.ID { - ctx.NotFound() + ctx.APIErrorNotFound() return } apiHook, err := webhook_service.ToHook(ctx.Doer.HomeLink(), hook) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, apiHook) diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go index 5a9125b4f3..aa69245e49 100644 --- a/routers/api/v1/user/key.go +++ b/routers/api/v1/user/key.go @@ -5,7 +5,7 @@ package user import ( std_ctx "context" - "fmt" + "errors" "net/http" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -24,9 +24,10 @@ import ( // appendPrivateInformation appends the owner and key type information to api.PublicKey func appendPrivateInformation(ctx std_ctx.Context, apiKey *api.PublicKey, key *asymkey_model.PublicKey, defaultUser *user_model.User) (*api.PublicKey, error) { - if key.Type == asymkey_model.KeyTypeDeploy { + switch key.Type { + case asymkey_model.KeyTypeDeploy: apiKey.KeyType = "deploy" - } else if key.Type == asymkey_model.KeyTypeUser { + case asymkey_model.KeyTypeUser: apiKey.KeyType = "user" if defaultUser.ID == key.OwnerID { @@ -38,7 +39,7 @@ func appendPrivateInformation(ctx std_ctx.Context, apiKey *api.PublicKey, key *a } apiKey.Owner = convert.ToUser(ctx, user, user) } - } else { + default: apiKey.KeyType = "unknown" } apiKey.ReadOnly = key.Mode == perm.AccessModeRead @@ -81,7 +82,7 @@ func listPublicKeys(ctx *context.APIContext, user *user_model.User) { } if err != nil { - ctx.Error(http.StatusInternalServerError, "ListPublicKeys", err) + ctx.APIErrorInternal(err) return } @@ -135,7 +136,7 @@ func ListPublicKeys(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose public keys are to be listed // type: string // required: true // - name: fingerprint @@ -182,9 +183,9 @@ func GetPublicKey(ctx *context.APIContext) { key, err := asymkey_model.GetPublicKeyByID(ctx, ctx.PathParamInt64("id")) if err != nil { if asymkey_model.IsErrKeyNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetPublicKeyByID", err) + ctx.APIErrorInternal(err) } return } @@ -200,7 +201,7 @@ func GetPublicKey(ctx *context.APIContext) { // CreateUserPublicKey creates new public key to given user by ID. func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid int64) { if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { - ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + ctx.APIErrorNotFound("Not Found", errors.New("ssh keys setting is not allowed to be visited")) return } @@ -270,7 +271,7 @@ func DeletePublicKey(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) { - ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited")) + ctx.APIErrorNotFound("Not Found", errors.New("ssh keys setting is not allowed to be visited")) return } @@ -278,23 +279,23 @@ func DeletePublicKey(ctx *context.APIContext) { externallyManaged, err := asymkey_model.PublicKeyIsExternallyManaged(ctx, id) if err != nil { if asymkey_model.IsErrKeyNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "PublicKeyIsExternallyManaged", err) + ctx.APIErrorInternal(err) } return } if externallyManaged { - ctx.Error(http.StatusForbidden, "", "SSH Key is externally managed for this user") + ctx.APIError(http.StatusForbidden, "SSH Key is externally managed for this user") return } if err := asymkey_service.DeletePublicKey(ctx, ctx.Doer, id); err != nil { if asymkey_model.IsErrKeyAccessDenied(err) { - ctx.Error(http.StatusForbidden, "", "You do not have access to this key") + ctx.APIError(http.StatusForbidden, "You do not have access to this key") } else { - ctx.Error(http.StatusInternalServerError, "DeletePublicKey", err) + ctx.APIErrorInternal(err) } return } diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go index 6111341423..6d0129681e 100644 --- a/routers/api/v1/user/repo.go +++ b/routers/api/v1/user/repo.go @@ -19,19 +19,19 @@ import ( func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) { opts := utils.GetListOptions(ctx) - repos, count, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{ + repos, count, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{ Actor: u, Private: private, ListOptions: opts, OrderBy: "id ASC", }) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserRepositories", err) + ctx.APIErrorInternal(err) return } if err := repos.LoadAttributes(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "RepositoryList.LoadAttributes", err) + ctx.APIErrorInternal(err) return } @@ -39,7 +39,7 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) { for i := range repos { permission, err := access_model.GetUserRepoPermission(ctx, repos[i], ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + ctx.APIErrorInternal(err) return } if ctx.IsSigned && ctx.Doer.IsAdmin || permission.HasAnyUnitAccess() { @@ -62,7 +62,7 @@ func ListUserRepos(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose owned repos are to be listed // type: string // required: true // - name: page @@ -103,7 +103,7 @@ func ListMyRepos(ctx *context.APIContext) { // "200": // "$ref": "#/responses/RepositoryList" - opts := &repo_model.SearchRepoOptions{ + opts := repo_model.SearchRepoOptions{ ListOptions: utils.GetListOptions(ctx), Actor: ctx.Doer, OwnerID: ctx.Doer.ID, @@ -113,19 +113,19 @@ func ListMyRepos(ctx *context.APIContext) { repos, count, err := repo_model.SearchRepository(ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "SearchRepository", err) + ctx.APIErrorInternal(err) return } results := make([]*api.Repository, len(repos)) for i, repo := range repos { if err = repo.LoadOwner(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadOwner", err) + ctx.APIErrorInternal(err) return } permission, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + ctx.APIErrorInternal(err) } results[i] = convert.ToRepo(ctx, repo, permission) } diff --git a/routers/api/v1/user/runners.go b/routers/api/v1/user/runners.go index 899218473e..be3f63cc5e 100644 --- a/routers/api/v1/user/runners.go +++ b/routers/api/v1/user/runners.go @@ -24,3 +24,81 @@ func GetRegistrationToken(ctx *context.APIContext) { shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0) } + +// CreateRegistrationToken returns the token to register user runners +func CreateRegistrationToken(ctx *context.APIContext) { + // swagger:operation POST /user/actions/runners/registration-token user userCreateRunnerRegistrationToken + // --- + // summary: Get an user's actions runner registration token + // produces: + // - application/json + // parameters: + // responses: + // "200": + // "$ref": "#/responses/RegistrationToken" + + shared.GetRegistrationToken(ctx, ctx.Doer.ID, 0) +} + +// ListRunners get user-level runners +func ListRunners(ctx *context.APIContext) { + // swagger:operation GET /user/actions/runners user getUserRunners + // --- + // summary: Get user-level runners + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/definitions/ActionRunnersResponse" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.ListRunners(ctx, ctx.Doer.ID, 0) +} + +// GetRunner get an user-level runner +func GetRunner(ctx *context.APIContext) { + // swagger:operation GET /user/actions/runners/{runner_id} user getUserRunner + // --- + // summary: Get an user-level runner + // produces: + // - application/json + // parameters: + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/definitions/ActionRunner" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.GetRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id")) +} + +// DeleteRunner delete an user-level runner +func DeleteRunner(ctx *context.APIContext) { + // swagger:operation DELETE /user/actions/runners/{runner_id} user deleteUserRunner + // --- + // summary: Delete an user-level runner + // produces: + // - application/json + // parameters: + // - name: runner_id + // in: path + // description: id of the runner + // type: string + // required: true + // responses: + // "204": + // description: runner has been deleted + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.DeleteRunner(ctx, ctx.Doer.ID, 0, ctx.PathParamInt64("runner_id")) +} diff --git a/routers/api/v1/user/settings.go b/routers/api/v1/user/settings.go index d0a8daaa85..d67c54b339 100644 --- a/routers/api/v1/user/settings.go +++ b/routers/api/v1/user/settings.go @@ -57,7 +57,7 @@ func UpdateUserSettings(ctx *context.APIContext) { KeepActivityPrivate: optional.FromPtr(form.HideActivity), } if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index ad9ed9548d..ee5d63063b 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -50,7 +50,7 @@ func GetStarredRepos(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose starred repos are to be listed // type: string // required: true // - name: page @@ -66,11 +66,13 @@ func GetStarredRepos(ctx *context.APIContext) { // "$ref": "#/responses/RepositoryList" // "404": // "$ref": "#/responses/notFound" + // "403": + // "$ref": "#/responses/forbidden" private := ctx.ContextUser.ID == ctx.Doer.ID repos, err := getStarredRepos(ctx, ctx.ContextUser, private) if err != nil { - ctx.Error(http.StatusInternalServerError, "getStarredRepos", err) + ctx.APIErrorInternal(err) return } @@ -97,10 +99,12 @@ func GetMyStarredRepos(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/RepositoryList" + // "403": + // "$ref": "#/responses/forbidden" repos, err := getStarredRepos(ctx, ctx.Doer, true) if err != nil { - ctx.Error(http.StatusInternalServerError, "getStarredRepos", err) + ctx.APIErrorInternal(err) } ctx.SetTotalCountHeader(int64(ctx.Doer.NumStars)) @@ -128,11 +132,13 @@ func IsStarring(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "404": // "$ref": "#/responses/notFound" + // "403": + // "$ref": "#/responses/forbidden" if repo_model.IsStaring(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) { ctx.Status(http.StatusNoContent) } else { - ctx.NotFound() + ctx.APIErrorNotFound() } } @@ -163,9 +169,9 @@ func Star(ctx *context.APIContext) { err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) if err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.Error(http.StatusForbidden, "BlockedUser", err) + ctx.APIError(http.StatusForbidden, err) } else { - ctx.Error(http.StatusInternalServerError, "StarRepo", err) + ctx.APIErrorInternal(err) } return } @@ -193,10 +199,12 @@ func Unstar(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "404": // "$ref": "#/responses/notFound" + // "403": + // "$ref": "#/responses/forbidden" err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) if err != nil { - ctx.Error(http.StatusInternalServerError, "StarRepo", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index 43dabe1b60..6de1125c40 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -73,7 +73,7 @@ func Search(ctx *context.APIContext) { if ctx.PublicOnly { visible = []structs.VisibleType{structs.VisibleTypePublic} } - users, maxResults, err = user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ + users, maxResults, err = user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Keyword: ctx.FormTrim("q"), UID: uid, @@ -110,7 +110,7 @@ func GetInfo(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user to get + // description: username of the user whose data is to be listed // type: string // required: true // responses: @@ -121,7 +121,7 @@ func GetInfo(ctx *context.APIContext) { if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) { // fake ErrUserNotExist error message to not leak information about existence - ctx.NotFound("GetUserByName", user_model.ErrUserNotExist{Name: ctx.PathParam("username")}) + ctx.APIErrorNotFound("GetUserByName", user_model.ErrUserNotExist{Name: ctx.PathParam("username")}) return } ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.ContextUser, ctx.Doer)) @@ -151,7 +151,7 @@ func GetUserHeatmapData(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user to get + // description: username of the user whose heatmap is to be obtained // type: string // required: true // responses: @@ -162,7 +162,7 @@ func GetUserHeatmapData(ctx *context.APIContext) { heatmap, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetUserHeatmapDataByUser", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, heatmap) @@ -177,7 +177,7 @@ func ListUserActivityFeeds(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose activity feeds are to be listed // type: string // required: true // - name: only-performed-by @@ -217,7 +217,7 @@ func ListUserActivityFeeds(ctx *context.APIContext) { feeds, count, err := feed_service.GetFeeds(ctx, opts) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetFeeds", err) + ctx.APIErrorInternal(err) return } ctx.SetTotalCountHeader(count) diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go index 2cc23ae476..844eac2c67 100644 --- a/routers/api/v1/user/watch.go +++ b/routers/api/v1/user/watch.go @@ -49,7 +49,7 @@ func GetWatchedRepos(ctx *context.APIContext) { // - name: username // type: string // in: path - // description: username of the user + // description: username of the user whose watched repos are to be listed // required: true // - name: page // in: query @@ -68,7 +68,7 @@ func GetWatchedRepos(ctx *context.APIContext) { private := ctx.ContextUser.ID == ctx.Doer.ID repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private) if err != nil { - ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err) + ctx.APIErrorInternal(err) } ctx.SetTotalCountHeader(total) @@ -97,7 +97,7 @@ func GetMyWatchedRepos(ctx *context.APIContext) { repos, total, err := getWatchedRepos(ctx, ctx.Doer, true) if err != nil { - ctx.Error(http.StatusInternalServerError, "getWatchedRepos", err) + ctx.APIErrorInternal(err) } ctx.SetTotalCountHeader(total) @@ -137,7 +137,7 @@ func IsWatching(ctx *context.APIContext) { RepositoryURL: ctx.Repo.Repository.APIURL(), }) } else { - ctx.NotFound() + ctx.APIErrorNotFound() } } @@ -168,9 +168,9 @@ func Watch(ctx *context.APIContext) { err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) if err != nil { if errors.Is(err, user_model.ErrBlockedUser) { - ctx.Error(http.StatusForbidden, "BlockedUser", err) + ctx.APIError(http.StatusForbidden, err) } else { - ctx.Error(http.StatusInternalServerError, "WatchRepo", err) + ctx.APIErrorInternal(err) } return } @@ -208,7 +208,7 @@ func Unwatch(ctx *context.APIContext) { err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) if err != nil { - ctx.Error(http.StatusInternalServerError, "UnwatchRepo", err) + ctx.APIErrorInternal(err) return } ctx.Status(http.StatusNoContent) diff --git a/routers/api/v1/utils/git.go b/routers/api/v1/utils/git.go index 4e25137817..1cfe01a639 100644 --- a/routers/api/v1/utils/git.go +++ b/routers/api/v1/utils/git.go @@ -4,53 +4,54 @@ package utils import ( - gocontext "context" - "fmt" - "net/http" + "errors" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" - "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/services/context" ) -// ResolveRefOrSha resolve ref to sha if exist -func ResolveRefOrSha(ctx *context.APIContext, ref string) string { - if len(ref) == 0 { - ctx.Error(http.StatusBadRequest, "ref not given", nil) - return "" - } +type RefCommit struct { + InputRef string + RefName git.RefName + Commit *git.Commit + CommitID string +} - sha := ref - // Search branches and tags - for _, refType := range []string{"heads", "tags"} { - refSHA, lastMethodName, err := searchRefCommitByType(ctx, refType, ref) - if err != nil { - ctx.Error(http.StatusInternalServerError, lastMethodName, err) - return "" - } - if refSHA != "" { - sha = refSHA - break - } +// ResolveRefCommit resolve ref to a commit if exist +func ResolveRefCommit(ctx reqctx.RequestContext, repo *repo_model.Repository, inputRef string, minCommitIDLen ...int) (_ *RefCommit, err error) { + gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) + if err != nil { + return nil, err } - - sha = MustConvertToSHA1(ctx, ctx.Repo, sha) - - if ctx.Repo.GitRepo != nil { - err := ctx.Repo.GitRepo.AddLastCommitCache(ctx.Repo.Repository.GetCommitsCountCacheKey(ref, ref != sha), ctx.Repo.Repository.FullName(), sha) - if err != nil { - log.Error("Unable to get commits count for %s in %s. Error: %v", sha, ctx.Repo.Repository.FullName(), err) - } + refCommit := RefCommit{InputRef: inputRef} + if gitrepo.IsBranchExist(ctx, repo, inputRef) { + refCommit.RefName = git.RefNameFromBranch(inputRef) + } else if gitrepo.IsTagExist(ctx, repo, inputRef) { + refCommit.RefName = git.RefNameFromTag(inputRef) + } else if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.ObjectFormatName), inputRef, minCommitIDLen...) { + refCommit.RefName = git.RefNameFromCommit(inputRef) + } + if refCommit.RefName == "" { + return nil, git.ErrNotExist{ID: inputRef} } + if refCommit.Commit, err = gitRepo.GetCommit(refCommit.RefName.String()); err != nil { + return nil, err + } + refCommit.CommitID = refCommit.Commit.ID.String() + return &refCommit, nil +} - return sha +func NewRefCommit(refName git.RefName, commit *git.Commit) *RefCommit { + return &RefCommit{InputRef: refName.ShortName(), RefName: refName, Commit: commit, CommitID: commit.ID.String()} } // GetGitRefs return git references based on filter func GetGitRefs(ctx *context.APIContext, filter string) ([]*git.Reference, string, error) { if ctx.Repo.GitRepo == nil { - return nil, "", fmt.Errorf("no open git repo found in context") + return nil, "", errors.New("no open git repo found in context") } if len(filter) > 0 { filter = "refs/" + filter @@ -58,42 +59,3 @@ func GetGitRefs(ctx *context.APIContext, filter string) ([]*git.Reference, strin refs, err := ctx.Repo.GitRepo.GetRefsFiltered(filter) return refs, "GetRefsFiltered", err } - -func searchRefCommitByType(ctx *context.APIContext, refType, filter string) (string, string, error) { - refs, lastMethodName, err := GetGitRefs(ctx, refType+"/"+filter) // Search by type - if err != nil { - return "", lastMethodName, err - } - if len(refs) > 0 { - return refs[0].Object.String(), "", nil // Return found SHA - } - return "", "", nil -} - -// ConvertToObjectID returns a full-length SHA1 from a potential ID string -func ConvertToObjectID(ctx gocontext.Context, repo *context.Repository, commitID string) (git.ObjectID, error) { - objectFormat := repo.GetObjectFormat() - if len(commitID) == objectFormat.FullLength() && objectFormat.IsValid(commitID) { - sha, err := git.NewIDFromString(commitID) - if err == nil { - return sha, nil - } - } - - gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo.Repository) - if err != nil { - return objectFormat.EmptyObjectID(), fmt.Errorf("RepositoryFromContextOrOpen: %w", err) - } - defer closer.Close() - - return gitRepo.ConvertToGitID(commitID) -} - -// MustConvertToSHA1 returns a full-length SHA1 string from a potential ID string, or returns origin input if it can't convert to SHA1 -func MustConvertToSHA1(ctx gocontext.Context, repo *context.Repository, commitID string) string { - sha, err := ConvertToObjectID(ctx, repo, commitID) - if err != nil { - return commitID - } - return sha.String() -} diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index 4328878e19..6f598f14c8 100644 --- a/routers/api/v1/utils/hook.go +++ b/routers/api/v1/utils/hook.go @@ -4,7 +4,6 @@ package utils import ( - "fmt" "net/http" "strconv" "strings" @@ -16,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" webhook_module "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/services/context" webhook_service "code.gitea.io/gitea/services/webhook" @@ -30,7 +30,7 @@ func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) { hooks, count, err := db.FindAndCount[webhook.Webhook](ctx, opts) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } @@ -38,7 +38,7 @@ func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) { for i, hook := range hooks { apiHooks[i], err = webhook_service.ToHook(owner.HomeLink(), hook) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } } @@ -52,9 +52,9 @@ func GetOwnerHook(ctx *context.APIContext, ownerID, hookID int64) (*webhook.Webh w, err := webhook.GetWebhookByOwnerID(ctx, ownerID, hookID) if err != nil { if webhook.IsErrWebhookNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetWebhookByOwnerID", err) + ctx.APIErrorInternal(err) } return nil, err } @@ -67,9 +67,9 @@ func GetRepoHook(ctx *context.APIContext, repoID, hookID int64) (*webhook.Webhoo w, err := webhook.GetWebhookByRepoID(ctx, repoID, hookID) if err != nil { if webhook.IsErrWebhookNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetWebhookByID", err) + ctx.APIErrorInternal(err) } return nil, err } @@ -80,17 +80,21 @@ func GetRepoHook(ctx *context.APIContext, repoID, hookID int64) (*webhook.Webhoo // write the appropriate error to `ctx`. Return whether the form is valid func checkCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption) bool { if !webhook_service.IsValidHookTaskType(form.Type) { - ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Invalid hook type: %s", form.Type)) + ctx.APIError(http.StatusUnprocessableEntity, "Invalid hook type: "+form.Type) return false } for _, name := range []string{"url", "content_type"} { if _, ok := form.Config[name]; !ok { - ctx.Error(http.StatusUnprocessableEntity, "", "Missing config option: "+name) + ctx.APIError(http.StatusUnprocessableEntity, "Missing config option: "+name) return false } } if !webhook.IsValidHookContentType(form.Config["content_type"]) { - ctx.Error(http.StatusUnprocessableEntity, "", "Invalid content type") + ctx.APIError(http.StatusUnprocessableEntity, "Invalid content type") + return false + } + if !validation.IsValidURL(form.Config["url"]) { + ctx.APIError(http.StatusUnprocessableEntity, "Invalid url") return false } return true @@ -102,7 +106,7 @@ func AddSystemHook(ctx *context.APIContext, form *api.CreateHookOption) { if ok { h, err := webhook_service.ToHook(setting.AppSubURL+"/-/admin", hook) if err != nil { - ctx.Error(http.StatusInternalServerError, "convert.ToHook", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusCreated, h) @@ -141,7 +145,7 @@ func AddRepoHook(ctx *context.APIContext, form *api.CreateHookOption) { func toAPIHook(ctx *context.APIContext, repoLink string, hook *webhook.Webhook) (*api.Hook, bool) { apiHook, err := webhook_service.ToHook(repoLink, hook) if err != nil { - ctx.Error(http.StatusInternalServerError, "ToHook", err) + ctx.APIErrorInternal(err) return nil, false } return apiHook, true @@ -155,6 +159,42 @@ func pullHook(events []string, event string) bool { return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventPullRequest), true) } +func updateHookEvents(events []string) webhook_module.HookEvents { + if len(events) == 0 { + events = []string{"push"} + } + hookEvents := make(webhook_module.HookEvents) + hookEvents[webhook_module.HookEventCreate] = util.SliceContainsString(events, string(webhook_module.HookEventCreate), true) + hookEvents[webhook_module.HookEventPush] = util.SliceContainsString(events, string(webhook_module.HookEventPush), true) + hookEvents[webhook_module.HookEventDelete] = util.SliceContainsString(events, string(webhook_module.HookEventDelete), true) + hookEvents[webhook_module.HookEventFork] = util.SliceContainsString(events, string(webhook_module.HookEventFork), true) + hookEvents[webhook_module.HookEventRepository] = util.SliceContainsString(events, string(webhook_module.HookEventRepository), true) + hookEvents[webhook_module.HookEventWiki] = util.SliceContainsString(events, string(webhook_module.HookEventWiki), true) + hookEvents[webhook_module.HookEventRelease] = util.SliceContainsString(events, string(webhook_module.HookEventRelease), true) + hookEvents[webhook_module.HookEventPackage] = util.SliceContainsString(events, string(webhook_module.HookEventPackage), true) + hookEvents[webhook_module.HookEventStatus] = util.SliceContainsString(events, string(webhook_module.HookEventStatus), true) + hookEvents[webhook_module.HookEventWorkflowRun] = util.SliceContainsString(events, string(webhook_module.HookEventWorkflowRun), true) + hookEvents[webhook_module.HookEventWorkflowJob] = util.SliceContainsString(events, string(webhook_module.HookEventWorkflowJob), true) + + // Issues + hookEvents[webhook_module.HookEventIssues] = issuesHook(events, "issues_only") + hookEvents[webhook_module.HookEventIssueAssign] = issuesHook(events, string(webhook_module.HookEventIssueAssign)) + hookEvents[webhook_module.HookEventIssueLabel] = issuesHook(events, string(webhook_module.HookEventIssueLabel)) + hookEvents[webhook_module.HookEventIssueMilestone] = issuesHook(events, string(webhook_module.HookEventIssueMilestone)) + hookEvents[webhook_module.HookEventIssueComment] = issuesHook(events, string(webhook_module.HookEventIssueComment)) + + // Pull requests + hookEvents[webhook_module.HookEventPullRequest] = pullHook(events, "pull_request_only") + hookEvents[webhook_module.HookEventPullRequestAssign] = pullHook(events, string(webhook_module.HookEventPullRequestAssign)) + hookEvents[webhook_module.HookEventPullRequestLabel] = pullHook(events, string(webhook_module.HookEventPullRequestLabel)) + hookEvents[webhook_module.HookEventPullRequestMilestone] = pullHook(events, string(webhook_module.HookEventPullRequestMilestone)) + hookEvents[webhook_module.HookEventPullRequestComment] = pullHook(events, string(webhook_module.HookEventPullRequestComment)) + hookEvents[webhook_module.HookEventPullRequestReview] = pullHook(events, "pull_request_review") + hookEvents[webhook_module.HookEventPullRequestReviewRequest] = pullHook(events, string(webhook_module.HookEventPullRequestReviewRequest)) + hookEvents[webhook_module.HookEventPullRequestSync] = pullHook(events, string(webhook_module.HookEventPullRequestSync)) + return hookEvents +} + // addHook add the hook specified by `form`, `ownerID` and `repoID`. If there is // an error, write to `ctx` accordingly. Return (webhook, ok) func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoID int64) (*webhook.Webhook, bool) { @@ -163,13 +203,10 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI return nil, false } - if len(form.Events) == 0 { - form.Events = []string{"push"} - } if form.Config["is_system_webhook"] != "" { sw, err := strconv.ParseBool(form.Config["is_system_webhook"]) if err != nil { - ctx.Error(http.StatusUnprocessableEntity, "", "Invalid is_system_webhook value") + ctx.APIError(http.StatusUnprocessableEntity, "Invalid is_system_webhook value") return nil, false } isSystemWebhook = sw @@ -184,28 +221,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI IsSystemWebhook: isSystemWebhook, HookEvent: &webhook_module.HookEvent{ ChooseEvents: true, - HookEvents: webhook_module.HookEvents{ - Create: util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true), - Delete: util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true), - Fork: util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true), - Issues: issuesHook(form.Events, "issues_only"), - IssueAssign: issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)), - IssueLabel: issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)), - IssueMilestone: issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)), - IssueComment: issuesHook(form.Events, string(webhook_module.HookEventIssueComment)), - Push: util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true), - PullRequest: pullHook(form.Events, "pull_request_only"), - PullRequestAssign: pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)), - PullRequestLabel: pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)), - PullRequestMilestone: pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)), - PullRequestComment: pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)), - PullRequestReview: pullHook(form.Events, "pull_request_review"), - PullRequestReviewRequest: pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)), - PullRequestSync: pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)), - Wiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true), - Repository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true), - Release: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true), - }, + HookEvents: updateHookEvents(form.Events), BranchFilter: form.BranchFilter, }, IsActive: form.Active, @@ -213,19 +229,19 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI } err := w.SetHeaderAuthorization(form.AuthorizationHeader) if err != nil { - ctx.Error(http.StatusInternalServerError, "SetHeaderAuthorization", err) + ctx.APIErrorInternal(err) return nil, false } if w.Type == webhook_module.SLACK { channel, ok := form.Config["channel"] if !ok { - ctx.Error(http.StatusUnprocessableEntity, "", "Missing config option: channel") + ctx.APIError(http.StatusUnprocessableEntity, "Missing config option: channel") return nil, false } channel = strings.TrimSpace(channel) if !webhook_service.IsValidSlackChannel(channel) { - ctx.Error(http.StatusBadRequest, "", "Invalid slack channel name") + ctx.APIError(http.StatusBadRequest, "Invalid slack channel name") return nil, false } @@ -236,17 +252,17 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI Color: form.Config["color"], }) if err != nil { - ctx.Error(http.StatusInternalServerError, "slack: JSON marshal failed", err) + ctx.APIErrorInternal(err) return nil, false } w.Meta = string(meta) } if err := w.UpdateEvent(); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateEvent", err) + ctx.APIErrorInternal(err) return nil, false } else if err := webhook.CreateWebhook(ctx, w); err != nil { - ctx.Error(http.StatusInternalServerError, "CreateWebhook", err) + ctx.APIErrorInternal(err) return nil, false } return w, true @@ -256,21 +272,21 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI func EditSystemHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64) { hook, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err) + ctx.APIErrorInternal(err) return } if !editHook(ctx, form, hook) { - ctx.Error(http.StatusInternalServerError, "editHook", err) + ctx.APIErrorInternal(err) return } updated, err := webhook.GetSystemOrDefaultWebhook(ctx, hookID) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err) + ctx.APIErrorInternal(err) return } h, err := webhook_service.ToHook(setting.AppURL+"/-/admin", updated) if err != nil { - ctx.Error(http.StatusInternalServerError, "convert.ToHook", err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, h) @@ -322,11 +338,15 @@ func EditRepoHook(ctx *context.APIContext, form *api.EditHookOption, hookID int6 func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webhook) bool { if form.Config != nil { if url, ok := form.Config["url"]; ok { + if !validation.IsValidURL(url) { + ctx.APIError(http.StatusUnprocessableEntity, "Invalid url") + return false + } w.URL = url } if ct, ok := form.Config["content_type"]; ok { if !webhook.IsValidHookContentType(ct) { - ctx.Error(http.StatusUnprocessableEntity, "", "Invalid content type") + ctx.APIError(http.StatusUnprocessableEntity, "Invalid content type") return false } w.ContentType = webhook.ToHookContentType(ct) @@ -341,7 +361,7 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh Color: form.Config["color"], }) if err != nil { - ctx.Error(http.StatusInternalServerError, "slack: JSON marshal failed", err) + ctx.APIErrorInternal(err) return false } w.Meta = string(meta) @@ -350,47 +370,20 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh } // Update events - if len(form.Events) == 0 { - form.Events = []string{"push"} - } + w.HookEvents = updateHookEvents(form.Events) w.PushOnly = false w.SendEverything = false w.ChooseEvents = true - w.Create = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true) - w.Push = util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true) - w.Create = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true) - w.Delete = util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true) - w.Fork = util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true) - w.Repository = util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true) - w.Wiki = util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true) - w.Release = util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true) w.BranchFilter = form.BranchFilter err := w.SetHeaderAuthorization(form.AuthorizationHeader) if err != nil { - ctx.Error(http.StatusInternalServerError, "SetHeaderAuthorization", err) + ctx.APIErrorInternal(err) return false } - // Issues - w.Issues = issuesHook(form.Events, "issues_only") - w.IssueAssign = issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)) - w.IssueLabel = issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)) - w.IssueMilestone = issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)) - w.IssueComment = issuesHook(form.Events, string(webhook_module.HookEventIssueComment)) - - // Pull requests - w.PullRequest = pullHook(form.Events, "pull_request_only") - w.PullRequestAssign = pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)) - w.PullRequestLabel = pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)) - w.PullRequestMilestone = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)) - w.PullRequestComment = pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)) - w.PullRequestReview = pullHook(form.Events, "pull_request_review") - w.PullRequestReviewRequest = pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)) - w.PullRequestSync = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)) - if err := w.UpdateEvent(); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateEvent", err) + ctx.APIErrorInternal(err) return false } @@ -399,7 +392,7 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh } if err := webhook.UpdateWebhook(ctx, w); err != nil { - ctx.Error(http.StatusInternalServerError, "UpdateWebhook", err) + ctx.APIErrorInternal(err) return false } return true @@ -409,9 +402,9 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh func DeleteOwnerHook(ctx *context.APIContext, owner *user_model.User, hookID int64) { if err := webhook.DeleteWebhookByOwnerID(ctx, owner.ID, hookID); err != nil { if webhook.IsErrWebhookNotExist(err) { - ctx.NotFound() + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "DeleteWebhookByOwnerID", err) + ctx.APIErrorInternal(err) } return } diff --git a/routers/api/v1/utils/hook_test.go b/routers/api/v1/utils/hook_test.go new file mode 100644 index 0000000000..e5e8ce07ce --- /dev/null +++ b/routers/api/v1/utils/hook_test.go @@ -0,0 +1,82 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package utils + +import ( + "net/http" + "testing" + + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/services/contexttest" + + "github.com/stretchr/testify/assert" +) + +func TestTestHookValidation(t *testing.T) { + unittest.PrepareTestEnv(t) + + t.Run("Test Validation", func(t *testing.T) { + ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks") + contexttest.LoadRepo(t, ctx, 1) + contexttest.LoadRepoCommit(t, ctx) + contexttest.LoadUser(t, ctx, 2) + + checkCreateHookOption(ctx, &structs.CreateHookOption{ + Type: "gitea", + Config: map[string]string{ + "content_type": "json", + "url": "https://example.com/webhook", + }, + }) + assert.Equal(t, 0, ctx.Resp.WrittenStatus()) // not written yet + }) + + t.Run("Test Validation with invalid URL", func(t *testing.T) { + ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks") + contexttest.LoadRepo(t, ctx, 1) + contexttest.LoadRepoCommit(t, ctx) + contexttest.LoadUser(t, ctx, 2) + + checkCreateHookOption(ctx, &structs.CreateHookOption{ + Type: "gitea", + Config: map[string]string{ + "content_type": "json", + "url": "example.com/webhook", + }, + }) + assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus()) + }) + + t.Run("Test Validation with invalid webhook type", func(t *testing.T) { + ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks") + contexttest.LoadRepo(t, ctx, 1) + contexttest.LoadRepoCommit(t, ctx) + contexttest.LoadUser(t, ctx, 2) + + checkCreateHookOption(ctx, &structs.CreateHookOption{ + Type: "unknown", + Config: map[string]string{ + "content_type": "json", + "url": "example.com/webhook", + }, + }) + assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus()) + }) + + t.Run("Test Validation with empty content type", func(t *testing.T) { + ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/hooks") + contexttest.LoadRepo(t, ctx, 1) + contexttest.LoadRepoCommit(t, ctx) + contexttest.LoadUser(t, ctx, 2) + + checkCreateHookOption(ctx, &structs.CreateHookOption{ + Type: "unknown", + Config: map[string]string{ + "url": "https://example.com/webhook", + }, + }) + assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus()) + }) +} diff --git a/routers/api/v1/utils/main_test.go b/routers/api/v1/utils/main_test.go new file mode 100644 index 0000000000..4eace1f369 --- /dev/null +++ b/routers/api/v1/utils/main_test.go @@ -0,0 +1,21 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package utils + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/setting" + webhook_service "code.gitea.io/gitea/services/webhook" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m, &unittest.TestOptions{ + SetUp: func() error { + setting.LoadQueueSettings() + return webhook_service.Init() + }, + }) +} |