diff options
Diffstat (limited to 'routers/api')
159 files changed, 6469 insertions, 4023 deletions
diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 0a7f92ac40..d71a6f487c 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -126,17 +126,16 @@ func ArtifactsRoutes(prefix string) *web.Router { func ArtifactContexter() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base, baseCleanUp := context.NewBaseContext(resp, req) - defer baseCleanUp() + base := context.NewBaseContext(resp, req) ctx := &ArtifactContext{Base: base} - ctx.AppendContextValue(artifactContextKey, ctx) + ctx.SetContextValue(artifactContextKey, ctx) // action task call server api with Bearer ACTIONS_RUNTIME_TOKEN // 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 } @@ -148,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 { @@ -163,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 } @@ -212,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 } @@ -251,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 } } @@ -262,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 } @@ -272,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 } @@ -286,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", @@ -308,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{ @@ -338,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 } @@ -403,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 } @@ -425,7 +428,7 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) { for _, artifact := range artifacts { var downloadURL string if setting.Actions.ArtifactStorage.ServeDirect() { - u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName, nil) + u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName, ctx.Req.Method, nil) if err != nil && !errors.Is(err, storage.ErrURLNotSupported) { log.Error("Error getting serve direct url: %v", err) } @@ -461,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 6dd36888d2..6d27479628 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"?> @@ -126,12 +126,9 @@ type artifactV4Routes struct { func ArtifactV4Contexter() func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - base, baseCleanUp := context.NewBaseContext(resp, req) - defer baseCleanUp() - + base := context.NewBaseContext(resp, req) ctx := &ArtifactContext{Base: base} - ctx.AppendContextValue(artifactContextKey, ctx) - + ctx.SetContextValue(artifactContextKey, ctx) next.ServeHTTP(ctx.Resp, ctx.Req) }) } @@ -165,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 } @@ -190,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 @@ -233,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 @@ -249,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") @@ -278,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 @@ -286,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 } @@ -312,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 } } @@ -344,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") @@ -392,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 } @@ -403,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 @@ -429,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 } @@ -451,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 } @@ -510,14 +505,19 @@ 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 } respData := GetSignedArtifactURLResponse{} if setting.Actions.ArtifactStorage.ServeDirect() { - u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath, nil) + u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath, ctx.Req.Method, nil) if u != nil && err == nil { respData.SignedUrl = u.String() } @@ -538,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 } @@ -562,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/main_test.go b/routers/api/actions/runner/main_test.go deleted file mode 100644 index 1e80a4f5ca..0000000000 --- a/routers/api/actions/runner/main_test.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package runner - -import ( - "testing" - - "code.gitea.io/gitea/models/unittest" -) - -func TestMain(m *testing.M) { - unittest.MainTest(m) -} diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go index 8f365cc926..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" @@ -69,7 +70,7 @@ func (s *Service) Register( labels := req.Msg.Labels // create new runner - name, _ := util.SplitStringAtByteN(req.Msg.Name, 255) + name := util.EllipsisDisplayString(req.Msg.Name, 255) runner := &actions_model.ActionRunner{ UUID: gouuid.New().String(), Name: name, @@ -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 539be8d889..0000000000 --- a/routers/api/actions/runner/utils.go +++ /dev/null @@ -1,217 +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" - "code.gitea.io/gitea/models/db" - secret_model "code.gitea.io/gitea/models/secret" - actions_module "code.gitea.io/gitea/modules/actions" - "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "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 { - event := map[string]any{} - _ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event) - - // TriggerEvent is added in https://github.com/go-gitea/gitea/pull/25229 - // This fallback is for the old ActionRun that doesn't have the TriggerEvent field - // and should be removed in 1.22 - eventName := t.Job.Run.TriggerEvent - if eventName == "" { - eventName = t.Job.Run.Event.Event() - } - - baseRef := "" - headRef := "" - ref := t.Job.Run.Ref - sha := t.Job.Run.CommitSHA - if pullPayload, err := t.Job.Run.GetPullRequestEventPayload(); err == nil && pullPayload.PullRequest != nil && pullPayload.PullRequest.Base != nil && pullPayload.PullRequest.Head != nil { - baseRef = pullPayload.PullRequest.Base.Ref - headRef = pullPayload.PullRequest.Head.Ref - - // if the TriggerEvent is pull_request_target, ref and sha need to be set according to the base of pull request - // In GitHub's documentation, ref should be the branch or tag that triggered workflow. But when the TriggerEvent is pull_request_target, - // the ref will be the base branch. - if t.Job.Run.TriggerEvent == actions_module.GithubEventPullRequestTarget { - ref = git.BranchPrefix + pullPayload.PullRequest.Base.Name - sha = pullPayload.PullRequest.Base.Sha - } - } - - refName := git.RefName(ref) - - giteaRuntimeToken, err := actions.CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID) - if err != nil { - log.Error("actions.CreateAuthorizationToken failed: %v", err) - } - - taskContext, err := structpb.NewStruct(map[string]any{ - // standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context - "action": "", // string, The name of the action currently running, or the id of a step. GitHub removes special characters, and uses the name __run when the current step runs a script without an id. If you use the same action more than once in the same job, the name will include a suffix with the sequence number with underscore before it. For example, the first script you run will have the name __run, and the second script will be named __run_2. Similarly, the second invocation of actions/checkout will be actionscheckout2. - "action_path": "", // string, The path where an action is located. This property is only supported in composite actions. You can use this path to access files located in the same repository as the action. - "action_ref": "", // string, For a step executing an action, this is the ref of the action being executed. For example, v2. - "action_repository": "", // string, For a step executing an action, this is the owner and repository name of the action. For example, actions/checkout. - "action_status": "", // string, For a composite action, the current result of the composite action. - "actor": t.Job.Run.TriggerUser.Name, // string, The username of the user that triggered the initial workflow run. If the workflow run is a re-run, this value may differ from github.triggering_actor. Any workflow re-runs will use the privileges of github.actor, even if the actor initiating the re-run (github.triggering_actor) has different privileges. - "api_url": setting.AppURL + "api/v1", // string, The URL of the GitHub REST API. - "base_ref": baseRef, // string, The base_ref or target branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. - "env": "", // string, Path on the runner to the file that sets environment variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." - "event": event, // object, The full event webhook payload. You can access individual properties of the event using this context. This object is identical to the webhook payload of the event that triggered the workflow run, and is different for each event. The webhooks for each GitHub Actions event is linked in "Events that trigger workflows." For example, for a workflow run triggered by the push event, this object contains the contents of the push webhook payload. - "event_name": eventName, // string, The name of the event that triggered the workflow run. - "event_path": "", // string, The path to the file on the runner that contains the full event webhook payload. - "graphql_url": "", // string, The URL of the GitHub GraphQL API. - "head_ref": headRef, // string, The head_ref or source branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. - "job": fmt.Sprint(t.JobID), // string, The job_id of the current job. - "ref": ref, // string, The fully-formed ref of the branch or tag that triggered the workflow run. For workflows triggered by push, this is the branch or tag ref that was pushed. For workflows triggered by pull_request, this is the pull request merge branch. For workflows triggered by release, this is the release tag created. For other triggers, this is the branch or tag ref that triggered the workflow run. This is only set if a branch or tag is available for the event type. The ref given is fully-formed, meaning that for branches the format is refs/heads/<branch_name>, for pull requests it is refs/pull/<pr_number>/merge, and for tags it is refs/tags/<tag_name>. For example, refs/heads/feature-branch-1. - "ref_name": refName.ShortName(), // string, The short ref name of the branch or tag that triggered the workflow run. This value matches the branch or tag name shown on GitHub. For example, feature-branch-1. - "ref_protected": false, // boolean, true if branch protections are configured for the ref that triggered the workflow run. - "ref_type": refName.RefType(), // string, The type of ref that triggered the workflow run. Valid values are branch or tag. - "path": "", // string, Path on the runner to the file that sets system PATH variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." - "repository": t.Job.Run.Repo.OwnerName + "/" + t.Job.Run.Repo.Name, // string, The owner and repository name. For example, Codertocat/Hello-World. - "repository_owner": t.Job.Run.Repo.OwnerName, // string, The repository owner's name. For example, Codertocat. - "repositoryUrl": t.Job.Run.Repo.HTMLURL(), // string, The Git URL to the repository. For example, git://github.com/codertocat/hello-world.git. - "retention_days": "", // string, The number of days that workflow run logs and artifacts are kept. - "run_id": fmt.Sprint(t.Job.RunID), // string, A unique number for each workflow run within a repository. This number does not change if you re-run the workflow run. - "run_number": fmt.Sprint(t.Job.Run.Index), // string, A unique number for each run of a particular workflow in a repository. This number begins at 1 for the workflow's first run, and increments with each new run. This number does not change if you re-run the workflow run. - "run_attempt": fmt.Sprint(t.Job.Attempt), // string, A unique number for each attempt of a particular workflow run in a repository. This number begins at 1 for the workflow run's first attempt, and increments with each re-run. - "secret_source": "Actions", // string, The source of a secret used in a workflow. Possible values are None, Actions, Dependabot, or Codespaces. - "server_url": setting.AppURL, // string, The URL of the GitHub server. For example: https://github.com. - "sha": sha, // string, The commit SHA that triggered the workflow. The value of this commit SHA depends on the event that triggered the workflow. For more information, see "Events that trigger workflows." For example, ffac537e6cbbf934b08745a378932722df287a53. - "token": t.Token, // string, A token to authenticate on behalf of the GitHub App installed on your repository. This is functionally equivalent to the GITHUB_TOKEN secret. For more information, see "Automatic token authentication." - "triggering_actor": "", // string, The username of the user that initiated the workflow run. If the workflow run is a re-run, this value may differ from github.actor. Any workflow re-runs will use the privileges of github.actor, even if the actor initiating the re-run (github.triggering_actor) has different privileges. - "workflow": t.Job.Run.WorkflowID, // string, The name of the workflow. If the workflow file doesn't specify a name, the value of this property is the full path of the workflow file in the repository. - "workspace": "", // string, The default working directory on the runner for steps, and the default location of your repository when using the checkout action. - - // additional contexts - "gitea_default_actions_url": setting.Actions.DefaultActionsURL.URL(), - "gitea_runtime_token": giteaRuntimeToken, - }) - 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("LoadAttributes: %w", err) - } - if len(task.Job.Needs) == 0 { - return nil, nil - } - needs := container.SetOf(task.Job.Needs...) - - jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: task.Job.RunID}) - if err != nil { - return nil, fmt.Errorf("FindRunJobs: %w", err) - } - - jobIDJobs := make(map[string][]*actions_model.ActionRunJob) - for _, job := range jobs { - jobIDJobs[job.JobID] = append(jobIDJobs[job.JobID], job) - } - - ret := make(map[string]*runnerv1.TaskNeed, len(needs)) - for jobID, jobsWithSameID := range jobIDJobs { - if !needs.Contains(jobID) { - continue - } - var jobOutputs map[string]string - for _, job := range jobsWithSameID { - if job.TaskID == 0 || !job.Status.IsDone() { - // it shouldn't happen, or the job has been rerun - continue - } - got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID) - if err != nil { - return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err) - } - outputs := make(map[string]string, len(got)) - for _, v := range got { - outputs[v.OutputKey] = v.OutputValue - } - if len(jobOutputs) == 0 { - jobOutputs = outputs - } else { - jobOutputs = mergeTwoOutputs(outputs, jobOutputs) - } - } - ret[jobID] = &runnerv1.TaskNeed{ - Outputs: jobOutputs, - Result: runnerv1.Result(actions_model.AggregateJobStatus(jobsWithSameID)), - } - } - - return ret, nil -} - -// mergeTwoOutputs merges two outputs from two different ActionRunJobs -// Values with the same output name may be overridden. The user should ensure the output names are unique. -// See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#using-job-outputs-in-a-matrix-job -func mergeTwoOutputs(o1, o2 map[string]string) map[string]string { - ret := make(map[string]string, len(o1)) - for k1, v1 := range o1 { - if len(v1) > 0 { - ret[k1] = v1 - } else { - ret[k1] = o2[k1] - } - } - return ret -} diff --git a/routers/api/actions/runner/utils_test.go b/routers/api/actions/runner/utils_test.go deleted file mode 100644 index d7a6f84550..0000000000 --- a/routers/api/actions/runner/utils_test.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package runner - -import ( - "context" - "testing" - - actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/models/unittest" - - "github.com/stretchr/testify/assert" -) - -func Test_findTaskNeeds(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 51}) - - ret, err := findTaskNeeds(context.Background(), task) - assert.NoError(t, err) - assert.Len(t, ret, 1) - assert.Contains(t, ret, "job1") - assert.Len(t, ret["job1"].Outputs, 2) - assert.Equal(t, "abc", ret["job1"].Outputs["output_a"]) - assert.Equal(t, "bbb", ret["job1"].Outputs["output_b"]) -} diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go index 4b652c9ecc..f250a1a549 100644 --- a/routers/api/packages/alpine/alpine.go +++ b/routers/api/packages/alpine/alpine.go @@ -25,9 +25,8 @@ import ( ) func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.PlainText(status, message) - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.PlainText(status, message) } func GetRepositoryKey(ctx *context.Context) { @@ -68,13 +67,14 @@ 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{ Filename: alpine_service.IndexArchiveFilename, CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.PathParam("branch"), ctx.PathParam("repository"), ctx.PathParam("architecture")), }, + ctx.Req.Method, ) if err != nil { if errors.Is(err, util.ErrNotExist) { @@ -114,7 +114,7 @@ func UploadPackageFile(ctx *context.Context) { pck, err := alpine_module.ParsePackage(buf) if err != nil { - if errors.Is(err, util.ErrInvalidArgument) || err == io.EOF { + if errors.Is(err, util.ErrInvalidArgument) || errors.Is(err, io.EOF) { apiError(ctx, http.StatusBadRequest, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -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], ctx.Req.Method) 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 47ea7137b8..f6ee5958b5 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, "Failed to authenticate user") return } ctx.IsSigned = ctx.Doer != nil @@ -138,45 +137,11 @@ func CommonRoutes() *web.Router { }, reqPackageAccess(perm.AccessModeRead)) r.Group("/arch", func() { r.Methods("HEAD,GET", "/repository.key", arch.GetRepositoryKey) - - r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) { - path := strings.Trim(ctx.PathParam("*"), "/") - - if ctx.Req.Method == "PUT" { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - ctx.SetPathParam("repository", path) - arch.UploadPackageFile(ctx) - return - } - - pathFields := strings.Split(path, "/") - pathFieldsLen := len(pathFields) - - if (ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET") && pathFieldsLen >= 2 { - ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-2], "/")) - ctx.SetPathParam("architecture", pathFields[pathFieldsLen-2]) - ctx.SetPathParam("filename", pathFields[pathFieldsLen-1]) - arch.GetPackageOrRepositoryFile(ctx) - return - } - - if ctx.Req.Method == "DELETE" && pathFieldsLen >= 3 { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-3], "/")) - ctx.SetPathParam("name", pathFields[pathFieldsLen-3]) - ctx.SetPathParam("version", pathFields[pathFieldsLen-2]) - ctx.SetPathParam("architecture", pathFields[pathFieldsLen-1]) - arch.DeletePackageVersion(ctx) - return - } - - ctx.Status(http.StatusNotFound) + r.Methods("PUT", "" /* no repository */, reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile) + r.PathGroup("/*", func(g *web.RouterPathGroup) { + g.MatchPath("PUT", "/<repository:*>", reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile) + g.MatchPath("HEAD,GET", "/<repository:*>/<architecture>/<filename>", arch.GetPackageOrRepositoryFile) + g.MatchPath("DELETE", "/<repository:*>/<name>/<version>/<architecture>", reqPackageAccess(perm.AccessModeWrite), arch.DeletePackageVersion) }) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/cargo", func() { @@ -315,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() { @@ -391,67 +324,22 @@ 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() { r.Group("/{packagename}/{packageversion}", func() { r.Delete("", reqPackageAccess(perm.AccessModeWrite), generic.DeletePackage) r.Group("/{filename}", func() { - r.Get("", generic.DownloadPackageFile) + r.Methods("HEAD,GET", "", generic.DownloadPackageFile) r.Group("", func() { r.Put("", generic.UploadPackage) r.Delete("", generic.DeletePackageFile) @@ -565,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) @@ -654,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) @@ -665,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) @@ -726,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) @@ -733,150 +549,24 @@ func ContainerRoutes() *web.Router { }) r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList) r.Group("/{username}", func() { - r.Group("/{image}", func() { - r.Group("/blobs/uploads", func() { - r.Post("", container.InitiateUploadBlob) - r.Group("/{uuid}", func() { - r.Get("", container.GetUploadBlob) - r.Patch("", container.UploadBlob) - r.Put("", container.EndUploadBlob) - r.Delete("", container.CancelUploadBlob) - }) - }, reqPackageAccess(perm.AccessModeWrite)) - r.Group("/blobs/{digest}", func() { - r.Head("", container.HeadBlob) - r.Get("", container.GetBlob) - r.Delete("", reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob) - }) - r.Group("/manifests/{reference}", func() { - r.Put("", reqPackageAccess(perm.AccessModeWrite), container.UploadManifest) - r.Head("", container.HeadManifest) - r.Get("", container.GetManifest) - r.Delete("", reqPackageAccess(perm.AccessModeWrite), container.DeleteManifest) - }) - r.Get("/tags/list", container.GetTagList) - }, container.VerifyImageName) - - var ( - blobsUploadsPattern = regexp.MustCompile(`\A(.+)/blobs/uploads/([a-zA-Z0-9-_.=]+)\z`) - blobsPattern = regexp.MustCompile(`\A(.+)/blobs/([^/]+)\z`) - manifestsPattern = regexp.MustCompile(`\A(.+)/manifests/([^/]+)\z`) - ) - - // Manual mapping of routes because {image} can contain slashes which chi does not support - r.Methods("HEAD,GET,POST,PUT,PATCH,DELETE", "/*", func(ctx *context.Context) { - path := ctx.PathParam("*") - isHead := ctx.Req.Method == "HEAD" - isGet := ctx.Req.Method == "GET" - isPost := ctx.Req.Method == "POST" - isPut := ctx.Req.Method == "PUT" - isPatch := ctx.Req.Method == "PATCH" - isDelete := ctx.Req.Method == "DELETE" - - if isPost && strings.HasSuffix(path, "/blobs/uploads") { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - - ctx.SetPathParam("image", path[:len(path)-14]) - container.VerifyImageName(ctx) - if ctx.Written() { - return - } - - container.InitiateUploadBlob(ctx) - return - } - if isGet && strings.HasSuffix(path, "/tags/list") { - ctx.SetPathParam("image", path[:len(path)-10]) - container.VerifyImageName(ctx) - if ctx.Written() { - return - } - - container.GetTagList(ctx) - return - } - - m := blobsUploadsPattern.FindStringSubmatch(path) - if len(m) == 3 && (isGet || isPut || isPatch || isDelete) { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - - ctx.SetPathParam("image", m[1]) - container.VerifyImageName(ctx) - if ctx.Written() { - return - } - - ctx.SetPathParam("uuid", m[2]) - - if isGet { - container.GetUploadBlob(ctx) - } else if isPatch { - container.UploadBlob(ctx) - } else if isPut { - container.EndUploadBlob(ctx) - } else { - container.CancelUploadBlob(ctx) - } - return - } - m = blobsPattern.FindStringSubmatch(path) - if len(m) == 3 && (isHead || isGet || isDelete) { - ctx.SetPathParam("image", m[1]) - container.VerifyImageName(ctx) - if ctx.Written() { - return - } - - ctx.SetPathParam("digest", m[2]) - - if isHead { - container.HeadBlob(ctx) - } else if isGet { - container.GetBlob(ctx) - } else { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - container.DeleteBlob(ctx) - } - return - } - m = manifestsPattern.FindStringSubmatch(path) - if len(m) == 3 && (isHead || isGet || isPut || isDelete) { - ctx.SetPathParam("image", m[1]) - container.VerifyImageName(ctx) - if ctx.Written() { - return - } - - ctx.SetPathParam("reference", m[2]) - - if isHead { - container.HeadManifest(ctx) - } else if isGet { - container.GetManifest(ctx) - } else { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - if isPut { - container.UploadManifest(ctx) - } else { - container.DeleteManifest(ctx) - } - } - return - } - - ctx.Status(http.StatusNotFound) + r.PathGroup("/*", func(g *web.RouterPathGroup) { + 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.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 573e93cfb0..061484785d 100644 --- a/routers/api/packages/arch/arch.go +++ b/routers/api/packages/arch/arch.go @@ -24,9 +24,8 @@ import ( ) func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.PlainText(status, message) - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.PlainText(status, message) } func GetRepositoryKey(ctx *context.Context) { @@ -62,7 +61,7 @@ func UploadPackageFile(ctx *context.Context) { pck, err := arch_module.ParsePackage(buf) if err != nil { - if errors.Is(err, util.ErrInvalidArgument) || err == io.EOF { + if errors.Is(err, util.ErrInvalidArgument) || errors.Is(err, io.EOF) { apiError(ctx, http.StatusBadRequest, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -239,7 +238,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], ctx.Req.Method) 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 3d8407e6b6..a7f00ee1cb 100644 --- a/routers/api/packages/cargo/cargo.go +++ b/routers/api/packages/cargo/cargo.go @@ -37,21 +37,20 @@ type StatusMessage struct { } func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.JSON(status, StatusResponse{ - OK: false, - Errors: []StatusMessage{ - { - Message: message, - }, + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.JSON(status, StatusResponse{ + OK: false, + Errors: []StatusMessage{ + { + Message: message, }, - }) + }, }) } // 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 +94,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 +164,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, @@ -179,9 +175,10 @@ func DownloadPackageFile(ctx *context.Context) { &packages_service.PackageFileInfo{ Filename: strings.ToLower(fmt.Sprintf("%s-%s.crate", ctx.PathParam("package"), ctx.PathParam("version"))), }, + ctx.Req.Method, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -276,7 +273,7 @@ func UnyankPackage(ctx *context.Context) { func yankPackage(ctx *context.Context, yank bool) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeCargo, ctx.PathParam("package"), ctx.PathParam("version")) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } 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 b3cdf12697..50011ab0b1 100644 --- a/routers/api/packages/chef/chef.go +++ b/routers/api/packages/chef/chef.go @@ -30,10 +30,9 @@ func apiError(ctx *context.Context, status int, obj any) { ErrorMessages []string `json:"error_messages"` } - helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.JSON(status, Error{ - ErrorMessages: []string{message}, - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.JSON(status, Error{ + ErrorMessages: []string{message}, }) } @@ -216,7 +215,7 @@ func PackageVersionMetadata(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -327,7 +326,7 @@ func UploadPackage(ctx *context.Context) { func DownloadPackage(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeChef, ctx.PathParam("name"), ctx.PathParam("version")) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -343,7 +342,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, ctx.Req.Method) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -368,7 +367,7 @@ func DeletePackageVersion(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) 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 40f72f6484..df04f49d2d 100644 --- a/routers/api/packages/composer/composer.go +++ b/routers/api/packages/composer/composer.go @@ -28,18 +28,17 @@ import ( ) func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - type Error struct { - Status int `json:"status"` - Message string `json:"message"` - } - ctx.JSON(status, struct { - Errors []Error `json:"errors"` - }{ - Errors: []Error{ - {Status: status, Message: message}, - }, - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + type Error struct { + Status int `json:"status"` + Message string `json:"message"` + } + ctx.JSON(status, struct { + Errors []Error `json:"errors"` + }{ + Errors: []Error{ + {Status: status, Message: message}, + }, }) } @@ -53,10 +52,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 +159,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, @@ -174,9 +170,10 @@ func DownloadPackageFile(ctx *context.Context) { &packages_service.PackageFileInfo{ Filename: ctx.PathParam("filename"), }, + ctx.Req.Method, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/conan/auth.go b/routers/api/packages/conan/auth.go index 9c03d01391..bce3235a2e 100644 --- a/routers/api/packages/conan/auth.go +++ b/routers/api/packages/conan/auth.go @@ -34,7 +34,6 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS u, err := user_model.GetUserByID(req.Context(), packageMeta.UserID) if err != nil { - log.Error("GetUserByID: %v", err) return nil, err } if packageMeta.Scope != "" { diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go index 4a9f0a3ffc..126b1593cd 100644 --- a/routers/api/packages/conan/conan.go +++ b/routers/api/packages/conan/conan.go @@ -5,6 +5,7 @@ package conan import ( std_ctx "context" + "errors" "fmt" "io" "net/http" @@ -54,16 +55,13 @@ func jsonResponse(ctx *context.Context, status int, obj any) { // https://github.com/conan-io/conan/issues/6613 ctx.Resp.Header().Set("Content-Type", "application/json") ctx.Status(status) - if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil { - log.Error("JSON encode: %v", err) - } + _ = json.NewEncoder(ctx.Resp).Encode(obj) } func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - jsonResponse(ctx, status, map[string]string{ - "message": message, - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + jsonResponse(ctx, status, map[string]string{ + "message": message, }) } @@ -183,7 +181,7 @@ func serveSnapshot(ctx *context.Context, fileKey string) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -244,7 +242,7 @@ func serveDownloadURLs(ctx *context.Context, fileKey, downloadURL string) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -392,7 +390,6 @@ func uploadFile(ctx *context.Context, fileFilter container.Set[string], fileKey if isConanfileFile { metadata, err := conan_module.ParseConanfile(buf) if err != nil { - log.Error("Error parsing package metadata: %v", err) apiError(ctx, http.StatusInternalServerError, err) return } @@ -418,7 +415,6 @@ func uploadFile(ctx *context.Context, fileFilter container.Set[string], fileKey } else { info, err := conan_module.ParseConaninfo(buf) if err != nil { - log.Error("Error parsing conan info: %v", err) apiError(ctx, http.StatusInternalServerError, err) return } @@ -479,7 +475,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, @@ -491,9 +487,10 @@ func downloadFile(ctx *context.Context, fileFilter container.Set[string], fileKe Filename: filename, CompositeKey: fileKey, }, + ctx.Req.Method, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -509,7 +506,7 @@ func DeleteRecipeV1(ctx *context.Context) { rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) if err := deleteRecipeOrPackage(ctx, rref, true, nil, false); err != nil { - if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -524,7 +521,7 @@ func DeleteRecipeV2(ctx *context.Context) { rref := ctx.Data[recipeReferenceKey].(*conan_module.RecipeReference) if err := deleteRecipeOrPackage(ctx, rref, rref.Revision == "", nil, false); err != nil { - if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -571,7 +568,7 @@ func DeletePackageV1(ctx *context.Context) { for _, reference := range references { pref, _ := conan_module.NewPackageReference(currentRref, reference.Value, conan_module.DefaultRevision) if err := deleteRecipeOrPackage(ctx, currentRref, true, pref, true); err != nil { - if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -590,7 +587,7 @@ func DeletePackageV2(ctx *context.Context) { if pref != nil { // has package reference if err := deleteRecipeOrPackage(ctx, rref, false, pref, pref.Revision == ""); err != nil { - if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -615,7 +612,7 @@ func DeletePackageV2(ctx *context.Context) { pref, _ := conan_module.NewPackageReference(rref, reference.Value, conan_module.DefaultRevision) if err := deleteRecipeOrPackage(ctx, rref, false, pref, true); err != nil { - if err == packages_model.ErrPackageNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -749,7 +746,7 @@ func LatestRecipeRevision(ctx *context.Context) { revision, err := conan_model.GetLastRecipeRevision(ctx, ctx.Package.Owner.ID, rref) if err != nil { - if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, conan_model.ErrRecipeReferenceNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -766,7 +763,7 @@ func LatestPackageRevision(ctx *context.Context) { revision, err := conan_model.GetLastPackageRevision(ctx, ctx.Package.Owner.ID, pref) if err != nil { - if err == conan_model.ErrRecipeReferenceNotExist || err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, conan_model.ErrRecipeReferenceNotExist) || errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -796,7 +793,7 @@ func listRevisionFiles(ctx *context.Context, fileKey string) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeConan, rref.Name, rref.Version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/conan/search.go b/routers/api/packages/conan/search.go index 7370c702cd..0dbbd500d2 100644 --- a/routers/api/packages/conan/search.go +++ b/routers/api/packages/conan/search.go @@ -4,6 +4,7 @@ package conan import ( + "errors" "net/http" "strings" @@ -76,7 +77,7 @@ func searchPackages(ctx *context.Context, searchAllRevisions bool) { if !searchAllRevisions && rref.Revision == "" { lastRevision, err := conan_model.GetLastRecipeRevision(ctx, ctx.Package.Owner.ID, rref) if err != nil { - if err == conan_model.ErrRecipeReferenceNotExist { + if errors.Is(err, conan_model.ErrRecipeReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -87,7 +88,7 @@ func searchPackages(ctx *context.Context, searchAllRevisions bool) { } else { has, err := conan_model.RecipeExists(ctx, ctx.Package.Owner.ID, rref) if err != nil { - if err == conan_model.ErrRecipeReferenceNotExist { + if errors.Is(err, conan_model.ErrRecipeReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -119,7 +120,7 @@ func searchPackages(ctx *context.Context, searchAllRevisions bool) { } packageReferences, err := conan_model.GetPackageReferences(ctx, ctx.Package.Owner.ID, currentRef) if err != nil { - if err == conan_model.ErrRecipeReferenceNotExist { + if errors.Is(err, conan_model.ErrRecipeReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -133,7 +134,7 @@ func searchPackages(ctx *context.Context, searchAllRevisions bool) { pref, _ := conan_module.NewPackageReference(currentRef, packageReference.Value, "") lastPackageRevision, err := conan_model.GetLastPackageRevision(ctx, ctx.Package.Owner.ID, pref) if err != nil { - if err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -143,7 +144,7 @@ func searchPackages(ctx *context.Context, searchAllRevisions bool) { pref = pref.WithRevision(lastPackageRevision.Value) infoRaw, err := conan_model.GetPackageInfo(ctx, ctx.Package.Owner.ID, pref) if err != nil { - if err == conan_model.ErrPackageReferenceNotExist { + if errors.Is(err, conan_model.ErrPackageReferenceNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go index 7a46681235..f496002bb5 100644 --- a/routers/api/packages/conda/conda.go +++ b/routers/api/packages/conda/conda.go @@ -13,7 +13,6 @@ import ( packages_model "code.gitea.io/gitea/models/packages" conda_model "code.gitea.io/gitea/models/packages/conda" "code.gitea.io/gitea/modules/json" - "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" conda_module "code.gitea.io/gitea/modules/packages/conda" "code.gitea.io/gitea/modules/util" @@ -25,17 +24,34 @@ import ( ) func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.JSON(status, struct { - Reason string `json:"reason"` - Message string `json:"message"` - }{ - Reason: http.StatusText(status), - Message: message, - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.JSON(status, struct { + Reason string `json:"reason"` + Message string `json:"message"` + }{ + Reason: http.StatusText(status), + Message: message, }) } +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 + } + http.NotFound(ctx.Resp, ctx.Req) +} + func EnumeratePackages(ctx *context.Context) { type Info struct { Subdir string `json:"subdir"` @@ -167,13 +183,16 @@ func EnumeratePackages(ctx *context.Context) { } resp.WriteHeader(http.StatusOK) - - if err := json.NewEncoder(w).Encode(repoData); err != nil { - log.Error("JSON encode: %v", err) - } + _ = json.NewEncoder(w).Encode(repoData) } 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 +210,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 +312,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, ctx.Req.Method) 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..19a931c405 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 { @@ -35,7 +35,6 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS u, err := user_model.GetPossibleUserByID(req.Context(), packageMeta.UserID) if err != nil { - log.Error("GetPossibleUserByID: %v", err) return nil, err } diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go index 4595b9a33d..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, @@ -111,46 +111,40 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI } var err error if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { - if err == packages_model.ErrDuplicatePackage { - created = false - } else { + if !errors.Is(err, packages_model.ErrDuplicatePackage) { log.Error("Error inserting package: %v", err) - return err + return nil, err } + created = false } 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 err != packages_model.ErrDuplicatePackageVersion { + 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, @@ -161,7 +155,7 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p } var err error if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil { - if err == packages_model.ErrDuplicatePackageFile { + if errors.Is(err, packages_model.ErrDuplicatePackageFile) { return nil } log.Error("Error inserting package file: %v", err) @@ -176,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 } @@ -187,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 3a470ad685..db81dd13c2 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,16 +89,13 @@ 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) { - helper.LogAndProcessError(ctx, status, err, func(message string) { - setResponseHeaders(ctx.Resp, &containerHeaders{ - Status: status, - }) + _ = helper.ProcessErrorForUser(ctx, status, err) + setResponseHeaders(ctx.Resp, &containerHeaders{ + Status: status, }) } @@ -126,14 +129,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 +155,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 +218,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 +233,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,19 +315,19 @@ 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) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -332,20 +335,26 @@ 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")) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -376,16 +385,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") @@ -396,19 +408,14 @@ func EndUploadBlob(ctx *context.Context) { uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid")) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) } 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 +448,10 @@ func EndUploadBlob(ctx *context.Context) { return } - if err := uploader.Close(); err != nil { - apiError(ctx, http.StatusInternalServerError, err) - return - } - doClose = false + // Some SDK (e.g.: minio) will close the Reader if it is also a Closer after "uploading". + // And we don't need to wrap the reader to anything else because the SDK will benefit from other interfaces like Seeker. + // 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,12 +466,12 @@ 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) if err != nil { - if err == packages_model.ErrPackageBlobUploadNotExist { + if errors.Is(err, packages_model.ErrPackageBlobUploadNotExist) { apiErrorDefined(ctx, errBlobUploadUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -484,16 +490,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), }) } @@ -501,7 +506,7 @@ func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescri func HeadBlob(ctx *context.Context) { blob, err := getBlobFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errBlobUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -511,7 +516,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, }) } @@ -520,7 +525,7 @@ func HeadBlob(ctx *context.Context) { func GetBlob(ctx *context.Context) { blob, err := getBlobFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errBlobUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -533,9 +538,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 +555,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 +567,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 +611,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 } @@ -639,7 +643,7 @@ func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDe func HeadManifest(ctx *context.Context) { manifest, err := getManifestFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errManifestUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -650,7 +654,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, }) } @@ -659,7 +663,7 @@ func HeadManifest(ctx *context.Context) { func GetManifest(ctx *context.Context) { manifest, err := getManifestFromContext(ctx) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errManifestUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -705,7 +709,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, ctx.Req.Method, serveDirectReqParams) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -714,14 +718,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 } @@ -729,17 +733,15 @@ func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) defer s.Close() setResponseHeaders(ctx.Resp, headers) - if _, err := io.Copy(ctx.Resp, s); err != nil { - log.Error("Error whilst copying content to response: %v", err) - } + _, _ = io.Copy(ctx.Resp, s) } // 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 { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiErrorDefined(ctx, errNameUnknown) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -780,7 +782,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 4a79a58f51..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 } @@ -240,7 +196,7 @@ func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, b IsManifest: true, }) if err != nil { - if err == container_model.ErrContainerBlobNotExist { + if errors.Is(err, container_model.ErrContainerBlobNotExist) { return errManifestBlobUnknown } return err @@ -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) { @@ -321,12 +239,11 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met } var err error if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { - if err == packages_model.ErrDuplicatePackage { - created = false - } else { + if !errors.Is(err, packages_model.ErrDuplicatePackage) { log.Error("Error inserting package: %v", err) return nil, err } + created = false } if created { @@ -350,24 +267,33 @@ 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 { - if err == packages_model.ErrDuplicatePackageVersion { - 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 { - log.Error("Error inserting package: %v", err) - return nil, err - } - } else { + 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 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 + } + } + } } if err := packages_service.CheckCountQuotaExceeded(ctx, mci.Creator, mci.Owner); err != nil { @@ -375,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 } } @@ -399,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 err == packages_model.ErrDuplicatePackageFile { + 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{ @@ -432,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) @@ -455,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..323690fd52 100644 --- a/routers/api/packages/cran/cran.go +++ b/routers/api/packages/cran/cran.go @@ -22,9 +22,8 @@ import ( ) func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.PlainText(status, message) - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.PlainText(status, message) } func EnumerateSourcePackages(ctx *context.Context) { @@ -250,7 +249,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, ctx.Req.Method) 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 162122ccbd..82c7952bdb 100644 --- a/routers/api/packages/debian/debian.go +++ b/routers/api/packages/debian/debian.go @@ -24,9 +24,8 @@ import ( ) func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.PlainText(status, message) - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.PlainText(status, message) } func GetRepositoryKey(ctx *context.Context) { @@ -59,16 +58,17 @@ 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{ Filename: ctx.PathParam("filename"), CompositeKey: key, }, + ctx.Req.Method, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -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], ctx.Req.Method) 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, @@ -222,6 +222,7 @@ func DownloadPackageFile(ctx *context.Context) { Filename: fmt.Sprintf("%s_%s_%s.deb", name, version, ctx.PathParam("architecture")), CompositeKey: fmt.Sprintf("%s|%s", ctx.PathParam("distribution"), ctx.PathParam("component")), }, + ctx.Req.Method, ) if err != nil { if errors.Is(err, util.ErrNotExist) { diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go index 868caf9cf0..5eb189e6d9 100644 --- a/routers/api/packages/generic/generic.go +++ b/routers/api/packages/generic/generic.go @@ -11,7 +11,6 @@ import ( "unicode" packages_model "code.gitea.io/gitea/models/packages" - "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" "code.gitea.io/gitea/routers/api/packages/helper" "code.gitea.io/gitea/services/context" @@ -24,14 +23,13 @@ var ( ) func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.PlainText(status, message) - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.PlainText(status, message) } // 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, @@ -42,9 +40,10 @@ func DownloadPackageFile(ctx *context.Context) { &packages_service.PackageFileInfo{ Filename: ctx.PathParam("filename"), }, + ctx.Req.Method, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -101,7 +100,6 @@ func UploadPackage(ctx *context.Context) { buf, err := packages_module.CreateHashedBufferFromReader(upload) if err != nil { - log.Error("Error creating hashed buffer: %v", err) apiError(ctx, http.StatusInternalServerError, err) return } @@ -155,7 +153,7 @@ func DeletePackage(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -182,7 +180,7 @@ func DeletePackageFile(ctx *context.Context) { return pv, pf, nil }() if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/goproxy/goproxy.go b/routers/api/packages/goproxy/goproxy.go index bde29df739..951f50053c 100644 --- a/routers/api/packages/goproxy/goproxy.go +++ b/routers/api/packages/goproxy/goproxy.go @@ -22,9 +22,8 @@ import ( ) func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.PlainText(status, message) - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.PlainText(status, message) } func EnumeratePackageVersions(ctx *context.Context) { @@ -106,7 +105,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], ctx.Req.Method) 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 cb30a20074..4c1b72d5c0 100644 --- a/routers/api/packages/helm/helm.go +++ b/routers/api/packages/helm/helm.go @@ -14,7 +14,6 @@ import ( packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/modules/json" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" helm_module "code.gitea.io/gitea/modules/packages/helm" @@ -28,13 +27,12 @@ import ( ) func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - type Error struct { - Error string `json:"error"` - } - ctx.JSON(status, Error{ - Error: message, - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + type Error struct { + Error string `json:"error"` + } + ctx.JSON(status, Error{ + Error: message, }) } @@ -87,16 +85,14 @@ func Index(ctx *context.Context) { } ctx.Resp.WriteHeader(http.StatusOK) - if err := yaml.NewEncoder(ctx.Resp).Encode(&Index{ + _ = yaml.NewEncoder(ctx.Resp).Encode(&Index{ APIVersion: "v1", Entries: entries, Generated: time.Now(), ServerInfo: &ServerInfo{ ContextPath: setting.AppSubURL + "/api/packages/" + url.PathEscape(ctx.Package.Owner.Name) + "/helm", }, - }); err != nil { - log.Error("YAML encode failed: %v", err) - } + }) } // DownloadPackageFile serves the content of a package @@ -122,15 +118,16 @@ 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{ Filename: filename, }, + ctx.Req.Method, ) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/helper/helper.go b/routers/api/packages/helper/helper.go index cdb64109ad..27d4e6ffdc 100644 --- a/routers/api/packages/helper/helper.go +++ b/routers/api/packages/helper/helper.go @@ -15,31 +15,29 @@ import ( "code.gitea.io/gitea/services/context" ) -// LogAndProcessError logs an error and calls a custom callback with the processed error message. -// If the error is an InternalServerError the message is stripped if the user is not an admin. -func LogAndProcessError(ctx *context.Context, status int, obj any, cb func(string)) { +// ProcessErrorForUser logs the error and returns a user-error message for the end user. +// If the status is http.StatusInternalServerError, the message is stripped for non-admin users in production. +func ProcessErrorForUser(ctx *context.Context, status int, errObj any) string { var message string - if err, ok := obj.(error); ok { + if err, ok := errObj.(error); ok { message = err.Error() - } else if obj != nil { - message = fmt.Sprintf("%s", obj) + } else if errObj != nil { + message = fmt.Sprint(errObj) } - if status == http.StatusInternalServerError { - log.ErrorWithSkip(1, message) + if status == http.StatusInternalServerError { + log.Log(2, log.ERROR, "Package registry API internal error: %d %s", status, message) if setting.IsProd && (ctx.Doer == nil || !ctx.Doer.IsAdmin) { - message = "" + message = "internal server error" } - } else { - log.Debug(message) + return message } - if cb != nil { - cb(message) - } + log.Log(2, log.DEBUG, "Package registry API user error: %d %s", status, message) + return message } -// Serves the content of the package file +// ServePackageFile the content of the package file // If the url is set it will redirect the request, otherwise the content is copied to the response. func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf *packages_model.PackageFile, forceOpts ...*context.ServeHeaderOptions) { if u != nil { 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 9474b17bc7..6c2916908b 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -13,7 +13,7 @@ import ( "errors" "io" "net/http" - "path/filepath" + "path" "regexp" "sort" "strconv" @@ -22,9 +22,9 @@ import ( packages_model "code.gitea.io/gitea/models/packages" "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" maven_module "code.gitea.io/gitea/modules/packages/maven" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" "code.gitea.io/gitea/services/context" packages_service "code.gitea.io/gitea/services/packages" @@ -44,18 +44,13 @@ const ( var ( errInvalidParameters = errors.New("request parameters are invalid") - illegalCharacters = regexp.MustCompile(`[\\/:"<>|?\*]`) + illegalCharacters = regexp.MustCompile(`[\\/:"<>|?*]`) ) func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - // The maven client does not present the error message to the user. Log it for users with access to server logs. - if status == http.StatusBadRequest || status == http.StatusInternalServerError { - log.Error(message) - } - - ctx.PlainText(status, message) - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + // Maven client doesn't present the error message to end users; site admin can check the server logs that outputted by ProcessErrorForUser + ctx.PlainText(status, message) } // DownloadPackageFile serves the content of a package @@ -83,14 +78,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] - - packageName := params.GroupID + "-" + params.ArtifactID - pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName) + // 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 @@ -107,7 +108,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 @@ -116,10 +117,10 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { latest := pds[len(pds)-1] // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat - lastModifed := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat) - ctx.Resp.Header().Set("Last-Modified", lastModifed) + lastModified := latest.Version.CreatedUnix.AsTime().UTC().Format(http.TimeFormat) + ctx.Resp.Header().Set("Last-Modified", lastModified) - ext := strings.ToLower(filepath.Ext(params.Filename)) + ext := strings.ToLower(path.Ext(params.Filename)) if isChecksumExtension(ext) { var hash []byte switch ext { @@ -147,11 +148,12 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { } func servePackageFile(ctx *context.Context, params parameters, serveContent bool) { - packageName := params.GroupID + "-" + params.ArtifactID - - pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version) + pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageName(), params.Version) + if errors.Is(err, util.ErrNotExist) { + pv, err = packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy(), params.Version) + } if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -161,14 +163,14 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool filename := params.Filename - ext := strings.ToLower(filepath.Ext(filename)) + ext := strings.ToLower(path.Ext(filename)) if isChecksumExtension(ext) { filename = filename[:len(filename)-len(ext)] } pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, filename, packages_model.EmptyFileKey) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) } else { apiError(ctx, http.StatusInternalServerError, err) @@ -215,7 +217,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, ctx.Req.Method, nil) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -238,15 +240,17 @@ func UploadPackageFile(ctx *context.Context) { return } - log.Trace("Parameters: %+v", params) - // Ignore the package index /<name>/maven-metadata.xml if params.IsMeta && params.Version == "" { ctx.Status(http.StatusOK) return } - packageName := params.GroupID + "-" + params.ArtifactID + packageName := params.toInternalPackageName() + if ctx.FormBool("use_legacy_package_name") { + // for testing purpose only + packageName = params.toInternalPackageNameLegacy() + } // for the same package, only one upload at a time releaser, err := globallock.Lock(ctx, mavenPkgNameKey(packageName)) @@ -274,13 +278,26 @@ func UploadPackageFile(ctx *context.Context) { Creator: ctx.Doer, } - ext := filepath.Ext(params.Filename) + // old maven package uses "groupId-artifactId" as package name, so we need to update to the new format "groupId:artifactId" + legacyPackage, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, params.toInternalPackageNameLegacy()) + if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) { + apiError(ctx, http.StatusInternalServerError, err) + return + } else if legacyPackage != nil { + err = packages_model.UpdatePackageNameByID(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, legacyPackage.ID, packageName) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + } + + ext := path.Ext(params.Filename) // Do not upload checksum files but compare the hashes. if isChecksumExtension(ext) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -289,7 +306,7 @@ func UploadPackageFile(ctx *context.Context) { } pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, params.Filename[:len(params.Filename)-len(ext)], packages_model.EmptyFileKey) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -343,7 +360,7 @@ func UploadPackageFile(ctx *context.Context) { if pvci.Metadata != nil { pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvci.Owner.ID, pvci.PackageType, pvci.Name, pvci.Version) - if err != nil && err != packages_model.ErrPackageNotExist { + if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusInternalServerError, err) return } @@ -399,9 +416,26 @@ type parameters struct { IsMeta bool } +func (p *parameters) toInternalPackageName() string { + // there cuold be 2 choices: "/" or ":" + // Maven says: "groupId:artifactId:version" in their document: https://maven.apache.org/pom.html#Maven_Coordinates + // but it would be slightly ugly in URL: "/-/packages/maven/group-id%3Aartifact-id" + return p.GroupID + ":" + p.ArtifactID +} + +func (p *parameters) toInternalPackageNameLegacy() string { + return p.GroupID + "-" + p.ArtifactID +} + func extractPathParameters(ctx *context.Context) (parameters, error) { parts := strings.Split(ctx.PathParam("*"), "/") + // formats: + // * /com/group/id/artifactId/maven-metadata.xml[.md5|.sha1|.sha256|.sha512] + // * /com/group/id/artifactId/version-SNAPSHOT/maven-metadata.xml[.md5|.sha1|.sha256|.sha512] + // * /com/group/id/artifactId/version/any-file + // * /com/group/id/artifactId/version-SNAPSHOT/any-file + p := parameters{ Filename: parts[len(parts)-1], } diff --git a/routers/api/packages/npm/api.go b/routers/api/packages/npm/api.go index b4379f3f49..636680242a 100644 --- a/routers/api/packages/npm/api.go +++ b/routers/api/packages/npm/api.go @@ -67,6 +67,7 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package BundleDependencies: metadata.BundleDependencies, DevDependencies: metadata.DevelopmentDependencies, PeerDependencies: metadata.PeerDependencies, + PeerDependenciesMeta: metadata.PeerDependenciesMeta, OptionalDependencies: metadata.OptionalDependencies, Readme: metadata.Readme, Bin: metadata.Bin, diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go index 284723e0d7..cc2aff8ea0 100644 --- a/routers/api/packages/npm/npm.go +++ b/routers/api/packages/npm/npm.go @@ -33,10 +33,9 @@ import ( var errInvalidTagName = errors.New("The tag name is invalid") func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.JSON(status, map[string]string{ - "error": message, - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.JSON(status, map[string]string{ + "error": message, }) } @@ -85,7 +84,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, @@ -96,9 +95,10 @@ func DownloadPackageFile(ctx *context.Context) { &packages_service.PackageFileInfo{ Filename: filename, }, + ctx.Req.Method, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -132,15 +132,16 @@ 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{ Filename: filename, }, + ctx.Req.Method, ) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -163,7 +164,7 @@ func UploadPackage(ctx *context.Context) { return } - repo, err := repo_model.GetRepositoryByURL(ctx, npmPackage.Metadata.Repository.URL) + repo, err := repo_model.GetRepositoryByURLRelax(ctx, npmPackage.Metadata.Repository.URL) if err == nil { canWrite := repo.OwnerID == ctx.Doer.ID @@ -267,7 +268,7 @@ func DeletePackageVersion(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -341,7 +342,7 @@ func AddPackageTag(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNpm, packageName, version) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } 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/api_v3.go b/routers/api/packages/nuget/api_v3.go index 2fe25dc0f8..3262f2d9af 100644 --- a/routers/api/packages/nuget/api_v3.go +++ b/routers/api/packages/nuget/api_v3.go @@ -53,15 +53,23 @@ type RegistrationIndexPageItem struct { // https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#catalog-entry type CatalogEntry struct { CatalogLeafURL string `json:"@id"` - PackageContentURL string `json:"packageContent"` + Authors string `json:"authors"` + Copyright string `json:"copyright"` + DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"` + Description string `json:"description"` + IconURL string `json:"iconUrl"` ID string `json:"id"` + IsPrerelease bool `json:"isPrerelease"` + Language string `json:"language"` + LicenseURL string `json:"licenseUrl"` + PackageContentURL string `json:"packageContent"` + ProjectURL string `json:"projectUrl"` + RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"` + Summary string `json:"summary"` + Tags string `json:"tags"` Version string `json:"version"` - Description string `json:"description"` ReleaseNotes string `json:"releaseNotes"` - Authors string `json:"authors"` - RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"` - ProjectURL string `json:"projectURL"` - DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"` + Published time.Time `json:"published"` } // https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency-group @@ -109,15 +117,24 @@ func createRegistrationIndexPageItem(l *linkBuilder, pd *packages_model.PackageD RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version), PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version), CatalogEntry: &CatalogEntry{ - CatalogLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version), - PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version), - ID: pd.Package.Name, - Version: pd.Version.Version, - Description: metadata.Description, - ReleaseNotes: metadata.ReleaseNotes, - Authors: metadata.Authors, - ProjectURL: metadata.ProjectURL, - DependencyGroups: createDependencyGroups(pd), + CatalogLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version), + Authors: metadata.Authors, + Copyright: metadata.Copyright, + DependencyGroups: createDependencyGroups(pd), + Description: metadata.Description, + IconURL: metadata.IconURL, + ID: pd.Package.Name, + IsPrerelease: pd.Version.IsPrerelease(), + Language: metadata.Language, + LicenseURL: metadata.LicenseURL, + PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version), + ProjectURL: metadata.ProjectURL, + RequireLicenseAcceptance: metadata.RequireLicenseAcceptance, + Summary: metadata.Summary, + Tags: metadata.Tags, + Version: pd.Version.Version, + ReleaseNotes: metadata.ReleaseNotes, + Published: pd.Version.CreatedUnix.AsLocalTime(), }, } } @@ -145,22 +162,42 @@ func createDependencyGroups(pd *packages_model.PackageDescriptor) []*PackageDepe // https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf type RegistrationLeafResponse struct { - RegistrationLeafURL string `json:"@id"` - Type []string `json:"@type"` - Listed bool `json:"listed"` - PackageContentURL string `json:"packageContent"` - Published time.Time `json:"published"` - RegistrationIndexURL string `json:"registration"` + RegistrationLeafURL string `json:"@id"` + Type []string `json:"@type"` + PackageContentURL string `json:"packageContent"` + RegistrationIndexURL string `json:"registration"` + CatalogEntry CatalogEntry `json:"catalogEntry"` } func createRegistrationLeafResponse(l *linkBuilder, pd *packages_model.PackageDescriptor) *RegistrationLeafResponse { + registrationLeafURL := l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version) + packageDownloadURL := l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version) + metadata := pd.Metadata.(*nuget_module.Metadata) return &RegistrationLeafResponse{ - Type: []string{"Package", "http://schema.nuget.org/catalog#Permalink"}, - Listed: true, - Published: pd.Version.CreatedUnix.AsLocalTime(), - RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version), - PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version), + RegistrationLeafURL: registrationLeafURL, RegistrationIndexURL: l.GetRegistrationIndexURL(pd.Package.Name), + PackageContentURL: packageDownloadURL, + Type: []string{"Package", "http://schema.nuget.org/catalog#Permalink"}, + CatalogEntry: CatalogEntry{ + CatalogLeafURL: registrationLeafURL, + Authors: metadata.Authors, + Copyright: metadata.Copyright, + DependencyGroups: createDependencyGroups(pd), + Description: metadata.Description, + IconURL: metadata.IconURL, + ID: pd.Package.Name, + IsPrerelease: pd.Version.IsPrerelease(), + Language: metadata.Language, + LicenseURL: metadata.LicenseURL, + PackageContentURL: packageDownloadURL, + ProjectURL: metadata.ProjectURL, + RequireLicenseAcceptance: metadata.RequireLicenseAcceptance, + Summary: metadata.Summary, + Tags: metadata.Tags, + Version: pd.Version.Version, + ReleaseNotes: metadata.ReleaseNotes, + Published: pd.Version.CreatedUnix.AsLocalTime(), + }, } } @@ -188,13 +225,24 @@ type SearchResultResponse struct { // https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result type SearchResult struct { - ID string `json:"id"` - Version string `json:"version"` - Versions []*SearchResultVersion `json:"versions"` - Description string `json:"description"` - Authors string `json:"authors"` - ProjectURL string `json:"projectURL"` - RegistrationIndexURL string `json:"registration"` + Authors string `json:"authors"` + Copyright string `json:"copyright"` + DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"` + Description string `json:"description"` + IconURL string `json:"iconUrl"` + ID string `json:"id"` + IsPrerelease bool `json:"isPrerelease"` + Language string `json:"language"` + LicenseURL string `json:"licenseUrl"` + ProjectURL string `json:"projectUrl"` + RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"` + Summary string `json:"summary"` + Tags string `json:"tags"` + Title string `json:"title"` + TotalDownloads int64 `json:"totalDownloads"` + Version string `json:"version"` + Versions []*SearchResultVersion `json:"versions"` + RegistrationIndexURL string `json:"registration"` } // https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result @@ -230,11 +278,12 @@ func createSearchResultResponse(l *linkBuilder, totalHits int64, pds []*packages func createSearchResult(l *linkBuilder, pds []*packages_model.PackageDescriptor) *SearchResult { latest := pds[0] versions := make([]*SearchResultVersion, 0, len(pds)) + totalDownloads := int64(0) for _, pd := range pds { if latest.SemVer.LessThan(pd.SemVer) { latest = pd } - + totalDownloads += pd.Version.DownloadCount versions = append(versions, &SearchResultVersion{ RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version), Version: pd.Version.Version, @@ -244,12 +293,23 @@ func createSearchResult(l *linkBuilder, pds []*packages_model.PackageDescriptor) metadata := latest.Metadata.(*nuget_module.Metadata) return &SearchResult{ - ID: latest.Package.Name, - Version: latest.Version.Version, - Versions: versions, - Description: metadata.Description, - Authors: metadata.Authors, - ProjectURL: metadata.ProjectURL, - RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.Name), + Authors: metadata.Authors, + Copyright: metadata.Copyright, + Description: metadata.Description, + DependencyGroups: createDependencyGroups(latest), + IconURL: metadata.IconURL, + ID: latest.Package.Name, + IsPrerelease: latest.Version.IsPrerelease(), + Language: metadata.Language, + LicenseURL: metadata.LicenseURL, + ProjectURL: metadata.ProjectURL, + RequireLicenseAcceptance: metadata.RequireLicenseAcceptance, + Summary: metadata.Summary, + Tags: metadata.Tags, + Title: metadata.Title, + TotalDownloads: totalDownloads, + Version: latest.Version.Version, + Versions: versions, + RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.Name), } } diff --git a/routers/api/packages/nuget/auth.go b/routers/api/packages/nuget/auth.go index e81ad01b2b..ce7df0ce0a 100644 --- a/routers/api/packages/nuget/auth.go +++ b/routers/api/packages/nuget/auth.go @@ -26,7 +26,6 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS token, err := auth_model.GetAccessTokenBySHA(req.Context(), req.Header.Get("X-NuGet-ApiKey")) if err != nil { if !(auth_model.IsErrAccessTokenNotExist(err) || auth_model.IsErrAccessTokenEmpty(err)) { - log.Error("GetAccessTokenBySHA: %v", err) return nil, err } return nil, nil @@ -34,7 +33,6 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS u, err := user_model.GetUserByID(req.Context(), token.UID) if err != nil { - log.Error("GetUserByID: %v", err) return nil, err } diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go index 70b95e6a77..c42fdd7db5 100644 --- a/routers/api/packages/nuget/nuget.go +++ b/routers/api/packages/nuget/nuget.go @@ -17,7 +17,6 @@ import ( "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" nuget_model "code.gitea.io/gitea/models/packages/nuget" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" nuget_module "code.gitea.io/gitea/modules/packages/nuget" @@ -29,22 +28,17 @@ import ( ) func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.JSON(status, map[string]string{ - "Message": message, - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.JSON(status, map[string]string{ + "Message": message, }) } -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 { - log.Error("Write failed: %v", err) - } - if err := xml.NewEncoder(ctx.Resp).Encode(obj); err != nil { - log.Error("XML encode failed: %v", err) - } + _, _ = ctx.Resp.Write([]byte(xml.Header)) + _ = xml.NewEncoder(ctx.Resp).Encode(obj) } // https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs @@ -259,7 +253,7 @@ func RegistrationLeafV2(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -288,7 +282,7 @@ func RegistrationLeafV3(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -405,7 +399,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, @@ -416,9 +410,10 @@ func DownloadPackageFile(ctx *context.Context) { &packages_service.PackageFileInfo{ Filename: filename, }, + ctx.Req.Method, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -488,7 +483,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,9 +664,9 @@ 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], ctx.Req.Method) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -699,7 +694,7 @@ func DeletePackage(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } diff --git a/routers/api/packages/pub/pub.go b/routers/api/packages/pub/pub.go index 2be27323fd..7564e14d0e 100644 --- a/routers/api/packages/pub/pub.go +++ b/routers/api/packages/pub/pub.go @@ -15,7 +15,6 @@ import ( packages_model "code.gitea.io/gitea/models/packages" "code.gitea.io/gitea/modules/json" - "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" pub_module "code.gitea.io/gitea/modules/packages/pub" "code.gitea.io/gitea/modules/setting" @@ -29,9 +28,7 @@ func jsonResponse(ctx *context.Context, status int, obj any) { resp := ctx.Resp resp.Header().Set("Content-Type", "application/vnd.pub.v2+json") resp.WriteHeader(status) - if err := json.NewEncoder(resp).Encode(obj); err != nil { - log.Error("JSON encode: %v", err) - } + _ = json.NewEncoder(resp).Encode(obj) } func apiError(ctx *context.Context, status int, obj any) { @@ -43,13 +40,12 @@ func apiError(ctx *context.Context, status int, obj any) { Error Error `json:"error"` } - helper.LogAndProcessError(ctx, status, obj, func(message string) { - jsonResponse(ctx, status, ErrorWrapper{ - Error: Error{ - Code: http.StatusText(status), - Message: message, - }, - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + jsonResponse(ctx, status, ErrorWrapper{ + Error: Error{ + Code: http.StatusText(status), + Message: message, + }, }) } @@ -124,7 +120,7 @@ func PackageVersionMetadata(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -233,7 +229,7 @@ func FinalizePackage(ctx *context.Context) { _, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -258,7 +254,7 @@ func DownloadPackageFile(ctx *context.Context) { pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypePub, packageName, packageVersion) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -274,7 +270,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, ctx.Req.Method) 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 5ea86071a9..1b8f4bea34 100644 --- a/routers/api/packages/pypi/pypi.go +++ b/routers/api/packages/pypi/pypi.go @@ -5,11 +5,13 @@ package pypi import ( "encoding/hex" + "errors" "io" "net/http" "regexp" "sort" "strings" + "unicode" packages_model "code.gitea.io/gitea/models/packages" packages_module "code.gitea.io/gitea/modules/packages" @@ -38,9 +40,8 @@ var versionMatcher = regexp.MustCompile(`\Av?` + `\z`) func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.PlainText(status, message) - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.PlainText(status, message) } // PackageMetadata returns the metadata for a single package @@ -80,7 +81,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, @@ -91,9 +92,10 @@ func DownloadPackageFile(ctx *context.Context) { &packages_service.PackageFileInfo{ Filename: filename, }, + ctx.Req.Method, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -139,9 +141,30 @@ func UploadPackageFile(ctx *context.Context) { return } - projectURL := ctx.Req.FormValue("home_page") - if !validation.IsValidURL(projectURL) { - projectURL = "" + // Ensure ctx.Req.Form exists. + _ = ctx.Req.ParseForm() + + var homepageURL string + projectURLs := ctx.Req.Form["project_urls"] + for _, purl := range projectURLs { + label, url, found := strings.Cut(purl, ",") + if !found { + continue + } + if normalizeLabel(label) != "homepage" { + continue + } + homepageURL = strings.TrimSpace(url) + break + } + + if len(homepageURL) == 0 { + // TODO: Home-page is a deprecated metadata field. Remove this branch once it's no longer apart of the spec. + homepageURL = ctx.Req.FormValue("home_page") + } + + if !validation.IsValidURL(homepageURL) { + homepageURL = "" } _, _, err = packages_service.CreatePackageOrAddFileToExisting( @@ -160,7 +183,7 @@ func UploadPackageFile(ctx *context.Context) { Description: ctx.Req.FormValue("description"), LongDescription: ctx.Req.FormValue("long_description"), Summary: ctx.Req.FormValue("summary"), - ProjectURL: projectURL, + ProjectURL: homepageURL, License: ctx.Req.FormValue("license"), RequiresPython: ctx.Req.FormValue("requires_python"), }, @@ -189,6 +212,23 @@ func UploadPackageFile(ctx *context.Context) { ctx.Status(http.StatusCreated) } +// Normalizes a Project-URL label. +// See https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization. +func normalizeLabel(label string) string { + var builder strings.Builder + + // "A label is normalized by deleting all ASCII punctuation and whitespace, and then converting the result + // to lowercase." + for _, r := range label { + if unicode.IsPunct(r) || unicode.IsSpace(r) { + continue + } + builder.WriteRune(unicode.ToLower(r)) + } + + return builder.String() +} + func isValidNameAndVersion(packageName, packageVersion string) bool { return nameMatcher.MatchString(packageName) && versionMatcher.MatchString(packageVersion) } diff --git a/routers/api/packages/pypi/pypi_test.go b/routers/api/packages/pypi/pypi_test.go index 3023692177..786105693f 100644 --- a/routers/api/packages/pypi/pypi_test.go +++ b/routers/api/packages/pypi/pypi_test.go @@ -36,3 +36,13 @@ func TestIsValidNameAndVersion(t *testing.T) { assert.False(t, isValidNameAndVersion("test-name", "1.0.1aa")) assert.False(t, isValidNameAndVersion("test-name", "1.0.0-alpha.beta")) } + +func TestNormalizeLabel(t *testing.T) { + // Cases fetched from https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization. + assert.Equal(t, "homepage", normalizeLabel("Homepage")) + assert.Equal(t, "homepage", normalizeLabel("Home-page")) + assert.Equal(t, "homepage", normalizeLabel("Home page")) + assert.Equal(t, "changelog", normalizeLabel("Change_Log")) + assert.Equal(t, "whatsnew", normalizeLabel("What's New?")) + assert.Equal(t, "github", normalizeLabel("github")) +} diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go index a00a61c079..5abbb0c8ae 100644 --- a/routers/api/packages/rpm/rpm.go +++ b/routers/api/packages/rpm/rpm.go @@ -26,9 +26,8 @@ import ( ) func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.PlainText(status, message) - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.PlainText(status, message) } // https://dnf.readthedocs.io/en/latest/conf_ref.html @@ -96,13 +95,14 @@ 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{ Filename: ctx.PathParam("filename"), CompositeKey: ctx.PathParam("group"), }, + ctx.Req.Method, ) if err != nil { if errors.Is(err, util.ErrNotExist) { @@ -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, @@ -232,6 +232,7 @@ func DownloadPackageFile(ctx *context.Context) { Filename: fmt.Sprintf("%s-%s.%s.rpm", name, version, ctx.PathParam("architecture")), CompositeKey: ctx.PathParam("group"), }, + ctx.Req.Method, ) if err != nil { if errors.Is(err, util.ErrNotExist) { diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index 958063e70a..1ecf93592e 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" @@ -24,9 +25,8 @@ import ( ) func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.PlainText(status, message) - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.PlainText(status, message) } // EnumeratePackages serves the package list @@ -177,15 +177,16 @@ 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{ Filename: filename, }, + ctx.Req.Method, ) if err != nil { - if err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -288,7 +289,7 @@ func DeletePackage(ctx *context.Context) { }, ) if err != nil { - if err == packages_model.ErrPackageNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) { apiError(ctx, http.StatusNotFound, err) return } @@ -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..e1c3b36834 100644 --- a/routers/api/packages/swift/swift.go +++ b/routers/api/packages/swift/swift.go @@ -77,17 +77,14 @@ func apiError(ctx *context.Context, status int, obj any) { Detail string `json:"detail"` } - helper.LogAndProcessError(ctx, status, obj, func(message string) { - setResponseHeaders(ctx.Resp, &headers{ - Status: status, - ContentType: "application/problem+json", - }) - if err := json.NewEncoder(ctx.Resp).Encode(Problem{ - Status: status, - Detail: message, - }); err != nil { - log.Error("JSON encode: %v", err) - } + message := helper.ProcessErrorForUser(ctx, status, obj) + setResponseHeaders(ctx.Resp, &headers{ + Status: status, + ContentType: "application/problem+json", + }) + _ = json.NewEncoder(ctx.Resp).Encode(Problem{ + Status: status, + Detail: message, }) } @@ -290,7 +287,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 +318,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 +332,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 +426,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, ctx.Req.Method) 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 1daf2a0527..36fc41f581 100644 --- a/routers/api/packages/vagrant/vagrant.go +++ b/routers/api/packages/vagrant/vagrant.go @@ -4,6 +4,7 @@ package vagrant import ( + "errors" "fmt" "io" "net/http" @@ -23,14 +24,13 @@ import ( ) func apiError(ctx *context.Context, status int, obj any) { - helper.LogAndProcessError(ctx, status, obj, func(message string) { - ctx.JSON(status, struct { - Errors []string `json:"errors"` - }{ - Errors: []string{ - message, - }, - }) + message := helper.ProcessErrorForUser(ctx, status, obj) + ctx.JSON(status, struct { + Errors []string `json:"errors"` + }{ + Errors: []string{ + message, + }, }) } @@ -217,7 +217,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, @@ -228,9 +228,10 @@ func DownloadPackageFile(ctx *context.Context) { &packages_service.PackageFileInfo{ Filename: ctx.PathParam("provider"), }, + ctx.Req.Method, ) if err != nil { - if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist { + if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) return } 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 613d123494..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 } @@ -80,39 +80,39 @@ func AdoptRepository(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // "403": // "$ref": "#/responses/forbidden" - ownerName := ctx.PathParam(":username") - repoName := ctx.PathParam(":reponame") + ownerName := ctx.PathParam("username") + repoName := ctx.PathParam("reponame") 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 } @@ -142,37 +142,37 @@ func DeleteUnadoptedRepository(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "403": // "$ref": "#/responses/forbidden" - ownerName := ctx.PathParam(":username") - repoName := ctx.PathParam(":reponame") + ownerName := ctx.PathParam("username") + repoName := ctx.PathParam("reponame") 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 fba9d33f25..b4dae11095 100644 --- a/routers/api/v1/admin/cron.go +++ b/routers/api/v1/admin/cron.go @@ -74,9 +74,9 @@ func PostCronTask(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "404": // "$ref": "#/responses/notFound" - task := cron.GetTask(ctx.PathParam(":task")) + 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 6fe418249b..ad078347a4 100644 --- a/routers/api/v1/admin/email.go +++ b/routers/api/v1/admin/email.go @@ -38,11 +38,11 @@ func GetAllEmails(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) emails, maxResults, err := user_model.SearchEmails(ctx, &user_model.SearchEmailOptions{ - Keyword: ctx.PathParam(":email"), + Keyword: ctx.PathParam("email"), ListOptions: listOptions, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetAllEmails", err) + ctx.APIErrorInternal(err) return } @@ -82,6 +82,6 @@ func SearchEmail(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" - ctx.SetPathParam(":email", ctx.FormTrim("q")) + ctx.SetPathParam("email", ctx.FormTrim("q")) GetAllEmails(ctx) } diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go index db481fbf59..a687541be5 100644 --- a/routers/api/v1/admin/hooks.go +++ b/routers/api/v1/admin/hooks.go @@ -34,20 +34,40 @@ func ListHooks(ctx *context.APIContext) { // in: query // description: page size of results // type: integer + // - type: string + // enum: + // - system + // - default + // - all + // description: system, default or both kinds of webhooks + // name: type + // default: system + // in: query + // // responses: // "200": // "$ref": "#/responses/HookList" - sysHooks, err := webhook.GetSystemWebhooks(ctx, optional.None[bool]()) + // for compatibility the default value is true + isSystemWebhook := optional.Some(true) + typeValue := ctx.FormString("type") + switch typeValue { + case "default": + isSystemWebhook = optional.Some(false) + 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 @@ -73,19 +93,19 @@ func GetHook(ctx *context.APIContext) { // "200": // "$ref": "#/responses/Hook" - hookID := ctx.PathParamInt64(":id") + hookID := ctx.PathParamInt64("id") 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) @@ -142,7 +162,7 @@ func EditHook(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.EditHookOption) // TODO in body params - hookID := ctx.PathParamInt64(":id") + hookID := ctx.PathParamInt64("id") utils.EditSystemHook(ctx, form, hookID) } @@ -164,12 +184,12 @@ func DeleteHook(ctx *context.APIContext) { // "204": // "$ref": "#/responses/empty" - hookID := ctx.PathParamInt64(":id") + 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 3f4a73dcad..494bace585 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,8 +239,8 @@ 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), - Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]), + IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin), + Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility), AllowGitHook: optional.FromPtr(form.AllowGitHook), AllowImportLocal: optional.FromPtr(form.AllowImportLocal), MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation), @@ -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 @@ -375,13 +375,13 @@ func DeleteUserPublicKey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := asymkey_service.DeletePublicKey(ctx, ctx.ContextUser, ctx.PathParamInt64(":id")); err != nil { + 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 96365e7c14..f412e8a06c 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 } @@ -145,7 +145,7 @@ func repoAssignment() func(ctx *context.APIContext) { ) // Check if the user is the same as the repository owner. - if ctx.IsSigned && ctx.Doer.LowerName == strings.ToLower(userName) { + if ctx.IsSigned && strings.EqualFold(ctx.Doer.LowerName, userName) { owner = ctx.Doer } else { owner, err = user_model.GetUserByName(ctx, userName) @@ -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 } } @@ -596,19 +623,19 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { var err error if assignOrg { - ctx.Org.Organization, err = organization.GetOrgByName(ctx, ctx.PathParam(":org")) + ctx.Org.Organization, err = organization.GetOrgByName(ctx, ctx.PathParam("org")) if err != nil { if organization.IsErrOrgNotExist(err) { - redirectUserID, err := user_model.LookupUserRedirect(ctx, ctx.PathParam(":org")) + redirectUserID, err := user_model.LookupUserRedirect(ctx, ctx.PathParam("org")) if err == nil { - context.RedirectToUser(ctx.Base, ctx.PathParam(":org"), redirectUserID) + 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 } @@ -616,12 +643,12 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) { } if assignTeam { - ctx.Org.Team, err = organization.GetTeamByID(ctx, ctx.PathParamInt64(":teamid")) + 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,9 +1247,10 @@ 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) m.Group("/branches", func() { m.Get("", repo.ListBranches) m.Get("/*", repo.GetBranch) @@ -1224,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). @@ -1247,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) @@ -1348,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) @@ -1380,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() { @@ -1488,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() { @@ -1509,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) @@ -1529,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() { @@ -1643,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 0761e684a3..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,28 +90,28 @@ 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)) } func getThread(ctx *context.APIContext) *activities_model.Notification { - n, err := activities_model.GetNotificationByID(ctx, ctx.PathParamInt64(":id")) + 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 24ee4ed642..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 } @@ -139,7 +139,7 @@ func GetLabel(ctx *context.APIContext) { label *issues_model.Label err error ) - strID := ctx.PathParam(":id") + strID := ctx.PathParam("id") if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil { label, err = issues_model.GetLabelInOrgByName(ctx, ctx.Org.Organization.ID, strID) } else { @@ -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 } @@ -190,12 +190,12 @@ func EditLabel(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.EditLabelOption) - l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64(":id")) + 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 } @@ -249,8 +249,8 @@ func DeleteLabel(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := issues_model.DeleteLabel(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64(":id")); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteLabel", err) + if err := issues_model.DeleteLabel(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id")); err != nil { + ctx.APIErrorInternal(err) return } diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index 294d33014d..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: @@ -143,27 +144,27 @@ func IsMember(ctx *context.APIContext) { // "404": // description: user is not a member - userToCheck := user.GetUserByParams(ctx) + userToCheck := user.GetContextUserByPathParam(ctx) if ctx.Written() { return } 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: @@ -194,19 +195,33 @@ func IsPublicMember(ctx *context.APIContext) { // "404": // description: user is not a public member - userToCheck := user.GetUserByParams(ctx) + userToCheck := user.GetContextUserByPathParam(ctx) if ctx.Written() { return } 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: @@ -236,17 +251,17 @@ func PublicizeMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - userToPublicize := user.GetUserByParams(ctx) + userToPublicize := user.GetContextUserByPathParam(ctx) 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: @@ -278,17 +293,17 @@ func ConcealMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - userToConceal := user.GetUserByParams(ctx) + userToConceal := user.GetContextUserByPathParam(ctx) 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: @@ -318,12 +333,12 @@ func DeleteMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - member := user.GetUserByParams(ctx) + member := user.GetContextUserByPathParam(ctx) if ctx.Written() { 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 3fb653bcb6..cd67686065 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 @@ -131,21 +129,21 @@ func GetUserOrgsPermissions(ctx *context.APIContext) { // "$ref": "#/responses/notFound" var o *user_model.User - if o = user.GetUserByParamsName(ctx, ":org"); o == nil { + if o = user.GetUserByPathParam(ctx, "org"); o == nil { return } 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 } } @@ -355,11 +391,11 @@ func Edit(ctx *context.APIContext) { Description: optional.Some(form.Description), Website: optional.Some(form.Website), Location: optional.Some(form.Location), - Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]), + Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility), 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 8164d2cfe9..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: @@ -449,17 +435,17 @@ func GetTeamMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - u := user.GetUserByParams(ctx) + u := user.GetContextUserByPathParam(ctx) if ctx.Written() { return } 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: @@ -492,15 +478,15 @@ func AddTeamMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - u := user.GetUserByParams(ctx) + u := user.GetContextUserByPathParam(ctx) if ctx.Written() { return } 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: @@ -532,13 +518,13 @@ func RemoveTeamMember(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - u := user.GetUserByParams(ctx) + u := user.GetContextUserByPathParam(ctx) if ctx.Written() { return } 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 } @@ -645,12 +631,12 @@ func GetTeamRepo(ctx *context.APIContext) { // getRepositoryByParams get repository by a team's organization ID and repo name func getRepositoryByParams(ctx *context.APIContext) *repo_model.Repository { - repo, err := repo_model.GetRepositoryByName(ctx, ctx.Org.Team.OrgID, ctx.PathParam(":reponame")) + 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..25aabe6dd2 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 @@ -31,7 +46,7 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) { // parameters: // - name: owner // in: path - // description: owner of the repository + // description: owner of the repo // type: string // required: true // - name: repo @@ -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 } @@ -200,7 +216,7 @@ func (Action) GetVariable(ctx *context.APIContext) { // parameters: // - name: owner // in: path - // description: name of the owner + // description: owner of the repo // type: string // required: true // - name: repo @@ -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) @@ -253,7 +270,7 @@ func (Action) DeleteVariable(ctx *context.APIContext) { // parameters: // - name: owner // in: path - // description: name of the owner + // description: owner of the repo // type: string // required: true // - name: repo @@ -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 } @@ -302,7 +319,7 @@ func (Action) CreateVariable(ctx *context.APIContext) { // parameters: // - name: owner // in: path - // description: name of the owner + // description: owner of the repo // type: string // required: true // - name: repo @@ -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 @@ -369,7 +386,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) { // parameters: // - name: owner // in: path - // description: name of the owner + // description: owner of the repo // type: string // required: true // - name: repo @@ -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 } @@ -436,7 +458,7 @@ func (Action) ListVariables(ctx *context.APIContext) { // parameters: // - name: owner // in: path - // description: name of the owner + // description: owner of the repo // type: string // required: true // - name: repo @@ -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: owner of the repo + // 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: owner of the repo + // 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/WorkflowRunsList" + // "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,862 @@ 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: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run + // in: path + // description: id of the run + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/WorkflowRun" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + runID := ctx.PathParamInt64("run") + job, has, err := db.GetByID[actions_model.ActionRun](ctx, runID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + if !has || job.RepoID != ctx.Repo.Repository.ID { + ctx.APIErrorNotFound(util.ErrNotExist) + return + } + + convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, job) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.JSON(http.StatusOK, convertedRun) +} + +// 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: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run + // in: path + // description: 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: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: job_id + // in: path + // description: id of the job + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/WorkflowJob" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + jobID := ctx.PathParamInt64("job_id") + job, has, err := db.GetByID[actions_model.ActionRunJob](ctx, jobID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + if !has || job.RepoID != ctx.Repo.Repository.ID { + ctx.APIErrorNotFound(util.ErrNotExist) + return + } + + convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, job) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.JSON(http.StatusOK, convertedWorkflowJob) +} + +// GetArtifactsOfRun 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: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run + // in: path + // description: 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: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run + // in: path + // description: 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.APIErrorNotFound(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: owner of the repo + // 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: owner of the repo + // 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: owner of the repo + // 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: owner of the repo + // 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..a12a6fdd6d --- /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: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: job_id + // in: path + // description: id of the job + // type: integer + // required: true + // responses: + // "200": + // description: output blob content + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + jobID := ctx.PathParamInt64("job_id") + curJob, err := actions_model.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 9a31aec314..9af958a5b7 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -6,18 +6,19 @@ package repo import ( "errors" - "fmt" "net/http" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/optional" repo_module "code.gitea.io/gitea/modules/repository" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/context" @@ -58,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 } @@ -122,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 } @@ -139,27 +139,27 @@ 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 } } - if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil { + 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 } @@ -204,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 } @@ -221,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 } } @@ -246,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 } @@ -323,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 } @@ -335,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 } @@ -367,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) @@ -431,26 +425,33 @@ 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 } msg, err := repo_service.RenameBranch(ctx, repo, ctx.Doer, ctx.Repo.GitRepo, oldName, opt.Name) if err != nil { - ctx.Error(http.StatusInternalServerError, "RenameBranch", err) + switch { + case repo_model.IsErrUserDoesNotHaveAccessToRepo(err): + ctx.APIError(http.StatusForbidden, "User must be a repo or site admin to rename default or protected branches.") + case errors.Is(err, git_model.ErrBranchIsProtected): + ctx.APIError(http.StatusForbidden, "Branch is protected by glob-based protection rules.") + default: + 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 } @@ -487,14 +488,14 @@ func GetBranchProtection(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo.Repository - bpName := ctx.PathParam(":name") + 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 } @@ -526,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)) @@ -578,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 } @@ -608,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 @@ -646,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 } } @@ -707,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, @@ -717,52 +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.OpenRepository(ctx, ctx.Repo.Repository) - if err != nil { - ctx.Error(http.StatusInternalServerError, "OpenRepository", err) - return - } - defer func() { - ctx.Repo.GitRepo.Close() - ctx.Repo.GitRepo = nil - }() - } - // 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 } @@ -809,14 +770,14 @@ func EditBranchProtection(ctx *context.APIContext) { // "$ref": "#/responses/repoArchivedError" form := web.GetForm(ctx).(*api.EditBranchProtectionOption) repo := ctx.Repo.Repository - bpName := ctx.PathParam(":name") + bpName := ctx.PathParam("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 } @@ -927,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 { @@ -940,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 { @@ -953,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 { @@ -966,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 { @@ -982,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 { @@ -995,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 { @@ -1008,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 { @@ -1021,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 { @@ -1043,45 +1004,41 @@ 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 { if !isPlainRule { if ctx.Repo.GitRepo == nil { - ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository) if err != nil { - ctx.Error(http.StatusInternalServerError, "OpenRepository", err) + ctx.APIErrorInternal(err) return } - defer func() { - ctx.Repo.GitRepo.Close() - ctx.Repo.GitRepo = nil - }() } // 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 } } @@ -1091,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 } @@ -1132,19 +1089,19 @@ func DeleteBranchProtection(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo.Repository - bpName := ctx.PathParam(":name") + 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 } @@ -1188,9 +1145,53 @@ 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 } ctx.Status(http.StatusNoContent) } + +func MergeUpstream(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/merge-upstream repository repoMergeUpstream + // --- + // summary: Merge a branch from upstream + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/MergeUpstreamRequest" + // responses: + // "200": + // "$ref": "#/responses/MergeUpstreamResponse" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + form := web.GetForm(ctx).(*api.MergeUpstreamRequest) + mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch, form.FfOnly) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.APIError(http.StatusBadRequest, err) + return + } else if errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusNotFound, err) + return + } + 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 0bbf5a1ea4..eed9c19fe1 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: @@ -103,24 +104,24 @@ func IsCollaborator(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - user, err := user_model.GetUserByName(ctx, ctx.PathParam(":collaborator")) + 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 @@ -163,31 +164,31 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.AddCollaboratorOption) - collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam(":collaborator")) + 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 } @@ -226,18 +227,18 @@ func DeleteCollaborator(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam(":collaborator")) + 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 && !strings.EqualFold(ctx.Doer.LowerName, collaboratorUsername) && !ctx.IsUserRepoAdmin() { + ctx.APIError(http.StatusForbidden, "Only admins can query all permissions, repo admins can query all repo permissions, collaborators can query only their own") return } - collaborator, err := user_model.GetUserByName(ctx, 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 788c75fab2..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" @@ -63,9 +63,9 @@ func GetSingleCommit(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - sha := ctx.PathParam(":sha") + 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 } } @@ -312,15 +339,15 @@ func DownloadCommitDiffOrPatch(ctx *context.APIContext) { // "$ref": "#/responses/string" // "404": // "$ref": "#/responses/notFound" - sha := ctx.PathParam(":sha") - diffType := git.RawDiffType(ctx.PathParam(":diffType")) + sha := ctx.PathParam("sha") + diffType := git.RawDiffType(ctx.PathParam("diffType")) if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, diffType, ctx.Resp); err != nil { if git.IsErrNotExist(err) { - ctx.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 1678bc033c..6d427c8073 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -44,13 +44,12 @@ func CompareDiff(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if ctx.Repo.GitRepo == nil { - gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + 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 } - ctx.Repo.GitRepo = gitRepo - defer gitRepo.Close() } infoPath := ctx.PathParam("*") @@ -83,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 3620c1465f..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" @@ -17,35 +16,34 @@ func DownloadArchive(ctx *context.APIContext) { var tp git.ArchiveType switch ballType := ctx.PathParam("ball_type"); ballType { case "tarball": - tp = git.TARGZ + tp = git.ArchiveTarGz case "zipball": - tp = git.ZIP + tp = git.ArchiveZip case "bundle": - tp = git.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 } if ctx.Repo.GitRepo == nil { - gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + 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 } - ctx.Repo.GitRepo = gitRepo - defer gitRepo.Close() } - r, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"), tp) + 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 83848b7add..a85dda79d0 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -11,22 +11,23 @@ import ( "fmt" "io" "net/http" - "path" "strings" "time" 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" @@ -61,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: @@ -73,7 +74,7 @@ func GetRawFile(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if ctx.Repo.Repository.IsEmpty { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -84,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) } } @@ -114,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: @@ -126,7 +127,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if ctx.Repo.Repository.IsEmpty { - ctx.NotFound() + ctx.APIErrorNotFound() return } @@ -138,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 } @@ -180,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 } @@ -189,7 +190,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid) // If there isn't one, just serve the data directly - if err == git_model.ErrLFSObjectNotExist { + if errors.Is(err, git_model.ErrLFSObjectNotExist) { // Handle caching for the blob SHA (not the LFS object OID) if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { return @@ -198,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 } @@ -209,7 +210,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { if setting.LFS.Storage.ServeDirect() { // If we have a signed url (S3, object storage), redirect to this directly. - u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name(), nil) + u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name(), ctx.Req.Method, nil) if u != nil && err == nil { ctx.Redirect(u.String()) return @@ -218,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() @@ -230,31 +231,26 @@ 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 } - info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:]) + latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetCommitsInfo", err) + ctx.APIErrorInternal(err) return nil, nil, nil } + when := &latestCommit.Committer.When - if len(info) == 1 { - // Not Modified - lastModified = &info[0].Commit.Committer.When - } - blob = entry.Blob() - - return blob, entry, lastModified + return entry.Blob(), entry, when } // GetArchive get archive of a repository @@ -287,41 +283,33 @@ func GetArchive(ctx *context.APIContext) { // "$ref": "#/responses/notFound" if ctx.Repo.GitRepo == nil { - gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) + 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 } - ctx.Repo.GitRepo = gitRepo - defer gitRepo.Close() } archiveDownload(ctx) } func archiveDownload(ctx *context.APIContext) { - uri := ctx.PathParam("*") - ext, tp, err := archiver_service.ParseFileName(uri) - if err != nil { - ctx.Error(http.StatusBadRequest, "ParseFileName", err) - return - } - - aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, strings.TrimSuffix(uri, ext), tp) + 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 } @@ -343,7 +331,7 @@ func download(ctx *context.APIContext, archiveName string, archiver *repo_model. rPath := archiver.RelativePath() if setting.RepoArchive.Storage.ServeDirect() { // If we have a signed url (S3, object storage), redirect to this directly. - u, err := storage.RepoArchives.URL(rPath, downloadName, nil) + u, err := storage.RepoArchives.URL(rPath, downloadName, ctx.Req.Method, nil) if u != nil && err == nil { ctx.Redirect(u.String()) return @@ -353,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() @@ -389,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: @@ -401,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 } @@ -411,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 { @@ -437,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 @@ -473,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, @@ -494,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) } @@ -576,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) @@ -673,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 @@ -776,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) @@ -834,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: @@ -933,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: @@ -941,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: @@ -984,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: @@ -996,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 0887a90096..487c74e183 100644 --- a/routers/api/v1/repo/git_hook.go +++ b/routers/api/v1/repo/git_hook.go @@ -4,6 +4,7 @@ package repo import ( + "errors" "net/http" "code.gitea.io/gitea/modules/git" @@ -39,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 } @@ -79,13 +80,13 @@ func GetGitHook(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - hookID := ctx.PathParam(":id") + hookID := ctx.PathParam("id") hook, err := ctx.Repo.GitRepo.GetHook(hookID) if err != nil { - if err == git.ErrNotValidHook { - ctx.NotFound() + if errors.Is(err, git.ErrNotValidHook) { + ctx.APIErrorNotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetHook", err) + ctx.APIErrorInternal(err) } return } @@ -126,20 +127,20 @@ func EditGitHook(ctx *context.APIContext) { // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.EditGitHookOption) - hookID := ctx.PathParam(":id") + hookID := ctx.PathParam("id") hook, err := ctx.Repo.GitRepo.GetHook(hookID) if err != nil { - if err == git.ErrNotValidHook { - ctx.NotFound() + if errors.Is(err, git.ErrNotValidHook) { + 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 } @@ -175,20 +176,20 @@ func DeleteGitHook(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - hookID := ctx.PathParam(":id") + hookID := ctx.PathParam("id") hook, err := ctx.Repo.GitRepo.GetHook(hookID) if err != nil { - if err == git.ErrNotValidHook { - ctx.NotFound() + if errors.Is(err, git.ErrNotValidHook) { + 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 9ef57da1b9..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 } } @@ -109,14 +109,14 @@ func GetHook(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo - hookID := ctx.PathParamInt64(":id") + hookID := ctx.PathParamInt64("id") hook, err := utils.GetRepoHook(ctx, repo.Repository.ID, hookID) if err != nil { return } apiHook, err := webhook_service.ToHook(repo.RepoLink, hook) if err != nil { - ctx.InternalServerError(err) + ctx.APIErrorInternal(err) return } ctx.JSON(http.StatusOK, apiHook) @@ -168,7 +168,7 @@ func TestHook(ctx *context.APIContext) { ref = r } - hookID := ctx.PathParamInt64(":id") + hookID := ctx.PathParamInt64("id") hook, err := utils.GetRepoHook(ctx, ctx.Repo.Repository.ID, hookID) if err != nil { return @@ -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 } @@ -263,7 +263,7 @@ func EditHook(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.EditHookOption) - hookID := ctx.PathParamInt64(":id") + hookID := ctx.PathParamInt64("id") utils.EditRepoHook(ctx, form, hookID) } @@ -296,11 +296,11 @@ func DeleteHook(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "404": // "$ref": "#/responses/notFound" - if err := webhook.DeleteWebhookByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")); err != nil { + 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 c2f3a972ef..f8d61ccf00 100644 --- a/routers/api/v1/repo/hook_test.go +++ b/routers/api/v1/repo/hook_test.go @@ -18,12 +18,12 @@ func TestTestHook(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/wiki/_pages") - ctx.SetPathParam(":id", "1") + ctx.SetPathParam("id", "1") contexttest.LoadRepo(t, ctx, 1) 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 cbe709c030..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 } @@ -613,17 +613,17 @@ func GetIssue(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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,22 +723,22 @@ 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 } if form.Closed { - if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", true); err != nil { + 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)) @@ -793,12 +793,12 @@ func EditIssue(ctx *context.APIContext) { // "$ref": "#/responses/error" form := web.GetForm(ctx).(*api.EditIssueOption) - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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,54 +895,47 @@ 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 } } - var isClosed bool - switch state := api.StateType(*form.State); state { - case api.StateOpen: - isClosed = false - case api.StateClosed: - isClosed = true - default: - ctx.Error(http.StatusPreconditionFailed, "UnknownIssueStateError", fmt.Sprintf("unknown state: %s", state)) + state := api.StateType(*form.State) + closeOrReopenIssue(ctx, issue, state) + if ctx.Written() { return } - - if issue.IsClosed != isClosed { - if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { - if issues_model.IsErrDependenciesLeft(err) { - ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies") - return - } - ctx.Error(http.StatusInternalServerError, "ChangeStatus", err) - return - } - } } // 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)) @@ -976,18 +969,18 @@ func DeleteIssue(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } @@ -1032,26 +1025,49 @@ func UpdateIssueDeadline(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.EditDeadlineOption) - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: deadlineUnix.AsTimePtr()}) } + +func closeOrReopenIssue(ctx *context.APIContext, issue *issues_model.Issue, state api.StateType) { + if state != api.StateOpen && state != api.StateClosed { + 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.APIError(http.StatusPreconditionFailed, "cannot close this issue or pull request because it still has open dependencies") + return + } + ctx.APIErrorInternal(err) + return + } + } else if state == api.StateOpen && issue.IsClosed { + if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil { + 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 f9b5aa816b..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")) + 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")) + 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 { @@ -380,28 +380,28 @@ func CreateIssueComment(ctx *context.APIContext) { // "$ref": "#/responses/repoArchivedError" form := web.GetForm(ctx).(*api.CreateIssueCommentOption) - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } @@ -445,18 +445,18 @@ func GetIssueComment(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) + 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 } @@ -579,18 +579,18 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) { } func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) { - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) + comment, err := issues_model.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)) @@ -696,18 +698,18 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) { } func deleteIssueComment(ctx *context.APIContext) { - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) + 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 ae7502c661..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")) + 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 @@ -499,12 +493,12 @@ func RemoveIssueBlocking(ctx *context.APIContext) { } func getParamsIssue(ctx *context.APIContext) *issues_model.Issue { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 cc517619e9..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" @@ -47,18 +47,18 @@ func ListIssueLabels(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } @@ -163,12 +163,12 @@ func DeleteIssueLabel(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } @@ -178,18 +178,18 @@ func DeleteIssueLabel(ctx *context.APIContext) { return } - label, err := issues_model.GetLabelByID(ctx, ctx.PathParamInt64(":id")) + 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 } @@ -285,12 +285,12 @@ func ClearIssueLabels(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } @@ -309,19 +309,19 @@ func ClearIssueLabels(ctx *context.APIContext) { } func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (*issues_model.Issue, []*issues_model.Label, error) { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 ( @@ -335,23 +335,35 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) labelIDs = append(labelIDs, int64(rv.Float())) case reflect.String: labelNames = append(labelNames, rv.String()) + default: + ctx.APIError(http.StatusBadRequest, "a label must be an integer or a string") + return nil, nil, errors.New("invalid label") } } if len(labelIDs) > 0 && len(labelNames) > 0 { - ctx.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 { - labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames) + 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.APIErrorInternal(err) + return nil, nil, err + } + labelIDs = append(labelIDs, orgLabelIDs...) + } } labels, err := issues_model.GetLabelsByIDs(ctx, labelIDs, "id", "repo_id", "org_id", "name", "exclusive") if err != nil { - ctx.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 0ef9033291..71985ac765 100644 --- a/routers/api/v1/repo/issue_pin.go +++ b/routers/api/v1/repo/issue_pin.go @@ -41,14 +41,14 @@ func PinIssue(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } @@ -98,12 +98,12 @@ func UnpinIssue(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } @@ -159,19 +159,19 @@ func MoveIssuePin(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 8d43cd518b..e535b5e009 100644 --- a/routers/api/v1/repo/issue_reaction.go +++ b/routers/api/v1/repo/issue_reaction.go @@ -51,39 +51,39 @@ func GetIssueCommentReactions(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) + 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 } @@ -188,33 +188,33 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) { } func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) { - comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64(":id")) + 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 @@ -295,29 +295,29 @@ func GetIssueReactions(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } @@ -419,18 +419,18 @@ func DeleteIssueReaction(ctx *context.APIContext) { } func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) { - issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 4605ae2110..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) { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) +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 e51baad0b6..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: @@ -104,23 +104,23 @@ func DelIssueSubscription(ctx *context.APIContext) { } func setIssueSubscription(ctx *context.APIContext, watch bool) { - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } - user, err := user_model.GetUserByName(ctx, ctx.PathParam(":user")) + 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 } @@ -185,12 +185,12 @@ func CheckIssueSubscription(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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{ @@ -251,12 +251,12 @@ func GetIssueSubscribers(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 8d5e9fdad4..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")) + 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 } @@ -181,19 +181,19 @@ func AddTime(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.AddTimeOption) - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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)) @@ -264,12 +264,12 @@ func ResetIssueTime(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } @@ -337,12 +337,12 @@ func DeleteTime(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } @@ -356,17 +356,17 @@ func DeleteTime(ctx *context.APIContext) { return } - time, err := issues_model.GetTrackedTimeByID(ctx, ctx.PathParamInt64(":id")) + 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")) + 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 060694d085..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]) @@ -143,24 +143,24 @@ func GetDeployKey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - key, err := asymkey_model.GetDeployKeyByID(ctx, ctx.PathParamInt64(":id")) + 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) } } @@ -279,11 +279,11 @@ func DeleteDeploykey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := asymkey_service.DeleteDeployKey(ctx, ctx.Repo.Repository, ctx.PathParamInt64(":id")); err != nil { + 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 c2c43db6a4..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 } @@ -99,7 +99,7 @@ func GetLabel(ctx *context.APIContext) { l *issues_model.Label err error ) - strID := ctx.PathParam(":id") + strID := ctx.PathParam("id") if intID, err2 := strconv.ParseInt(strID, 10, 64); err2 != nil { l, err = issues_model.GetLabelInRepoByName(ctx, ctx.Repo.Repository.ID, strID) } else { @@ -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 } @@ -212,12 +212,12 @@ func EditLabel(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.EditLabelOption) - l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")) + 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 } @@ -276,8 +276,8 @@ func DeleteLabel(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := issues_model.DeleteLabel(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")); err != nil { - ctx.Error(http.StatusInternalServerError, "DeleteLabel", err) + if err := issues_model.DeleteLabel(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id")); err != nil { + 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 78907c85a5..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) @@ -280,7 +280,7 @@ func DeleteMilestone(ctx *context.APIContext) { // getMilestoneByIDOrName get milestone by ID and if not available by name func getMilestoneByIDOrName(ctx *context.APIContext) *issues_model.Milestone { - mile := ctx.PathParam(":id") + mile := ctx.PathParam("id") mileID, _ := strconv.ParseInt(mile, 0, 64) if mileID != 0 { @@ -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 047203501e..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,27 +218,27 @@ 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 } - mirrorName := ctx.PathParam(":name") + mirrorName := ctx.PathParam("name") // Get push mirror of a specific repo by remoteName pushMirror, exist, err := db.Get[repo_model.PushMirror](ctx, repo_model.PushMirrorOptions{ RepoID: ctx.Repo.Repository.ID, RemoteName: mirrorName, }.ToConds()) if err != nil { - ctx.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,15 +319,15 @@ 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 } - remoteName := ctx.PathParam(":name") + remoteName := ctx.PathParam("name") // Delete push mirror on repo by name. err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: remoteName}) if err != nil { - ctx.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 8689d25e15..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" @@ -52,9 +52,9 @@ func GetNote(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - sha := ctx.PathParam(":sha") + 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 71c4c81b67..09729200d5 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" @@ -51,7 +52,7 @@ func ListPullRequests(ctx *context.APIContext) { // parameters: // - name: owner // in: path - // description: Owner of the repo + // description: owner of the repo // type: string // required: true // - name: repo @@ -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 } @@ -179,24 +185,28 @@ func GetPullRequest(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } @@ -264,24 +274,28 @@ func GetPullRequestByBaseHead(ctx *context.APIContext) { headBranch = head } - pr, err := issues_model.GetPullRequestByBaseHeadInfo(ctx, ctx.Repo.Repository.ID, headRepoID, ctx.PathParam(":base"), headBranch) + 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)) } @@ -324,17 +338,17 @@ func DownloadPullDiffOrPatch(ctx *context.APIContext) { // "$ref": "#/responses/string" // "404": // "$ref": "#/responses/notFound" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } var patch bool - if ctx.PathParam(":diffType") == "diff" { + if ctx.PathParam("diffType") == "diff" { patch = false } else { patch = true @@ -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 } @@ -603,26 +617,26 @@ func EditPullRequest(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.EditPullRequestOption) - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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,58 +736,42 @@ 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 } - var isClosed bool - switch state := api.StateType(*form.State); state { - case api.StateOpen: - isClosed = false - case api.StateClosed: - isClosed = true - default: - ctx.Error(http.StatusPreconditionFailed, "UnknownPRStateError", fmt.Sprintf("unknown state: %s", state)) + state := api.StateType(*form.State) + closeOrReopenIssue(ctx, issue, state) + if ctx.Written() { return } - - if issue.IsClosed != isClosed { - if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil { - if issues_model.IsErrDependenciesLeft(err) { - ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies") - return - } - ctx.Error(http.StatusInternalServerError, "ChangeStatus", err) - return - } - } } // 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) @@ -778,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 } } @@ -790,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 } @@ -831,12 +834,12 @@ func IsPullRequestMerged(ctx *context.APIContext) { // "404": // description: pull request has not been merged - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } @@ -844,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 @@ -889,23 +892,23 @@ func MergePullRequest(ctx *context.APIContext) { form := web.GetForm(ctx).(*forms.MergePullRequestForm) - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 @@ -913,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 } } @@ -931,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 } @@ -954,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) @@ -976,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 } } @@ -987,13 +990,13 @@ func MergePullRequest(ctx *context.APIContext) { } if form.MergeWhenChecksSucceed { - scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message) + 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 ... @@ -1004,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) @@ -1015,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 } @@ -1040,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 { @@ -1054,32 +1057,25 @@ 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() } - if err := pull_service.RetargetChildrenOnMerge(ctx, ctx.Doer, pr); err != nil { - ctx.Error(http.StatusInternalServerError, "RetargetChildrenOnMerge", err) - return - } - if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, headRepo, pr.HeadBranch); err != nil { + + 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 } - if err := issues_model.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil { - // Do not fail here as branch has already been deleted - log.Error("DeleteBranch: %v", err) - } } } @@ -1115,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 } @@ -1133,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. @@ -1155,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() } @@ -1169,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 } @@ -1183,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 } @@ -1201,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 } @@ -1256,37 +1252,37 @@ func UpdatePullRequest(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } @@ -1294,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 } @@ -1306,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 } @@ -1355,41 +1351,41 @@ func CancelScheduledAutoMerge(ctx *context.APIContext) { // "423": // "$ref": "#/responses/repoArchivedError" - pullIndex := ctx.PathParamInt64(":index") + pullIndex := ctx.PathParamInt64("index") 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) } @@ -1441,36 +1437,36 @@ func GetPullRequestCommits(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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() if pr.HasMerged { - prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.MergeBase, pr.GetGitRefName(), false, false) + prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.MergeBase, pr.GetGitHeadRefName(), false, false) } else { - prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName(), false, false) + prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitHeadRefName(), false, false) } if err != nil { - ctx.ServerError("GetCompareInfo", err) + ctx.APIErrorInternal(err) return } commits := prInfo.Commits @@ -1499,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) @@ -1564,23 +1560,23 @@ func GetPullRequestFiles(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } @@ -1588,18 +1584,18 @@ func GetPullRequestFiles(ctx *context.APIContext) { var prInfo *git.CompareInfo if pr.HasMerged { - prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.MergeBase, pr.GetGitRefName(), true, false) + prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.MergeBase, pr.GetGitHeadRefName(), true, false) } else { - prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName(), true, false) + prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitHeadRefName(), true, false) } if err != nil { - ctx.ServerError("GetCompareInfo", err) + ctx.APIErrorInternal(err) return } - headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName()) + headCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitHeadRefName()) if err != nil { - ctx.ServerError("GetRefCommitID", err) + ctx.APIErrorInternal(err) return } @@ -1609,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, @@ -1620,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() @@ -1637,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 def860eee8..3c00193fac 100644 --- a/routers/api/v1/repo/pull_review.go +++ b/routers/api/v1/repo/pull_review.go @@ -61,23 +61,23 @@ func ListPullReviews(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } @@ -306,12 +306,12 @@ func CreatePullReview(ctx *context.APIContext) { // "$ref": "#/responses/validationError" opts := web.GetForm(ctx).(*api.CreatePullReviewOptions) - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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()) + headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitHeadRefName()) 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()) + headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pr.GetGitHeadRefName()) 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 } @@ -533,40 +533,40 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest // prepareSingleReview return review, related pull and false or nil, nil and true if an error happen func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues_model.PullRequest, bool) { - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 } - review, err := issues_model.GetReviewByID(ctx, ctx.PathParamInt64(":id")) + 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 } @@ -698,24 +698,24 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN } func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) { - pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + 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 141f812172..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" @@ -50,19 +51,19 @@ func GetRelease(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - id := ctx.PathParamInt64(":id") + 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 } } @@ -319,14 +322,14 @@ func EditRelease(ctx *context.APIContext) { // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.EditReleaseOption) - id := ctx.PathParamInt64(":id") + 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)) @@ -396,22 +399,22 @@ func DeleteRelease(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - id := ctx.PathParamInt64(":id") + 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 ed6cc8e1ea..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 @@ -72,24 +72,24 @@ func GetReleaseAttachment(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - releaseID := ctx.PathParamInt64(":id") + releaseID := ctx.PathParamInt64("id") if !checkReleaseMatchRepo(ctx, releaseID) { return } - attachID := ctx.PathParamInt64(":attachment_id") + attachID := ctx.PathParamInt64("attachment_id") 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 @@ -126,22 +126,22 @@ func ListReleaseAttachments(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - releaseID := ctx.PathParamInt64(":id") + releaseID := ctx.PathParamInt64("id") 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,12 +194,12 @@ 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 } // Check if release exists an load release - releaseID := ctx.PathParamInt64(":id") + releaseID := ctx.PathParamInt64("id") if !checkReleaseMatchRepo(ctx, releaseID) { 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 } @@ -299,24 +299,24 @@ func EditReleaseAttachment(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.EditAttachmentOptions) // Check if release exists an load release - releaseID := ctx.PathParamInt64(":id") + releaseID := ctx.PathParamInt64("id") if !checkReleaseMatchRepo(ctx, releaseID) { return } - attachID := ctx.PathParamInt64(":attachment_id") + attachID := ctx.PathParamInt64("attachment_id") 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)) @@ -372,30 +372,30 @@ func DeleteReleaseAttachment(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // Check if release exists an load release - releaseID := ctx.PathParamInt64(":id") + releaseID := ctx.PathParamInt64("id") if !checkReleaseMatchRepo(ctx, releaseID) { return } - attachID := ctx.PathParamInt64(":attachment_id") + attachID := ctx.PathParamInt64("attachment_id") 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 99f7a8cbf2..b5e7d83b2a 100644 --- a/routers/api/v1/repo/release_tags.go +++ b/routers/api/v1/repo/release_tags.go @@ -41,25 +41,25 @@ func GetReleaseByTag(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - tag := ctx.PathParam(":tag") + tag := ctx.PathParam("tag") 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)) @@ -94,29 +94,29 @@ func DeleteReleaseByTag(ctx *context.APIContext) { // "422": // "$ref": "#/responses/validationError" - tag := ctx.PathParam(":tag") + tag := ctx.PathParam("tag") 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 40990a28cb..e69b7729a0 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) @@ -329,7 +329,7 @@ func Generate(ctx *context.APIContext) { // parameters: // - name: template_owner // in: path - // description: name of the template repository owner + // description: owner of the template repository // type: string // required: true // - name: template_repo @@ -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 } @@ -495,28 +495,28 @@ func CreateOrgRepo(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" opt := web.GetForm(ctx).(*api.CreateRepoOption) - org, err := organization.GetOrgByName(ctx, ctx.PathParam(":org")) + 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 } @@ -575,22 +575,22 @@ func GetByID(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - repo, err := repo_model.GetRepositoryByID(ctx, ctx.PathParamInt64(":id")) + 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 } @@ -669,17 +669,17 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err newRepoName = *opts.Name } // Check if repository name has been changed and not just a case change - if repo.LowerName != strings.ToLower(newRepoName) { + if !strings.EqualFold(repo.LowerName, newRepoName) { 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 } @@ -726,20 +726,19 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err if ctx.Repo.GitRepo == nil && !repo.IsEmpty { var err error - ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, repo) + ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) if err != nil { - ctx.Error(http.StatusInternalServerError, "Unable to OpenRepository", err) + ctx.APIErrorInternal(err) return err } - defer ctx.Repo.GitRepo.Close() } // 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 @@ -748,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 } @@ -756,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 } } @@ -773,22 +772,17 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { var units []repo_model.RepoUnit var deleteUnitTypes []unit_model.Type - currHasIssues := repo.UnitEnabled(ctx, unit_model.TypeIssues) - newHasIssues := currHasIssues if opts.HasIssues != nil { - newHasIssues = *opts.HasIssues - } - if currHasIssues || newHasIssues { - if newHasIssues && opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() { + if *opts.HasIssues && 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 } @@ -803,7 +797,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { }, }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues) - } else if newHasIssues && opts.ExternalTracker == nil && !unit_model.TypeIssues.UnitGlobalDisabled() { + } else if *opts.HasIssues && opts.ExternalTracker == nil && !unit_model.TypeIssues.UnitGlobalDisabled() { // Default to built-in tracker var config *repo_model.IssuesConfig @@ -830,7 +824,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { Config: config, }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) - } else if !newHasIssues { + } else if !*opts.HasIssues { if !unit_model.TypeExternalTracker.UnitGlobalDisabled() { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) } @@ -840,17 +834,12 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { } } - currHasWiki := repo.UnitEnabled(ctx, unit_model.TypeWiki) - newHasWiki := currHasWiki if opts.HasWiki != nil { - newHasWiki = *opts.HasWiki - } - if currHasWiki || newHasWiki { - if newHasWiki && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() { + if *opts.HasWiki && 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 } @@ -862,7 +851,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { }, }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki) - } else if newHasWiki && opts.ExternalWiki == nil && !unit_model.TypeWiki.UnitGlobalDisabled() { + } else if *opts.HasWiki && opts.ExternalWiki == nil && !unit_model.TypeWiki.UnitGlobalDisabled() { config := &repo_model.UnitConfig{} units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, @@ -870,7 +859,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { Config: config, }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) - } else if !newHasWiki { + } else if !*opts.HasWiki { if !unit_model.TypeExternalWiki.UnitGlobalDisabled() { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki) } @@ -880,13 +869,20 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { } } - currHasPullRequests := repo.UnitEnabled(ctx, unit_model.TypePullRequests) - newHasPullRequests := currHasPullRequests - if opts.HasPullRequests != nil { - newHasPullRequests = *opts.HasPullRequests + if opts.HasCode != nil && !unit_model.TypeCode.UnitGlobalDisabled() { + if *opts.HasCode { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeCode, + Config: &repo_model.UnitConfig{}, + }) + } else { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode) + } } - if currHasPullRequests || newHasPullRequests { - if newHasPullRequests && !unit_model.TypePullRequests.UnitGlobalDisabled() { + + if opts.HasPullRequests != nil && !unit_model.TypePullRequests.UnitGlobalDisabled() { + if *opts.HasPullRequests { // We do allow setting individual PR settings through the API, so // we get the config settings and then set them // if those settings were provided in the opts. @@ -954,18 +950,13 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { Type: unit_model.TypePullRequests, Config: config, }) - } else if !newHasPullRequests && !unit_model.TypePullRequests.UnitGlobalDisabled() { + } else { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests) } } - currHasProjects := repo.UnitEnabled(ctx, unit_model.TypeProjects) - newHasProjects := currHasProjects - if opts.HasProjects != nil { - newHasProjects = *opts.HasProjects - } - if currHasProjects || newHasProjects { - if newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() { + if opts.HasProjects != nil && !unit_model.TypeProjects.UnitGlobalDisabled() { + if *opts.HasProjects { unit, err := repo.GetUnit(ctx, unit_model.TypeProjects) var config *repo_model.ProjectsConfig if err != nil { @@ -985,7 +976,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { Type: unit_model.TypeProjects, Config: config, }) - } else if !newHasProjects && !unit_model.TypeProjects.UnitGlobalDisabled() { + } else { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) } } @@ -1025,7 +1016,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 } } @@ -1040,24 +1031,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) { @@ -1085,7 +1076,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 } @@ -1095,14 +1086,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 } @@ -1121,7 +1112,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 } @@ -1159,10 +1150,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 } @@ -1171,7 +1162,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 } @@ -1316,7 +1307,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 fe0910c735..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)) @@ -357,15 +357,15 @@ func GetTagProtection(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo.Repository - id := ctx.PathParamInt64(":id") + 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 } @@ -521,15 +521,15 @@ func EditTagProtection(ctx *context.APIContext) { repo := ctx.Repo.Repository form := web.GetForm(ctx).(*api.EditTagProtectionOption) - id := ctx.PathParamInt64(":id") + 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 } @@ -616,21 +616,21 @@ func DeleteTagProtection(ctx *context.APIContext) { // "$ref": "#/responses/notFound" repo := ctx.Repo.Repository - id := ctx.PathParamInt64(":id") + 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 42fb0a1d75..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 } @@ -221,13 +221,13 @@ func changeRepoTeam(ctx *context.APIContext, add bool) { } func getTeamByParam(ctx *context.APIContext) *organization.Team { - team, err := organization.GetTeam(ctx, ctx.Repo.Owner.ID, ctx.PathParam(":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 6b9eedf6e0..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 } @@ -162,7 +162,7 @@ func AddTopic(ctx *context.APIContext) { // "422": // "$ref": "#/responses/invalidTopicsError" - topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam(":topic"))) + topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic"))) if !repo_model.ValidateTopic(topicName) { ctx.JSON(http.StatusUnprocessableEntity, map[string]any{ @@ -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 } @@ -229,7 +229,7 @@ func DeleteTopic(ctx *context.APIContext) { // "422": // "$ref": "#/responses/invalidTopicsError" - topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam(":topic"))) + topicName := strings.TrimSpace(strings.ToLower(ctx.PathParam("topic"))) if !repo_model.ValidateTopic(topicName) { ctx.JSON(http.StatusUnprocessableEntity, map[string]any{ @@ -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 787ec34404..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 } @@ -100,27 +101,24 @@ func Transfer(ctx *context.APIContext) { } if ctx.Repo.GitRepo != nil { - ctx.Repo.GitRepo.Close() + _ = ctx.Repo.GitRepo.Close() ctx.Repo.GitRepo = nil } oldFullname := ctx.Repo.Repository.FullName() if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil { - 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 efb247c19e..dfd69600fb 100644 --- a/routers/api/v1/repo/tree.go +++ b/routers/api/v1/repo/tree.go @@ -56,13 +56,13 @@ func GetTree(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - sha := ctx.PathParam(":sha") + 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 f9906ed250..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 } @@ -136,7 +136,7 @@ func EditWikiPage(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateWikiPageOptions) - oldWikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName")) + oldWikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName")) newWikiName := wiki_service.UserTitleToWebPath("", form.Title) if len(newWikiName) == 0 { @@ -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 } @@ -242,14 +242,14 @@ func DeleteWikiPage(ctx *context.APIContext) { // "423": // "$ref": "#/responses/repoArchivedError" - wikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName")) + wikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName")) if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil { if err.Error() == "file does not exist" { - ctx.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)) @@ -370,7 +367,7 @@ func GetWikiPage(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // get requested pagename - pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName")) + pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName")) wikiPage := getWikiPage(ctx, pageName) if !ctx.Written() { @@ -420,7 +417,7 @@ func ListPageRevisions(ctx *context.APIContext) { } // get requested pagename - pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName")) + pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName")) if len(pageName) == 0 { pageName = "Home" } @@ -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..0606505950 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.ActionWorkflowResponse `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 b9d2a0217c..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,8 +449,76 @@ 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 Body api.Compare `json:"body"` } + +// swagger:response MergeUpstreamRequest +type swaggerMergeUpstreamRequest struct { + // in:body + Body api.MergeUpstreamRequest `json:"body"` +} + +// swagger:response MergeUpstreamResponse +type swaggerMergeUpstreamResponse struct { + // in:body + Body api.MergeUpstreamResponse `json:"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 9583bb548c..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 @@ -165,7 +167,7 @@ func DeleteAccessToken(ctx *context.APIContext) { // "422": // "$ref": "#/responses/error" - token := ctx.PathParam(":id") + token := ctx.PathParam("id") tokenID, _ := strconv.ParseInt(token, 0, 64) if tokenID == 0 { @@ -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 } @@ -306,12 +308,12 @@ func DeleteOauth2Application(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "404": // "$ref": "#/responses/notFound" - appID := ctx.PathParamInt64(":id") + 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 } @@ -338,18 +340,18 @@ func GetOauth2Application(ctx *context.APIContext) { // "$ref": "#/responses/OAuth2Application" // "404": // "$ref": "#/responses/notFound" - appID := ctx.PathParamInt64(":id") + appID := ctx.PathParamInt64("id") 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 } @@ -382,7 +384,7 @@ func UpdateOauth2Application(ctx *context.APIContext) { // "$ref": "#/responses/OAuth2Application" // "404": // "$ref": "#/responses/notFound" - appID := ctx.PathParamInt64(":id") + appID := ctx.PathParamInt64("id") data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions) @@ -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 6abb70de19..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: @@ -201,7 +201,7 @@ func CheckFollowing(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - target := GetUserByParamsName(ctx, ":target") + target := GetUserByPathParam(ctx, "target") // FIXME: it is not right to call this function, it should load the "target" directly if ctx.Written() { return } @@ -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 ba5c0fdc45..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 @@ -116,17 +116,17 @@ func GetGPGKey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - key, err := asymkey_model.GetGPGKeyForUserByID(ctx, ctx.Doer.ID, ctx.PathParamInt64(":id")) + 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 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 23a526cd67..f49bbbd6db 100644 --- a/routers/api/v1/user/helper.go +++ b/routers/api/v1/user/helper.go @@ -4,14 +4,13 @@ package user import ( - "net/http" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/services/context" ) -// GetUserByParamsName get user by name -func GetUserByParamsName(ctx *context.APIContext, name string) *user_model.User { +// GetUserByPathParam get user by the path param name +// it will redirect to the user's new name if the user's name has been changed +func GetUserByPathParam(ctx *context.APIContext, name string) *user_model.User { username := ctx.PathParam(name) user, err := user_model.GetUserByName(ctx, username) if err != nil { @@ -19,17 +18,17 @@ func GetUserByParamsName(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 } return user } -// GetUserByParams returns user whose name is presented in URL (":username"). -func GetUserByParams(ctx *context.APIContext) *user_model.User { - return GetUserByParamsName(ctx, ":username") +// GetContextUserByPathParam returns user whose name is presented in URL (path param "username"). +func GetContextUserByPathParam(ctx *context.APIContext) *user_model.User { + return GetUserByPathParam(ctx, "username") } 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 e4278c2ec0..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 @@ -179,12 +180,12 @@ func GetPublicKey(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - key, err := asymkey_model.GetPublicKeyByID(ctx, ctx.PathParamInt64(":id")) + 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,31 +271,31 @@ 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 } - id := ctx.PathParamInt64(":id") + id := ctx.PathParamInt64("id") 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 e668326861..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() + }, + }) +} |