aboutsummaryrefslogtreecommitdiffstats
path: root/routers/api
diff options
context:
space:
mode:
Diffstat (limited to 'routers/api')
-rw-r--r--routers/api/packages/api.go234
-rw-r--r--routers/api/packages/conda/conda.go26
-rw-r--r--routers/api/packages/container/blob.go8
-rw-r--r--routers/api/packages/container/container.go6
-rw-r--r--routers/api/packages/container/manifest.go32
-rw-r--r--routers/api/v1/admin/action.go93
-rw-r--r--routers/api/v1/api.go78
-rw-r--r--routers/api/v1/org/action.go90
-rw-r--r--routers/api/v1/repo/action.go263
-rw-r--r--routers/api/v1/repo/blob.go2
-rw-r--r--routers/api/v1/repo/branch.go2
-rw-r--r--routers/api/v1/repo/file.go401
-rw-r--r--routers/api/v1/repo/issue_stopwatch.go56
-rw-r--r--routers/api/v1/repo/patch.go68
-rw-r--r--routers/api/v1/repo/wiki.go2
-rw-r--r--routers/api/v1/shared/action.go187
-rw-r--r--routers/api/v1/swagger/repo.go34
-rw-r--r--routers/api/v1/user/action.go84
-rw-r--r--routers/api/v1/utils/hook.go1
19 files changed, 1100 insertions, 567 deletions
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index 41e89ae567..f65c4b99ff 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"
@@ -282,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() {
@@ -358,60 +324,15 @@ func CommonRoutes() *web.Router {
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/go", func() {
r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage)
- r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) {
- ctx.Status(http.StatusNotFound)
- })
+ r.Get("/sumdb/sum.golang.org/supported", http.NotFound)
- // Manual mapping of routes because the package name contains slashes which chi does not support
// https://go.dev/ref/mod#goproxy-protocol
- r.Get("/*", func(ctx *context.Context) {
- path := ctx.PathParam("*")
-
- if strings.HasSuffix(path, "/@latest") {
- ctx.SetPathParam("name", path[:len(path)-len("/@latest")])
- ctx.SetPathParam("version", "latest")
-
- goproxy.PackageVersionMetadata(ctx)
- return
- }
-
- parts := strings.SplitN(path, "/@v/", 2)
- if len(parts) != 2 {
- ctx.Status(http.StatusNotFound)
- return
- }
-
- ctx.SetPathParam("name", parts[0])
-
- // <package/name>/@v/list
- if parts[1] == "list" {
- goproxy.EnumeratePackageVersions(ctx)
- return
- }
-
- // <package/name>/@v/<version>.zip
- if strings.HasSuffix(parts[1], ".zip") {
- ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".zip")])
-
- goproxy.DownloadPackageFile(ctx)
- return
- }
- // <package/name>/@v/<version>.info
- if strings.HasSuffix(parts[1], ".info") {
- ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".info")])
-
- goproxy.PackageVersionMetadata(ctx)
- return
- }
- // <package/name>/@v/<version>.mod
- if strings.HasSuffix(parts[1], ".mod") {
- ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".mod")])
-
- goproxy.PackageVersionGoModContent(ctx)
- return
- }
-
- ctx.Status(http.StatusNotFound)
+ r.PathGroup("/*", func(g *web.RouterPathGroup) {
+ g.MatchPath("GET", "/<name:*>/@<version:latest>", goproxy.PackageVersionMetadata)
+ g.MatchPath("GET", "/<name:*>/@v/list", goproxy.EnumeratePackageVersions)
+ g.MatchPath("GET", "/<name:*>/@v/<version>.zip", goproxy.DownloadPackageFile)
+ g.MatchPath("GET", "/<name:*>/@v/<version>.info", goproxy.PackageVersionMetadata)
+ g.MatchPath("GET", "/<name:*>/@v/<version>.mod", goproxy.PackageVersionGoModContent)
})
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/generic", func() {
@@ -532,82 +453,24 @@ 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 == http.MethodHead
- isGetHead := ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet
- isPut := ctx.Req.Method == http.MethodPut
- isDelete := ctx.Req.Method == http.MethodDelete
-
- 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)
+ g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>", 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)
@@ -621,6 +484,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)
@@ -632,31 +496,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)
@@ -693,6 +538,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)
@@ -703,18 +550,13 @@ func ContainerRoutes() *web.Router {
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)
- g.MatchPath("GET,PATCH,PUT,DELETE", `/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, func(ctx *context.Context) {
- switch ctx.Req.Method {
- case http.MethodGet:
- container.GetBlobsUpload(ctx)
- case http.MethodPatch:
- container.PatchBlobsUpload(ctx)
- case http.MethodPut:
- container.PutBlobsUpload(ctx)
- default: /* DELETE */
- container.DeleteBlobsUpload(ctx)
- }
- })
+
+ 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)
diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go
index fe7542dd18..cfe069d6db 100644
--- a/routers/api/packages/conda/conda.go
+++ b/routers/api/packages/conda/conda.go
@@ -36,6 +36,24 @@ func apiError(ctx *context.Context, status int, obj any) {
})
}
+func isCondaPackageFileName(filename string) bool {
+ return strings.HasSuffix(filename, ".tar.bz2") || strings.HasSuffix(filename, ".conda")
+}
+
+func ListOrGetPackages(ctx *context.Context) {
+ filename := ctx.PathParam("filename")
+ switch filename {
+ case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2":
+ EnumeratePackages(ctx)
+ return
+ }
+ if isCondaPackageFileName(filename) {
+ DownloadPackageFile(ctx)
+ return
+ }
+ ctx.NotFound(nil)
+}
+
func EnumeratePackages(ctx *context.Context) {
type Info struct {
Subdir string `json:"subdir"`
@@ -174,6 +192,12 @@ func EnumeratePackages(ctx *context.Context) {
}
func UploadPackageFile(ctx *context.Context) {
+ filename := ctx.PathParam("filename")
+ if !isCondaPackageFileName(filename) {
+ apiError(ctx, http.StatusBadRequest, nil)
+ return
+ }
+
upload, needToClose, err := ctx.UploadStream()
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -191,7 +215,7 @@ func UploadPackageFile(ctx *context.Context) {
defer buf.Close()
var pck *conda_module.Package
- if strings.HasSuffix(strings.ToLower(ctx.PathParam("filename")), ".tar.bz2") {
+ if strings.HasSuffix(filename, ".tar.bz2") {
pck, err = conda_module.ParsePackageBZ2(buf)
} else {
pck, err = conda_module.ParsePackageConda(buf, buf.Size())
diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go
index 2ea9b3839c..abfc21f95a 100644
--- a/routers/api/packages/container/blob.go
+++ b/routers/api/packages/container/blob.go
@@ -90,14 +90,14 @@ 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
}
@@ -178,7 +178,7 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p
}
func deleteBlob(ctx context.Context, ownerID int64, image string, digest digest.Digest) error {
- releaser, err := globallock.Lock(ctx, containerPkgName(ownerID, image))
+ releaser, err := globallock.Lock(ctx, containerGlobalLockKey(ownerID, image, "blob"))
if err != nil {
return err
}
diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go
index 972506103a..aeec16be4b 100644
--- a/routers/api/packages/container/container.go
+++ b/routers/api/packages/container/container.go
@@ -32,7 +32,7 @@ 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
@@ -320,6 +320,7 @@ func PostBlobsUploads(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)
@@ -334,6 +335,7 @@ func GetBlobsUpload(ctx *context.Context) {
// 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,
}
@@ -386,7 +388,7 @@ func PatchBlobsUpload(ctx *context.Context) {
UploadUUID: uploader.ID,
Status: http.StatusAccepted,
}
- if contentRange != "" {
+ if uploader.Size() > 0 {
respHeaders.Range = fmt.Sprintf("0-%d", uploader.Size()-1)
}
setResponseHeaders(ctx.Resp, respHeaders)
diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go
index a640bcda25..22ea11c8ce 100644
--- a/routers/api/packages/container/manifest.go
+++ b/routers/api/packages/container/manifest.go
@@ -16,6 +16,7 @@ import (
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"
@@ -29,18 +30,6 @@ import (
oci "github.com/opencontainers/image-spec/specs-go/v1"
)
-func isMediaTypeValid(mt string) bool {
- return strings.HasPrefix(mt, "application/vnd.docker.") || strings.HasPrefix(mt, "application/vnd.oci.")
-}
-
-func isMediaTypeImageManifest(mt string) bool {
- return strings.EqualFold(mt, oci.MediaTypeImageManifest) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.v2+json")
-}
-
-func isMediaTypeImageIndex(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
@@ -66,16 +55,23 @@ func processManifest(ctx context.Context, mci *manifestCreationInfo, buf *packag
return "", err
}
- if !isMediaTypeValid(mci.MediaType) {
+ if !container_module.IsMediaTypeValid(mci.MediaType) {
mci.MediaType = index.MediaType
- if !isMediaTypeValid(mci.MediaType) {
+ if !container_module.IsMediaTypeValid(mci.MediaType) {
return "", errManifestInvalid.WithMessage("MediaType not recognized")
}
}
- if isMediaTypeImageManifest(mci.MediaType) {
+ // .../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 isMediaTypeImageIndex(mci.MediaType) {
+ } else if container_module.IsMediaTypeImageIndex(mci.MediaType) {
return processOciImageIndex(ctx, mci, buf)
}
return "", errManifestInvalid
@@ -201,7 +197,7 @@ func processOciImageIndex(ctx context.Context, mci *manifestCreationInfo, buf *p
}
for _, manifest := range index.Manifests {
- if !isMediaTypeImageManifest(manifest.MediaType) {
+ if !container_module.IsMediaTypeImageManifest(manifest.MediaType) {
return errManifestInvalid
}
@@ -336,7 +332,7 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
return nil, err
}
- if isMediaTypeImageIndex(mci.MediaType) {
+ 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
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/api.go b/routers/api/v1/api.go
index 2c138ad61a..4a4bf12657 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -228,7 +228,7 @@ func repoAssignment() func(ctx *context.APIContext) {
}
}
- if !ctx.Repo.Permission.HasAnyUnitAccess() {
+ if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() {
ctx.APIErrorNotFound()
return
}
@@ -455,15 +455,6 @@ func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) {
}
}
-// 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.APIError(http.StatusForbidden, "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) {
@@ -744,9 +735,17 @@ func mustEnableWiki(ctx *context.APIContext) {
}
}
+// FIXME: for consistency, maybe most mustNotBeArchived checks should be replaced with mustEnableEditor
func mustNotBeArchived(ctx *context.APIContext) {
if ctx.Repo.Repository.IsArchived {
- ctx.APIError(http.StatusLocked, fmt.Errorf("%s is archived", ctx.Repo.Repository.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
}
}
@@ -942,6 +941,8 @@ func Routes() *web.Router {
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)
})
}
@@ -1078,6 +1079,9 @@ func Routes() *web.Router {
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)
@@ -1202,6 +1206,7 @@ func Routes() *web.Router {
}, 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))
@@ -1242,7 +1247,7 @@ func Routes() *web.Router {
}, reqToken())
m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile)
m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS)
- m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
+ m.Methods("HEAD,GET", "/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
m.Combo("/forks").Get(repo.ListForks).
Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
m.Post("/merge-upstream", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeCode), bind(api.MergeUpstreamRequest{}), repo.MergeUpstream)
@@ -1280,9 +1285,13 @@ func Routes() *web.Router {
}, reqToken(), reqAdmin())
m.Group("/actions", func() {
m.Get("/tasks", repo.ListActionTasks)
- m.Group("/runs/{run}", func() {
- m.Get("/artifacts", repo.GetArtifactsOfRun)
- m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
+ 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() {
@@ -1414,20 +1423,27 @@ 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.Get("/*", repo.GetContents)
- m.Post("", reqToken(), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.ChangeFiles)
- 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())
+ 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) // POST method requires "write" permission, so we also support "GET" method above
+ 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() {
@@ -1450,7 +1466,7 @@ 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))
@@ -1734,11 +1750,15 @@ func Routes() *web.Router {
Patch(bind(api.EditHookOption{}), admin.EditHook).
Delete(admin.DeleteHook)
})
- m.Group("/actions/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.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/org/action.go b/routers/api/v1/org/action.go
index 5cd3ea07f4..3ae5e60585 100644
--- a/routers/api/v1/org/action.go
+++ b/routers/api/v1/org/action.go
@@ -570,6 +570,96 @@ func (Action) DeleteRunner(ctx *context.APIContext) {
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/repo/action.go b/routers/api/v1/repo/action.go
index 3f64e2477d..a57db015f0 100644
--- a/routers/api/v1/repo/action.go
+++ b/routers/api/v1/repo/action.go
@@ -650,6 +650,114 @@ func (Action) DeleteRunner(ctx *context.APIContext) {
shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
}
+// GetWorkflowRunJobs Lists all jobs for a workflow run.
+func (Action) ListWorkflowJobs(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/actions/jobs repository listWorkflowJobs
+ // ---
+ // summary: Lists all jobs for a repository
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: status
+ // in: query
+ // description: workflow status (pending, queued, in_progress, failure, success, skipped)
+ // type: string
+ // required: false
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // responses:
+ // "200":
+ // "$ref": "#/responses/WorkflowJobsList"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ repoID := ctx.Repo.Repository.ID
+
+ shared.ListJobs(ctx, 0, repoID, 0)
+}
+
+// ListWorkflowRuns Lists all runs for a repository run.
+func (Action) ListWorkflowRuns(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/actions/runs repository getWorkflowRuns
+ // ---
+ // summary: Lists all runs for a repository run
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: event
+ // in: query
+ // description: workflow event name
+ // type: string
+ // required: false
+ // - name: branch
+ // in: query
+ // description: workflow branch
+ // type: string
+ // required: false
+ // - name: status
+ // in: query
+ // description: workflow status (pending, queued, in_progress, failure, success, skipped)
+ // type: string
+ // required: false
+ // - name: actor
+ // in: query
+ // description: triggered by user
+ // type: string
+ // required: false
+ // - name: head_sha
+ // in: query
+ // description: triggering sha of the workflow run
+ // type: string
+ // required: false
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ArtifactsList"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ repoID := ctx.Repo.Repository.ID
+
+ shared.ListRuns(ctx, 0, repoID)
+}
+
var _ actions_service.API = new(Action)
// Action implements actions_service.API
@@ -756,7 +864,7 @@ func ActionsListRepositoryWorkflows(ctx *context.APIContext) {
// "500":
// "$ref": "#/responses/error"
- workflows, err := actions_service.ListActionWorkflows(ctx)
+ workflows, err := convert.ListActionWorkflows(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -802,7 +910,7 @@ func ActionsGetWorkflow(ctx *context.APIContext) {
// "$ref": "#/responses/error"
workflowID := ctx.PathParam("workflow_id")
- workflow, err := actions_service.GetActionWorkflow(ctx, workflowID)
+ 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)
@@ -992,6 +1100,157 @@ func ActionsEnableWorkflow(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
+// GetWorkflowRun Gets a specific workflow run.
+func GetWorkflowRun(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun
+ // ---
+ // summary: Gets a specific workflow run
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: run
+ // in: path
+ // description: id of the run
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/WorkflowRun"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ runID := ctx.PathParamInt64("run")
+ job, _, err := db.GetByID[actions_model.ActionRun](ctx, runID)
+
+ if err != nil || job.RepoID != ctx.Repo.Repository.ID {
+ ctx.APIError(http.StatusNotFound, util.ErrNotExist)
+ }
+
+ convertedArtifact, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, job)
+ if err != nil {
+ ctx.APIErrorInternal(err)
+ return
+ }
+ ctx.JSON(http.StatusOK, convertedArtifact)
+}
+
+// ListWorkflowRunJobs Lists all jobs for a workflow run.
+func ListWorkflowRunJobs(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs repository listWorkflowRunJobs
+ // ---
+ // summary: Lists all jobs for a workflow run
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: run
+ // in: path
+ // description: runid of the workflow run
+ // type: integer
+ // required: true
+ // - name: status
+ // in: query
+ // description: workflow status (pending, queued, in_progress, failure, success, skipped)
+ // type: string
+ // required: false
+ // - name: page
+ // in: query
+ // description: page number of results to return (1-based)
+ // type: integer
+ // - name: limit
+ // in: query
+ // description: page size of results
+ // type: integer
+ // responses:
+ // "200":
+ // "$ref": "#/responses/WorkflowJobsList"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ repoID := ctx.Repo.Repository.ID
+
+ runID := ctx.PathParamInt64("run")
+
+ // Avoid the list all jobs functionality for this api route to be used with a runID == 0.
+ if runID <= 0 {
+ ctx.APIError(http.StatusBadRequest, util.NewInvalidArgumentErrorf("runID must be a positive integer"))
+ return
+ }
+
+ // runID is used as an additional filter next to repoID to ensure that we only list jobs for the specified repoID and runID.
+ // no additional checks for runID are needed here
+ shared.ListJobs(ctx, 0, repoID, runID)
+}
+
+// GetWorkflowJob Gets a specific workflow job for a workflow run.
+func GetWorkflowJob(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id} repository getWorkflowJob
+ // ---
+ // summary: Gets a specific workflow job for a workflow run
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: job_id
+ // in: path
+ // description: id of the job
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/WorkflowJob"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ jobID := ctx.PathParamInt64("job_id")
+ job, _, err := db.GetByID[actions_model.ActionRunJob](ctx, jobID)
+
+ if err != nil || job.RepoID != ctx.Repo.Repository.ID {
+ ctx.APIError(http.StatusNotFound, util.ErrNotExist)
+ }
+
+ convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, job)
+ if err != nil {
+ ctx.APIErrorInternal(err)
+ return
+ }
+ ctx.JSON(http.StatusOK, convertedWorkflowJob)
+}
+
// GetArtifacts Lists all artifacts for a repository.
func GetArtifactsOfRun(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/artifacts repository getArtifactsOfRun
diff --git a/routers/api/v1/repo/blob.go b/routers/api/v1/repo/blob.go
index d1cb72f5f1..9a17fc1bbf 100644
--- a/routers/api/v1/repo/blob.go
+++ b/routers/api/v1/repo/blob.go
@@ -47,7 +47,7 @@ func GetBlob(ctx *context.APIContext) {
return
}
- if blob, err := files_service.GetBlobBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil {
+ 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 fe82550fdd..6e496e6fd3 100644
--- a/routers/api/v1/repo/branch.go
+++ b/routers/api/v1/repo/branch.go
@@ -1181,7 +1181,7 @@ func MergeUpstream(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.MergeUpstreamRequest)
- mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch)
+ mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch, form.FfOnly)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index f40d39a251..8ce52c2cd4 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -62,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:
@@ -115,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:
@@ -139,27 +139,27 @@ 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 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.APIErrorInternal(err)
return
}
- // FIXME: code from #19689, what if the file is large ... OOM ...
buf, err := io.ReadAll(dataRc)
if err != nil {
_ = dataRc.Close()
@@ -181,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
}
@@ -405,13 +405,6 @@ func GetEditorconfig(ctx *context.APIContext) {
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
-}
-
func base64Reader(s string) (io.ReadSeeker, error) {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
@@ -420,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
@@ -456,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.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,
@@ -477,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{
- GitUserName: apiOpts.Committer.Name,
- GitUserEmail: apiOpts.Committer.Email,
- },
- Author: &files_service.IdentityOptions{
- GitUserName: apiOpts.Author.Name,
- GitUserEmail: 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)
}
@@ -559,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.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{
- GitUserName: apiOpts.Committer.Name,
- GitUserEmail: apiOpts.Committer.Email,
- },
- Author: &files_service.IdentityOptions{
- GitUserName: apiOpts.Author.Name,
- GitUserEmail: 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)
@@ -656,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.APIError(http.StatusUnprocessableEntity, errors.New("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.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{
- GitUserName: apiOpts.Committer.Name,
- GitUserEmail: apiOpts.Committer.Email,
- },
- Author: &files_service.IdentityOptions{
- GitUserName: apiOpts.Author.Name,
- GitUserEmail: 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.APIError(http.StatusForbidden, err)
return
}
if git_model.IsErrBranchAlreadyExists(err) || files_service.IsErrFilenameInvalid(err) || pull_service.IsErrSHADoesNotMatch(err) ||
- files_service.IsErrFilePathInvalid(err) || files_service.IsErrRepoFileAlreadyExists(err) {
+ files_service.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) {
+ if git.IsErrBranchNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
return
}
-
- ctx.APIErrorInternal(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
@@ -759,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)
@@ -817,74 +751,27 @@ 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.APIError(http.StatusForbidden, 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{
- GitUserName: apiOpts.Committer.Name,
- GitUserEmail: apiOpts.Committer.Email,
- },
- Author: &files_service.IdentityOptions{
- GitUserName: apiOpts.Author.Name,
- GitUserEmail: 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.APIError(http.StatusNotFound, 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.APIError(http.StatusBadRequest, err)
- return
- } else if files_service.IsErrUserCannotCommit(err) {
- ctx.APIError(http.StatusForbidden, err)
- return
- }
- ctx.APIErrorInternal(err)
+ handleChangeRepoFilesError(ctx, err)
} else {
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
@@ -902,11 +789,72 @@ func resolveRefCommit(ctx *context.APIContext, ref string, minCommitIDLen ...int
return refCommit
}
-// GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
+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
+ // 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, option "lfs_metadata" will try to retrieve LFS metadata.
+ // type: string
+ // required: false
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ContentsExtResponse"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ 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
+ 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:
@@ -935,29 +883,34 @@ func GetContents(ctx *context.APIContext) {
// "$ref": "#/responses/ContentsResponse"
// "404":
// "$ref": "#/responses/notFound"
-
- treePath := ctx.PathParam("*")
- refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref"))
+ ret := getRepoContents(ctx, files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*"), IncludeSingleFileContent: true})
if ctx.Written() {
return
}
+ ctx.JSON(http.StatusOK, util.Iif[any](ret.FileContents != nil, ret.FileContents, ret.DirContents))
+}
- if fileList, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, refCommit, treePath); 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.APIErrorNotFound("GetContentsOrList", err)
- return
+ return nil
}
ctx.APIErrorInternal(err)
- } else {
- ctx.JSON(http.StatusOK, fileList)
}
+ 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:
@@ -990,7 +943,7 @@ 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 to use JSON encoded request body in query parameter.
+ // description: See the POST method. This GET method supports using JSON encoded request body in query parameter.
// produces:
// - application/json
// parameters:
@@ -1020,7 +973,7 @@ func GetFileContentsGet(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
- // POST method requires "write" permission, so we also support this "GET" method
+ // The POST method requires "write" permission, so we also support this "GET" method
handleGetFileContents(ctx)
}
@@ -1064,7 +1017,7 @@ func GetFileContentsPost(ctx *context.APIContext) {
// 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.
+ // At the moment, there is no other way to get around the permission check, so there is a "GET" workaround method above.
handleGetFileContents(ctx)
}
@@ -1081,6 +1034,6 @@ func handleGetFileContents(ctx *context.APIContext) {
if ctx.Written() {
return
}
- filesResponse := files_service.GetContentsListFromTreePaths(ctx, ctx.Repo.Repository, refCommit, opts.Files)
+ 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/issue_stopwatch.go b/routers/api/v1/repo/issue_stopwatch.go
index b18e172b37..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,14 +48,17 @@ 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 {
+ if ok, err := issues_model.CreateIssueStopwatch(ctx, ctx.Doer, issue); err != nil {
ctx.APIErrorInternal(err)
return
+ } else if !ok {
+ ctx.APIError(http.StatusConflict, "cannot start a stopwatch again if it already exists")
+ return
}
ctx.Status(http.StatusCreated)
@@ -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 {
+ 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,22 +149,25 @@ 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 {
+ if ok, err := issues_model.CancelStopwatch(ctx, ctx.Doer, issue); err != nil {
ctx.APIErrorInternal(err)
return
+ } else if !ok {
+ ctx.APIError(http.StatusConflict, "cannot cancel a non-existent stopwatch")
+ return
}
ctx.Status(http.StatusNoContent)
}
-func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*issues_model.Issue, error) {
+func prepareIssueForStopwatch(ctx *context.APIContext) *issues_model.Issue {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
@@ -168,32 +175,19 @@ func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*issues_m
} else {
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.APIError(http.StatusConflict, "cannot stop/cancel a non existent stopwatch")
- err = errors.New("cannot stop/cancel a non existent stopwatch")
- } else {
- ctx.APIError(http.StatusConflict, "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
diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go
index bcf498bf7e..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{
- GitUserName: apiOpts.Committer.Name,
- GitUserEmail: apiOpts.Committer.Email,
- },
- Author: &files.IdentityOptions{
- GitUserName: apiOpts.Author.Name,
- GitUserEmail: 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.APIErrorInternal(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.APIError(http.StatusForbidden, err)
- return
- }
- if git_model.IsErrBranchAlreadyExists(err) || files.IsErrFilenameInvalid(err) || pull_service.IsErrSHADoesNotMatch(err) ||
- files.IsErrFilePathInvalid(err) || files.IsErrRepoFileAlreadyExists(err) {
- ctx.APIError(http.StatusUnprocessableEntity, err)
- return
- }
- if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) {
- ctx.APIError(http.StatusNotFound, err)
- return
- }
- ctx.APIErrorInternal(err)
+ handleChangeRepoFilesError(ctx, err)
} else {
ctx.JSON(http.StatusCreated, fileResponse)
}
diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go
index 3094c1947c..8e24ffa465 100644
--- a/routers/api/v1/repo/wiki.go
+++ b/routers/api/v1/repo/wiki.go
@@ -499,7 +499,7 @@ 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.APIErrorInternal(err)
return ""
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/swagger/repo.go b/routers/api/v1/swagger/repo.go
index df0c8a805a..9e20c0533b 100644
--- a/routers/api/v1/swagger/repo.go
+++ b/routers/api/v1/swagger/repo.go
@@ -331,6 +331,12 @@ type swaggerContentsListResponse struct {
Body []api.ContentsResponse `json:"body"`
}
+// swagger:response ContentsExtResponse
+type swaggerContentsExtResponse struct {
+ // in:body
+ Body api.ContentsExtResponse `json:"body"`
+}
+
// FileDeleteResponse
// swagger:response FileDeleteResponse
type swaggerFileDeleteResponse struct {
@@ -443,6 +449,34 @@ 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 {
diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go
index 7e4ffd9403..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"
@@ -356,3 +357,86 @@ func ListVariables(ctx *context.APIContext) {
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/utils/hook.go b/routers/api/v1/utils/hook.go
index 3bbd292718..6f598f14c8 100644
--- a/routers/api/v1/utils/hook.go
+++ b/routers/api/v1/utils/hook.go
@@ -173,6 +173,7 @@ func updateHookEvents(events []string) webhook_module.HookEvents {
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