aboutsummaryrefslogtreecommitdiffstats
path: root/routers
diff options
context:
space:
mode:
Diffstat (limited to 'routers')
-rw-r--r--routers/api/actions/artifacts_utils.go2
-rw-r--r--routers/api/packages/alpine/alpine.go4
-rw-r--r--routers/api/packages/api.go242
-rw-r--r--routers/api/packages/arch/arch.go2
-rw-r--r--routers/api/packages/cargo/cargo.go7
-rw-r--r--routers/api/packages/chef/chef.go2
-rw-r--r--routers/api/packages/composer/composer.go7
-rw-r--r--routers/api/packages/conan/conan.go2
-rw-r--r--routers/api/packages/conda/conda.go28
-rw-r--r--routers/api/packages/container/auth.go2
-rw-r--r--routers/api/packages/container/blob.go37
-rw-r--r--routers/api/packages/container/container.go122
-rw-r--r--routers/api/packages/container/manifest.go329
-rw-r--r--routers/api/packages/cran/cran.go2
-rw-r--r--routers/api/packages/debian/debian.go6
-rw-r--r--routers/api/packages/generic/generic.go2
-rw-r--r--routers/api/packages/goproxy/goproxy.go2
-rw-r--r--routers/api/packages/helm/helm.go2
-rw-r--r--routers/api/packages/maven/maven.go2
-rw-r--r--routers/api/packages/npm/npm.go4
-rw-r--r--routers/api/packages/nuget/api_v2.go46
-rw-r--r--routers/api/packages/nuget/nuget.go6
-rw-r--r--routers/api/packages/pub/pub.go2
-rw-r--r--routers/api/packages/pypi/pypi.go2
-rw-r--r--routers/api/packages/rpm/rpm.go4
-rw-r--r--routers/api/packages/rubygems/rubygems.go50
-rw-r--r--routers/api/packages/rubygems/rubygems_test.go41
-rw-r--r--routers/api/packages/swift/swift.go2
-rw-r--r--routers/api/packages/vagrant/vagrant.go2
-rw-r--r--routers/api/v1/admin/action.go93
-rw-r--r--routers/api/v1/admin/org.go4
-rw-r--r--routers/api/v1/admin/repo.go2
-rw-r--r--routers/api/v1/admin/user.go16
-rw-r--r--routers/api/v1/admin/user_badge.go6
-rw-r--r--routers/api/v1/api.go83
-rw-r--r--routers/api/v1/misc/markup.go4
-rw-r--r--routers/api/v1/misc/signing.go78
-rw-r--r--routers/api/v1/org/action.go102
-rw-r--r--routers/api/v1/org/block.go6
-rw-r--r--routers/api/v1/org/member.go10
-rw-r--r--routers/api/v1/org/org.go14
-rw-r--r--routers/api/v1/org/team.go6
-rw-r--r--routers/api/v1/repo/action.go325
-rw-r--r--routers/api/v1/repo/blob.go2
-rw-r--r--routers/api/v1/repo/branch.go10
-rw-r--r--routers/api/v1/repo/collaborators.go6
-rw-r--r--routers/api/v1/repo/commits.go36
-rw-r--r--routers/api/v1/repo/file.go401
-rw-r--r--routers/api/v1/repo/issue.go11
-rw-r--r--routers/api/v1/repo/issue_comment.go18
-rw-r--r--routers/api/v1/repo/issue_dependency.go10
-rw-r--r--routers/api/v1/repo/issue_stopwatch.go56
-rw-r--r--routers/api/v1/repo/issue_subscription.go4
-rw-r--r--routers/api/v1/repo/issue_tracked_time.go2
-rw-r--r--routers/api/v1/repo/migrate.go2
-rw-r--r--routers/api/v1/repo/patch.go68
-rw-r--r--routers/api/v1/repo/pull.go10
-rw-r--r--routers/api/v1/repo/release.go4
-rw-r--r--routers/api/v1/repo/repo.go2
-rw-r--r--routers/api/v1/repo/status.go11
-rw-r--r--routers/api/v1/repo/wiki.go12
-rw-r--r--routers/api/v1/shared/action.go187
-rw-r--r--routers/api/v1/shared/runners.go45
-rw-r--r--routers/api/v1/swagger/repo.go34
-rw-r--r--routers/api/v1/user/action.go94
-rw-r--r--routers/api/v1/user/app.go6
-rw-r--r--routers/api/v1/user/block.go6
-rw-r--r--routers/api/v1/user/follower.go14
-rw-r--r--routers/api/v1/user/gpg_key.go2
-rw-r--r--routers/api/v1/user/key.go2
-rw-r--r--routers/api/v1/user/repo.go6
-rw-r--r--routers/api/v1/user/star.go2
-rw-r--r--routers/api/v1/user/user.go8
-rw-r--r--routers/api/v1/user/watch.go2
-rw-r--r--routers/api/v1/utils/hook.go102
-rw-r--r--routers/api/v1/utils/hook_test.go82
-rw-r--r--routers/api/v1/utils/main_test.go21
-rw-r--r--routers/common/markup.go6
-rw-r--r--routers/install/install.go14
-rw-r--r--routers/private/hook_post_receive.go26
-rw-r--r--routers/private/hook_verification_test.go4
-rw-r--r--routers/web/admin/auths.go2
-rw-r--r--routers/web/admin/config.go4
-rw-r--r--routers/web/admin/diagnosis.go8
-rw-r--r--routers/web/admin/notice.go5
-rw-r--r--routers/web/admin/orgs.go2
-rw-r--r--routers/web/admin/packages.go5
-rw-r--r--routers/web/admin/users.go13
-rw-r--r--routers/web/auth/auth.go14
-rw-r--r--routers/web/auth/oauth.go8
-rw-r--r--routers/web/auth/openid.go5
-rw-r--r--routers/web/devtest/devtest.go2
-rw-r--r--routers/web/explore/code.go10
-rw-r--r--routers/web/explore/org.go2
-rw-r--r--routers/web/explore/repo.go3
-rw-r--r--routers/web/explore/user.go4
-rw-r--r--routers/web/feed/branch.go2
-rw-r--r--routers/web/home.go4
-rw-r--r--routers/web/misc/markup.go2
-rw-r--r--routers/web/misc/misc.go2
-rw-r--r--routers/web/org/home.go2
-rw-r--r--routers/web/org/members.go5
-rw-r--r--routers/web/org/org_labels.go36
-rw-r--r--routers/web/org/projects.go5
-rw-r--r--routers/web/org/setting.go107
-rw-r--r--routers/web/org/teams.go15
-rw-r--r--routers/web/repo/actions/actions.go10
-rw-r--r--routers/web/repo/actions/view.go109
-rw-r--r--routers/web/repo/branch.go9
-rw-r--r--routers/web/repo/cherry_pick.go193
-rw-r--r--routers/web/repo/commit.go17
-rw-r--r--routers/web/repo/compare.go8
-rw-r--r--routers/web/repo/editor.go1051
-rw-r--r--routers/web/repo/editor_apply_patch.go51
-rw-r--r--routers/web/repo/editor_cherry_pick.go86
-rw-r--r--routers/web/repo/editor_error.go82
-rw-r--r--routers/web/repo/editor_fork.go31
-rw-r--r--routers/web/repo/editor_preview.go41
-rw-r--r--routers/web/repo/editor_test.go79
-rw-r--r--routers/web/repo/editor_uploader.go61
-rw-r--r--routers/web/repo/editor_util.go110
-rw-r--r--routers/web/repo/fork.go42
-rw-r--r--routers/web/repo/githttp.go8
-rw-r--r--routers/web/repo/issue.go16
-rw-r--r--routers/web/repo/issue_comment.go34
-rw-r--r--routers/web/repo/issue_label.go36
-rw-r--r--routers/web/repo/issue_label_test.go94
-rw-r--r--routers/web/repo/issue_list.go2
-rw-r--r--routers/web/repo/issue_new.go5
-rw-r--r--routers/web/repo/issue_stopwatch.go40
-rw-r--r--routers/web/repo/issue_view.go16
-rw-r--r--routers/web/repo/milestone.go5
-rw-r--r--routers/web/repo/packages.go5
-rw-r--r--routers/web/repo/patch.go126
-rw-r--r--routers/web/repo/projects.go5
-rw-r--r--routers/web/repo/pull.go20
-rw-r--r--routers/web/repo/release.go15
-rw-r--r--routers/web/repo/repo.go4
-rw-r--r--routers/web/repo/setting/lfs.go14
-rw-r--r--routers/web/repo/setting/protected_branch.go3
-rw-r--r--routers/web/repo/setting/protected_tag.go3
-rw-r--r--routers/web/repo/setting/setting.go31
-rw-r--r--routers/web/repo/setting/settings_test.go24
-rw-r--r--routers/web/repo/setting/webhook.go1
-rw-r--r--routers/web/repo/view.go60
-rw-r--r--routers/web/repo/view_file.go391
-rw-r--r--routers/web/repo/view_home.go13
-rw-r--r--routers/web/repo/view_readme.go15
-rw-r--r--routers/web/repo/wiki.go193
-rw-r--r--routers/web/repo/wiki_test.go9
-rw-r--r--routers/web/shared/actions/runners.go10
-rw-r--r--routers/web/shared/label/label.go26
-rw-r--r--routers/web/shared/packages/packages.go10
-rw-r--r--routers/web/shared/user/header.go9
-rw-r--r--routers/web/user/code.go10
-rw-r--r--routers/web/user/home.go13
-rw-r--r--routers/web/user/home_test.go2
-rw-r--r--routers/web/user/notification.go20
-rw-r--r--routers/web/user/package.go71
-rw-r--r--routers/web/user/profile.go39
-rw-r--r--routers/web/user/search.go2
-rw-r--r--routers/web/user/setting/profile.go9
-rw-r--r--routers/web/web.go57
163 files changed, 3629 insertions, 3122 deletions
diff --git a/routers/api/actions/artifacts_utils.go b/routers/api/actions/artifacts_utils.go
index 77ce765098..35868c290e 100644
--- a/routers/api/actions/artifacts_utils.go
+++ b/routers/api/actions/artifacts_utils.go
@@ -43,7 +43,7 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) {
return task, runID, true
}
-func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) { //nolint:unparam
+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 {
diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go
index f35cff3df2..ba4a4f23ce 100644
--- a/routers/api/packages/alpine/alpine.go
+++ b/routers/api/packages/alpine/alpine.go
@@ -68,7 +68,7 @@ func GetRepositoryFile(ctx *context.Context) {
return
}
- s, u, pf, err := packages_service.GetFileStreamByPackageVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion(
ctx,
pv,
&packages_service.PackageFileInfo{
@@ -216,7 +216,7 @@ func DownloadPackageFile(ctx *context.Context) {
}
}
- s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0])
+ s, u, pf, err := packages_service.OpenFileForDownload(ctx, pfs[0])
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index ae4ea7ea87..878e0f9945 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -5,8 +5,6 @@ package packages
import (
"net/http"
- "regexp"
- "strings"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/perm"
@@ -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,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 == 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)
+ // 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)
@@ -621,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)
@@ -632,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)
@@ -693,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)
@@ -701,27 +550,22 @@ func ContainerRoutes() *web.Router {
r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList)
r.Group("/{username}", func() {
r.PathGroup("/*", func(g *web.RouterPathGroup) {
- g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.InitiateUploadBlob)
- g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagList)
- g.MatchPath("GET,PATCH,PUT,DELETE", `/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, func(ctx *context.Context) {
- switch ctx.Req.Method {
- case http.MethodGet:
- container.GetUploadBlob(ctx)
- case http.MethodPatch:
- container.UploadBlob(ctx)
- case http.MethodPut:
- container.EndUploadBlob(ctx)
- default: /* DELETE */
- container.CancelUploadBlob(ctx)
- }
- })
+ g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.PostBlobsUploads)
+ g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagsList)
+
+ patternBlobsUploadsUUID := g.PatternRegexp(`/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName)
+ g.MatchPattern("GET", patternBlobsUploadsUUID, container.GetBlobsUpload)
+ g.MatchPattern("PATCH", patternBlobsUploadsUUID, container.PatchBlobsUpload)
+ g.MatchPattern("PUT", patternBlobsUploadsUUID, container.PutBlobsUpload)
+ g.MatchPattern("DELETE", patternBlobsUploadsUUID, container.DeleteBlobsUpload)
+
g.MatchPath("HEAD", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.HeadBlob)
g.MatchPath("GET", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.GetBlob)
g.MatchPath("DELETE", `/<image:*>/blobs/<digest>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob)
g.MatchPath("HEAD", `/<image:*>/manifests/<reference>`, container.VerifyImageName, container.HeadManifest)
g.MatchPath("GET", `/<image:*>/manifests/<reference>`, container.VerifyImageName, container.GetManifest)
- g.MatchPath("PUT", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.UploadManifest)
+ g.MatchPath("PUT", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.PutManifest)
g.MatchPath("DELETE", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteManifest)
})
}, container.ReqContainerAccess, context.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go
index f5dc6c1d01..bf9cc3f1b8 100644
--- a/routers/api/packages/arch/arch.go
+++ b/routers/api/packages/arch/arch.go
@@ -239,7 +239,7 @@ func GetPackageOrRepositoryFile(ctx *context.Context) {
return
}
- s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0])
+ s, u, pf, err := packages_service.OpenFileForDownload(ctx, pfs[0])
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
diff --git a/routers/api/packages/cargo/cargo.go b/routers/api/packages/cargo/cargo.go
index 710c614c6e..cfcf79244f 100644
--- a/routers/api/packages/cargo/cargo.go
+++ b/routers/api/packages/cargo/cargo.go
@@ -95,10 +95,7 @@ type SearchResultMeta struct {
// https://doc.rust-lang.org/cargo/reference/registries.html#search
func SearchPackages(ctx *context.Context) {
- page := ctx.FormInt("page")
- if page < 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
perPage := ctx.FormInt("per_page")
paginator := db.ListOptions{
Page: page,
@@ -168,7 +165,7 @@ func ListOwners(ctx *context.Context) {
// DownloadPackageFile serves the content of a package
func DownloadPackageFile(ctx *context.Context) {
- s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
diff --git a/routers/api/packages/chef/chef.go b/routers/api/packages/chef/chef.go
index a0c8c5696c..1f11afe548 100644
--- a/routers/api/packages/chef/chef.go
+++ b/routers/api/packages/chef/chef.go
@@ -343,7 +343,7 @@ func DownloadPackage(ctx *context.Context) {
pf := pd.Files[0].File
- s, u, _, err := packages_service.GetPackageFileStream(ctx, pf)
+ s, u, _, err := packages_service.OpenFileForDownload(ctx, pf)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go
index c6c14e5cf4..9daf0ffeff 100644
--- a/routers/api/packages/composer/composer.go
+++ b/routers/api/packages/composer/composer.go
@@ -53,10 +53,7 @@ func ServiceIndex(ctx *context.Context) {
// SearchPackages searches packages, only "q" is supported
// https://packagist.org/apidoc#search-packages
func SearchPackages(ctx *context.Context) {
- page := ctx.FormInt("page")
- if page < 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
perPage := ctx.FormInt("per_page")
paginator := db.ListOptions{
Page: page,
@@ -163,7 +160,7 @@ func PackageMetadata(ctx *context.Context) {
// DownloadPackageFile serves the content of a package
func DownloadPackageFile(ctx *context.Context) {
- s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go
index 8019eee9f7..fe70e02cd6 100644
--- a/routers/api/packages/conan/conan.go
+++ b/routers/api/packages/conan/conan.go
@@ -480,7 +480,7 @@ func downloadFile(ctx *context.Context, fileFilter container.Set[string], fileKe
return
}
- s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go
index 7a46681235..cfe069d6db 100644
--- a/routers/api/packages/conda/conda.go
+++ b/routers/api/packages/conda/conda.go
@@ -36,6 +36,24 @@ func apiError(ctx *context.Context, status int, obj any) {
})
}
+func isCondaPackageFileName(filename string) bool {
+ return strings.HasSuffix(filename, ".tar.bz2") || strings.HasSuffix(filename, ".conda")
+}
+
+func ListOrGetPackages(ctx *context.Context) {
+ filename := ctx.PathParam("filename")
+ switch filename {
+ case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2":
+ EnumeratePackages(ctx)
+ return
+ }
+ if isCondaPackageFileName(filename) {
+ DownloadPackageFile(ctx)
+ return
+ }
+ ctx.NotFound(nil)
+}
+
func EnumeratePackages(ctx *context.Context) {
type Info struct {
Subdir string `json:"subdir"`
@@ -174,6 +192,12 @@ func EnumeratePackages(ctx *context.Context) {
}
func UploadPackageFile(ctx *context.Context) {
+ filename := ctx.PathParam("filename")
+ if !isCondaPackageFileName(filename) {
+ apiError(ctx, http.StatusBadRequest, nil)
+ return
+ }
+
upload, needToClose, err := ctx.UploadStream()
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -191,7 +215,7 @@ func UploadPackageFile(ctx *context.Context) {
defer buf.Close()
var pck *conda_module.Package
- if strings.HasSuffix(strings.ToLower(ctx.PathParam("filename")), ".tar.bz2") {
+ if strings.HasSuffix(filename, ".tar.bz2") {
pck, err = conda_module.ParsePackageBZ2(buf)
} else {
pck, err = conda_module.ParsePackageConda(buf, buf.Size())
@@ -293,7 +317,7 @@ func DownloadPackageFile(ctx *context.Context) {
pf := pfs[0]
- s, u, _, err := packages_service.GetPackageFileStream(ctx, pf)
+ s, u, _, err := packages_service.OpenFileForDownload(ctx, pf)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
diff --git a/routers/api/packages/container/auth.go b/routers/api/packages/container/auth.go
index 1d8ae6af7d..1e1b87eb79 100644
--- a/routers/api/packages/container/auth.go
+++ b/routers/api/packages/container/auth.go
@@ -21,7 +21,7 @@ func (a *Auth) Name() string {
}
// Verify extracts the user from the Bearer token
-// If it's an anonymous session a ghost user is returned
+// If it's an anonymous session, a ghost user is returned
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
packageMeta, err := packages.ParseAuthorizationRequest(req)
if err != nil {
diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go
index 4a2320ab76..4b7bcee9d0 100644
--- a/routers/api/packages/container/blob.go
+++ b/routers/api/packages/container/blob.go
@@ -20,11 +20,13 @@ import (
container_module "code.gitea.io/gitea/modules/packages/container"
"code.gitea.io/gitea/modules/util"
packages_service "code.gitea.io/gitea/services/packages"
+
+ "github.com/opencontainers/go-digest"
)
// saveAsPackageBlob creates a package blob from an upload
// The uploaded blob gets stored in a special upload version to link them to the package/image
-func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam
+func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam // PackageBlob is never used
pb := packages_service.NewPackageBlob(hsr)
exists := false
@@ -88,20 +90,18 @@ func mountBlob(ctx context.Context, pi *packages_service.PackageInfo, pb *packag
})
}
-func containerPkgName(piOwnerID int64, piName string) string {
- return fmt.Sprintf("pkg_%d_container_%s", piOwnerID, strings.ToLower(piName))
+func containerGlobalLockKey(piOwnerID int64, piName, usage string) string {
+ return fmt.Sprintf("pkg_%d_container_%s_%s", piOwnerID, strings.ToLower(piName), usage)
}
func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageInfo) (*packages_model.PackageVersion, error) {
- var uploadVersion *packages_model.PackageVersion
-
- releaser, err := globallock.Lock(ctx, containerPkgName(pi.Owner.ID, pi.Name))
+ releaser, err := globallock.Lock(ctx, containerGlobalLockKey(pi.Owner.ID, pi.Name, "package"))
if err != nil {
return nil, err
}
defer releaser()
- err = db.WithTx(ctx, func(ctx context.Context) error {
+ return db.WithTx2(ctx, func(ctx context.Context) (*packages_model.PackageVersion, error) {
created := true
p := &packages_model.Package{
OwnerID: pi.Owner.ID,
@@ -113,7 +113,7 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
if !errors.Is(err, packages_model.ErrDuplicatePackage) {
log.Error("Error inserting package: %v", err)
- return err
+ return nil, err
}
created = false
}
@@ -121,31 +121,26 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI
if created {
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, strings.ToLower(pi.Owner.LowerName+"/"+pi.Name)); err != nil {
log.Error("Error setting package property: %v", err)
- return err
+ return nil, err
}
}
pv := &packages_model.PackageVersion{
PackageID: p.ID,
CreatorID: pi.Owner.ID,
- Version: container_model.UploadVersion,
- LowerVersion: container_model.UploadVersion,
+ Version: container_module.UploadVersion,
+ LowerVersion: container_module.UploadVersion,
IsInternal: true,
MetadataJSON: "null",
}
if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil {
if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
log.Error("Error inserting package: %v", err)
- return err
+ return nil, err
}
}
-
- uploadVersion = pv
-
- return nil
+ return pv, nil
})
-
- return uploadVersion, err
}
func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, pb *packages_model.PackageBlob) error {
@@ -175,8 +170,8 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p
return nil
}
-func deleteBlob(ctx context.Context, ownerID int64, image, digest string) error {
- releaser, err := globallock.Lock(ctx, containerPkgName(ownerID, image))
+func deleteBlob(ctx context.Context, ownerID int64, image string, digest digest.Digest) error {
+ releaser, err := globallock.Lock(ctx, containerGlobalLockKey(ownerID, image, "blob"))
if err != nil {
return err
}
@@ -186,7 +181,7 @@ func deleteBlob(ctx context.Context, ownerID int64, image, digest string) error
pfds, err := container_model.GetContainerBlobs(ctx, &container_model.BlobSearchOptions{
OwnerID: ownerID,
Image: image,
- Digest: digest,
+ Digest: string(digest),
})
if err != nil {
return err
diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go
index 6ef1655235..d532f698ad 100644
--- a/routers/api/packages/container/container.go
+++ b/routers/api/packages/container/container.go
@@ -13,6 +13,7 @@ import (
"regexp"
"strconv"
"strings"
+ "sync"
auth_model "code.gitea.io/gitea/models/auth"
packages_model "code.gitea.io/gitea/models/packages"
@@ -21,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
container_module "code.gitea.io/gitea/modules/packages/container"
"code.gitea.io/gitea/modules/setting"
@@ -31,17 +33,21 @@ import (
packages_service "code.gitea.io/gitea/services/packages"
container_service "code.gitea.io/gitea/services/packages/container"
- digest "github.com/opencontainers/go-digest"
+ "github.com/opencontainers/go-digest"
)
// maximum size of a container manifest
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
const maxManifestSize = 10 * 1024 * 1024
-var (
- imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`)
- referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`)
-)
+var globalVars = sync.OnceValue(func() (ret struct {
+ imageNamePattern, referencePattern *regexp.Regexp
+},
+) {
+ ret.imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`)
+ ret.referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`)
+ return ret
+})
type containerHeaders struct {
Status int
@@ -50,7 +56,7 @@ type containerHeaders struct {
Range string
Location string
ContentType string
- ContentLength int64
+ ContentLength optional.Option[int64]
}
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers
@@ -64,8 +70,8 @@ func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
if h.ContentType != "" {
resp.Header().Set("Content-Type", h.ContentType)
}
- if h.ContentLength != 0 {
- resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10))
+ if h.ContentLength.Has() {
+ resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength.Value(), 10))
}
if h.UploadUUID != "" {
resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID)
@@ -83,9 +89,7 @@ func jsonResponse(ctx *context.Context, status int, obj any) {
Status: status,
ContentType: "application/json",
})
- if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil {
- log.Error("JSON encode: %v", err)
- }
+ _ = json.NewEncoder(ctx.Resp).Encode(obj) // ignore network errors
}
func apiError(ctx *context.Context, status int, err error) {
@@ -133,7 +137,7 @@ func ReqContainerAccess(ctx *context.Context) {
// 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)
}
}
@@ -215,7 +219,7 @@ func GetRepositoryList(ctx *context.Context) {
if len(repositories) == n {
v := url.Values{}
if n > 0 {
- v.Add("n", strconv.Itoa(n))
+ v.Add("n", strconv.Itoa(n)) // FIXME: "n" can't be zero here, the logic is inconsistent with GetTagsList
}
v.Add("last", repositories[len(repositories)-1])
@@ -230,7 +234,7 @@ func GetRepositoryList(ctx *context.Context) {
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
-func InitiateUploadBlob(ctx *context.Context) {
+func PostBlobsUploads(ctx *context.Context) {
image := ctx.PathParam("image")
mount := ctx.FormTrim("mount")
@@ -312,14 +316,14 @@ func InitiateUploadBlob(ctx *context.Context) {
setResponseHeaders(ctx.Resp, &containerHeaders{
Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID),
- Range: "0-0",
UploadUUID: upload.ID,
Status: http.StatusAccepted,
})
}
-// https://docs.docker.com/registry/spec/api/#get-blob-upload
-func GetUploadBlob(ctx *context.Context) {
+// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
+func GetBlobsUpload(ctx *context.Context) {
+ image := ctx.PathParam("image")
uuid := ctx.PathParam("uuid")
upload, err := packages_model.GetBlobUploadByID(ctx, uuid)
@@ -332,15 +336,21 @@ func GetUploadBlob(ctx *context.Context) {
return
}
- setResponseHeaders(ctx.Resp, &containerHeaders{
- Range: fmt.Sprintf("0-%d", upload.BytesReceived),
+ // FIXME: undefined behavior when the uploaded content is empty: https://github.com/opencontainers/distribution-spec/issues/578
+ respHeaders := &containerHeaders{
+ Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID),
UploadUUID: upload.ID,
Status: http.StatusNoContent,
- })
+ }
+ if upload.BytesReceived > 0 {
+ respHeaders.Range = fmt.Sprintf("0-%d", upload.BytesReceived-1)
+ }
+ setResponseHeaders(ctx.Resp, respHeaders)
}
+// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
-func UploadBlob(ctx *context.Context) {
+func PatchBlobsUpload(ctx *context.Context) {
image := ctx.PathParam("image")
uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid"))
@@ -376,16 +386,19 @@ func UploadBlob(ctx *context.Context) {
return
}
- setResponseHeaders(ctx.Resp, &containerHeaders{
+ respHeaders := &containerHeaders{
Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, uploader.ID),
- Range: fmt.Sprintf("0-%d", uploader.Size()-1),
UploadUUID: uploader.ID,
Status: http.StatusAccepted,
- })
+ }
+ if uploader.Size() > 0 {
+ respHeaders.Range = fmt.Sprintf("0-%d", uploader.Size()-1)
+ }
+ setResponseHeaders(ctx.Resp, respHeaders)
}
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
-func EndUploadBlob(ctx *context.Context) {
+func PutBlobsUpload(ctx *context.Context) {
image := ctx.PathParam("image")
digest := ctx.FormTrim("digest")
@@ -403,12 +416,7 @@ func EndUploadBlob(ctx *context.Context) {
}
return
}
- doClose := true
- defer func() {
- if doClose {
- uploader.Close()
- }
- }()
+ defer uploader.Close()
if ctx.Req.Body != nil {
if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
@@ -441,11 +449,10 @@ func EndUploadBlob(ctx *context.Context) {
return
}
- if err := uploader.Close(); err != nil {
- apiError(ctx, http.StatusInternalServerError, err)
- return
- }
- doClose = false
+ // There was a strange bug: the "Close" fails with error "close .../tmp/package-upload/....: file already closed"
+ // AFAIK there should be no other "Close" call to the uploader between NewBlobUploader and this line.
+ // At least it's safe to call Close twice, so ignore the error.
+ _ = uploader.Close()
if err := container_service.RemoveBlobUploadByID(ctx, uploader.ID); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
@@ -460,7 +467,7 @@ func EndUploadBlob(ctx *context.Context) {
}
// https://docs.docker.com/registry/spec/api/#delete-blob-upload
-func CancelUploadBlob(ctx *context.Context) {
+func DeleteBlobsUpload(ctx *context.Context) {
uuid := ctx.PathParam("uuid")
_, err := packages_model.GetBlobUploadByID(ctx, uuid)
@@ -484,16 +491,15 @@ func CancelUploadBlob(ctx *context.Context) {
}
func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
- d := ctx.PathParam("digest")
-
- if digest.Digest(d).Validate() != nil {
+ d := digest.Digest(ctx.PathParam("digest"))
+ if d.Validate() != nil {
return nil, container_model.ErrContainerBlobNotExist
}
return workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Image: ctx.PathParam("image"),
- Digest: d,
+ Digest: string(d),
})
}
@@ -511,7 +517,7 @@ func HeadBlob(ctx *context.Context) {
setResponseHeaders(ctx.Resp, &containerHeaders{
ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest),
- ContentLength: blob.Blob.Size,
+ ContentLength: optional.Some(blob.Blob.Size),
Status: http.StatusOK,
})
}
@@ -533,9 +539,8 @@ func GetBlob(ctx *context.Context) {
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs
func DeleteBlob(ctx *context.Context) {
- d := ctx.PathParam("digest")
-
- if digest.Digest(d).Validate() != nil {
+ d := digest.Digest(ctx.PathParam("digest"))
+ if d.Validate() != nil {
apiErrorDefined(ctx, errBlobUnknown)
return
}
@@ -551,7 +556,7 @@ func DeleteBlob(ctx *context.Context) {
}
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
-func UploadManifest(ctx *context.Context) {
+func PutManifest(ctx *context.Context) {
reference := ctx.PathParam("reference")
mci := &manifestCreationInfo{
@@ -563,7 +568,7 @@ func UploadManifest(ctx *context.Context) {
IsTagged: digest.Digest(reference).Validate() != nil,
}
- if mci.IsTagged && !referencePattern.MatchString(reference) {
+ if mci.IsTagged && !globalVars().referencePattern.MatchString(reference) {
apiErrorDefined(ctx, errManifestInvalid.WithMessage("Tag is invalid"))
return
}
@@ -607,18 +612,18 @@ func UploadManifest(ctx *context.Context) {
}
func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.BlobSearchOptions, error) {
- reference := ctx.PathParam("reference")
-
opts := &container_model.BlobSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Image: ctx.PathParam("image"),
IsManifest: true,
}
- if digest.Digest(reference).Validate() == nil {
- opts.Digest = reference
- } else if referencePattern.MatchString(reference) {
+ reference := ctx.PathParam("reference")
+ if d := digest.Digest(reference); d.Validate() == nil {
+ opts.Digest = string(d)
+ } else if globalVars().referencePattern.MatchString(reference) {
opts.Tag = reference
+ opts.OnlyLead = true
} else {
return nil, container_model.ErrContainerBlobNotExist
}
@@ -650,7 +655,7 @@ func HeadManifest(ctx *context.Context) {
setResponseHeaders(ctx.Resp, &containerHeaders{
ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest),
ContentType: manifest.Properties.GetByName(container_module.PropertyMediaType),
- ContentLength: manifest.Blob.Size,
+ ContentLength: optional.Some(manifest.Blob.Size),
Status: http.StatusOK,
})
}
@@ -705,7 +710,7 @@ func DeleteManifest(ctx *context.Context) {
func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) {
serveDirectReqParams := make(url.Values)
serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType))
- s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob, serveDirectReqParams)
+ s, u, _, err := packages_service.OpenBlobForDownload(ctx, pfd.File, pfd.Blob, serveDirectReqParams)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
@@ -714,14 +719,14 @@ func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor)
headers := &containerHeaders{
ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest),
ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType),
- ContentLength: pfd.Blob.Size,
+ ContentLength: optional.Some(pfd.Blob.Size),
Status: http.StatusOK,
}
if u != nil {
headers.Status = http.StatusTemporaryRedirect
headers.Location = u.String()
-
+ headers.ContentLength = optional.None[int64]() // do not set Content-Length for redirect responses
setResponseHeaders(ctx.Resp, headers)
return
}
@@ -735,7 +740,7 @@ func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor)
}
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery
-func GetTagList(ctx *context.Context) {
+func GetTagsList(ctx *context.Context) {
image := ctx.PathParam("image")
if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil {
@@ -780,7 +785,8 @@ func GetTagList(ctx *context.Context) {
})
}
-// FIXME: Workaround to be removed in v1.20
+// FIXME: Workaround to be removed in v1.20.
+// Update maybe we should never really remote it, as long as there is legacy data?
// https://github.com/go-gitea/gitea/issues/19586
func workaroundGetContainerBlob(ctx *context.Context, opts *container_model.BlobSearchOptions) (*packages_model.PackageFileDescriptor, error) {
blob, err := container_model.GetContainerBlob(ctx, opts)
diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go
index 26faa7b024..de40215aa7 100644
--- a/routers/api/packages/container/manifest.go
+++ b/routers/api/packages/container/manifest.go
@@ -10,11 +10,13 @@ import (
"io"
"os"
"strings"
+ "time"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
container_model "code.gitea.io/gitea/models/packages/container"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
@@ -22,23 +24,12 @@ import (
"code.gitea.io/gitea/modules/util"
notify_service "code.gitea.io/gitea/services/notify"
packages_service "code.gitea.io/gitea/services/packages"
+ container_service "code.gitea.io/gitea/services/packages/container"
- digest "github.com/opencontainers/go-digest"
+ "github.com/opencontainers/go-digest"
oci "github.com/opencontainers/image-spec/specs-go/v1"
)
-func isValidMediaType(mt string) bool {
- return strings.HasPrefix(mt, "application/vnd.docker.") || strings.HasPrefix(mt, "application/vnd.oci.")
-}
-
-func isImageManifestMediaType(mt string) bool {
- return strings.EqualFold(mt, oci.MediaTypeImageManifest) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.v2+json")
-}
-
-func isImageIndexMediaType(mt string) bool {
- return strings.EqualFold(mt, oci.MediaTypeImageIndex) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.list.v2+json")
-}
-
// manifestCreationInfo describes a manifest to create
type manifestCreationInfo struct {
MediaType string
@@ -55,71 +46,71 @@ func processManifest(ctx context.Context, mci *manifestCreationInfo, buf *packag
if err := json.NewDecoder(buf).Decode(&index); err != nil {
return "", err
}
-
if index.SchemaVersion != 2 {
return "", errUnsupported.WithMessage("Schema version is not supported")
}
-
if _, err := buf.Seek(0, io.SeekStart); err != nil {
return "", err
}
- if !isValidMediaType(mci.MediaType) {
+ if !container_module.IsMediaTypeValid(mci.MediaType) {
mci.MediaType = index.MediaType
- if !isValidMediaType(mci.MediaType) {
+ if !container_module.IsMediaTypeValid(mci.MediaType) {
return "", errManifestInvalid.WithMessage("MediaType not recognized")
}
}
- if isImageManifestMediaType(mci.MediaType) {
- return processImageManifest(ctx, mci, buf)
- } else if isImageIndexMediaType(mci.MediaType) {
- return processImageManifestIndex(ctx, mci, buf)
+ // .../container/manifest.go:453:createManifestBlob() [E] Error inserting package blob: Error 1062 (23000): Duplicate entry '..........' for key 'package_blob.UQE_package_blob_md5'
+ releaser, err := globallock.Lock(ctx, containerGlobalLockKey(mci.Owner.ID, mci.Image, "manifest"))
+ if err != nil {
+ return "", err
+ }
+ defer releaser()
+
+ if container_module.IsMediaTypeImageManifest(mci.MediaType) {
+ return processOciImageManifest(ctx, mci, buf)
+ } else if container_module.IsMediaTypeImageIndex(mci.MediaType) {
+ return processOciImageIndex(ctx, mci, buf)
}
return "", errManifestInvalid
}
-func processImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) {
- manifestDigest := ""
-
- err := func() error {
- var manifest oci.Manifest
- if err := json.NewDecoder(buf).Decode(&manifest); err != nil {
- return err
- }
-
- if _, err := buf.Seek(0, io.SeekStart); err != nil {
- return err
- }
-
- ctx, committer, err := db.TxContext(ctx)
- if err != nil {
- return err
- }
- defer committer.Close()
-
- configDescriptor, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
- OwnerID: mci.Owner.ID,
- Image: mci.Image,
- Digest: string(manifest.Config.Digest),
- })
- if err != nil {
- return err
- }
+type processManifestTxRet struct {
+ pv *packages_model.PackageVersion
+ pb *packages_model.PackageBlob
+ created bool
+ digest string
+}
- configReader, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(configDescriptor.Blob.HashSHA256))
- if err != nil {
- return err
+func handleCreateManifestResult(ctx context.Context, err error, mci *manifestCreationInfo, contentStore *packages_module.ContentStore, txRet *processManifestTxRet) (string, error) {
+ if err != nil && txRet.created && txRet.pb != nil {
+ if err := contentStore.Delete(packages_module.BlobHash256Key(txRet.pb.HashSHA256)); err != nil {
+ log.Error("Error deleting package blob from content store: %v", err)
}
- defer configReader.Close()
+ return "", err
+ }
+ pd, err := packages_model.GetPackageDescriptor(ctx, txRet.pv)
+ if err != nil {
+ log.Error("Error getting package descriptor: %v", err) // ignore this error
+ } else {
+ notify_service.PackageCreate(ctx, mci.Creator, pd)
+ }
+ return txRet.digest, nil
+}
- metadata, err := container_module.ParseImageConfig(manifest.Config.MediaType, configReader)
- if err != nil {
- return err
- }
+func processOciImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (manifestDigest string, errRet error) {
+ manifest, configDescriptor, metadata, err := container_service.ParseManifestMetadata(ctx, buf, mci.Owner.ID, mci.Image)
+ if err != nil {
+ return "", err
+ }
+ if _, err = buf.Seek(0, io.SeekStart); err != nil {
+ return "", err
+ }
+ contentStore := packages_module.NewContentStore()
+ var txRet processManifestTxRet
+ err = db.WithTx(ctx, func(ctx context.Context) (err error) {
blobReferences := make([]*blobReference, 0, 1+len(manifest.Layers))
-
blobReferences = append(blobReferences, &blobReference{
Digest: manifest.Config.Digest,
MediaType: manifest.Config.MediaType,
@@ -150,78 +141,43 @@ func processImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *p
return err
}
- uploadVersion, err := packages_model.GetInternalVersionByNameAndVersion(ctx, mci.Owner.ID, packages_model.TypeContainer, mci.Image, container_model.UploadVersion)
- if err != nil && err != packages_model.ErrPackageNotExist {
+ uploadVersion, err := packages_model.GetInternalVersionByNameAndVersion(ctx, mci.Owner.ID, packages_model.TypeContainer, mci.Image, container_module.UploadVersion)
+ if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) {
return err
}
for _, ref := range blobReferences {
- if err := createFileFromBlobReference(ctx, pv, uploadVersion, ref); err != nil {
+ if _, err = createFileFromBlobReference(ctx, pv, uploadVersion, ref); err != nil {
return err
}
}
+ txRet.pv = pv
+ txRet.pb, txRet.created, txRet.digest, err = createManifestBlob(ctx, contentStore, mci, pv, buf)
+ return err
+ })
- pb, created, digest, err := createManifestBlob(ctx, mci, pv, buf)
- removeBlob := false
- defer func() {
- if removeBlob {
- contentStore := packages_module.NewContentStore()
- if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
- log.Error("Error deleting package blob from content store: %v", err)
- }
- }
- }()
- if err != nil {
- removeBlob = created
- return err
- }
-
- if err := committer.Commit(); err != nil {
- removeBlob = created
- return err
- }
-
- if err := notifyPackageCreate(ctx, mci.Creator, pv); err != nil {
- return err
- }
-
- manifestDigest = digest
+ return handleCreateManifestResult(ctx, err, mci, contentStore, &txRet)
+}
- return nil
- }()
- if err != nil {
+func processOciImageIndex(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (manifestDigest string, errRet error) {
+ var index oci.Index
+ if err := json.NewDecoder(buf).Decode(&index); err != nil {
+ return "", err
+ }
+ if _, err := buf.Seek(0, io.SeekStart); err != nil {
return "", err
}
- return manifestDigest, nil
-}
-
-func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) {
- manifestDigest := ""
-
- err := func() error {
- var index oci.Index
- if err := json.NewDecoder(buf).Decode(&index); err != nil {
- return err
- }
-
- if _, err := buf.Seek(0, io.SeekStart); err != nil {
- return err
- }
-
- ctx, committer, err := db.TxContext(ctx)
- if err != nil {
- return err
- }
- defer committer.Close()
-
+ contentStore := packages_module.NewContentStore()
+ var txRet processManifestTxRet
+ err := db.WithTx(ctx, func(ctx context.Context) (err error) {
metadata := &container_module.Metadata{
Type: container_module.TypeOCI,
Manifests: make([]*container_module.Manifest, 0, len(index.Manifests)),
}
for _, manifest := range index.Manifests {
- if !isImageManifestMediaType(manifest.MediaType) {
+ if !container_module.IsMediaTypeImageManifest(manifest.MediaType) {
return errManifestInvalid
}
@@ -265,50 +221,12 @@ func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, b
return err
}
- pb, created, digest, err := createManifestBlob(ctx, mci, pv, buf)
- removeBlob := false
- defer func() {
- if removeBlob {
- contentStore := packages_module.NewContentStore()
- if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
- log.Error("Error deleting package blob from content store: %v", err)
- }
- }
- }()
- if err != nil {
- removeBlob = created
- return err
- }
-
- if err := committer.Commit(); err != nil {
- removeBlob = created
- return err
- }
-
- if err := notifyPackageCreate(ctx, mci.Creator, pv); err != nil {
- return err
- }
-
- manifestDigest = digest
-
- return nil
- }()
- if err != nil {
- return "", err
- }
-
- return manifestDigest, nil
-}
-
-func notifyPackageCreate(ctx context.Context, doer *user_model.User, pv *packages_model.PackageVersion) error {
- pd, err := packages_model.GetPackageDescriptor(ctx, pv)
- if err != nil {
+ txRet.pv = pv
+ txRet.pb, txRet.created, txRet.digest, err = createManifestBlob(ctx, contentStore, mci, pv, buf)
return err
- }
-
- notify_service.PackageCreate(ctx, doer, pd)
+ })
- return nil
+ return handleCreateManifestResult(ctx, err, mci, contentStore, &txRet)
}
func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, metadata *container_module.Metadata) (*packages_model.PackageVersion, error) {
@@ -349,24 +267,31 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
LowerVersion: strings.ToLower(mci.Reference),
MetadataJSON: string(metadataJSON),
}
- var pv *packages_model.PackageVersion
- if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
+ pv, err := packages_model.GetOrInsertVersion(ctx, _pv)
+ if err != nil {
if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
log.Error("Error inserting package: %v", err)
return nil, err
}
- if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
- return nil, err
- }
-
- // keep download count on overwrite
- _pv.DownloadCount = pv.DownloadCount
-
- if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
- if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
- log.Error("Error inserting package: %v", err)
- return nil, err
+ if container_module.IsMediaTypeImageIndex(mci.MediaType) {
+ if pv.CreatedUnix.AsTime().Before(time.Now().Add(-24 * time.Hour)) {
+ if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
+ return nil, err
+ }
+ // keep download count on overwriting
+ _pv.DownloadCount = pv.DownloadCount
+ if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
+ if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
+ log.Error("Error inserting package: %v", err)
+ return nil, err
+ }
+ }
+ } else {
+ err = packages_model.UpdateVersion(ctx, &packages_model.PackageVersion{ID: pv.ID, MetadataJSON: _pv.MetadataJSON})
+ if err != nil {
+ return nil, err
+ }
}
}
}
@@ -376,14 +301,20 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
}
if mci.IsTagged {
- if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil {
- log.Error("Error setting package version property: %v", err)
+ if err = packages_model.InsertOrUpdateProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil {
+ return nil, err
+ }
+ } else {
+ if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged); err != nil {
return nil, err
}
}
+
+ if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference); err != nil {
+ return nil, err
+ }
for _, manifest := range metadata.Manifests {
- if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil {
- log.Error("Error setting package version property: %v", err)
+ if _, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil {
return nil, err
}
}
@@ -400,9 +331,9 @@ 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 == "" {
@@ -410,20 +341,21 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package
}
pf := &packages_model.PackageFile{
- VersionID: pv.ID,
- BlobID: ref.File.Blob.ID,
- Name: ref.Name,
- LowerName: ref.Name,
- IsLead: ref.IsLead,
+ VersionID: pv.ID,
+ BlobID: ref.File.Blob.ID,
+ Name: ref.Name,
+ LowerName: ref.Name,
+ CompositeKey: string(ref.Digest),
+ IsLead: ref.IsLead,
}
var err error
if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil {
if errors.Is(err, packages_model.ErrDuplicatePackageFile) {
// Skip this blob because the manifest contains the same filesystem layer multiple times.
- return nil
+ return pf, nil
}
log.Error("Error inserting package file: %v", err)
- return err
+ return nil, err
}
props := map[string]string{
@@ -433,21 +365,21 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package
for name, value := range props {
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, name, value); err != nil {
log.Error("Error setting package file property: %v", err)
- return err
+ return nil, err
}
}
- // Remove the file from the blob upload version
+ // Remove the ref file (old file) from the blob upload version
if uploadVersion != nil && ref.File.File != nil && uploadVersion.ID == ref.File.File.VersionID {
if err := packages_service.DeletePackageFile(ctx, ref.File.File); err != nil {
- return err
+ return nil, err
}
}
- return nil
+ return pf, nil
}
-func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *packages_model.PackageVersion, buf *packages_module.HashedBuffer) (*packages_model.PackageBlob, bool, string, error) {
+func createManifestBlob(ctx context.Context, contentStore *packages_module.ContentStore, mci *manifestCreationInfo, pv *packages_model.PackageVersion, buf *packages_module.HashedBuffer) (_ *packages_model.PackageBlob, created bool, manifestDigest string, _ error) {
pb, exists, err := packages_model.GetOrInsertBlob(ctx, packages_service.NewPackageBlob(buf))
if err != nil {
log.Error("Error inserting package blob: %v", err)
@@ -456,29 +388,48 @@ func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *pack
// FIXME: Workaround to be removed in v1.20
// https://github.com/go-gitea/gitea/issues/19586
if exists {
- err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(pb.HashSHA256))
+ err = contentStore.Has(packages_module.BlobHash256Key(pb.HashSHA256))
if err != nil && (errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist)) {
log.Debug("Package registry inconsistent: blob %s does not exist on file system", pb.HashSHA256)
exists = false
}
}
if !exists {
- contentStore := packages_module.NewContentStore()
if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), buf, buf.Size()); err != nil {
log.Error("Error saving package blob in content store: %v", err)
return nil, false, "", err
}
}
- manifestDigest := digestFromHashSummer(buf)
- err = createFileFromBlobReference(ctx, pv, nil, &blobReference{
+ manifestDigest = digestFromHashSummer(buf)
+ pf, err := createFileFromBlobReference(ctx, pv, nil, &blobReference{
Digest: digest.Digest(manifestDigest),
MediaType: mci.MediaType,
- Name: container_model.ManifestFilename,
+ Name: container_module.ManifestFilename,
File: &packages_model.PackageFileDescriptor{Blob: pb},
ExpectedSize: pb.Size,
IsLead: true,
})
+ if err != nil {
+ return nil, false, "", err
+ }
+ oldManifestFiles, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
+ OwnerID: mci.Owner.ID,
+ PackageType: packages_model.TypeContainer,
+ VersionID: pv.ID,
+ Query: container_module.ManifestFilename,
+ })
+ if err != nil {
+ return nil, false, "", err
+ }
+ for _, oldManifestFile := range oldManifestFiles {
+ if oldManifestFile.ID != pf.ID && oldManifestFile.IsLead {
+ err = packages_model.UpdateFile(ctx, &packages_model.PackageFile{ID: oldManifestFile.ID, IsLead: false}, []string{"is_lead"})
+ if err != nil {
+ return nil, false, "", err
+ }
+ }
+ }
return pb, !exists, manifestDigest, err
}
diff --git a/routers/api/packages/cran/cran.go b/routers/api/packages/cran/cran.go
index 8a20072cb6..732acd215f 100644
--- a/routers/api/packages/cran/cran.go
+++ b/routers/api/packages/cran/cran.go
@@ -250,7 +250,7 @@ func downloadPackageFile(ctx *context.Context, opts *cran_model.SearchOptions) {
return
}
- s, u, _, err := packages_service.GetPackageFileStream(ctx, pf)
+ s, u, _, err := packages_service.OpenFileForDownload(ctx, pf)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
diff --git a/routers/api/packages/debian/debian.go b/routers/api/packages/debian/debian.go
index fec34c91a6..346f71fa5d 100644
--- a/routers/api/packages/debian/debian.go
+++ b/routers/api/packages/debian/debian.go
@@ -59,7 +59,7 @@ func GetRepositoryFile(ctx *context.Context) {
key += "|" + component + "|" + architecture
}
- s, u, pf, err := packages_service.GetFileStreamByPackageVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion(
ctx,
pv,
&packages_service.PackageFileInfo{
@@ -106,7 +106,7 @@ func GetRepositoryFileByHash(ctx *context.Context) {
return
}
- s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0])
+ s, u, pf, err := packages_service.OpenFileForDownload(ctx, pfs[0])
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
@@ -210,7 +210,7 @@ func DownloadPackageFile(ctx *context.Context) {
name := ctx.PathParam("name")
version := ctx.PathParam("version")
- s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go
index 0b5daa7334..db7aeace50 100644
--- a/routers/api/packages/generic/generic.go
+++ b/routers/api/packages/generic/generic.go
@@ -31,7 +31,7 @@ func apiError(ctx *context.Context, status int, obj any) {
// DownloadPackageFile serves the specific generic package.
func DownloadPackageFile(ctx *context.Context) {
- s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
diff --git a/routers/api/packages/goproxy/goproxy.go b/routers/api/packages/goproxy/goproxy.go
index bde29df739..89ec86bce9 100644
--- a/routers/api/packages/goproxy/goproxy.go
+++ b/routers/api/packages/goproxy/goproxy.go
@@ -106,7 +106,7 @@ func DownloadPackageFile(ctx *context.Context) {
return
}
- s, u, _, err := packages_service.GetPackageFileStream(ctx, pfs[0])
+ s, u, _, err := packages_service.OpenFileForDownload(ctx, pfs[0])
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go
index fb12daaa46..39c34f4da4 100644
--- a/routers/api/packages/helm/helm.go
+++ b/routers/api/packages/helm/helm.go
@@ -122,7 +122,7 @@ func DownloadPackageFile(ctx *context.Context) {
return
}
- s, u, pf, err := packages_service.GetFileStreamByPackageVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion(
ctx,
pvs[0],
&packages_service.PackageFileInfo{
diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go
index 9089c2eccf..40a8ff8242 100644
--- a/routers/api/packages/maven/maven.go
+++ b/routers/api/packages/maven/maven.go
@@ -223,7 +223,7 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool
return
}
- s, u, _, err := packages_service.GetPackageBlobStream(ctx, pf, pb, nil)
+ s, u, _, err := packages_service.OpenBlobForDownload(ctx, pf, pb, nil)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go
index 6ec46bcb36..1f09816d32 100644
--- a/routers/api/packages/npm/npm.go
+++ b/routers/api/packages/npm/npm.go
@@ -85,7 +85,7 @@ func DownloadPackageFile(ctx *context.Context) {
packageVersion := ctx.PathParam("version")
filename := ctx.PathParam("filename")
- s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
@@ -132,7 +132,7 @@ func DownloadPackageFileByName(ctx *context.Context) {
return
}
- s, u, pf, err := packages_service.GetFileStreamByPackageVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion(
ctx,
pvs[0],
&packages_service.PackageFileInfo{
diff --git a/routers/api/packages/nuget/api_v2.go b/routers/api/packages/nuget/api_v2.go
index a726065ad0..801c60af13 100644
--- a/routers/api/packages/nuget/api_v2.go
+++ b/routers/api/packages/nuget/api_v2.go
@@ -246,21 +246,30 @@ type TypedValue[T any] struct {
}
type FeedEntryProperties struct {
- Version string `xml:"d:Version"`
- NormalizedVersion string `xml:"d:NormalizedVersion"`
Authors string `xml:"d:Authors"`
+ Copyright string `xml:"d:Copyright,omitempty"`
+ Created TypedValue[time.Time] `xml:"d:Created"`
Dependencies string `xml:"d:Dependencies"`
Description string `xml:"d:Description"`
- VersionDownloadCount TypedValue[int64] `xml:"d:VersionDownloadCount"`
+ DevelopmentDependency TypedValue[bool] `xml:"d:DevelopmentDependency"`
DownloadCount TypedValue[int64] `xml:"d:DownloadCount"`
- PackageSize TypedValue[int64] `xml:"d:PackageSize"`
- Created TypedValue[time.Time] `xml:"d:Created"`
+ ID string `xml:"d:Id"`
+ IconURL string `xml:"d:IconUrl,omitempty"`
+ Language string `xml:"d:Language,omitempty"`
LastUpdated TypedValue[time.Time] `xml:"d:LastUpdated"`
- Published TypedValue[time.Time] `xml:"d:Published"`
+ LicenseURL string `xml:"d:LicenseUrl,omitempty"`
+ MinClientVersion string `xml:"d:MinClientVersion,omitempty"`
+ NormalizedVersion string `xml:"d:NormalizedVersion"`
+ Owners string `xml:"d:Owners,omitempty"`
+ PackageSize TypedValue[int64] `xml:"d:PackageSize"`
ProjectURL string `xml:"d:ProjectUrl,omitempty"`
+ Published TypedValue[time.Time] `xml:"d:Published"`
ReleaseNotes string `xml:"d:ReleaseNotes,omitempty"`
RequireLicenseAcceptance TypedValue[bool] `xml:"d:RequireLicenseAcceptance"`
- Title string `xml:"d:Title"`
+ Tags string `xml:"d:Tags,omitempty"`
+ Title string `xml:"d:Title,omitempty"`
+ Version string `xml:"d:Version"`
+ VersionDownloadCount TypedValue[int64] `xml:"d:VersionDownloadCount"`
}
type FeedEntry struct {
@@ -353,21 +362,30 @@ func createEntry(l *linkBuilder, pd *packages_model.PackageDescriptor, withNames
Author: metadata.Authors,
Content: content,
Properties: &FeedEntryProperties{
- Version: pd.Version.Version,
- NormalizedVersion: pd.Version.Version,
Authors: metadata.Authors,
+ Copyright: metadata.Copyright,
+ Created: createdValue,
Dependencies: buildDependencyString(metadata),
Description: metadata.Description,
- VersionDownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount},
+ DevelopmentDependency: TypedValue[bool]{Type: "Edm.Boolean", Value: metadata.DevelopmentDependency},
DownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount},
- PackageSize: TypedValue[int64]{Type: "Edm.Int64", Value: pd.CalculateBlobSize()},
- Created: createdValue,
+ ID: pd.Package.Name,
+ IconURL: metadata.IconURL,
+ Language: metadata.Language,
LastUpdated: createdValue,
- Published: createdValue,
+ LicenseURL: metadata.LicenseURL,
+ MinClientVersion: metadata.MinClientVersion,
+ NormalizedVersion: pd.Version.Version,
+ Owners: metadata.Owners,
+ PackageSize: TypedValue[int64]{Type: "Edm.Int64", Value: pd.CalculateBlobSize()},
ProjectURL: metadata.ProjectURL,
+ Published: createdValue,
ReleaseNotes: metadata.ReleaseNotes,
RequireLicenseAcceptance: TypedValue[bool]{Type: "Edm.Boolean", Value: metadata.RequireLicenseAcceptance},
- Title: pd.Package.Name,
+ Tags: metadata.Tags,
+ Title: metadata.Title,
+ Version: pd.Version.Version,
+ VersionDownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount},
},
}
diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go
index fa5067a278..92d62d90b1 100644
--- a/routers/api/packages/nuget/nuget.go
+++ b/routers/api/packages/nuget/nuget.go
@@ -36,7 +36,7 @@ func apiError(ctx *context.Context, status int, obj any) {
})
}
-func xmlResponse(ctx *context.Context, status int, obj any) { //nolint:unparam
+func xmlResponse(ctx *context.Context, status int, obj any) { //nolint:unparam // status is always StatusOK
ctx.Resp.Header().Set("Content-Type", "application/atom+xml; charset=utf-8")
ctx.Resp.WriteHeader(status)
if _, err := ctx.Resp.Write([]byte(xml.Header)); err != nil {
@@ -405,7 +405,7 @@ func DownloadPackageFile(ctx *context.Context) {
packageVersion := ctx.PathParam("version")
filename := ctx.PathParam("filename")
- s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
@@ -669,7 +669,7 @@ func DownloadSymbolFile(ctx *context.Context) {
return
}
- s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0])
+ s, u, pf, err := packages_service.OpenFileForDownload(ctx, pfs[0])
if err != nil {
if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) {
apiError(ctx, http.StatusNotFound, err)
diff --git a/routers/api/packages/pub/pub.go b/routers/api/packages/pub/pub.go
index e7b07aefd0..4bd36e94b6 100644
--- a/routers/api/packages/pub/pub.go
+++ b/routers/api/packages/pub/pub.go
@@ -274,7 +274,7 @@ func DownloadPackageFile(ctx *context.Context) {
pf := pd.Files[0].File
- s, u, _, err := packages_service.GetPackageFileStream(ctx, pf)
+ s, u, _, err := packages_service.OpenFileForDownload(ctx, pf)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go
index 199f4e7478..9b5ae6c89d 100644
--- a/routers/api/packages/pypi/pypi.go
+++ b/routers/api/packages/pypi/pypi.go
@@ -82,7 +82,7 @@ func DownloadPackageFile(ctx *context.Context) {
packageVersion := ctx.PathParam("version")
filename := ctx.PathParam("filename")
- s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go
index a00a61c079..938c35341d 100644
--- a/routers/api/packages/rpm/rpm.go
+++ b/routers/api/packages/rpm/rpm.go
@@ -96,7 +96,7 @@ func GetRepositoryFile(ctx *context.Context) {
return
}
- s, u, pf, err := packages_service.GetFileStreamByPackageVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion(
ctx,
pv,
&packages_service.PackageFileInfo{
@@ -220,7 +220,7 @@ func DownloadPackageFile(ctx *context.Context) {
name := ctx.PathParam("name")
version := ctx.PathParam("version")
- s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go
index de8c7ef3ed..774d5520fd 100644
--- a/routers/api/packages/rubygems/rubygems.go
+++ b/routers/api/packages/rubygems/rubygems.go
@@ -14,6 +14,7 @@ import (
"strings"
packages_model "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages"
rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
@@ -177,7 +178,7 @@ func DownloadPackageFile(ctx *context.Context) {
return
}
- s, u, pf, err := packages_service.GetFileStreamByPackageVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion(
ctx,
pvs[0],
&packages_service.PackageFileInfo{
@@ -309,7 +310,7 @@ func GetPackageInfo(ctx *context.Context) {
apiError(ctx, http.StatusNotFound, nil)
return
}
- infoContent, err := makePackageInfo(ctx, versions)
+ infoContent, err := makePackageInfo(ctx, versions, cache.NewEphemeralCache())
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
@@ -317,7 +318,7 @@ func GetPackageInfo(ctx *context.Context) {
ctx.PlainText(http.StatusOK, infoContent)
}
-// GetAllPackagesVersions returns a custom text based format containing information about all versions of all rubygems.
+// GetAllPackagesVersions returns a custom text-based format containing information about all versions of all rubygems.
// ref: https://guides.rubygems.org/rubygems-org-compact-index-api/
func GetAllPackagesVersions(ctx *context.Context) {
packages, err := packages_model.GetPackagesByType(ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems)
@@ -326,6 +327,7 @@ func GetAllPackagesVersions(ctx *context.Context) {
return
}
+ ephemeralCache := cache.NewEphemeralCache()
out := &strings.Builder{}
out.WriteString("---\n")
for _, pkg := range packages {
@@ -338,7 +340,7 @@ func GetAllPackagesVersions(ctx *context.Context) {
continue
}
- info, err := makePackageInfo(ctx, versions)
+ info, err := makePackageInfo(ctx, versions, ephemeralCache)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
@@ -348,7 +350,14 @@ func GetAllPackagesVersions(ctx *context.Context) {
_, _ = fmt.Fprintf(out, "%s ", pkg.Name)
for i, v := range versions {
sep := util.Iif(i == len(versions)-1, "", ",")
- _, _ = fmt.Fprintf(out, "%s%s", v.Version, sep)
+ pd, err := packages_model.GetPackageDescriptorWithCache(ctx, v, ephemeralCache)
+ if errors.Is(err, util.ErrNotExist) {
+ continue
+ } else if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+ writePackageVersionForList(pd.Metadata, v.Version, sep, out)
}
_, _ = fmt.Fprintf(out, " %x\n", md5.Sum([]byte(info)))
}
@@ -356,6 +365,16 @@ func GetAllPackagesVersions(ctx *context.Context) {
ctx.PlainText(http.StatusOK, out.String())
}
+func writePackageVersionForList(metadata any, version, sep string, out *strings.Builder) {
+ if metadata, _ := metadata.(*rubygems_module.Metadata); metadata != nil && metadata.Platform != "" && metadata.Platform != "ruby" {
+ // VERSION_PLATFORM (see comment above in GetAllPackagesVersions)
+ _, _ = fmt.Fprintf(out, "%s_%s%s", version, metadata.Platform, sep)
+ } else {
+ // VERSION only
+ _, _ = fmt.Fprintf(out, "%s%s", version, sep)
+ }
+}
+
func writePackageVersionRequirements(prefix string, reqs []rubygems_module.VersionRequirement, out *strings.Builder) {
out.WriteString(prefix)
if len(reqs) == 0 {
@@ -367,11 +386,21 @@ func writePackageVersionRequirements(prefix string, reqs []rubygems_module.Versi
}
}
-func makePackageVersionDependency(ctx *context.Context, version *packages_model.PackageVersion) (string, error) {
+func writePackageVersionForDependency(version, platform string, out *strings.Builder) {
+ if platform != "" && platform != "ruby" {
+ // VERSION-PLATFORM (see comment below in makePackageVersionDependency)
+ _, _ = fmt.Fprintf(out, "%s-%s ", version, platform)
+ } else {
+ // VERSION only
+ _, _ = fmt.Fprintf(out, "%s ", version)
+ }
+}
+
+func makePackageVersionDependency(ctx *context.Context, version *packages_model.PackageVersion, c *cache.EphemeralCache) (string, error) {
// format: VERSION[-PLATFORM] [DEPENDENCY[,DEPENDENCY,...]]|REQUIREMENT[,REQUIREMENT,...]
// DEPENDENCY: GEM:CONSTRAINT[&CONSTRAINT]
// REQUIREMENT: KEY:VALUE (always contains "checksum")
- pd, err := packages_model.GetPackageDescriptor(ctx, version)
+ pd, err := packages_model.GetPackageDescriptorWithCache(ctx, version, c)
if err != nil {
return "", err
}
@@ -388,8 +417,7 @@ func makePackageVersionDependency(ctx *context.Context, version *packages_model.
}
buf := &strings.Builder{}
- buf.WriteString(version.Version)
- buf.WriteByte(' ')
+ writePackageVersionForDependency(version.Version, metadata.Platform, buf)
for i, dep := range metadata.RuntimeDependencies {
sep := util.Iif(i == 0, "", ",")
writePackageVersionRequirements(fmt.Sprintf("%s%s:", sep, dep.Name), dep.Version, buf)
@@ -404,10 +432,10 @@ func makePackageVersionDependency(ctx *context.Context, version *packages_model.
return buf.String(), nil
}
-func makePackageInfo(ctx *context.Context, versions []*packages_model.PackageVersion) (string, error) {
+func makePackageInfo(ctx *context.Context, versions []*packages_model.PackageVersion, c *cache.EphemeralCache) (string, error) {
ret := "---\n"
for _, v := range versions {
- dep, err := makePackageVersionDependency(ctx, v)
+ dep, err := makePackageVersionDependency(ctx, v, c)
if err != nil {
return "", err
}
diff --git a/routers/api/packages/rubygems/rubygems_test.go b/routers/api/packages/rubygems/rubygems_test.go
new file mode 100644
index 0000000000..a07e12a7d3
--- /dev/null
+++ b/routers/api/packages/rubygems/rubygems_test.go
@@ -0,0 +1,41 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package rubygems
+
+import (
+ "strings"
+ "testing"
+
+ rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestWritePackageVersion(t *testing.T) {
+ buf := &strings.Builder{}
+
+ writePackageVersionForList(nil, "1.0", " ", buf)
+ assert.Equal(t, "1.0 ", buf.String())
+ buf.Reset()
+
+ writePackageVersionForList(&rubygems_module.Metadata{Platform: "ruby"}, "1.0", " ", buf)
+ assert.Equal(t, "1.0 ", buf.String())
+ buf.Reset()
+
+ writePackageVersionForList(&rubygems_module.Metadata{Platform: "linux"}, "1.0", " ", buf)
+ assert.Equal(t, "1.0_linux ", buf.String())
+ buf.Reset()
+
+ writePackageVersionForDependency("1.0", "", buf)
+ assert.Equal(t, "1.0 ", buf.String())
+ buf.Reset()
+
+ writePackageVersionForDependency("1.0", "ruby", buf)
+ assert.Equal(t, "1.0 ", buf.String())
+ buf.Reset()
+
+ writePackageVersionForDependency("1.0", "os", buf)
+ assert.Equal(t, "1.0-os ", buf.String())
+ buf.Reset()
+}
diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go
index 47439c4c3b..bf542f33a7 100644
--- a/routers/api/packages/swift/swift.go
+++ b/routers/api/packages/swift/swift.go
@@ -429,7 +429,7 @@ func DownloadPackageFile(ctx *context.Context) {
pf := pd.Files[0].File
- s, u, _, err := packages_service.GetPackageFileStream(ctx, pf)
+ s, u, _, err := packages_service.OpenFileForDownload(ctx, pf)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
diff --git a/routers/api/packages/vagrant/vagrant.go b/routers/api/packages/vagrant/vagrant.go
index 3afaa5de1f..9eb67e5397 100644
--- a/routers/api/packages/vagrant/vagrant.go
+++ b/routers/api/packages/vagrant/vagrant.go
@@ -218,7 +218,7 @@ func UploadPackageFile(ctx *context.Context) {
}
func DownloadPackageFile(ctx *context.Context) {
- s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
+ s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
diff --git a/routers/api/v1/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/org.go b/routers/api/v1/admin/org.go
index 8808a1587d..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
@@ -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,
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/user.go b/routers/api/v1/admin/user.go
index 3ba77604ec..8a267cc418 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -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
@@ -239,7 +239,7 @@ func EditUser(ctx *context.APIContext) {
Location: optional.FromPtr(form.Location),
Description: optional.FromPtr(form.Description),
IsActive: optional.FromPtr(form.Active),
- IsAdmin: optional.FromPtr(form.Admin),
+ IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin),
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
AllowGitHook: optional.FromPtr(form.AllowGitHook),
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
@@ -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
@@ -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
@@ -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"),
@@ -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
diff --git a/routers/api/v1/admin/user_badge.go b/routers/api/v1/admin/user_badge.go
index 6d9665a72b..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:
@@ -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
@@ -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
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index b98863b418..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)
})
}
@@ -971,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)
@@ -1077,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)
@@ -1201,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))
@@ -1241,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)
@@ -1279,7 +1285,14 @@ func Routes() *web.Router {
}, reqToken(), reqAdmin())
m.Group("/actions", func() {
m.Get("/tasks", repo.ListActionTasks)
- m.Get("/runs/{run}/artifacts", repo.GetArtifactsOfRun)
+ 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)
@@ -1410,21 +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.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
- m.Get("/signing-key.gpg", misc.SigningKey)
+ 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)
@@ -1445,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))
@@ -1729,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/misc/markup.go b/routers/api/v1/misc/markup.go
index 0cd4b8c5c5..909310b4c8 100644
--- a/routers/api/v1/misc/markup.go
+++ b/routers/api/v1/misc/markup.go
@@ -42,7 +42,7 @@ func Markup(ctx *context.APIContext) {
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)
}
@@ -73,7 +73,7 @@ func Markdown(ctx *context.APIContext) {
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, "")
}
diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go
index 667396e39c..db70e04b8f 100644
--- a/routers/api/v1/misc/signing.go
+++ b/routers/api/v1/misc/signing.go
@@ -4,14 +4,35 @@
package misc
import (
- "fmt"
-
+ "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
@@ -44,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.APIErrorInternal(err)
- return
- }
- _, err = ctx.Write([]byte(content))
- if err != nil {
- ctx.APIErrorInternal(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/org/action.go b/routers/api/v1/org/action.go
index 700a5ef8ea..3ae5e60585 100644
--- a/routers/api/v1/org/action.go
+++ b/routers/api/v1/org/action.go
@@ -384,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)
@@ -419,7 +419,7 @@ func (Action) CreateVariable(ctx *context.APIContext) {
return
}
- ctx.Status(http.StatusNoContent)
+ ctx.Status(http.StatusCreated)
}
// UpdateVariable update an org-level variable
@@ -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/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/member.go b/routers/api/v1/org/member.go
index a1875a7886..1c12b0cc94 100644
--- a/routers/api/v1/org/member.go
+++ b/routers/api/v1/org/member.go
@@ -133,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:
@@ -186,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:
@@ -240,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:
@@ -282,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:
@@ -324,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:
diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go
index c9208f4757..05744ba155 100644
--- a/routers/api/v1/org/org.go
+++ b/routers/api/v1/org/org.go
@@ -26,12 +26,10 @@ 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 {
@@ -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
@@ -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,
diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go
index 71c21f2dde..1a1710750a 100644
--- a/routers/api/v1/org/team.go
+++ b/routers/api/v1/org/team.go
@@ -426,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:
@@ -467,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:
@@ -509,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:
diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go
index 6aef529f98..a57db015f0 100644
--- a/routers/api/v1/repo/action.go
+++ b/routers/api/v1/repo/action.go
@@ -339,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)
@@ -373,7 +373,7 @@ func (Action) CreateVariable(ctx *context.APIContext) {
return
}
- ctx.Status(http.StatusNoContent)
+ ctx.Status(http.StatusCreated)
}
// UpdateVariable update a repo-level variable
@@ -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
@@ -1061,6 +1320,58 @@ func GetArtifactsOfRun(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, &res)
}
+// DeleteActionRun Delete a workflow run
+func DeleteActionRun(ctx *context.APIContext) {
+ // swagger:operation DELETE /repos/{owner}/{repo}/actions/runs/{run} repository deleteActionRun
+ // ---
+ // summary: Delete a workflow run
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: name of the owner
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repository
+ // type: string
+ // required: true
+ // - name: run
+ // in: path
+ // description: runid of the workflow run
+ // type: integer
+ // required: true
+ // responses:
+ // "204":
+ // description: "No Content"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ runID := ctx.PathParamInt64("run")
+ run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.APIError(http.StatusNotFound, err)
+ return
+ } else if err != nil {
+ ctx.APIErrorInternal(err)
+ return
+ }
+ if !run.Status.IsDone() {
+ ctx.APIError(http.StatusBadRequest, "this workflow run is not done")
+ return
+ }
+
+ if err := actions_service.DeleteRun(ctx, run); err != nil {
+ ctx.APIErrorInternal(err)
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
+
// GetArtifacts Lists all artifacts for a repository.
func GetArtifacts(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/artifacts repository getArtifacts
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..9af958a5b7 100644
--- a/routers/api/v1/repo/branch.go
+++ b/routers/api/v1/repo/branch.go
@@ -224,9 +224,9 @@ func CreateBranch(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
- } else if len(opt.OldBranchName) > 0 { //nolint
- if gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, 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.APIErrorInternal(err)
return
@@ -579,7 +579,7 @@ 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.APIError(http.StatusBadRequest, "both rule_name and branch_name are empty")
@@ -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/collaborators.go b/routers/api/v1/repo/collaborators.go
index d1652c1d51..c2c10cc695 100644
--- a/routers/api/v1/repo/collaborators.go
+++ b/routers/api/v1/repo/collaborators.go
@@ -93,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:
@@ -145,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
@@ -264,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:
diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go
index 20258064a0..6a93be624f 100644
--- a/routers/api/v1/repo/commits.go
+++ b/routers/api/v1/repo/commits.go
@@ -8,6 +8,7 @@ import (
"math"
"net/http"
"strconv"
+ "time"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
@@ -116,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')
@@ -148,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.",
@@ -198,6 +226,8 @@ 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.APIErrorInternal(err)
@@ -205,7 +235,7 @@ func GetAllCommits(ctx *context.APIContext) {
}
// 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.APIErrorInternal(err)
return
@@ -221,6 +251,8 @@ func GetAllCommits(ctx *context.APIContext) {
Not: not,
Revision: []string{sha},
RelPath: []string{path},
+ Since: since,
+ Until: until,
})
if err != nil {
@@ -237,6 +269,8 @@ func GetAllCommits(ctx *context.APIContext) {
File: path,
Not: not,
Page: listOptions.Page,
+ Since: since,
+ Until: until,
})
if err != nil {
ctx.APIErrorInternal(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.go b/routers/api/v1/repo/issue.go
index e678db5262..d4a5872fd1 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -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,
@@ -895,6 +895,15 @@ 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.APIErrorInternal(err)
return
diff --git a/routers/api/v1/repo/issue_comment.go b/routers/api/v1/repo/issue_comment.go
index 0c572a06a8..cc342a9313 100644
--- a/routers/api/v1/repo/issue_comment.go
+++ b/routers/api/v1/repo/issue_comment.go
@@ -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.APIError(http.StatusForbidden, err)
- } else {
- ctx.APIErrorInternal(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))
diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go
index 2048c76ea0..1b58beb7b6 100644
--- a/routers/api/v1/repo/issue_dependency.go
+++ b/routers/api/v1/repo/issue_dependency.go
@@ -77,10 +77,7 @@ func GetIssueDependencies(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 == 0 {
limit = setting.API.DefaultPagingNum
@@ -328,10 +325,7 @@ func GetIssueBlocks(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
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/issue_subscription.go b/routers/api/v1/repo/issue_subscription.go
index 21e549496d..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:
diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go
index dd6abf94c6..171da272cc 100644
--- a/routers/api/v1/repo/issue_tracked_time.go
+++ b/routers/api/v1/repo/issue_tracked_time.go
@@ -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:
diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go
index f2e0cad86c..c1e0b47d33 100644
--- a/routers/api/v1/repo/migrate.go
+++ b/routers/api/v1/repo/migrate.go
@@ -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)
}
}
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/pull.go b/routers/api/v1/repo/pull.go
index c0ab381bc8..2c194f9253 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
@@ -73,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
@@ -706,6 +707,11 @@ 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.APIErrorInternal(err)
return
@@ -1296,7 +1302,7 @@ func UpdatePullRequest(ctx *context.APIContext) {
// default merge commit message
message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch)
- if err = pull_service.Update(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.APIError(http.StatusConflict, "merge failed because of conflict")
return
diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go
index b6f5a3ac9e..272b395dfb 100644
--- a/routers/api/v1/repo/release.go
+++ b/routers/api/v1/repo/release.go
@@ -247,7 +247,9 @@ 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.APIError(http.StatusConflict, err)
} else if release_service.IsErrProtectedTagName(err) {
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 7caf004b4a..8acc912796 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -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"),
diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go
index 756adcf3a3..40007ea1e5 100644
--- a/routers/api/v1/repo/status.go
+++ b/routers/api/v1/repo/status.go
@@ -258,19 +258,24 @@ func GetCombinedCommitStatusByRef(ctx *context.APIContext) {
repo := ctx.Repo.Repository
- statuses, count, err := git_model.GetLatestCommitStatus(ctx, repo.ID, refCommit.Commit.ID.String(), utils.GetListOptions(ctx))
+ statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, refCommit.Commit.ID.String(), utils.GetListOptions(ctx))
if err != nil {
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/wiki.go b/routers/api/v1/repo/wiki.go
index d5840b4149..8e24ffa465 100644
--- a/routers/api/v1/repo/wiki.go
+++ b/routers/api/v1/repo/wiki.go
@@ -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
@@ -434,10 +431,7 @@ func ListPageRevisions(ctx *context.APIContext) {
// get commit count - wiki revisions
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(
@@ -505,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/shared/runners.go b/routers/api/v1/shared/runners.go
index d42f330d1c..e9834aff9f 100644
--- a/routers/api/v1/shared/runners.go
+++ b/routers/api/v1/shared/runners.go
@@ -67,6 +67,28 @@ func ListRunners(ctx *context.APIContext, ownerID, repoID int64) {
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
@@ -77,13 +99,8 @@ 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, err := actions_model.GetRunnerByID(ctx, runnerID)
- if err != nil {
- ctx.APIErrorNotFound(err)
- return
- }
- if !runner.EditableInContext(ownerID, repoID) {
- ctx.APIErrorNotFound("No permission to get this runner")
+ runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID)
+ if !ok {
return
}
ctx.JSON(http.StatusOK, convert.ToActionRunner(ctx, runner))
@@ -96,20 +113,12 @@ func GetRunner(ctx *context.APIContext, ownerID, repoID, runnerID int64) {
// 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) {
- 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 {
- ctx.APIErrorInternal(err)
- return
- }
- if !runner.EditableInContext(ownerID, repoID) {
- ctx.APIErrorNotFound("No permission to delete this runner")
+ runner, ok := getRunnerByID(ctx, ownerID, repoID, runnerID)
+ if !ok {
return
}
- err = actions_model.DeleteRunner(ctx, runner.ID)
+ err := actions_model.DeleteRunner(ctx, runner.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
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 04097fcc95..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"
@@ -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)
@@ -162,7 +161,7 @@ func CreateVariable(ctx *context.APIContext) {
return
}
- ctx.Status(http.StatusNoContent)
+ ctx.Status(http.StatusCreated)
}
// UpdateVariable update a user-level variable which is created by current doer
@@ -358,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/user/app.go b/routers/api/v1/user/app.go
index 7201010161..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
@@ -83,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
@@ -149,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
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/follower.go b/routers/api/v1/user/follower.go
index 0d0c0be7e0..339b994af4 100644
--- a/routers/api/v1/user/follower.go
+++ b/routers/api/v1/user/follower.go
@@ -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
@@ -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
@@ -167,7 +167,7 @@ func CheckMyFollowing(ctx *context.APIContext) {
// parameters:
// - name: username
// in: path
- // description: username of followed user
+ // description: username of the user to check for authenticated followers
// type: string
// required: true
// responses:
@@ -187,12 +187,12 @@ func CheckFollowing(ctx *context.APIContext) {
// parameters:
// - name: username
// in: path
- // description: username of following user
+ // description: username of the following user
// type: string
// required: true
// - name: target
// in: path
- // description: username of followed user
+ // description: username of the followed user
// type: string
// required: true
// responses:
@@ -216,7 +216,7 @@ func Follow(ctx *context.APIContext) {
// parameters:
// - name: username
// in: path
- // description: username of user to follow
+ // description: username of the user to follow
// type: string
// required: true
// responses:
@@ -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:
diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go
index b76bd8a1ee..9ec4d2c938 100644
--- a/routers/api/v1/user/gpg_key.go
+++ b/routers/api/v1/user/gpg_key.go
@@ -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
diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go
index 628f5d6cac..aa69245e49 100644
--- a/routers/api/v1/user/key.go
+++ b/routers/api/v1/user/key.go
@@ -136,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
diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go
index 6aabc4fb90..6d0129681e 100644
--- a/routers/api/v1/user/repo.go
+++ b/routers/api/v1/user/repo.go
@@ -19,7 +19,7 @@ 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,
@@ -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,
diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go
index 4b0cb45d67..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
diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go
index 757a548518..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:
@@ -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:
@@ -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
diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go
index 76d7c81793..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
diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go
index 532d157e35..6f598f14c8 100644
--- a/routers/api/v1/utils/hook.go
+++ b/routers/api/v1/utils/hook.go
@@ -15,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"
@@ -92,6 +93,10 @@ func checkCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption)
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
}
@@ -154,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) {
@@ -162,9 +203,6 @@ 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 {
@@ -183,31 +221,7 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
IsSystemWebhook: isSystemWebhook,
HookEvent: &webhook_module.HookEvent{
ChooseEvents: true,
- HookEvents: webhook_module.HookEvents{
- webhook_module.HookEventCreate: util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true),
- webhook_module.HookEventDelete: util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true),
- webhook_module.HookEventFork: util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true),
- webhook_module.HookEventIssues: issuesHook(form.Events, "issues_only"),
- webhook_module.HookEventIssueAssign: issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)),
- webhook_module.HookEventIssueLabel: issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)),
- webhook_module.HookEventIssueMilestone: issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)),
- webhook_module.HookEventIssueComment: issuesHook(form.Events, string(webhook_module.HookEventIssueComment)),
- webhook_module.HookEventPush: util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true),
- webhook_module.HookEventPullRequest: pullHook(form.Events, "pull_request_only"),
- webhook_module.HookEventPullRequestAssign: pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)),
- webhook_module.HookEventPullRequestLabel: pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)),
- webhook_module.HookEventPullRequestMilestone: pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)),
- webhook_module.HookEventPullRequestComment: pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)),
- webhook_module.HookEventPullRequestReview: pullHook(form.Events, "pull_request_review"),
- webhook_module.HookEventPullRequestReviewRequest: pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)),
- webhook_module.HookEventPullRequestSync: pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)),
- webhook_module.HookEventWiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true),
- webhook_module.HookEventRepository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true),
- webhook_module.HookEventRelease: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true),
- webhook_module.HookEventPackage: util.SliceContainsString(form.Events, string(webhook_module.HookEventPackage), true),
- webhook_module.HookEventStatus: util.SliceContainsString(form.Events, string(webhook_module.HookEventStatus), true),
- webhook_module.HookEventWorkflowJob: util.SliceContainsString(form.Events, string(webhook_module.HookEventWorkflowJob), true),
- },
+ HookEvents: updateHookEvents(form.Events),
BranchFilter: form.BranchFilter,
},
IsActive: form.Active,
@@ -324,6 +338,10 @@ 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 {
@@ -352,19 +370,10 @@ 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.HookEvents[webhook_module.HookEventCreate] = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true)
- w.HookEvents[webhook_module.HookEventPush] = util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true)
- w.HookEvents[webhook_module.HookEventDelete] = util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true)
- w.HookEvents[webhook_module.HookEventFork] = util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true)
- w.HookEvents[webhook_module.HookEventRepository] = util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true)
- w.HookEvents[webhook_module.HookEventWiki] = util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true)
- w.HookEvents[webhook_module.HookEventRelease] = util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true)
w.BranchFilter = form.BranchFilter
err := w.SetHeaderAuthorization(form.AuthorizationHeader)
@@ -373,23 +382,6 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
return false
}
- // Issues
- w.HookEvents[webhook_module.HookEventIssues] = issuesHook(form.Events, "issues_only")
- w.HookEvents[webhook_module.HookEventIssueAssign] = issuesHook(form.Events, string(webhook_module.HookEventIssueAssign))
- w.HookEvents[webhook_module.HookEventIssueLabel] = issuesHook(form.Events, string(webhook_module.HookEventIssueLabel))
- w.HookEvents[webhook_module.HookEventIssueMilestone] = issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone))
- w.HookEvents[webhook_module.HookEventIssueComment] = issuesHook(form.Events, string(webhook_module.HookEventIssueComment))
-
- // Pull requests
- w.HookEvents[webhook_module.HookEventPullRequest] = pullHook(form.Events, "pull_request_only")
- w.HookEvents[webhook_module.HookEventPullRequestAssign] = pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign))
- w.HookEvents[webhook_module.HookEventPullRequestLabel] = pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel))
- w.HookEvents[webhook_module.HookEventPullRequestMilestone] = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone))
- w.HookEvents[webhook_module.HookEventPullRequestComment] = pullHook(form.Events, string(webhook_module.HookEventPullRequestComment))
- w.HookEvents[webhook_module.HookEventPullRequestReview] = pullHook(form.Events, "pull_request_review")
- w.HookEvents[webhook_module.HookEventPullRequestReviewRequest] = pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest))
- w.HookEvents[webhook_module.HookEventPullRequestSync] = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync))
-
if err := w.UpdateEvent(); err != nil {
ctx.APIErrorInternal(err)
return false
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()
+ },
+ })
+}
diff --git a/routers/common/markup.go b/routers/common/markup.go
index 4c77ff33ed..00b2dd07c6 100644
--- a/routers/common/markup.go
+++ b/routers/common/markup.go
@@ -76,7 +76,11 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur
})
rctx = rctx.WithMarkupType(markdown.MarkupName)
case "comment":
- rctx = renderhelper.NewRenderContextRepoComment(ctx, repoModel, renderhelper.RepoCommentOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName})
+ rctx = renderhelper.NewRenderContextRepoComment(ctx, repoModel, renderhelper.RepoCommentOptions{
+ DeprecatedOwnerName: repoOwnerName,
+ DeprecatedRepoName: repoName,
+ FootnoteContextID: "preview",
+ })
rctx = rctx.WithMarkupType(markdown.MarkupName)
case "wiki":
rctx = renderhelper.NewRenderContextRepoWiki(ctx, repoModel, renderhelper.RepoWikiOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName})
diff --git a/routers/install/install.go b/routers/install/install.go
index 2962f3948f..dc8f209f3b 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -10,6 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
+ "slices"
"strconv"
"strings"
"time"
@@ -98,14 +99,7 @@ func Install(ctx *context.Context) {
form.SSLMode = setting.Database.SSLMode
curDBType := setting.Database.Type.String()
- var isCurDBTypeSupported bool
- for _, dbType := range setting.SupportedDatabaseTypes {
- if dbType == curDBType {
- isCurDBTypeSupported = true
- break
- }
- }
- if !isCurDBTypeSupported {
+ if !slices.Contains(setting.SupportedDatabaseTypes, curDBType) {
curDBType = "mysql"
}
ctx.Data["CurDbType"] = curDBType
@@ -606,6 +600,8 @@ func SubmitInstall(ctx *context.Context) {
// InstallDone shows the "post-install" page, makes it easier to develop the page.
// The name is not called as "PostInstall" to avoid misinterpretation as a handler for "POST /install"
-func InstallDone(ctx *context.Context) { //nolint
+func InstallDone(ctx *context.Context) { //nolint:revive // export stutter
+ hasUsers, _ := user_model.HasUsers(ctx)
+ ctx.Data["IsAccountCreated"] = hasUsers.HasAnyUser
ctx.HTML(http.StatusOK, tplPostInstall)
}
diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go
index 8b1e849e7a..e8bef7d6c1 100644
--- a/routers/private/hook_post_receive.go
+++ b/routers/private/hook_post_receive.go
@@ -207,25 +207,19 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
return
}
- cols := make([]string, 0, 2)
-
- if isPrivate.Has() {
+ // FIXME: these options are not quite right, for example: changing visibility should do more works than just setting the is_private flag
+ // These options should only be used for "push-to-create"
+ if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() {
+ // TODO: it needs to do more work
repo.IsPrivate = isPrivate.Value()
- cols = append(cols, "is_private")
+ if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
+ ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to change visibility"})
+ }
}
-
- if isTemplate.Has() {
+ if isTemplate.Has() && repo.IsTemplate != isTemplate.Value() {
repo.IsTemplate = isTemplate.Value()
- cols = append(cols, "is_template")
- }
-
- if len(cols) > 0 {
- if err := repo_model.UpdateRepositoryCols(ctx, repo, cols...); err != nil {
- log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err)
- ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{
- Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err),
- })
- return
+ if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_template"); err != nil {
+ ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to change template status"})
}
}
}
diff --git a/routers/private/hook_verification_test.go b/routers/private/hook_verification_test.go
index f6c2e1087f..8653e34daa 100644
--- a/routers/private/hook_verification_test.go
+++ b/routers/private/hook_verification_test.go
@@ -18,7 +18,9 @@ func TestVerifyCommits(t *testing.T) {
unittest.PrepareTestEnv(t)
gitRepo, err := git.OpenRepository(t.Context(), testReposDir+"repo1_hook_verification")
- defer gitRepo.Close()
+ if err != nil {
+ defer gitRepo.Close()
+ }
assert.NoError(t, err)
objectFormat, err := gitRepo.GetObjectFormat()
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 80d554b6e3..0f6f31b884 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -177,7 +177,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
customURLMapping = nil
}
var scopes []string
- for _, s := range strings.Split(form.Oauth2Scopes, ",") {
+ for s := range strings.SplitSeq(form.Oauth2Scopes, ",") {
s = strings.TrimSpace(s)
if s != "" {
scopes = append(scopes, s)
diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go
index 520f14e89f..0e5b23db6d 100644
--- a/routers/web/admin/config.go
+++ b/routers/web/admin/config.go
@@ -61,7 +61,7 @@ func TestCache(ctx *context.Context) {
func shadowPasswordKV(cfgItem, splitter string) string {
fields := strings.Split(cfgItem, splitter)
- for i := 0; i < len(fields); i++ {
+ for i := range fields {
if strings.HasPrefix(fields[i], "password=") {
fields[i] = "password=******"
break
@@ -200,7 +200,7 @@ func ChangeConfig(ctx *context.Context) {
value := ctx.FormString("value")
cfg := setting.Config()
- marshalBool := func(v string) (string, error) { //nolint:unparam
+ marshalBool := func(v string) (string, error) { //nolint:unparam // error is always nil
if b, _ := strconv.ParseBool(v); b {
return "true", nil
}
diff --git a/routers/web/admin/diagnosis.go b/routers/web/admin/diagnosis.go
index d040dbe0ba..5395529d66 100644
--- a/routers/web/admin/diagnosis.go
+++ b/routers/web/admin/diagnosis.go
@@ -16,13 +16,7 @@ import (
)
func MonitorDiagnosis(ctx *context.Context) {
- seconds := ctx.FormInt64("seconds")
- if seconds <= 1 {
- seconds = 1
- }
- if seconds > 300 {
- seconds = 300
- }
+ seconds := min(max(ctx.FormInt64("seconds"), 1), 300)
httplib.ServeSetHeaders(ctx.Resp, &httplib.ServeHeaderOptions{
ContentType: "application/zip",
diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go
index 21a8ab0d17..e9d6abbe92 100644
--- a/routers/web/admin/notice.go
+++ b/routers/web/admin/notice.go
@@ -26,10 +26,7 @@ func Notices(ctx *context.Context) {
ctx.Data["PageIsAdminNotices"] = true
total := system_model.CountNotices(ctx)
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
notices, err := system_model.Notices(ctx, page, setting.UI.Admin.NoticePagingNum)
if err != nil {
diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go
index 35e61efa17..e34f203aaf 100644
--- a/routers/web/admin/orgs.go
+++ b/routers/web/admin/orgs.go
@@ -27,7 +27,7 @@ func Organizations(ctx *context.Context) {
ctx.SetFormString("sort", UserSearchDefaultAdminSort)
}
- explore.RenderUserSearch(ctx, &user_model.SearchUserOptions{
+ explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer,
Type: user_model.UserTypeOrganization,
IncludeReserved: true, // administrator needs to list all accounts include reserved
diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go
index 5122342259..1904bfee11 100644
--- a/routers/web/admin/packages.go
+++ b/routers/web/admin/packages.go
@@ -24,10 +24,7 @@ const (
// Packages shows all packages
func Packages(ctx *context.Context) {
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
query := ctx.FormTrim("q")
packageType := ctx.FormTrim("type")
sort := ctx.FormTrim("sort")
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index e42cbb316c..27577cd35b 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/explore"
@@ -64,7 +65,7 @@ func Users(ctx *context.Context) {
"SortType": sortType,
}
- explore.RenderUserSearch(ctx, &user_model.SearchUserOptions{
+ explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer,
Type: user_model.UserTypeIndividual,
ListOptions: db.ListOptions{
@@ -268,7 +269,7 @@ func ViewUser(ctx *context.Context) {
return
}
- repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptionsAll,
OwnerID: u.ID,
OrderBy: db.SearchOrderByAlphabetically,
@@ -292,9 +293,9 @@ func ViewUser(ctx *context.Context) {
ctx.Data["EmailsTotal"] = len(emails)
orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
- ListOptions: db.ListOptionsAll,
- UserID: u.ID,
- IncludePrivate: true,
+ ListOptions: db.ListOptionsAll,
+ UserID: u.ID,
+ IncludeVisibility: structs.VisibleTypePrivate,
})
if err != nil {
ctx.ServerError("FindOrgs", err)
@@ -431,7 +432,7 @@ func EditUserPost(ctx *context.Context) {
Website: optional.Some(form.Website),
Location: optional.Some(form.Location),
IsActive: optional.Some(form.Active),
- IsAdmin: optional.Some(form.Admin),
+ IsAdmin: user_service.UpdateOptionFieldFromValue(form.Admin),
AllowGitHook: optional.Some(form.AllowGitHook),
AllowImportLocal: optional.Some(form.AllowImportLocal),
MaxRepoCreation: optional.Some(form.MaxRepoCreation),
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 69b9d285b7..94f75f69ff 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -421,9 +421,11 @@ func SignOut(ctx *context.Context) {
// SignUp render the register page
func SignUp(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("sign_up")
-
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
+ hasUsers, _ := user_model.HasUsers(ctx)
+ ctx.Data["IsFirstTimeRegistration"] = !hasUsers.HasAnyUser
+
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
ctx.ServerError("UserSignUp", err)
@@ -610,10 +612,16 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any,
// sends a confirmation email if required.
func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) {
// Auto-set admin for the only user.
- if user_model.CountUsers(ctx, nil) == 1 {
+ hasUsers, err := user_model.HasUsers(ctx)
+ if err != nil {
+ ctx.ServerError("HasUsers", err)
+ return false
+ }
+ if hasUsers.HasOnlyOneUser {
+ // the only user is the one just created, will set it as admin
opts := &user_service.UpdateOptions{
IsActive: optional.Some(true),
- IsAdmin: optional.Some(true),
+ IsAdmin: user_service.UpdateOptionFieldFromValue(true),
SetLastLogin: true,
}
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 96c1dcf358..a13b987aab 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -193,8 +193,8 @@ func SignInOAuthCallback(ctx *context.Context) {
source := authSource.Cfg.(*oauth2.Source)
isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(source, &gothUser)
- u.IsAdmin = isAdmin.ValueOrDefault(false)
- u.IsRestricted = isRestricted.ValueOrDefault(false)
+ u.IsAdmin = isAdmin.ValueOrDefault(user_service.UpdateOptionField[bool]{FieldValue: false}).FieldValue
+ u.IsRestricted = isRestricted.ValueOrDefault(setting.Service.DefaultUserIsRestricted)
if !createAndHandleCreatedUser(ctx, templates.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
// error already handled
@@ -258,11 +258,11 @@ func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[
return claimValueToStringSet(groupClaims)
}
-func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin, isRestricted optional.Option[bool]) {
+func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin optional.Option[user_service.UpdateOptionField[bool]], isRestricted optional.Option[bool]) {
groups := getClaimedGroups(source, gothUser)
if source.AdminGroup != "" {
- isAdmin = optional.Some(groups.Contains(source.AdminGroup))
+ isAdmin = user_service.UpdateOptionFieldFromSync(groups.Contains(source.AdminGroup))
}
if source.RestrictedGroup != "" {
isRestricted = optional.Some(groups.Contains(source.RestrictedGroup))
diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go
index 8c2d3276a8..2ef4a86022 100644
--- a/routers/web/auth/openid.go
+++ b/routers/web/auth/openid.go
@@ -349,10 +349,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
context.VerifyCaptcha(ctx, tplSignUpOID, form)
}
- length := setting.MinPasswordLength
- if length < 256 {
- length = 256
- }
+ length := max(setting.MinPasswordLength, 256)
password, err := util.CryptoRandomString(int64(length))
if err != nil {
ctx.RenderWithErr(err.Error(), tplSignUpOID, form)
diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go
index 765931a730..a22d376579 100644
--- a/routers/web/devtest/devtest.go
+++ b/routers/web/devtest/devtest.go
@@ -132,7 +132,7 @@ func prepareMockDataBadgeActionsSvg(ctx *context.Context) {
selectedStyle := ctx.FormString("style", badge.DefaultStyle)
var badges []badge.Badge
badges = append(badges, badge.GenerateBadge("啊啊啊啊啊啊啊啊啊啊啊啊", "🌞🌞🌞🌞🌞", "green"))
- for r := rune(0); r < 256; r++ {
+ for r := range rune(256) {
if unicode.IsPrint(r) {
s := strings.Repeat(string(r), 15)
badges = append(badges, badge.GenerateBadge(s, util.TruncateRunes(s, 7), "green"))
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
index 8f6518a4fc..3bb50ef397 100644
--- a/routers/web/explore/code.go
+++ b/routers/web/explore/code.go
@@ -5,6 +5,7 @@ package explore
import (
"net/http"
+ "slices"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
@@ -93,14 +94,7 @@ func Code(ctx *context.Context) {
loadRepoIDs := make([]int64, 0, len(searchResults))
for _, result := range searchResults {
- var find bool
- for _, id := range loadRepoIDs {
- if id == result.RepoID {
- find = true
- break
- }
- }
- if !find {
+ if !slices.Contains(loadRepoIDs, result.RepoID) {
loadRepoIDs = append(loadRepoIDs, result.RepoID)
}
}
diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go
index 7bb71acfd7..f8f7f5c18c 100644
--- a/routers/web/explore/org.go
+++ b/routers/web/explore/org.go
@@ -44,7 +44,7 @@ func Organizations(ctx *context.Context) {
ctx.SetFormString("sort", sortOrder)
}
- RenderUserSearch(ctx, &user_model.SearchUserOptions{
+ RenderUserSearch(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer,
Type: user_model.UserTypeOrganization,
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go
index cf3128314b..f0d7d0ce7d 100644
--- a/routers/web/explore/repo.go
+++ b/routers/web/explore/repo.go
@@ -94,7 +94,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
private := ctx.FormOptionalBool("private")
ctx.Data["IsPrivate"] = private
- repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: opts.PageSize,
@@ -151,6 +151,7 @@ func Repos(ctx *context.Context) {
ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage
ctx.Data["Title"] = ctx.Tr("explore")
ctx.Data["PageIsExplore"] = true
+ ctx.Data["ShowRepoOwnerOnList"] = true
ctx.Data["PageIsExploreRepositories"] = true
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go
index e1e1ec1cfd..af48e6fb79 100644
--- a/routers/web/explore/user.go
+++ b/routers/web/explore/user.go
@@ -32,7 +32,7 @@ func isKeywordValid(keyword string) bool {
}
// RenderUserSearch render user search page
-func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions, tplName templates.TplName) {
+func RenderUserSearch(ctx *context.Context, opts user_model.SearchUserOptions, tplName templates.TplName) {
// Sitemap index for sitemap paths
opts.Page = int(ctx.PathParamInt64("idx"))
isSitemap := ctx.PathParam("idx") != ""
@@ -151,7 +151,7 @@ func Users(ctx *context.Context) {
ctx.SetFormString("sort", sortOrder)
}
- RenderUserSearch(ctx, &user_model.SearchUserOptions{
+ RenderUserSearch(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer,
Type: user_model.UserTypeIndividual,
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
diff --git a/routers/web/feed/branch.go b/routers/web/feed/branch.go
index 4a6fe9603d..094fd987ac 100644
--- a/routers/web/feed/branch.go
+++ b/routers/web/feed/branch.go
@@ -15,7 +15,7 @@ import (
// ShowBranchFeed shows tags and/or releases on the repo as RSS / Atom feed
func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType string) {
- commits, err := ctx.Repo.Commit.CommitsByRange(0, 10, "")
+ commits, err := ctx.Repo.Commit.CommitsByRange(0, 10, "", "", "")
if err != nil {
ctx.ServerError("ShowBranchFeed", err)
return
diff --git a/routers/web/home.go b/routers/web/home.go
index 208cc36dfb..4b15ee83c2 100644
--- a/routers/web/home.go
+++ b/routers/web/home.go
@@ -68,7 +68,7 @@ func Home(ctx *context.Context) {
func HomeSitemap(ctx *context.Context) {
m := sitemap.NewSitemapIndex()
if !setting.Service.Explore.DisableUsersPage {
- _, cnt, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
+ _, cnt, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
Type: user_model.UserTypeIndividual,
ListOptions: db.ListOptions{PageSize: 1},
IsActive: optional.Some(true),
@@ -86,7 +86,7 @@ func HomeSitemap(ctx *context.Context) {
}
}
- _, cnt, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ _, cnt, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: 1,
},
diff --git a/routers/web/misc/markup.go b/routers/web/misc/markup.go
index 0c7ec6c2eb..f90cf3ffed 100644
--- a/routers/web/misc/markup.go
+++ b/routers/web/misc/markup.go
@@ -15,6 +15,6 @@ import (
// Markup render markup document to HTML
func Markup(ctx *context.Context) {
form := web.GetForm(ctx).(*api.MarkupOption)
- 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)
}
diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go
index d42afafe9e..59b97c1717 100644
--- a/routers/web/misc/misc.go
+++ b/routers/web/misc/misc.go
@@ -20,7 +20,7 @@ func SSHInfo(rw http.ResponseWriter, req *http.Request) {
return
}
rw.Header().Set("content-type", "text/json;charset=UTF-8")
- _, err := rw.Write([]byte(`{"type":"gitea","version":1}`))
+ _, err := rw.Write([]byte(`{"type":"agit","version":1}`))
if err != nil {
log.Error("fail to write result: err: %v", err)
rw.WriteHeader(http.StatusInternalServerError)
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index 8981af1691..63ae6c683b 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -115,7 +115,7 @@ func home(ctx *context.Context, viewRepositories bool) {
ctx.Data["PageIsViewOverview"] = isViewOverview
ctx.Data["ShowOrgProfileReadmeSelector"] = isViewOverview && prepareResult.ProfilePublicReadmeBlob != nil && prepareResult.ProfilePrivateReadmeBlob != nil
- repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
diff --git a/routers/web/org/members.go b/routers/web/org/members.go
index 2cbe75989a..61022d3f09 100644
--- a/routers/web/org/members.go
+++ b/routers/web/org/members.go
@@ -28,10 +28,7 @@ func Members(ctx *context.Context) {
ctx.Data["Title"] = org.FullName
ctx.Data["PageIsOrgMembers"] = true
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
opts := &organization.FindOrgMembersOpts{
Doer: ctx.Doer,
diff --git a/routers/web/org/org_labels.go b/routers/web/org/org_labels.go
index 456ed3f01e..2a4aa7f557 100644
--- a/routers/web/org/org_labels.go
+++ b/routers/web/org/org_labels.go
@@ -4,13 +4,15 @@
package org
import (
- "net/http"
+ "errors"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/label"
repo_module "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ shared_label "code.gitea.io/gitea/routers/web/shared/label"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
@@ -32,14 +34,8 @@ func RetrieveLabels(ctx *context.Context) {
// NewLabel create new label for organization
func NewLabel(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateLabelForm)
- ctx.Data["Title"] = ctx.Tr("repo.labels")
- ctx.Data["PageIsLabels"] = true
- ctx.Data["PageIsOrgSettings"] = true
-
- if ctx.HasError() {
- ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
- ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+ form := shared_label.GetLabelEditForm(ctx)
+ if ctx.Written() {
return
}
@@ -55,20 +51,22 @@ func NewLabel(ctx *context.Context) {
ctx.ServerError("NewLabel", err)
return
}
- ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+ ctx.JSONRedirect(ctx.Org.OrgLink + "/settings/labels")
}
// UpdateLabel update a label's name and color
func UpdateLabel(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateLabelForm)
+ form := shared_label.GetLabelEditForm(ctx)
+ if ctx.Written() {
+ return
+ }
+
l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, form.ID)
- if err != nil {
- switch {
- case issues_model.IsErrOrgLabelNotExist(err):
- ctx.HTTPError(http.StatusNotFound)
- default:
- ctx.ServerError("UpdateLabel", err)
- }
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.JSONErrorNotFound()
+ return
+ } else if err != nil {
+ ctx.ServerError("GetLabelInOrgByID", err)
return
}
@@ -82,7 +80,7 @@ func UpdateLabel(ctx *context.Context) {
ctx.ServerError("UpdateLabel", err)
return
}
- ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+ ctx.JSONRedirect(ctx.Org.OrgLink + "/settings/labels")
}
// DeleteLabel delete a label
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index f423e9cb36..059cce8281 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -53,10 +53,7 @@ func Projects(ctx *context.Context) {
isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed"
keyword := ctx.FormTrim("q")
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
var projectType project_model.Type
if ctx.ContextUser.IsOrganization() {
diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go
index 82c3bce722..2bc1e8bc43 100644
--- a/routers/web/org/setting.go
+++ b/routers/web/org/setting.go
@@ -18,6 +18,7 @@ import (
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
@@ -31,8 +32,6 @@ import (
const (
// tplSettingsOptions template path for render settings
tplSettingsOptions templates.TplName = "org/settings/options"
- // tplSettingsDelete template path for render delete repository
- tplSettingsDelete templates.TplName = "org/settings/delete"
// tplSettingsHooks template path for render hook settings
tplSettingsHooks templates.TplName = "org/settings/hooks"
// tplSettingsLabels template path for render labels settings
@@ -71,26 +70,6 @@ func SettingsPost(ctx *context.Context) {
org := ctx.Org.Organization
- if org.Name != form.Name {
- if err := user_service.RenameUser(ctx, org.AsUser(), form.Name); err != nil {
- if user_model.IsErrUserAlreadyExist(err) {
- ctx.Data["Err_Name"] = true
- ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
- } else if db.IsErrNameReserved(err) {
- ctx.Data["Err_Name"] = true
- ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
- } else if db.IsErrNamePatternNotAllowed(err) {
- ctx.Data["Err_Name"] = true
- ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
- } else {
- ctx.ServerError("RenameUser", err)
- }
- return
- }
-
- ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name)
- }
-
if form.Email != "" {
if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil {
ctx.Data["Err_Email"] = true
@@ -120,7 +99,7 @@ func SettingsPost(ctx *context.Context) {
// update forks visibility
if visibilityChanged {
- repos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
+ repos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
Actor: org.AsUser(), Private: true, ListOptions: db.ListOptions{Page: 1, PageSize: org.NumRepos},
})
if err != nil {
@@ -163,42 +142,27 @@ func SettingsDeleteAvatar(ctx *context.Context) {
ctx.JSONRedirect(ctx.Org.OrgLink + "/settings")
}
-// SettingsDelete response for deleting an organization
-func SettingsDelete(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("org.settings")
- ctx.Data["PageIsOrgSettings"] = true
- ctx.Data["PageIsSettingsDelete"] = true
-
- if ctx.Req.Method == http.MethodPost {
- if ctx.Org.Organization.Name != ctx.FormString("org_name") {
- ctx.Data["Err_OrgName"] = true
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_org_name"), tplSettingsDelete, nil)
- return
- }
-
- if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil {
- if repo_model.IsErrUserOwnRepos(err) {
- ctx.Flash.Error(ctx.Tr("form.org_still_own_repo"))
- ctx.Redirect(ctx.Org.OrgLink + "/settings/delete")
- } else if packages_model.IsErrUserOwnPackages(err) {
- ctx.Flash.Error(ctx.Tr("form.org_still_own_packages"))
- ctx.Redirect(ctx.Org.OrgLink + "/settings/delete")
- } else {
- ctx.ServerError("DeleteOrganization", err)
- }
- } else {
- log.Trace("Organization deleted: %s", ctx.Org.Organization.Name)
- ctx.Redirect(setting.AppSubURL + "/")
- }
+// SettingsDeleteOrgPost response for deleting an organization
+func SettingsDeleteOrgPost(ctx *context.Context) {
+ if ctx.Org.Organization.Name != ctx.FormString("org_name") {
+ ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name"))
return
}
- if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
- ctx.ServerError("RenderUserOrgHeader", err)
+ if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false /* no purge */); err != nil {
+ if repo_model.IsErrUserOwnRepos(err) {
+ ctx.JSONError(ctx.Tr("form.org_still_own_repo"))
+ } else if packages_model.IsErrUserOwnPackages(err) {
+ ctx.JSONError(ctx.Tr("form.org_still_own_packages"))
+ } else {
+ log.Error("DeleteOrganization: %v", err)
+ ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.delete_failed"))))
+ }
return
}
- ctx.HTML(http.StatusOK, tplSettingsDelete)
+ ctx.Flash.Success(ctx.Tr("org.settings.delete_successful", ctx.Org.Organization.Name))
+ ctx.JSONRedirect(setting.AppSubURL + "/")
}
// Webhooks render webhook list page
@@ -250,3 +214,40 @@ func Labels(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplSettingsLabels)
}
+
+// SettingsRenamePost response for renaming organization
+func SettingsRenamePost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RenameOrgForm)
+ if ctx.HasError() {
+ ctx.JSONError(ctx.GetErrMsg())
+ return
+ }
+
+ oldOrgName, newOrgName := ctx.Org.Organization.Name, form.NewOrgName
+
+ if form.OrgName != oldOrgName {
+ ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name"))
+ return
+ }
+ if newOrgName == oldOrgName {
+ ctx.JSONError(ctx.Tr("org.settings.rename_no_change"))
+ return
+ }
+
+ if err := user_service.RenameUser(ctx, ctx.Org.Organization.AsUser(), newOrgName); err != nil {
+ if user_model.IsErrUserAlreadyExist(err) {
+ ctx.JSONError(ctx.Tr("org.form.name_been_taken", newOrgName))
+ } else if db.IsErrNameReserved(err) {
+ ctx.JSONError(ctx.Tr("org.form.name_reserved", newOrgName))
+ } else if db.IsErrNamePatternNotAllowed(err) {
+ ctx.JSONError(ctx.Tr("org.form.name_pattern_not_allowed", newOrgName))
+ } else {
+ log.Error("RenameOrganization: %v", err)
+ ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.rename_failed"))))
+ }
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("org.settings.rename_success", oldOrgName, newOrgName))
+ ctx.JSONRedirect(setting.AppSubURL + "/org/" + url.PathEscape(newOrgName) + "/settings")
+}
diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go
index 676c6d0c63..0ec7cfddc5 100644
--- a/routers/web/org/teams.go
+++ b/routers/web/org/teams.go
@@ -283,11 +283,22 @@ func NewTeam(ctx *context.Context) {
}
// FIXME: TEAM-UNIT-PERMISSION: this design is not right, when a new unit is added in the future,
-// admin team won't inherit the correct admin permission for the new unit.
+// The existing teams won't inherit the correct admin permission for the new unit.
+// The full history is like this:
+// 1. There was only "team", no "team unit", so "team.authorize" was used to determine the team permission.
+// 2. Later, "team unit" was introduced, then the usage of "team.authorize" became inconsistent, and causes various bugs.
+// - Sometimes, "team.authorize" is used to determine the team permission, e.g. admin, owner
+// - Sometimes, "team unit" is used not really used and "team unit" is used.
+// - Some functions like `GetTeamsWithAccessToAnyRepoUnit` use both.
+//
+// 3. After introducing "team unit" and more unclear changes, it becomes difficult to maintain team permissions.
+// - Org owner need to click the permission for each unit, but can't just set a common "write" permission for all units.
+//
+// Ideally, "team.authorize=write" should mean the team has write access to all units including newly (future) added ones.
func getUnitPerms(forms url.Values, teamPermission perm.AccessMode) map[unit_model.Type]perm.AccessMode {
unitPerms := make(map[unit_model.Type]perm.AccessMode)
for _, ut := range unit_model.AllRepoUnitTypes {
- // Default accessmode is none
+ // Default access mode is none
unitPerms[ut] = perm.AccessModeNone
v, ok := forms[fmt.Sprintf("unit_%d", ut)]
diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go
index 5014ff52e3..202da407d2 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -126,7 +126,7 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (
var curWorkflow *model.Workflow
- entries, err := actions.ListWorkflows(commit)
+ _, entries, err := actions.ListWorkflows(commit)
if err != nil {
ctx.ServerError("ListWorkflows", err)
return nil
@@ -317,6 +317,8 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
+
+ ctx.Data["CanWriteRepoUnitActions"] = ctx.Repo.CanWrite(unit.TypeActions)
}
// loadIsRefDeleted loads the IsRefDeleted field for each run in the list.
@@ -375,10 +377,8 @@ func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch {
if !decodeNode(w.RawOn, &val) {
return nil
}
- for _, v := range val {
- if v == "workflow_dispatch" {
- return &WorkflowDispatch{}
- }
+ if slices.Contains(val, "workflow_dispatch") {
+ return &WorkflowDispatch{}
}
case yaml.MappingNode:
var val map[string]yaml.Node
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index dd18c8380d..7e1b923fa4 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -64,6 +64,36 @@ func View(ctx *context_module.Context) {
ctx.HTML(http.StatusOK, tplViewActions)
}
+func ViewWorkflowFile(ctx *context_module.Context) {
+ runIndex := getRunIndex(ctx)
+ run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
+ if err != nil {
+ ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
+ return errors.Is(err, util.ErrNotExist)
+ }, err)
+ return
+ }
+ commit, err := ctx.Repo.GitRepo.GetCommit(run.CommitSHA)
+ if err != nil {
+ ctx.NotFoundOrServerError("GetCommit", func(err error) bool {
+ return errors.Is(err, util.ErrNotExist)
+ }, err)
+ return
+ }
+ rpath, entries, err := actions.ListWorkflows(commit)
+ if err != nil {
+ ctx.ServerError("ListWorkflows", err)
+ return
+ }
+ for _, entry := range entries {
+ if entry.Name() == run.WorkflowID {
+ ctx.Redirect(fmt.Sprintf("%s/src/commit/%s/%s/%s", ctx.Repo.RepoLink, url.PathEscape(run.CommitSHA), util.PathEscapeSegments(rpath), util.PathEscapeSegments(run.WorkflowID)))
+ return
+ }
+ }
+ ctx.NotFound(nil)
+}
+
type LogCursor struct {
Step int `json:"step"`
Cursor int64 `json:"cursor"`
@@ -274,7 +304,7 @@ func ViewPost(ctx *context_module.Context) {
if task != nil {
steps, logs, err := convertToViewModel(ctx, req.LogCursors, task)
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("convertToViewModel", err)
return
}
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, steps...)
@@ -378,7 +408,7 @@ func Rerun(ctx *context_module.Context) {
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetRunByIndex", err)
return
}
@@ -396,7 +426,7 @@ func Rerun(ctx *context_module.Context) {
run.Started = 0
run.Stopped = 0
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("UpdateRun", err)
return
}
}
@@ -411,7 +441,7 @@ func Rerun(ctx *context_module.Context) {
// if the job has needs, it should be set to "blocked" status to wait for other jobs
shouldBlock := len(j.Needs) > 0
if err := rerunJob(ctx, j, shouldBlock); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("RerunJob", err)
return
}
}
@@ -425,7 +455,7 @@ func Rerun(ctx *context_module.Context) {
// jobs other than the specified one should be set to "blocked" status
shouldBlock := j.JobID != job.JobID
if err := rerunJob(ctx, j, shouldBlock); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("RerunJob", err)
return
}
}
@@ -455,7 +485,7 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou
}
actions_service.CreateCommitStatus(ctx, job)
- _ = job.LoadAttributes(ctx)
+ actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
return nil
@@ -517,7 +547,7 @@ func Cancel(ctx *context_module.Context) {
}
return nil
}); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("StopTask", err)
return
}
@@ -527,7 +557,11 @@ func Cancel(ctx *context_module.Context) {
_ = job.LoadAttributes(ctx)
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
}
-
+ if len(updatedjobs) > 0 {
+ job := updatedjobs[0]
+ actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
+ notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
+ }
ctx.JSON(http.StatusOK, struct{}{})
}
@@ -563,12 +597,18 @@ func Approve(ctx *context_module.Context) {
}
return nil
}); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("UpdateRunJob", err)
return
}
actions_service.CreateCommitStatus(ctx, jobs...)
+ if len(updatedjobs) > 0 {
+ job := updatedjobs[0]
+ actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
+ notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
+ }
+
for _, job := range updatedjobs {
_ = job.LoadAttributes(ctx)
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
@@ -577,6 +617,33 @@ func Approve(ctx *context_module.Context) {
ctx.JSON(http.StatusOK, struct{}{})
}
+func Delete(ctx *context_module.Context) {
+ runIndex := getRunIndex(ctx)
+ repoID := ctx.Repo.Repository.ID
+
+ run, err := actions_model.GetRunByIndex(ctx, repoID, runIndex)
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.JSONErrorNotFound()
+ return
+ }
+ ctx.ServerError("GetRunByIndex", err)
+ return
+ }
+
+ if !run.Status.IsDone() {
+ ctx.JSONError(ctx.Tr("actions.runs.not_done"))
+ return
+ }
+
+ if err := actions_service.DeleteRun(ctx, run); err != nil {
+ ctx.ServerError("DeleteRun", err)
+ return
+ }
+
+ ctx.JSONOK()
+}
+
// getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs.
// Any error will be written to the ctx.
// It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0.
@@ -584,20 +651,20 @@ func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
- ctx.HTTPError(http.StatusNotFound, err.Error())
+ ctx.NotFound(nil)
return nil, nil
}
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetRunByIndex", err)
return nil, nil
}
run.Repo = ctx.Repo.Repository
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetRunJobsByRunID", err)
return nil, nil
}
if len(jobs) == 0 {
- ctx.HTTPError(http.StatusNotFound)
+ ctx.NotFound(nil)
return nil, nil
}
@@ -623,7 +690,7 @@ func ArtifactsDeleteView(ctx *context_module.Context) {
return
}
if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("SetArtifactNeedDelete", err)
return
}
ctx.JSON(http.StatusOK, struct{}{})
@@ -639,7 +706,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
ctx.HTTPError(http.StatusNotFound, err.Error())
return
}
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetRunByIndex", err)
return
}
@@ -648,7 +715,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
ArtifactName: artifactName,
})
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("FindArtifacts", err)
return
}
if len(artifacts) == 0 {
@@ -669,7 +736,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) {
err := actions.DownloadArtifactV4(ctx.Base, artifacts[0])
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("DownloadArtifactV4", err)
return
}
return
@@ -682,7 +749,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
for _, art := range artifacts {
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("ActionsArtifacts.Open", err)
return
}
@@ -690,7 +757,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
if art.ContentEncoding == "gzip" {
r, err = gzip.NewReader(f)
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("gzip.NewReader", err)
return
}
} else {
@@ -700,11 +767,11 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
w, err := writer.Create(art.ArtifactPath)
if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("writer.Create", err)
return
}
if _, err := io.Copy(w, r); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("io.Copy", err)
return
}
}
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index 5d963eff66..96d1d87836 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -45,10 +45,7 @@ func Branches(ctx *context.Context) {
ctx.Data["PageIsViewCode"] = true
ctx.Data["PageIsBranches"] = true
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
pageSize := setting.Git.BranchesRangeSize
kw := ctx.FormString("q")
@@ -261,10 +258,10 @@ func CreateBranch(ctx *context.Context) {
func MergeUpstream(ctx *context.Context) {
branchName := ctx.FormString("branch")
- _, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName)
+ _, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName, false)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
- ctx.JSONError(ctx.Tr("error.not_found"))
+ ctx.JSONErrorNotFound()
return
} else if pull_service.IsErrMergeConflicts(err) {
ctx.JSONError(ctx.Tr("repo.pulls.merge_conflict"))
diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go
deleted file mode 100644
index 690b830bc2..0000000000
--- a/routers/web/repo/cherry_pick.go
+++ /dev/null
@@ -1,193 +0,0 @@
-// Copyright 2021 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package repo
-
-import (
- "bytes"
- "errors"
- "net/http"
- "strings"
-
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/repository/files"
-)
-
-var tplCherryPick templates.TplName = "repo/editor/cherry_pick"
-
-// CherryPick handles cherrypick GETs
-func CherryPick(ctx *context.Context) {
- ctx.Data["SHA"] = ctx.PathParam("sha")
- cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(ctx.PathParam("sha"))
- if err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound(err)
- return
- }
- ctx.ServerError("GetCommit", err)
- return
- }
-
- if ctx.FormString("cherry-pick-type") == "revert" {
- ctx.Data["CherryPickType"] = "revert"
- ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha")
- ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message()
- } else {
- ctx.Data["CherryPickType"] = "cherry-pick"
- splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2)
- ctx.Data["commit_summary"] = splits[0]
- ctx.Data["commit_message"] = splits[1]
- }
-
- canCommit := renderCommitRights(ctx)
- ctx.Data["TreePath"] = ""
-
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- }
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
-
- ctx.HTML(http.StatusOK, tplCherryPick)
-}
-
-// CherryPickPost handles cherrypick POSTs
-func CherryPickPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CherryPickForm)
-
- sha := ctx.PathParam("sha")
- ctx.Data["SHA"] = sha
- if form.Revert {
- ctx.Data["CherryPickType"] = "revert"
- } else {
- ctx.Data["CherryPickType"] = "cherry-pick"
- }
-
- canCommit := renderCommitRights(ctx)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplCherryPick)
- return
- }
-
- // Cannot commit to a an existing branch if user doesn't have rights
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplCherryPick, &form)
- return
- }
-
- message := strings.TrimSpace(form.CommitSummary)
- if message == "" {
- if form.Revert {
- message = ctx.Locale.TrString("repo.commit.revert-header", sha)
- } else {
- message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha)
- }
- }
-
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
- }
-
- gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
- if !valid {
- ctx.Data["Err_CommitEmail"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplCherryPick, &form)
- return
- }
- opts := &files.ApplyDiffPatchOptions{
- LastCommitID: form.LastCommit,
- OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
- Message: message,
- Author: gitCommitter,
- Committer: gitCommitter,
- }
-
- // First lets try the simple plain read-tree -m approach
- opts.Content = sha
- if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, form.Revert, opts); err != nil {
- if git_model.IsErrBranchAlreadyExists(err) {
- // User has specified a branch that already exists
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form)
- return
- } else if files.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
- return
- }
- // Drop through to the apply technique
-
- buf := &bytes.Buffer{}
- if form.Revert {
- if err := git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), sha, buf); err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist."))
- return
- }
- ctx.ServerError("GetRawDiff", err)
- return
- }
- } else {
- if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, git.RawDiffType("patch"), buf); err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist."))
- return
- }
- ctx.ServerError("GetRawDiff", err)
- return
- }
- }
-
- opts.Content = buf.String()
- ctx.Data["FileContent"] = opts.Content
-
- if _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
- if git_model.IsErrBranchAlreadyExists(err) {
- // User has specified a branch that already exists
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form)
- return
- } else if files.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
- return
- }
- ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form)
- return
- }
- }
-
- if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
- ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
- } else {
- ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName))
- }
-}
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index ae5baa9c47..b3af138461 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -67,10 +67,7 @@ func Commits(ctx *context.Context) {
commitsCount := ctx.Repo.CommitsCount
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
pageSize := ctx.FormInt("limit")
if pageSize <= 0 {
@@ -78,7 +75,7 @@ func Commits(ctx *context.Context) {
}
// Both `git log branchName` and `git log commitId` work.
- commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize, "")
+ commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize, "", "", "")
if err != nil {
ctx.ServerError("CommitsByRange", err)
return
@@ -230,10 +227,7 @@ func FileHistory(ctx *context.Context) {
return
}
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
@@ -377,7 +371,7 @@ func Diff(ctx *context.Context) {
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
}
- statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
+ statuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
@@ -457,6 +451,9 @@ func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) ([]*git_m
}
if !ctx.Repo.CanRead(unit_model.TypeActions) {
for _, commit := range commits {
+ if commit.Status == nil {
+ continue
+ }
commit.Status.HideActionsURL(ctx)
git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses)
}
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 8b99dd95da..de34a9375c 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -575,7 +575,13 @@ func PrepareCompareDiff(
ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link()
ctx.Data["AfterCommitID"] = headCommitID
- ctx.Data["ExpandNewPrForm"] = ctx.FormBool("expand")
+
+ // follow GitHub's behavior: autofill the form and expand
+ newPrFormTitle := ctx.FormTrim("title")
+ newPrFormBody := ctx.FormTrim("body")
+ ctx.Data["ExpandNewPrForm"] = ctx.FormBool("expand") || ctx.FormBool("quick_pull") || newPrFormTitle != "" || newPrFormBody != ""
+ ctx.Data["TitleQuery"] = newPrFormTitle
+ ctx.Data["BodyQuery"] = newPrFormBody
if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) ||
headCommitID == ci.CompareInfo.BaseCommitID {
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 03e5b830a0..2a5ac10282 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -4,6 +4,7 @@
package repo
import (
+ "bytes"
"fmt"
"io"
"net/http"
@@ -11,18 +12,17 @@ import (
"strings"
git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/forms"
@@ -34,869 +34,422 @@ const (
tplEditDiffPreview templates.TplName = "repo/editor/diff_preview"
tplDeleteFile templates.TplName = "repo/editor/delete"
tplUploadFile templates.TplName = "repo/editor/upload"
+ tplPatchFile templates.TplName = "repo/editor/patch"
+ tplCherryPick templates.TplName = "repo/editor/cherry_pick"
- frmCommitChoiceDirect string = "direct"
- frmCommitChoiceNewBranch string = "commit-to-new-branch"
+ editorCommitChoiceDirect string = "direct"
+ editorCommitChoiceNewBranch string = "commit-to-new-branch"
)
-func canCreateBasePullRequest(ctx *context.Context) bool {
- baseRepo := ctx.Repo.Repository.BaseRepo
- return baseRepo != nil && baseRepo.UnitEnabled(ctx, unit.TypePullRequests)
-}
-
-func renderCommitRights(ctx *context.Context) bool {
- canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx, ctx.Doer)
- if err != nil {
- log.Error("CanCommitToBranch: %v", err)
- }
- ctx.Data["CanCommitToBranch"] = canCommitToBranch
- ctx.Data["CanCreatePullRequest"] = ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) || canCreateBasePullRequest(ctx)
-
- return canCommitToBranch.CanCommitToBranch
-}
-
-// redirectForCommitChoice redirects after committing the edit to a branch
-func redirectForCommitChoice(ctx *context.Context, commitChoice, newBranchName, treePath string) {
- if commitChoice == frmCommitChoiceNewBranch {
- // Redirect to a pull request when possible
- redirectToPullRequest := false
- repo := ctx.Repo.Repository
- baseBranch := ctx.Repo.BranchName
- headBranch := newBranchName
- if repo.UnitEnabled(ctx, unit.TypePullRequests) {
- redirectToPullRequest = true
- } else if canCreateBasePullRequest(ctx) {
- redirectToPullRequest = true
- baseBranch = repo.BaseRepo.DefaultBranch
- headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
- repo = repo.BaseRepo
- }
-
- if redirectToPullRequest {
- ctx.Redirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
- return
+func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions {
+ cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
+ if cleanedTreePath != ctx.Repo.TreePath {
+ redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath))
+ if ctx.Req.URL.RawQuery != "" {
+ redirectTo += "?" + ctx.Req.URL.RawQuery
}
+ ctx.Redirect(redirectTo)
+ return nil
}
- returnURI := ctx.FormString("return_uri")
-
- ctx.RedirectToCurrentSite(
- returnURI,
- ctx.Repo.RepoLink+"/src/branch/"+util.PathEscapeSegments(newBranchName)+"/"+util.PathEscapeSegments(treePath),
- )
-}
+ commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
+ if err != nil {
+ ctx.ServerError("PrepareCommitFormOptions", err)
+ return nil
+ }
-// getParentTreeFields returns list of parent tree names and corresponding tree paths
-// based on given tree path.
-func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
- if len(treePath) == 0 {
- return treeNames, treePaths
+ if commitFormOptions.NeedFork {
+ ForkToEdit(ctx)
+ return nil
}
- treeNames = strings.Split(treePath, "/")
- treePaths = make([]string, len(treeNames))
- for i := range treeNames {
- treePaths[i] = strings.Join(treeNames[:i+1], "/")
+ if commitFormOptions.WillSubmitToFork && !commitFormOptions.TargetRepo.CanEnableEditor() {
+ ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.editor.fork_not_editable")
+ ctx.NotFound(nil)
}
- return treeNames, treePaths
-}
-func editFileCommon(ctx *context.Context, isNewFile bool) {
- ctx.Data["PageIsEdit"] = true
- ctx.Data["IsNewFile"] = isNewFile
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
+ ctx.Data["TreePath"] = ctx.Repo.TreePath
+ ctx.Data["CommitFormOptions"] = commitFormOptions
+
+ // for online editor
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
-}
-
-func editFile(ctx *context.Context, isNewFile bool) {
- editFileCommon(ctx, isNewFile)
- canCommit := renderCommitRights(ctx)
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
- if treePath != ctx.Repo.TreePath {
- if isNewFile {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_new", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
- } else {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
- }
- return
- }
-
- // Check if the filename (and additional path) is specified in the querystring
- // (filename is a misnomer, but kept for compatibility with GitHub)
- filePath, fileName := path.Split(ctx.Req.URL.Query().Get("filename"))
- filePath = strings.Trim(filePath, "/")
- treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath))
-
- if !isNewFile {
- entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
- if err != nil {
- HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
- return
- }
-
- // No way to edit a directory online.
- if entry.IsDir() {
- ctx.NotFound(nil)
- return
- }
-
- blob := entry.Blob()
- if blob.Size() >= setting.UI.MaxDisplayFileSize {
- ctx.NotFound(err)
- return
- }
-
- buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
- if err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound(err)
- } else {
- ctx.ServerError("getFileReader", err)
- }
- return
- }
-
- defer dataRc.Close()
-
- ctx.Data["FileSize"] = blob.Size()
-
- // Only some file types are editable online as text.
- if !fInfo.st.IsRepresentableAsText() || fInfo.isLFSFile {
- ctx.NotFound(nil)
- return
- }
-
- d, _ := io.ReadAll(dataRc)
-
- buf = append(buf, d...)
- if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
- log.Error("ToUTF8: %v", err)
- ctx.Data["FileContent"] = string(buf)
- } else {
- ctx.Data["FileContent"] = content
- }
- } else {
- // Append filename from query, or empty string to allow username the new file.
- treeNames = append(treeNames, fileName)
- }
-
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
+ // form fields
ctx.Data["commit_summary"] = ""
ctx.Data["commit_message"] = ""
- ctx.Data["commit_choice"] = util.Iif(canCommit, frmCommitChoiceDirect, frmCommitChoiceNewBranch)
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
+ ctx.Data["commit_choice"] = util.Iif(commitFormOptions.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch)
+ ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, commitFormOptions.TargetRepo)
ctx.Data["last_commit"] = ctx.Repo.CommitID
-
- ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
-
- ctx.HTML(http.StatusOK, tplEditFile)
+ return commitFormOptions
}
-// GetEditorConfig returns a editorconfig JSON string for given treePath or "null"
-func GetEditorConfig(ctx *context.Context, treePath string) string {
- ec, _, err := ctx.Repo.GetEditorconfig()
- if err == nil {
- def, err := ec.GetDefinitionForFilename(treePath)
- if err == nil {
- jsonStr, _ := json.Marshal(def)
- return string(jsonStr)
- }
- }
- return "null"
+func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) {
+ // show the tree path fields in the "breadcrumb" and help users to edit the target tree path
+ ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(strings.TrimPrefix(treePath, "/"))
}
-// EditFile render edit file page
-func EditFile(ctx *context.Context) {
- editFile(ctx, false)
+type preparedEditorCommitForm[T any] struct {
+ form T
+ commonForm *forms.CommitCommonForm
+ CommitFormOptions *context.CommitFormOptions
+ OldBranchName string
+ NewBranchName string
+ GitCommitter *files_service.IdentityOptions
}
-// NewFile render create file page
-func NewFile(ctx *context.Context) {
- editFile(ctx, true)
+func (f *preparedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string {
+ commitMessage := util.IfZero(strings.TrimSpace(f.commonForm.CommitSummary), defaultCommitMessage)
+ if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" {
+ commitMessage += "\n\n" + body
+ }
+ return commitMessage
}
-func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) {
- editFileCommon(ctx, isNewFile)
- ctx.Data["PageHasPosted"] = true
-
- canCommit := renderCommitRights(ctx)
- treeNames, treePaths := getParentTreeFields(form.TreePath)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
-
- ctx.Data["TreePath"] = form.TreePath
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
- ctx.Data["FileContent"] = form.Content
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath)
-
+func prepareEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *preparedEditorCommitForm[T] {
+ form := web.GetForm(ctx).(T)
if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplEditFile)
- return
+ ctx.JSONError(ctx.GetErrMsg())
+ return nil
}
- // Cannot commit to an existing branch if user doesn't have rights
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
- return
- }
+ commonForm := form.GetCommitCommonForm()
+ commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath)
- // CommitSummary is optional in the web form, if empty, give it a default message based on add or update
- // `message` will be both the summary and message combined
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- if isNewFile {
- message = ctx.Locale.TrString("repo.editor.add", form.TreePath)
- } else {
- message = ctx.Locale.TrString("repo.editor.update", form.TreePath)
- }
+ commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
+ if err != nil {
+ ctx.ServerError("PrepareCommitFormOptions", err)
+ return nil
}
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
+ if commitFormOptions.NeedFork {
+ // It shouldn't happen, because we should have done the checks in the "GET" request. But just in case.
+ ctx.JSONError(ctx.Locale.TrString("error.not_found"))
+ return nil
}
- gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
- if !valid {
- ctx.Data["Err_CommitEmail"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplEditFile, &form)
- return
+ // check commit behavior
+ fromBaseBranch := ctx.FormString("from_base_branch")
+ commitToNewBranch := commonForm.CommitChoice == editorCommitChoiceNewBranch || fromBaseBranch != ""
+ targetBranchName := util.Iif(commitToNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName)
+ if targetBranchName == ctx.Repo.BranchName && !commitFormOptions.CanCommitToBranch {
+ ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", targetBranchName))
+ return nil
}
- operation := "update"
- if isNewFile {
- operation = "create"
+ if !issues.CanMaintainerWriteToBranch(ctx, ctx.Repo.Permission, targetBranchName, ctx.Doer) {
+ ctx.NotFound(nil)
+ return nil
}
- if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
- LastCommitID: form.LastCommit,
- OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
- Message: message,
- Files: []*files_service.ChangeRepoFile{
- {
- Operation: operation,
- FromTreePath: ctx.Repo.TreePath,
- TreePath: form.TreePath,
- ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")),
- },
- },
- Signoff: form.Signoff,
- Author: gitCommitter,
- Committer: gitCommitter,
- }); err != nil {
- // This is where we handle all the errors thrown by files_service.ChangeRepoFiles
- if git.IsErrNotExist(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
- } else if git_model.IsErrLFSFileLocked(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplEditFile, &form)
- } else if files_service.IsErrFilenameInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form)
- } else if files_service.IsErrFilePathInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok {
- switch fileErr.Type {
- case git.EntryModeSymlink:
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form)
- case git.EntryModeTree:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form)
- case git.EntryModeBlob:
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form)
- default:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", fileErr.Path), tplEditFile, &form)
- }
- } else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if files_service.IsErrRepoFileAlreadyExists(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form)
- } else if git.IsErrBranchNotExist(err) {
- // For when a user adds/updates a file to a branch that no longer exists
- if branchErr, ok := err.(git.ErrBranchNotExist); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form)
- } else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if git_model.IsErrBranchAlreadyExists(err) {
- // For when a user specifies a new branch that already exists
- ctx.Data["Err_NewBranchName"] = true
- if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
- } else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if files_service.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.commit_id_not_matching"), tplEditFile, &form)
- } else if git.IsErrPushOutOfDate(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_out_of_date"), tplEditFile, &form)
- } else if git.IsErrPushRejected(err) {
- errPushRej := err.(*git.ErrPushRejected)
- if len(errPushRej.Message) == 0 {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
+ // Committer user info
+ gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, commonForm.CommitEmail)
+ if !valid {
+ ctx.JSONError(ctx.Tr("repo.editor.invalid_commit_email"))
+ return nil
+ }
+
+ if commitToNewBranch {
+ // if target branch exists, we should stop
+ targetBranchExists, err := git_model.IsBranchExist(ctx, commitFormOptions.TargetRepo.ID, targetBranchName)
+ if err != nil {
+ ctx.ServerError("IsBranchExist", err)
+ return nil
+ } else if targetBranchExists {
+ if fromBaseBranch != "" {
+ ctx.JSONError(ctx.Tr("repo.editor.fork_branch_exists", targetBranchName))
} else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.push_rejected"),
- "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
- "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
- })
- if err != nil {
- ctx.ServerError("editFilePost.HTMLString", err)
- return
- }
- ctx.RenderWithErr(flashError, tplEditFile, &form)
+ ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", targetBranchName))
}
- } else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath),
- "Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"),
- "Details": utils.SanitizeFlashErrorString(err.Error()),
- })
- if err != nil {
- ctx.ServerError("editFilePost.HTMLString", err)
- return
- }
- ctx.RenderWithErr(flashError, tplEditFile, &form)
+ return nil
}
}
- if ctx.Repo.Repository.IsEmpty {
- if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
- _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
+ oldBranchName := ctx.Repo.BranchName
+ if fromBaseBranch != "" {
+ err = editorPushBranchToForkedRepository(ctx, ctx.Doer, ctx.Repo.Repository.BaseRepo, fromBaseBranch, commitFormOptions.TargetRepo, targetBranchName)
+ if err != nil {
+ log.Error("Unable to editorPushBranchToForkedRepository: %v", err)
+ ctx.JSONError(ctx.Tr("repo.editor.fork_failed_to_push_branch", targetBranchName))
+ return nil
}
+ // we have pushed the base branch as the new branch, now we need to commit the changes directly to the new branch
+ oldBranchName = targetBranchName
}
- redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
-}
-
-// EditFilePost response for editing file
-func EditFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditRepoFileForm)
- editFilePost(ctx, *form, false)
-}
-
-// NewFilePost response for creating file
-func NewFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditRepoFileForm)
- editFilePost(ctx, *form, true)
-}
-
-// DiffPreviewPost render preview diff page
-func DiffPreviewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditPreviewDiffForm)
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
- if len(treePath) == 0 {
- ctx.HTTPError(http.StatusInternalServerError, "file name to diff is invalid")
- return
+ return &preparedEditorCommitForm[T]{
+ form: form,
+ commonForm: commonForm,
+ CommitFormOptions: commitFormOptions,
+ OldBranchName: oldBranchName,
+ NewBranchName: targetBranchName,
+ GitCommitter: gitCommitter,
}
-
- entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, "GetTreeEntryByPath: "+err.Error())
- return
- } else if entry.IsDir() {
- ctx.HTTPError(http.StatusUnprocessableEntity)
- return
- }
-
- diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, "GetDiffPreview: "+err.Error())
- return
- }
-
- if len(diff.Files) != 0 {
- ctx.Data["File"] = diff.Files[0]
- }
-
- ctx.HTML(http.StatusOK, tplEditDiffPreview)
}
-// DeleteFile render delete file page
-func DeleteFile(ctx *context.Context) {
- ctx.Data["PageIsDelete"] = true
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
-
- if treePath != ctx.Repo.TreePath {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_delete", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
+// redirectForCommitChoice redirects after committing the edit to a branch
+func redirectForCommitChoice[T any](ctx *context.Context, parsed *preparedEditorCommitForm[T], treePath string) {
+ // when editing a file in a PR, it should return to the origin location
+ if returnURI := ctx.FormString("return_uri"); returnURI != "" && httplib.IsCurrentGiteaSiteURL(ctx, returnURI) {
+ ctx.JSONRedirect(returnURI)
return
}
- ctx.Data["TreePath"] = treePath
- canCommit := renderCommitRights(ctx)
-
- ctx.Data["commit_summary"] = ""
- ctx.Data["commit_message"] = ""
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
+ if parsed.commonForm.CommitChoice == editorCommitChoiceNewBranch {
+ // Redirect to a pull request when possible
+ redirectToPullRequest := false
+ repo, baseBranch, headBranch := ctx.Repo.Repository, parsed.OldBranchName, parsed.NewBranchName
+ if ctx.Repo.Repository.IsFork && parsed.CommitFormOptions.CanCreateBasePullRequest {
+ redirectToPullRequest = true
+ baseBranch = repo.BaseRepo.DefaultBranch
+ headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
+ repo = repo.BaseRepo
+ } else if repo.UnitEnabled(ctx, unit.TypePullRequests) {
+ redirectToPullRequest = true
+ }
+ if redirectToPullRequest {
+ ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
+ return
+ }
}
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
- ctx.HTML(http.StatusOK, tplDeleteFile)
+ // redirect to the newly updated file
+ redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath))
+ ctx.JSONRedirect(redirectTo)
}
-// DeleteFilePost response for deleting file
-func DeleteFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.DeleteRepoFileForm)
- canCommit := renderCommitRights(ctx)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
-
- ctx.Data["PageIsDelete"] = true
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- ctx.Data["TreePath"] = ctx.Repo.TreePath
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplDeleteFile)
- return
- }
-
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form)
- return
- }
-
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath)
- }
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
+func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) {
+ entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
+ if err != nil {
+ HandleGitError(ctx, "GetTreeEntryByPath", err)
+ return nil, nil, nil
}
- gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
- if !valid {
- ctx.Data["Err_CommitEmail"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplDeleteFile, &form)
- return
+ // No way to edit a directory online.
+ if entry.IsDir() {
+ ctx.NotFound(nil)
+ return nil, nil, nil
}
- if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
- LastCommitID: form.LastCommit,
- OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
- Files: []*files_service.ChangeRepoFile{
- {
- Operation: "delete",
- TreePath: ctx.Repo.TreePath,
- },
- },
- Message: message,
- Signoff: form.Signoff,
- Author: gitCommitter,
- Committer: gitCommitter,
- }); err != nil {
- // This is where we handle all the errors thrown by repofiles.DeleteRepoFile
- if git.IsErrNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplDeleteFile, &form)
- } else if files_service.IsErrFilenameInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplDeleteFile, &form)
- } else if files_service.IsErrFilePathInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok {
- switch fileErr.Type {
- case git.EntryModeSymlink:
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplDeleteFile, &form)
- case git.EntryModeTree:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplDeleteFile, &form)
- case git.EntryModeBlob:
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplDeleteFile, &form)
- default:
- ctx.ServerError("DeleteRepoFile", err)
- }
- } else {
- ctx.ServerError("DeleteRepoFile", err)
- }
- } else if git.IsErrBranchNotExist(err) {
- // For when a user deletes a file to a branch that no longer exists
- if branchErr, ok := err.(git.ErrBranchNotExist); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplDeleteFile, &form)
- } else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if git_model.IsErrBranchAlreadyExists(err) {
- // For when a user specifies a new branch that already exists
- if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form)
- } else {
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplDeleteFile, &form)
- } else if git.IsErrPushRejected(err) {
- errPushRej := err.(*git.ErrPushRejected)
- if len(errPushRej.Message) == 0 {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
- } else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.push_rejected"),
- "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
- "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
- })
- if err != nil {
- ctx.ServerError("DeleteFilePost.HTMLString", err)
- return
- }
- ctx.RenderWithErr(flashError, tplDeleteFile, &form)
- }
+ blob := entry.Blob()
+ buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
+ if err != nil {
+ if git.IsErrNotExist(err) {
+ ctx.NotFound(err)
} else {
- ctx.ServerError("DeleteRepoFile", err)
+ ctx.ServerError("getFileReader", err)
}
- return
+ return nil, nil, nil
}
- ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
- treePath := path.Dir(ctx.Repo.TreePath)
- if treePath == "." {
- treePath = "" // the file deleted was in the root, so we return the user to the root directory
- }
- if len(treePath) > 0 {
- // Need to get the latest commit since it changed
- commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
- if err == nil && commit != nil {
- // We have the comment, now find what directory we can return the user to
- // (must have entries)
- treePath = GetClosestParentWithFiles(treePath, commit)
- } else {
- treePath = "" // otherwise return them to the root of the repo
+ if fInfo.isLFSFile() {
+ lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
+ if err != nil {
+ _ = dataRc.Close()
+ ctx.ServerError("GetTreePathLock", err)
+ return nil, nil, nil
+ } else if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
+ _ = dataRc.Close()
+ ctx.NotFound(nil)
+ return nil, nil, nil
}
}
- redirectForCommitChoice(ctx, form.CommitChoice, branchName, treePath)
+ return buf, dataRc, fInfo
}
-// UploadFile render upload file page
-func UploadFile(ctx *context.Context) {
- ctx.Data["PageIsUpload"] = true
- upload.AddUploadContext(ctx, "repo")
- canCommit := renderCommitRights(ctx)
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
- if treePath != ctx.Repo.TreePath {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
- return
- }
- ctx.Repo.TreePath = treePath
-
- treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath)
- if len(treeNames) == 0 {
- // We must at least have one element for user to input.
- treeNames = []string{""}
- }
-
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- ctx.Data["commit_summary"] = ""
- ctx.Data["commit_message"] = ""
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- }
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
-
- ctx.HTML(http.StatusOK, tplUploadFile)
-}
-
-// UploadFilePost response for uploading file
-func UploadFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.UploadRepoFileForm)
- ctx.Data["PageIsUpload"] = true
- upload.AddUploadContext(ctx, "repo")
- canCommit := renderCommitRights(ctx)
-
- oldBranchName := ctx.Repo.BranchName
- branchName := oldBranchName
-
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
-
- form.TreePath = cleanUploadFileName(form.TreePath)
+func EditFile(ctx *context.Context) {
+ editorAction := ctx.PathParam("editor_action")
+ isNewFile := editorAction == "_new"
+ ctx.Data["IsNewFile"] = isNewFile
- treeNames, treePaths := getParentTreeFields(form.TreePath)
- if len(treeNames) == 0 {
- // We must at least have one element for user to input.
- treeNames = []string{""}
+ // Check if the filename (and additional path) is specified in the querystring
+ // (filename is a misnomer, but kept for compatibility with GitHub)
+ urlQuery := ctx.Req.URL.Query()
+ queryFilename := urlQuery.Get("filename")
+ if queryFilename != "" {
+ newTreePath := path.Join(ctx.Repo.TreePath, queryFilename)
+ redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(newTreePath))
+ urlQuery.Del("filename")
+ if newQueryParams := urlQuery.Encode(); newQueryParams != "" {
+ redirectTo += "?" + newQueryParams
+ }
+ ctx.Redirect(redirectTo)
+ return
}
- ctx.Data["TreePath"] = form.TreePath
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName)
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = branchName
+ // on the "New File" page, we should add an empty path field to make end users could input a new name
+ prepareTreePathFieldsAndPaths(ctx, util.Iif(isNewFile, ctx.Repo.TreePath+"/", ctx.Repo.TreePath))
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplUploadFile)
+ prepareEditorCommitFormOptions(ctx, editorAction)
+ if ctx.Written() {
return
}
- if oldBranchName != branchName {
- if exist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, branchName); err == nil && exist {
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplUploadFile, &form)
+ if !isNewFile {
+ prefetch, dataRc, fInfo := editFileOpenExisting(ctx)
+ if ctx.Written() {
return
}
- } else if !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplUploadFile, &form)
- return
- }
+ defer dataRc.Close()
- if !ctx.Repo.Repository.IsEmpty {
- var newTreePath string
- for _, part := range treeNames {
- newTreePath = path.Join(newTreePath, part)
- entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath)
+ ctx.Data["FileSize"] = fInfo.fileSize
+
+ // Only some file types are editable online as text.
+ if fInfo.isLFSFile() {
+ ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
+ } else if !fInfo.st.IsRepresentableAsText() {
+ ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
+ } else if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
+ ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_too_large_file")
+ }
+
+ if ctx.Data["NotEditableReason"] == nil {
+ buf, err := io.ReadAll(io.MultiReader(bytes.NewReader(prefetch), dataRc))
if err != nil {
- if git.IsErrNotExist(err) {
- break // Means there is no item with that name, so we're good
- }
- ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err)
+ ctx.ServerError("ReadAll", err)
return
}
-
- // User can only upload files to a directory, the directory name shouldn't be an existing file.
- if !entry.IsDir() {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form)
- return
+ if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
+ ctx.Data["FileContent"] = string(buf)
+ } else {
+ ctx.Data["FileContent"] = content
}
}
}
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- dir := form.TreePath
- if dir == "" {
- dir = "/"
- }
- message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir)
- }
-
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
- }
+ ctx.Data["EditorconfigJson"] = getContextRepoEditorConfig(ctx, ctx.Repo.TreePath)
+ ctx.HTML(http.StatusOK, tplEditFile)
+}
- gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
- if !valid {
- ctx.Data["Err_CommitEmail"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplUploadFile, &form)
+func EditFilePost(ctx *context.Context) {
+ editorAction := ctx.PathParam("editor_action")
+ isNewFile := editorAction == "_new"
+ parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
+ if ctx.Written() {
return
}
- if err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
- LastCommitID: ctx.Repo.CommitID,
- OldBranch: oldBranchName,
- NewBranch: branchName,
- TreePath: form.TreePath,
- Message: message,
- Files: form.Files,
- Signoff: form.Signoff,
- Author: gitCommitter,
- Committer: gitCommitter,
- }); err != nil {
- if git_model.IsErrLFSFileLocked(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplUploadFile, &form)
- } else if files_service.IsErrFilenameInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form)
- } else if files_service.IsErrFilePathInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- fileErr := err.(files_service.ErrFilePathInvalid)
- switch fileErr.Type {
- case git.EntryModeSymlink:
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplUploadFile, &form)
- case git.EntryModeTree:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplUploadFile, &form)
- case git.EntryModeBlob:
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplUploadFile, &form)
- default:
- ctx.HTTPError(http.StatusInternalServerError, err.Error())
- }
- } else if files_service.IsErrRepoFileAlreadyExists(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplUploadFile, &form)
- } else if git.IsErrBranchNotExist(err) {
- branchErr := err.(git.ErrBranchNotExist)
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form)
- } else if git_model.IsErrBranchAlreadyExists(err) {
- // For when a user specifies a new branch that already exists
- ctx.Data["Err_NewBranchName"] = true
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form)
- } else if git.IsErrPushOutOfDate(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form)
- } else if git.IsErrPushRejected(err) {
- errPushRej := err.(*git.ErrPushRejected)
- if len(errPushRej.Message) == 0 {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
- } else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.push_rejected"),
- "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
- "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
- })
- if err != nil {
- ctx.ServerError("UploadFilePost.HTMLString", err)
- return
- }
- ctx.RenderWithErr(flashError, tplUploadFile, &form)
- }
- } else {
- // os.ErrNotExist - upload file missing in the intervening time?!
- log.Error("Error during upload to repo: %-v to filepath: %s on %s from %s: %v", ctx.Repo.Repository, form.TreePath, oldBranchName, form.NewBranchName, err)
- ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form)
- }
+ defaultCommitMessage := util.Iif(isNewFile, ctx.Locale.TrString("repo.editor.add", parsed.form.TreePath), ctx.Locale.TrString("repo.editor.update", parsed.form.TreePath))
+
+ var operation string
+ if isNewFile {
+ operation = "create"
+ } else if parsed.form.Content.Has() {
+ // The form content only has data if the file is representable as text, is not too large and not in lfs.
+ operation = "update"
+ } else if ctx.Repo.TreePath != parsed.form.TreePath {
+ // If it doesn't have data, the only possible operation is a "rename"
+ operation = "rename"
+ } else {
+ // It should never happen, just in case
+ ctx.JSONError(ctx.Tr("error.occurred"))
return
}
- if ctx.Repo.Repository.IsEmpty {
- if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
- _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
- }
+ _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: parsed.OldBranchName,
+ NewBranch: parsed.NewBranchName,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: operation,
+ FromTreePath: ctx.Repo.TreePath,
+ TreePath: parsed.form.TreePath,
+ ContentReader: strings.NewReader(strings.ReplaceAll(parsed.form.Content.Value(), "\r", "")),
+ },
+ },
+ Signoff: parsed.form.Signoff,
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ })
+ if err != nil {
+ editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
+ return
}
- redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
}
-func cleanUploadFileName(name string) string {
- // Rebase the filename
- name = util.PathJoinRel(name)
- // Git disallows any filenames to have a .git directory in them.
- for _, part := range strings.Split(name, "/") {
- if strings.ToLower(part) == ".git" {
- return ""
- }
+// DeleteFile render delete file page
+func DeleteFile(ctx *context.Context) {
+ prepareEditorCommitFormOptions(ctx, "_delete")
+ if ctx.Written() {
+ return
}
- return name
+ ctx.Data["PageIsDelete"] = true
+ ctx.HTML(http.StatusOK, tplDeleteFile)
}
-// UploadFileToServer upload file to server file dir not git
-func UploadFileToServer(ctx *context.Context) {
- file, header, err := ctx.Req.FormFile("file")
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
+// DeleteFilePost response for deleting file
+func DeleteFilePost(ctx *context.Context) {
+ parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
+ if ctx.Written() {
return
}
- defer file.Close()
-
- buf := make([]byte, 1024)
- n, _ := util.ReadAtMost(file, buf)
- if n > 0 {
- buf = buf[:n]
- }
- err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
+ treePath := ctx.Repo.TreePath
+ _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: parsed.OldBranchName,
+ NewBranch: parsed.NewBranchName,
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "delete",
+ TreePath: treePath,
+ },
+ },
+ Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)),
+ Signoff: parsed.form.Signoff,
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ })
if err != nil {
- ctx.HTTPError(http.StatusBadRequest, err.Error())
+ editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return
}
- name := cleanUploadFileName(header.Filename)
- if len(name) == 0 {
- ctx.HTTPError(http.StatusInternalServerError, "Upload file name is invalid")
- return
- }
+ ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
+ redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
+ redirectForCommitChoice(ctx, parsed, redirectTreePath)
+}
- upload, err := repo_model.NewUpload(ctx, name, buf, file)
- if err != nil {
- ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("NewUpload: %v", err))
+func UploadFile(ctx *context.Context) {
+ ctx.Data["PageIsUpload"] = true
+ prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
+ opts := prepareEditorCommitFormOptions(ctx, "_upload")
+ if ctx.Written() {
return
}
+ upload.AddUploadContextForRepo(ctx, opts.TargetRepo)
- log.Trace("New file uploaded: %s", upload.UUID)
- ctx.JSON(http.StatusOK, map[string]string{
- "uuid": upload.UUID,
- })
+ ctx.HTML(http.StatusOK, tplUploadFile)
}
-// RemoveUploadFileFromServer remove file from server file dir
-func RemoveUploadFileFromServer(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.RemoveUploadFileForm)
- if len(form.File) == 0 {
- ctx.Status(http.StatusNoContent)
+func UploadFilePost(ctx *context.Context) {
+ parsed := prepareEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
+ if ctx.Written() {
return
}
- if err := repo_model.DeleteUploadByUUID(ctx, form.File); err != nil {
- ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("DeleteUploadByUUID: %v", err))
+ defaultCommitMessage := ctx.Locale.TrString("repo.editor.upload_files_to_dir", util.IfZero(parsed.form.TreePath, "/"))
+ err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: parsed.OldBranchName,
+ NewBranch: parsed.NewBranchName,
+ TreePath: parsed.form.TreePath,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
+ Files: parsed.form.Files,
+ Signoff: parsed.form.Signoff,
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ })
+ if err != nil {
+ editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return
}
-
- log.Trace("Upload file removed: %s", form.File)
- ctx.Status(http.StatusNoContent)
-}
-
-// GetUniquePatchBranchName Gets a unique branch name for a new patch branch
-// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
-// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
-// type in the branch name themselves (will be an empty field)
-func GetUniquePatchBranchName(ctx *context.Context) string {
- prefix := ctx.Doer.LowerName + "-patch-"
- for i := 1; i <= 1000; i++ {
- branchName := fmt.Sprintf("%s%d", prefix, i)
- if exist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, branchName); err != nil {
- log.Error("GetUniquePatchBranchName: %v", err)
- return ""
- } else if !exist {
- return branchName
- }
- }
- return ""
-}
-
-// GetClosestParentWithFiles Recursively gets the path of parent in a tree that has files (used when file in a tree is
-// deleted). Returns "" for the root if no parents other than the root have files. If the given treePath isn't a
-// SubTree or it has no entries, we go up one dir and see if we can return the user to that listing.
-func GetClosestParentWithFiles(treePath string, commit *git.Commit) string {
- if len(treePath) == 0 || treePath == "." {
- return ""
- }
- // see if the tree has entries
- if tree, err := commit.SubTree(treePath); err != nil {
- // failed to get tree, going up a dir
- return GetClosestParentWithFiles(path.Dir(treePath), commit)
- } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
- // no files in this dir, going up a dir
- return GetClosestParentWithFiles(path.Dir(treePath), commit)
- }
- return treePath
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
}
diff --git a/routers/web/repo/editor_apply_patch.go b/routers/web/repo/editor_apply_patch.go
new file mode 100644
index 0000000000..bd2811cc5f
--- /dev/null
+++ b/routers/web/repo/editor_apply_patch.go
@@ -0,0 +1,51 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+ "strings"
+
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/repository/files"
+)
+
+func NewDiffPatch(ctx *context.Context) {
+ prepareEditorCommitFormOptions(ctx, "_diffpatch")
+ if ctx.Written() {
+ return
+ }
+
+ ctx.Data["PageIsPatch"] = true
+ ctx.HTML(http.StatusOK, tplPatchFile)
+}
+
+// NewDiffPatchPost response for sending patch page
+func NewDiffPatchPost(ctx *context.Context) {
+ parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
+ if ctx.Written() {
+ return
+ }
+
+ defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch")
+ _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: parsed.OldBranchName,
+ NewBranch: parsed.NewBranchName,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
+ Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"),
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ })
+ if err != nil {
+ err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
+ }
+ if err != nil {
+ editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
+ return
+ }
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
+}
diff --git a/routers/web/repo/editor_cherry_pick.go b/routers/web/repo/editor_cherry_pick.go
new file mode 100644
index 0000000000..10c2741b1c
--- /dev/null
+++ b/routers/web/repo/editor_cherry_pick.go
@@ -0,0 +1,86 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "bytes"
+ "net/http"
+ "strings"
+
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/repository/files"
+)
+
+func CherryPick(ctx *context.Context) {
+ prepareEditorCommitFormOptions(ctx, "_cherrypick")
+ if ctx.Written() {
+ return
+ }
+
+ fromCommitID := ctx.PathParam("sha")
+ ctx.Data["FromCommitID"] = fromCommitID
+ cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(fromCommitID)
+ if err != nil {
+ HandleGitError(ctx, "GetCommit", err)
+ return
+ }
+
+ if ctx.FormString("cherry-pick-type") == "revert" {
+ ctx.Data["CherryPickType"] = "revert"
+ ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha")
+ ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message()
+ } else {
+ ctx.Data["CherryPickType"] = "cherry-pick"
+ splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2)
+ ctx.Data["commit_summary"] = splits[0]
+ ctx.Data["commit_message"] = splits[1]
+ }
+
+ ctx.HTML(http.StatusOK, tplCherryPick)
+}
+
+func CherryPickPost(ctx *context.Context) {
+ fromCommitID := ctx.PathParam("sha")
+ parsed := prepareEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
+ if ctx.Written() {
+ return
+ }
+
+ defaultCommitMessage := util.Iif(parsed.form.Revert, ctx.Locale.TrString("repo.commit.revert-header", fromCommitID), ctx.Locale.TrString("repo.commit.cherry-pick-header", fromCommitID))
+ opts := &files.ApplyDiffPatchOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: parsed.OldBranchName,
+ NewBranch: parsed.NewBranchName,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ }
+
+ // First try the simple plain read-tree -m approach
+ opts.Content = fromCommitID
+ if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, parsed.form.Revert, opts); err != nil {
+ // Drop through to the "apply" method
+ buf := &bytes.Buffer{}
+ if parsed.form.Revert {
+ err = git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), fromCommitID, buf)
+ } else {
+ err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, "patch", buf)
+ }
+ if err == nil {
+ opts.Content = buf.String()
+ _, err = files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts)
+ if err != nil {
+ err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
+ }
+ }
+ if err != nil {
+ editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
+ return
+ }
+ }
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
+}
diff --git a/routers/web/repo/editor_error.go b/routers/web/repo/editor_error.go
new file mode 100644
index 0000000000..245226a039
--- /dev/null
+++ b/routers/web/repo/editor_error.go
@@ -0,0 +1,82 @@
+// Copyright 2025 Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "errors"
+
+ git_model "code.gitea.io/gitea/models/git"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/utils"
+ context_service "code.gitea.io/gitea/services/context"
+ files_service "code.gitea.io/gitea/services/repository/files"
+)
+
+func errorAs[T error](v error) (e T, ok bool) {
+ if errors.As(v, &e) {
+ return e, true
+ }
+ return e, false
+}
+
+func editorHandleFileOperationErrorRender(ctx *context_service.Context, message, summary, details string) {
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
+ "Message": message,
+ "Summary": summary,
+ "Details": utils.SanitizeFlashErrorString(details),
+ })
+ if err == nil {
+ ctx.JSONError(flashError)
+ } else {
+ log.Error("RenderToHTML: %v", err)
+ ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details))
+ }
+}
+
+func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) {
+ if errAs := util.ErrorAsLocale(err); errAs != nil {
+ ctx.JSONError(ctx.Tr(errAs.TrKey, errAs.TrArgs...))
+ } else if errAs, ok := errorAs[git.ErrNotExist](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath))
+ } else if errAs, ok := errorAs[git_model.ErrLFSFileLocked](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.upload_file_is_locked", errAs.Path, errAs.UserName))
+ } else if errAs, ok := errorAs[files_service.ErrFilenameInvalid](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path))
+ } else if errAs, ok := errorAs[files_service.ErrFilePathInvalid](err); ok {
+ switch errAs.Type {
+ case git.EntryModeSymlink:
+ ctx.JSONError(ctx.Tr("repo.editor.file_is_a_symlink", errAs.Path))
+ case git.EntryModeTree:
+ ctx.JSONError(ctx.Tr("repo.editor.filename_is_a_directory", errAs.Path))
+ case git.EntryModeBlob:
+ ctx.JSONError(ctx.Tr("repo.editor.directory_is_a_file", errAs.Path))
+ default:
+ ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path))
+ }
+ } else if errAs, ok := errorAs[files_service.ErrRepoFileAlreadyExists](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.file_already_exists", errAs.Path))
+ } else if errAs, ok := errorAs[git.ErrBranchNotExist](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.branch_does_not_exist", errAs.Name))
+ } else if errAs, ok := errorAs[git_model.ErrBranchAlreadyExists](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", errAs.BranchName))
+ } else if files_service.IsErrCommitIDDoesNotMatch(err) {
+ ctx.JSONError(ctx.Tr("repo.editor.commit_id_not_matching"))
+ } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
+ ctx.JSONError(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(targetBranchName)))
+ } else if errAs, ok := errorAs[*git.ErrPushRejected](err); ok {
+ if errAs.Message == "" {
+ ctx.JSONError(ctx.Tr("repo.editor.push_rejected_no_message"))
+ } else {
+ editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.push_rejected"), ctx.Locale.TrString("repo.editor.push_rejected_summary"), errAs.Message)
+ }
+ } else if errors.Is(err, util.ErrNotExist) {
+ ctx.JSONError(ctx.Tr("error.not_found"))
+ } else {
+ setting.PanicInDevOrTesting("unclear err %T: %v", err, err)
+ editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.failed_to_commit"), ctx.Locale.TrString("repo.editor.failed_to_commit_summary"), err.Error())
+ }
+}
diff --git a/routers/web/repo/editor_fork.go b/routers/web/repo/editor_fork.go
new file mode 100644
index 0000000000..b78a634c00
--- /dev/null
+++ b/routers/web/repo/editor_fork.go
@@ -0,0 +1,31 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/context"
+ repo_service "code.gitea.io/gitea/services/repository"
+)
+
+const tplEditorFork templates.TplName = "repo/editor/fork"
+
+func ForkToEdit(ctx *context.Context) {
+ ctx.HTML(http.StatusOK, tplEditorFork)
+}
+
+func ForkToEditPost(ctx *context.Context) {
+ ForkRepoTo(ctx, ctx.Doer, repo_service.ForkRepoOptions{
+ BaseRepo: ctx.Repo.Repository,
+ Name: getUniqueRepositoryName(ctx, ctx.Doer.ID, ctx.Repo.Repository.Name),
+ Description: ctx.Repo.Repository.Description,
+ SingleBranch: ctx.Repo.Repository.DefaultBranch, // maybe we only need the default branch in the fork?
+ })
+ if ctx.Written() {
+ return
+ }
+ ctx.JSONRedirect("") // reload the page, the new fork should be editable now
+}
diff --git a/routers/web/repo/editor_preview.go b/routers/web/repo/editor_preview.go
new file mode 100644
index 0000000000..14be5b72b6
--- /dev/null
+++ b/routers/web/repo/editor_preview.go
@@ -0,0 +1,41 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/services/context"
+ files_service "code.gitea.io/gitea/services/repository/files"
+)
+
+func DiffPreviewPost(ctx *context.Context) {
+ content := ctx.FormString("content")
+ treePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
+ if treePath == "" {
+ ctx.HTTPError(http.StatusBadRequest, "file name to diff is invalid")
+ return
+ }
+
+ entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
+ if err != nil {
+ ctx.ServerError("GetTreeEntryByPath", err)
+ return
+ } else if entry.IsDir() {
+ ctx.HTTPError(http.StatusUnprocessableEntity)
+ return
+ }
+
+ diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, content)
+ if err != nil {
+ ctx.ServerError("GetDiffPreview", err)
+ return
+ }
+
+ if len(diff.Files) != 0 {
+ ctx.Data["File"] = diff.Files[0]
+ }
+
+ ctx.HTML(http.StatusOK, tplEditDiffPreview)
+}
diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go
index 89bf8f309c..6e2c1d6219 100644
--- a/routers/web/repo/editor_test.go
+++ b/routers/web/repo/editor_test.go
@@ -6,76 +6,27 @@ package repo
import (
"testing"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
-func TestCleanUploadName(t *testing.T) {
+func TestEditorUtils(t *testing.T) {
unittest.PrepareTestEnv(t)
-
- kases := map[string]string{
- ".git/refs/master": "",
- "/root/abc": "root/abc",
- "./../../abc": "abc",
- "a/../.git": "",
- "a/../../../abc": "abc",
- "../../../acd": "acd",
- "../../.git/abc": "",
- "..\\..\\.git/abc": "..\\..\\.git/abc",
- "..\\../.git/abc": "",
- "..\\../.git": "",
- "abc/../def": "def",
- ".drone.yml": ".drone.yml",
- ".abc/def/.drone.yml": ".abc/def/.drone.yml",
- "..drone.yml.": "..drone.yml.",
- "..a.dotty...name...": "..a.dotty...name...",
- "..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...",
- }
- for k, v := range kases {
- assert.Equal(t, cleanUploadFileName(k), v)
- }
-}
-
-func TestGetUniquePatchBranchName(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1")
- ctx.SetPathParam("id", "1")
- contexttest.LoadRepo(t, ctx, 1)
- contexttest.LoadRepoCommit(t, ctx)
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- expectedBranchName := "user2-patch-1"
- branchName := GetUniquePatchBranchName(ctx)
- assert.Equal(t, expectedBranchName, branchName)
-}
-
-func TestGetClosestParentWithFiles(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1")
- ctx.SetPathParam("id", "1")
- contexttest.LoadRepo(t, ctx, 1)
- contexttest.LoadRepoCommit(t, ctx)
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- repo := ctx.Repo.Repository
- branch := repo.DefaultBranch
- gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo)
- defer gitRepo.Close()
- commit, _ := gitRepo.GetBranchCommit(branch)
- var expectedTreePath string // Should return the root dir, empty string, since there are no subdirs in this repo
- for _, deletedFile := range []string{
- "dir1/dir2/dir3/file.txt",
- "file.txt",
- } {
- treePath := GetClosestParentWithFiles(deletedFile, commit)
- assert.Equal(t, expectedTreePath, treePath)
- }
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ t.Run("getUniquePatchBranchName", func(t *testing.T) {
+ branchName := getUniquePatchBranchName(t.Context(), "user2", repo)
+ assert.Equal(t, "user2-patch-1", branchName)
+ })
+ t.Run("getClosestParentWithFiles", func(t *testing.T) {
+ gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo)
+ defer gitRepo.Close()
+ treePath := getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "docs/foo/bar")
+ assert.Equal(t, "docs", treePath)
+ treePath = getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "any/other")
+ assert.Empty(t, treePath)
+ })
}
diff --git a/routers/web/repo/editor_uploader.go b/routers/web/repo/editor_uploader.go
new file mode 100644
index 0000000000..1ce9a1aca4
--- /dev/null
+++ b/routers/web/repo/editor_uploader.go
@@ -0,0 +1,61 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
+ files_service "code.gitea.io/gitea/services/repository/files"
+)
+
+// UploadFileToServer upload file to server file dir not git
+func UploadFileToServer(ctx *context.Context) {
+ file, header, err := ctx.Req.FormFile("file")
+ if err != nil {
+ ctx.ServerError("FormFile", err)
+ return
+ }
+ defer file.Close()
+
+ buf := make([]byte, 1024)
+ n, _ := util.ReadAtMost(file, buf)
+ if n > 0 {
+ buf = buf[:n]
+ }
+
+ err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
+ if err != nil {
+ ctx.HTTPError(http.StatusBadRequest, err.Error())
+ return
+ }
+
+ name := files_service.CleanGitTreePath(header.Filename)
+ if len(name) == 0 {
+ ctx.HTTPError(http.StatusBadRequest, "Upload file name is invalid")
+ return
+ }
+
+ uploaded, err := repo_model.NewUpload(ctx, name, buf, file)
+ if err != nil {
+ ctx.ServerError("NewUpload", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]string{"uuid": uploaded.UUID})
+}
+
+// RemoveUploadFileFromServer remove file from server file dir
+func RemoveUploadFileFromServer(ctx *context.Context) {
+ fileUUID := ctx.FormString("file")
+ if err := repo_model.DeleteUploadByUUID(ctx, fileUUID); err != nil {
+ ctx.ServerError("DeleteUploadByUUID", err)
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/web/repo/editor_util.go b/routers/web/repo/editor_util.go
new file mode 100644
index 0000000000..f910f0bd40
--- /dev/null
+++ b/routers/web/repo/editor_util.go
@@ -0,0 +1,110 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "context"
+ "fmt"
+ "path"
+ "strings"
+
+ git_model "code.gitea.io/gitea/models/git"
+ 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/json"
+ "code.gitea.io/gitea/modules/log"
+ repo_module "code.gitea.io/gitea/modules/repository"
+ context_service "code.gitea.io/gitea/services/context"
+)
+
+// getUniquePatchBranchName Gets a unique branch name for a new patch branch
+// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
+// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
+// type in the branch name themselves (will be an empty field)
+func getUniquePatchBranchName(ctx context.Context, prefixName string, repo *repo_model.Repository) string {
+ prefix := prefixName + "-patch-"
+ for i := 1; i <= 1000; i++ {
+ branchName := fmt.Sprintf("%s%d", prefix, i)
+ if exist, err := git_model.IsBranchExist(ctx, repo.ID, branchName); err != nil {
+ log.Error("getUniquePatchBranchName: %v", err)
+ return ""
+ } else if !exist {
+ return branchName
+ }
+ }
+ return ""
+}
+
+// getClosestParentWithFiles Recursively gets the closest path of parent in a tree that has files when a file in a tree is
+// deleted. It returns "" for the tree root if no parents other than the root have files.
+func getClosestParentWithFiles(gitRepo *git.Repository, branchName, originTreePath string) string {
+ var f func(treePath string, commit *git.Commit) string
+ f = func(treePath string, commit *git.Commit) string {
+ if treePath == "" || treePath == "." {
+ return ""
+ }
+ // see if the tree has entries
+ if tree, err := commit.SubTree(treePath); err != nil {
+ return f(path.Dir(treePath), commit) // failed to get the tree, going up a dir
+ } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
+ return f(path.Dir(treePath), commit) // no files in this dir, going up a dir
+ }
+ return treePath
+ }
+ commit, err := gitRepo.GetBranchCommit(branchName) // must get the commit again to get the latest change
+ if err != nil {
+ log.Error("GetBranchCommit: %v", err)
+ return ""
+ }
+ return f(originTreePath, commit)
+}
+
+// getContextRepoEditorConfig returns the editorconfig JSON string for given treePath or "null"
+func getContextRepoEditorConfig(ctx *context_service.Context, treePath string) string {
+ ec, _, err := ctx.Repo.GetEditorconfig()
+ if err == nil {
+ def, err := ec.GetDefinitionForFilename(treePath)
+ if err == nil {
+ jsonStr, _ := json.Marshal(def)
+ return string(jsonStr)
+ }
+ }
+ return "null"
+}
+
+// getParentTreeFields returns list of parent tree names and corresponding tree paths based on given treePath.
+// eg: []{"a", "b", "c"}, []{"a", "a/b", "a/b/c"}
+// or: []{""}, []{""} for the root treePath
+func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
+ treeNames = strings.Split(treePath, "/")
+ treePaths = make([]string, len(treeNames))
+ for i := range treeNames {
+ treePaths[i] = strings.Join(treeNames[:i+1], "/")
+ }
+ return treeNames, treePaths
+}
+
+// getUniqueRepositoryName Gets a unique repository name for a user
+// It will append a -<num> postfix if the name is already taken
+func getUniqueRepositoryName(ctx context.Context, ownerID int64, name string) string {
+ uniqueName := name
+ for i := 1; i < 1000; i++ {
+ _, err := repo_model.GetRepositoryByName(ctx, ownerID, uniqueName)
+ if err != nil || repo_model.IsErrRepoNotExist(err) {
+ return uniqueName
+ }
+ uniqueName = fmt.Sprintf("%s-%d", name, i)
+ i++
+ }
+ return ""
+}
+
+func editorPushBranchToForkedRepository(ctx context.Context, doer *user_model.User, baseRepo *repo_model.Repository, baseBranchName string, targetRepo *repo_model.Repository, targetBranchName string) error {
+ return git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{
+ Remote: targetRepo.RepoPath(),
+ Branch: baseBranchName + ":" + targetBranchName,
+ Env: repo_module.PushingEnvironment(doer, targetRepo),
+ })
+}
diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go
index 79f033659b..c2694e540f 100644
--- a/routers/web/repo/fork.go
+++ b/routers/web/repo/fork.go
@@ -151,7 +151,7 @@ func ForkPost(ctx *context.Context) {
ctx.Data["ContextUser"] = ctxUser
if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplFork)
+ ctx.JSONError(ctx.GetErrMsg())
return
}
@@ -159,12 +159,12 @@ func ForkPost(ctx *context.Context) {
traverseParentRepo := forkRepo
for {
if !repository.CanUserForkBetweenOwners(ctxUser.ID, traverseParentRepo.OwnerID) {
- ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+ ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo"))
return
}
repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID)
if repo != nil {
- ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+ ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
return
}
if !traverseParentRepo.IsFork {
@@ -189,44 +189,50 @@ func ForkPost(ctx *context.Context) {
}
}
- repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{
+ repo := ForkRepoTo(ctx, ctxUser, repo_service.ForkRepoOptions{
BaseRepo: forkRepo,
Name: form.RepoName,
Description: form.Description,
SingleBranch: form.ForkSingleBranch,
})
+ if ctx.Written() {
+ return
+ }
+ ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+}
+
+func ForkRepoTo(ctx *context.Context, owner *user_model.User, forkOpts repo_service.ForkRepoOptions) *repo_model.Repository {
+ repo, err := repo_service.ForkRepository(ctx, ctx.Doer, owner, forkOpts)
if err != nil {
ctx.Data["Err_RepoName"] = true
switch {
case repo_model.IsErrReachLimitOfRepo(err):
- maxCreationLimit := ctxUser.MaxCreationLimit()
+ maxCreationLimit := owner.MaxCreationLimit()
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
- ctx.RenderWithErr(msg, tplFork, &form)
+ ctx.JSONError(msg)
case repo_model.IsErrRepoAlreadyExist(err):
- ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+ ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo"))
case repo_model.IsErrRepoFilesAlreadyExist(err):
switch {
case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form)
+ ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"))
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form)
+ ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt"))
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form)
+ ctx.JSONError(ctx.Tr("form.repository_files_already_exist.delete"))
default:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form)
+ ctx.JSONError(ctx.Tr("form.repository_files_already_exist"))
}
case db.IsErrNameReserved(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
+ ctx.JSONError(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name))
case db.IsErrNamePatternNotAllowed(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
+ ctx.JSONError(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern))
case errors.Is(err, user_model.ErrBlockedUser):
- ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form)
+ ctx.JSONError(ctx.Tr("repo.fork.blocked_user"))
default:
ctx.ServerError("ForkPost", err)
}
- return
+ return nil
}
-
- log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
- ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+ return repo
}
diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go
index 61606f8c5f..deb3ae4f3a 100644
--- a/routers/web/repo/githttp.go
+++ b/routers/web/repo/githttp.go
@@ -13,6 +13,7 @@ import (
"os"
"path/filepath"
"regexp"
+ "slices"
"strconv"
"strings"
"sync"
@@ -363,12 +364,7 @@ func containsParentDirectorySeparator(v string) bool {
if !strings.Contains(v, "..") {
return false
}
- for _, ent := range strings.FieldsFunc(v, isSlashRune) {
- if ent == ".." {
- return true
- }
- }
- return false
+ return slices.Contains(strings.FieldsFunc(v, isSlashRune), "..")
}
func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 86ee56b467..54b7e5df2a 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -212,7 +212,7 @@ func getActionIssues(ctx *context.Context) issues_model.IssueList {
return nil
}
issueIDs := make([]int64, 0, 10)
- for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
+ for stringIssueID := range strings.SplitSeq(commaSeparatedIssueIDs, ",") {
issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
if err != nil {
ctx.ServerError("ParseInt", err)
@@ -364,7 +364,9 @@ func UpdateIssueContent(ctx *context.Context) {
}
}
- rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
+ FootnoteContextID: "0",
+ })
content, err := markdown.RenderString(rctx, issue.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -418,6 +420,16 @@ func UpdateIssueMilestone(ctx *context.Context) {
continue
}
issue.MilestoneID = milestoneID
+ if milestoneID > 0 {
+ var err error
+ issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
+ if err != nil {
+ ctx.ServerError("GetMilestoneByRepoID", err)
+ return
+ }
+ } else {
+ issue.Milestone = nil
+ }
if err := issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
ctx.ServerError("ChangeMilestoneAssign", err)
return
diff --git a/routers/web/repo/issue_comment.go b/routers/web/repo/issue_comment.go
index 8adce26ccc..c2a7f6b682 100644
--- a/routers/web/repo/issue_comment.go
+++ b/routers/web/repo/issue_comment.go
@@ -8,6 +8,7 @@ import (
"fmt"
"html/template"
"net/http"
+ "strconv"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/renderhelper"
@@ -239,21 +240,28 @@ func UpdateCommentContent(ctx *context.Context) {
return
}
- oldContent := comment.Content
newContent := ctx.FormString("content")
contentVersion := ctx.FormInt("content_version")
+ if contentVersion != comment.ContentVersion {
+ ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
+ return
+ }
- // allow to save empty content
- comment.Content = newContent
- if err = issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
- if errors.Is(err, user_model.ErrBlockedUser) {
- ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
- } else if errors.Is(err, issues_model.ErrCommentAlreadyChanged) {
- ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
- } else {
- ctx.ServerError("UpdateComment", err)
+ if newContent != comment.Content {
+ // allow to save empty content
+ oldContent := comment.Content
+ comment.Content = newContent
+
+ if err = issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
+ } else if errors.Is(err, issues_model.ErrCommentAlreadyChanged) {
+ ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
+ } else {
+ ctx.ServerError("UpdateComment", err)
+ }
+ return
}
- return
}
if err := comment.LoadAttachments(ctx); err != nil {
@@ -271,7 +279,9 @@ func UpdateCommentContent(ctx *context.Context) {
var renderedContent template.HTML
if comment.Content != "" {
- rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
+ FootnoteContextID: strconv.FormatInt(comment.ID, 10),
+ })
renderedContent, err = markdown.RenderString(rctx, comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index f9c41adbcf..72a316e98d 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -4,6 +4,7 @@
package repo
import (
+ "errors"
"net/http"
"code.gitea.io/gitea/models/db"
@@ -13,7 +14,9 @@ import (
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ shared_label "code.gitea.io/gitea/routers/web/shared/label"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
issue_service "code.gitea.io/gitea/services/issue"
@@ -100,13 +103,8 @@ func RetrieveLabelsForList(ctx *context.Context) {
// NewLabel create new label for repository
func NewLabel(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateLabelForm)
- ctx.Data["Title"] = ctx.Tr("repo.labels")
- ctx.Data["PageIsLabels"] = true
-
- if ctx.HasError() {
- ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
- ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+ form := shared_label.GetLabelEditForm(ctx)
+ if ctx.Written() {
return
}
@@ -122,34 +120,36 @@ func NewLabel(ctx *context.Context) {
ctx.ServerError("NewLabel", err)
return
}
- ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/labels")
}
// UpdateLabel update a label's name and color
func UpdateLabel(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateLabelForm)
+ form := shared_label.GetLabelEditForm(ctx)
+ if ctx.Written() {
+ return
+ }
+
l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, form.ID)
- if err != nil {
- switch {
- case issues_model.IsErrRepoLabelNotExist(err):
- ctx.HTTPError(http.StatusNotFound)
- default:
- ctx.ServerError("UpdateLabel", err)
- }
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.JSONErrorNotFound()
+ return
+ } else if err != nil {
+ ctx.ServerError("GetLabelInRepoByID", err)
return
}
+
l.Name = form.Title
l.Exclusive = form.Exclusive
l.ExclusiveOrder = form.ExclusiveOrder
l.Description = form.Description
l.Color = form.Color
-
l.SetArchived(form.IsArchived)
if err := issues_model.UpdateLabel(ctx, l); err != nil {
ctx.ServerError("UpdateLabel", err)
return
}
- ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/labels")
}
// DeleteLabel delete a label
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
index c3fba07034..f4eca26f8e 100644
--- a/routers/web/repo/issue_label_test.go
+++ b/routers/web/repo/issue_label_test.go
@@ -6,10 +6,12 @@ package repo
import (
"net/http"
"strconv"
+ "strings"
"testing"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
@@ -19,19 +21,20 @@ import (
"github.com/stretchr/testify/assert"
)
-func int64SliceToCommaSeparated(a []int64) string {
- s := ""
- for i, n := range a {
- if i > 0 {
- s += ","
- }
- s += strconv.Itoa(int(n))
- }
- return s
+func TestIssueLabel(t *testing.T) {
+ unittest.PrepareTestEnv(t)
+ t.Run("RetrieveLabels", testRetrieveLabels)
+ t.Run("NewLabel", testNewLabel)
+ t.Run("NewLabelInvalidColor", testNewLabelInvalidColor)
+ t.Run("UpdateLabel", testUpdateLabel)
+ t.Run("UpdateLabelInvalidColor", testUpdateLabelInvalidColor)
+ t.Run("UpdateIssueLabelClear", testUpdateIssueLabelClear)
+ t.Run("UpdateIssueLabelToggle", testUpdateIssueLabelToggle)
+ t.Run("InitializeLabels", testInitializeLabels)
+ t.Run("DeleteLabel", testDeleteLabel)
}
-func TestInitializeLabels(t *testing.T) {
- unittest.PrepareTestEnv(t)
+func testInitializeLabels(t *testing.T) {
assert.NoError(t, repository.LoadRepoConfig())
ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/initialize")
contexttest.LoadUser(t, ctx, 2)
@@ -47,8 +50,7 @@ func TestInitializeLabels(t *testing.T) {
assert.Equal(t, "/user2/repo2/labels", test.RedirectURL(ctx.Resp))
}
-func TestRetrieveLabels(t *testing.T) {
- unittest.PrepareTestEnv(t)
+func testRetrieveLabels(t *testing.T) {
for _, testCase := range []struct {
RepoID int64
Sort string
@@ -74,9 +76,8 @@ func TestRetrieveLabels(t *testing.T) {
}
}
-func TestNewLabel(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/edit")
+func testNewLabel(t *testing.T) {
+ ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
web.SetForm(ctx, &forms.CreateLabelForm{
@@ -84,17 +85,32 @@ func TestNewLabel(t *testing.T) {
Color: "#abcdef",
})
NewLabel(ctx)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
Name: "newlabel",
Color: "#abcdef",
})
- assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(ctx.Resp))
+ assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(respWriter))
}
-func TestUpdateLabel(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/edit")
+func testNewLabelInvalidColor(t *testing.T) {
+ ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit")
+ contexttest.LoadUser(t, ctx, 2)
+ contexttest.LoadRepo(t, ctx, 1)
+ web.SetForm(ctx, &forms.CreateLabelForm{
+ Title: "newlabel-x",
+ Color: "bad-label-code",
+ })
+ NewLabel(ctx)
+ assert.Equal(t, http.StatusBadRequest, ctx.Resp.WrittenStatus())
+ assert.Equal(t, "repo.issues.label_color_invalid", test.ParseJSONError(respWriter.Body.Bytes()).ErrorMessage)
+ unittest.AssertNotExistsBean(t, &issues_model.Label{
+ Name: "newlabel-x",
+ })
+}
+
+func testUpdateLabel(t *testing.T) {
+ ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
web.SetForm(ctx, &forms.CreateLabelForm{
@@ -104,17 +120,37 @@ func TestUpdateLabel(t *testing.T) {
IsArchived: true,
})
UpdateLabel(ctx)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
ID: 2,
Name: "newnameforlabel",
Color: "#abcdef",
})
- assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(ctx.Resp))
+ assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(respWriter))
}
-func TestDeleteLabel(t *testing.T) {
- unittest.PrepareTestEnv(t)
+func testUpdateLabelInvalidColor(t *testing.T) {
+ ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit")
+ contexttest.LoadUser(t, ctx, 2)
+ contexttest.LoadRepo(t, ctx, 1)
+ web.SetForm(ctx, &forms.CreateLabelForm{
+ ID: 1,
+ Title: "label1",
+ Color: "bad-label-code",
+ })
+
+ UpdateLabel(ctx)
+
+ assert.Equal(t, http.StatusBadRequest, ctx.Resp.WrittenStatus())
+ assert.Equal(t, "repo.issues.label_color_invalid", test.ParseJSONError(respWriter.Body.Bytes()).ErrorMessage)
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
+ ID: 1,
+ Name: "label1",
+ Color: "#abcdef",
+ })
+}
+
+func testDeleteLabel(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/delete")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
@@ -126,8 +162,7 @@ func TestDeleteLabel(t *testing.T) {
assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
}
-func TestUpdateIssueLabel_Clear(t *testing.T) {
- unittest.PrepareTestEnv(t)
+func testUpdateIssueLabelClear(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "user2/repo1/issues/labels")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
@@ -140,7 +175,7 @@ func TestUpdateIssueLabel_Clear(t *testing.T) {
unittest.CheckConsistencyFor(t, &issues_model.Label{})
}
-func TestUpdateIssueLabel_Toggle(t *testing.T) {
+func testUpdateIssueLabelToggle(t *testing.T) {
for _, testCase := range []struct {
Action string
IssueIDs []int64
@@ -156,7 +191,8 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "user2/repo1/issues/labels")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
- ctx.Req.Form.Set("issue_ids", int64SliceToCommaSeparated(testCase.IssueIDs))
+
+ ctx.Req.Form.Set("issue_ids", strings.Join(base.Int64sToStrings(testCase.IssueIDs), ","))
ctx.Req.Form.Set("action", testCase.Action)
ctx.Req.Form.Set("id", strconv.Itoa(int(testCase.LabelID)))
UpdateIssueLabel(ctx)
diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go
index 35107bc585..b55f4bcc90 100644
--- a/routers/web/repo/issue_list.go
+++ b/routers/web/repo/issue_list.go
@@ -61,7 +61,7 @@ func SearchIssues(ctx *context.Context) {
)
{
// find repos user can access (for issue search)
- opts := &repo_model.SearchRepoOptions{
+ opts := repo_model.SearchRepoOptions{
Private: false,
AllPublic: true,
TopicOnly: false,
diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go
index d8863961ff..887019b146 100644
--- a/routers/web/repo/issue_new.go
+++ b/routers/web/repo/issue_new.go
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"html/template"
+ "maps"
"net/http"
"slices"
"sort"
@@ -136,9 +137,7 @@ func NewIssue(ctx *context.Context) {
ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, pageMetaData)
- for k, v := range errs {
- ret.TemplateErrors[k] = v
- }
+ maps.Copy(ret.TemplateErrors, errs)
if ctx.Written() {
return
}
diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go
index 5a8d203771..2de3a7cfec 100644
--- a/routers/web/repo/issue_stopwatch.go
+++ b/routers/web/repo/issue_stopwatch.go
@@ -10,33 +10,47 @@ import (
"code.gitea.io/gitea/services/context"
)
-// IssueStopwatch creates or stops a stopwatch for the given issue.
-func IssueStopwatch(c *context.Context) {
+// IssueStartStopwatch creates a stopwatch for the given issue.
+func IssueStartStopwatch(c *context.Context) {
issue := GetActionIssue(c)
if c.Written() {
return
}
- var showSuccessMessage bool
-
- if !issues_model.StopwatchExists(c, c.Doer.ID, issue.ID) {
- showSuccessMessage = true
- }
-
if !c.Repo.CanUseTimetracker(c, issue, c.Doer) {
c.NotFound(nil)
return
}
- if err := issues_model.CreateOrStopIssueStopwatch(c, c.Doer, issue); err != nil {
- c.ServerError("CreateOrStopIssueStopwatch", err)
+ if ok, err := issues_model.CreateIssueStopwatch(c, c.Doer, issue); err != nil {
+ c.ServerError("CreateIssueStopwatch", err)
return
+ } else if !ok {
+ c.Flash.Warning(c.Tr("repo.issues.stopwatch_already_created"))
+ } else {
+ c.Flash.Success(c.Tr("repo.issues.tracker_auto_close"))
}
+ c.JSONRedirect("")
+}
- if showSuccessMessage {
- c.Flash.Success(c.Tr("repo.issues.tracker_auto_close"))
+// IssueStopStopwatch stops a stopwatch for the given issue.
+func IssueStopStopwatch(c *context.Context) {
+ issue := GetActionIssue(c)
+ if c.Written() {
+ return
+ }
+
+ if !c.Repo.CanUseTimetracker(c, issue, c.Doer) {
+ c.NotFound(nil)
+ return
}
+ if ok, err := issues_model.FinishIssueStopwatch(c, c.Doer, issue); err != nil {
+ c.ServerError("FinishIssueStopwatch", err)
+ return
+ } else if !ok {
+ c.Flash.Warning(c.Tr("repo.issues.stopwatch_already_stopped"))
+ }
c.JSONRedirect("")
}
@@ -51,7 +65,7 @@ func CancelStopwatch(c *context.Context) {
return
}
- if err := issues_model.CancelStopwatch(c, c.Doer, issue); err != nil {
+ if _, err := issues_model.CancelStopwatch(c, c.Doer, issue); err != nil {
c.ServerError("CancelStopwatch", err)
return
}
diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go
index 13b9d83da4..2897652d51 100644
--- a/routers/web/repo/issue_view.go
+++ b/routers/web/repo/issue_view.go
@@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"sort"
+ "strconv"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
@@ -624,7 +625,9 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue
comment.Issue = issue
if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
- rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
+ FootnoteContextID: strconv.FormatInt(comment.ID, 10),
+ })
comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -700,7 +703,9 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue
}
}
} else if comment.Type.HasContentSupport() {
- rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
+ FootnoteContextID: strconv.FormatInt(comment.ID, 10),
+ })
comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -757,6 +762,9 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue
}
if !ctx.Repo.CanRead(unit.TypeActions) {
for _, commit := range comment.Commits {
+ if commit.Status == nil {
+ continue
+ }
commit.Status.HideActionsURL(ctx)
git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses)
}
@@ -981,7 +989,9 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) {
var err error
- rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
+ FootnoteContextID: "0", // Set footnote context ID to 0 for the issue content
+ })
issue.RenderedContent, err = markdown.RenderString(rctx, issue.Content)
if err != nil {
ctx.ServerError("RenderString", err)
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index 8a26a0dcc3..dd53b1d3f1 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -38,10 +38,7 @@ func Milestones(ctx *context.Context) {
isShowClosed := ctx.FormString("state") == "closed"
sortType := ctx.FormString("sort")
keyword := ctx.FormTrim("q")
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
miles, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
ListOptions: db.ListOptions{
diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go
index 65a340a799..d09a57c03f 100644
--- a/routers/web/repo/packages.go
+++ b/routers/web/repo/packages.go
@@ -21,10 +21,7 @@ const (
// Packages displays a list of all packages in the repository
func Packages(ctx *context.Context) {
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
query := ctx.FormTrim("q")
packageType := ctx.FormTrim("type")
diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go
deleted file mode 100644
index ca346b7e6c..0000000000
--- a/routers/web/repo/patch.go
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright 2021 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package repo
-
-import (
- "net/http"
- "strings"
-
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/repository/files"
-)
-
-const (
- tplPatchFile templates.TplName = "repo/editor/patch"
-)
-
-// NewDiffPatch render create patch page
-func NewDiffPatch(ctx *context.Context) {
- canCommit := renderCommitRights(ctx)
-
- ctx.Data["PageIsPatch"] = true
-
- ctx.Data["commit_summary"] = ""
- ctx.Data["commit_message"] = ""
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- }
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
-
- ctx.HTML(http.StatusOK, tplPatchFile)
-}
-
-// NewDiffPatchPost response for sending patch page
-func NewDiffPatchPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditRepoFileForm)
-
- canCommit := renderCommitRights(ctx)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
- ctx.Data["PageIsPatch"] = true
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- ctx.Data["FileContent"] = form.Content
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplPatchFile)
- return
- }
-
- // Cannot commit to an existing branch if user doesn't have rights
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
- return
- }
-
- // CommitSummary is optional in the web form, if empty, give it a default message based on add or update
- // `message` will be both the summary and message combined
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- message = ctx.Locale.TrString("repo.editor.patch")
- }
-
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
- }
-
- gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail)
- if !valid {
- ctx.Data["Err_CommitEmail"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplPatchFile, &form)
- return
- }
-
- fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
- LastCommitID: form.LastCommit,
- OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
- Message: message,
- Content: strings.ReplaceAll(form.Content, "\r", ""),
- Author: gitCommitter,
- Committer: gitCommitter,
- })
- if err != nil {
- if git_model.IsErrBranchAlreadyExists(err) {
- // User has specified a branch that already exists
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
- return
- } else if files.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
- return
- }
- ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form)
- return
- }
-
- if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
- ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
- } else {
- ctx.Redirect(ctx.Repo.RepoLink + "/commit/" + fileResponse.Commit.SHA)
- }
-}
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 0bf1f64d09..a57976b4ca 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -61,10 +61,7 @@ func Projects(ctx *context.Context) {
isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed"
keyword := ctx.FormTrim("q")
repo := ctx.Repo.Repository
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
ctx.Data["OpenCount"] = repo.NumOpenProjects
ctx.Data["ClosedCount"] = repo.NumClosedProjects
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 43ddc265cf..f662152e2e 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -27,6 +27,7 @@ import (
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/graceful"
issue_template "code.gitea.io/gitea/modules/issue/template"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -291,7 +292,7 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
if len(compareInfo.Commits) != 0 {
sha := compareInfo.Commits[0].ID.String()
- commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptionsAll)
+ commitStatuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
@@ -358,7 +359,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
return nil
}
- commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
+ commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
@@ -454,7 +455,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
return nil
}
- commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
+ commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
@@ -986,7 +987,9 @@ func UpdatePullRequest(ctx *context.Context) {
// default merge commit message
message := fmt.Sprintf("Merge branch '%s' into %s", issue.PullRequest.BaseBranch, issue.PullRequest.HeadBranch)
- if err = pull_service.Update(ctx, issue.PullRequest, ctx.Doer, message, rebase); err != nil {
+ // The update process should not be cancelled by the user
+ // so we set the context to be a background context
+ if err = pull_service.Update(graceful.GetManager().ShutdownContext(), issue.PullRequest, ctx.Doer, message, rebase); err != nil {
if pull_service.IsErrMergeConflicts(err) {
conflictError := err.(pull_service.ErrMergeConflicts)
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
@@ -1255,13 +1258,8 @@ func CancelAutoMergePullRequest(ctx *context.Context) {
}
func stopTimerIfAvailable(ctx *context.Context, user *user_model.User, issue *issues_model.Issue) error {
- if issues_model.StopwatchExists(ctx, user.ID, issue.ID) {
- if err := issues_model.CreateOrStopIssueStopwatch(ctx, user, issue); err != nil {
- return err
- }
- }
-
- return nil
+ _, err := issues_model.FinishIssueStopwatch(ctx, user, issue)
+ return err
}
func PullsNewRedirect(ctx *context.Context) {
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index 553bdbf6e5..36ea20c23e 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"net/http"
+ "strconv"
"strings"
"code.gitea.io/gitea/models/db"
@@ -102,7 +103,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
releaseInfos := make([]*ReleaseInfo, 0, len(releases))
for _, r := range releases {
if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok {
- r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
+ r.Publisher, err = user_model.GetPossibleUserByID(ctx, r.PublisherID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
r.Publisher = user_model.NewGhostUser()
@@ -113,7 +114,9 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
cacheUsers[r.PublisherID] = r.Publisher
}
- rctx := renderhelper.NewRenderContextRepoComment(ctx, r.Repo)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, r.Repo, renderhelper.RepoCommentOptions{
+ FootnoteContextID: strconv.FormatInt(r.ID, 10),
+ })
r.RenderedNote, err = markdown.RenderString(rctx, r.Note)
if err != nil {
return nil, err
@@ -130,7 +133,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
}
if canReadActions {
- statuses, _, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptionsAll)
+ statuses, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptionsAll)
if err != nil {
return nil, err
}
@@ -378,7 +381,7 @@ func NewRelease(ctx *context.Context) {
ctx.Data["ShowCreateTagOnlyButton"] = false
ctx.Data["tag_name"] = rel.TagName
- ctx.Data["tag_target"] = rel.Target
+ ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
ctx.Data["title"] = rel.Title
ctx.Data["content"] = rel.Note
ctx.Data["attachments"] = rel.Attachments
@@ -534,7 +537,7 @@ func EditRelease(ctx *context.Context) {
}
ctx.Data["ID"] = rel.ID
ctx.Data["tag_name"] = rel.TagName
- ctx.Data["tag_target"] = rel.Target
+ ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
ctx.Data["title"] = rel.Title
ctx.Data["content"] = rel.Note
ctx.Data["prerelease"] = rel.IsPrerelease
@@ -580,7 +583,7 @@ func EditReleasePost(ctx *context.Context) {
return
}
ctx.Data["tag_name"] = rel.TagName
- ctx.Data["tag_target"] = rel.Target
+ ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
ctx.Data["title"] = rel.Title
ctx.Data["content"] = rel.Note
ctx.Data["prerelease"] = rel.IsPrerelease
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index ee112b83f2..828ec08a8a 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -88,7 +88,7 @@ func checkContextUser(ctx *context.Context, uid int64) *user_model.User {
}
var orgsAvailable []*organization.Organization
- for i := 0; i < len(orgs); i++ {
+ for i := range orgs {
if ctx.Doer.CanCreateRepoIn(orgs[i].AsUser()) {
orgsAvailable = append(orgsAvailable, orgs[i])
}
@@ -461,7 +461,7 @@ func SearchRepo(ctx *context.Context) {
if page <= 0 {
page = 1
}
- opts := &repo_model.SearchRepoOptions{
+ opts := repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go
index a065620b2b..af6708e841 100644
--- a/routers/web/repo/setting/lfs.go
+++ b/routers/web/repo/setting/lfs.go
@@ -45,10 +45,7 @@ func LFSFiles(ctx *context.Context) {
ctx.NotFound(nil)
return
}
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
total, err := git_model.CountLFSMetaObjects(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("LFSFiles", err)
@@ -77,10 +74,7 @@ func LFSLocks(ctx *context.Context) {
}
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
total, err := git_model.CountLFSLockByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("LFSLocks", err)
@@ -273,8 +267,10 @@ func LFSFileGet(ctx *context.Context) {
buf = buf[:n]
st := typesniffer.DetectContentType(buf)
+ // FIXME: there is no IsPlainText set, but template uses it
ctx.Data["IsTextFile"] = st.IsText()
ctx.Data["FileSize"] = meta.Size
+ // FIXME: the last field is the URL-base64-encoded filename, it should not be "direct"
ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s/%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct")
switch {
case st.IsRepresentableAsText():
@@ -315,8 +311,6 @@ func LFSFileGet(ctx *context.Context) {
}
ctx.Data["LineNums"] = gotemplate.HTML(output.String())
- case st.IsPDF():
- ctx.Data["IsPDFFile"] = true
case st.IsVideo():
ctx.Data["IsVideoFile"] = true
case st.IsAudio():
diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go
index f241242f02..0eea5e3f34 100644
--- a/routers/web/repo/setting/protected_branch.go
+++ b/routers/web/repo/setting/protected_branch.go
@@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
@@ -89,7 +90,7 @@ func SettingsProtectedBranch(c *context.Context) {
c.Data["recent_status_checks"] = contexts
if c.Repo.Owner.IsOrganization() {
- teams, err := organization.OrgFromUser(c.Repo.Owner).TeamsWithAccessToRepo(c, c.Repo.Repository.ID, perm.AccessModeRead)
+ teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(c, c.Repo.Owner.ID, c.Repo.Repository.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil {
c.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
return
diff --git a/routers/web/repo/setting/protected_tag.go b/routers/web/repo/setting/protected_tag.go
index 33692778d5..50f5a28c4c 100644
--- a/routers/web/repo/setting/protected_tag.go
+++ b/routers/web/repo/setting/protected_tag.go
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
+ "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
@@ -156,7 +157,7 @@ func setTagsContext(ctx *context.Context) error {
ctx.Data["Users"] = users
if ctx.Repo.Owner.IsOrganization() {
- teams, err := organization.OrgFromUser(ctx.Repo.Owner).TeamsWithAccessToRepo(ctx, ctx.Repo.Repository.ID, perm.AccessModeRead)
+ teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, ctx.Repo.Owner.ID, ctx.Repo.Repository.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil {
ctx.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
return err
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index 5552a8726c..6e16ead183 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -62,7 +62,7 @@ func SettingsCtxData(ctx *context.Context) {
ctx.Data["CanConvertFork"] = ctx.Repo.Repository.IsFork && ctx.Doer.CanCreateRepoIn(ctx.Repo.Repository.Owner)
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
- ctx.Data["SigningKeyAvailable"] = len(signing) > 0
+ ctx.Data["SigningKeyAvailable"] = signing != nil
ctx.Data["SigningSettings"] = setting.Repository.Signing
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
@@ -105,7 +105,7 @@ func SettingsPost(ctx *context.Context) {
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
- ctx.Data["SigningKeyAvailable"] = len(signing) > 0
+ ctx.Data["SigningKeyAvailable"] = signing != nil
ctx.Data["SigningSettings"] = setting.Repository.Signing
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
@@ -663,44 +663,37 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
func handleSettingsPostSigning(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoSettingForm)
repo := ctx.Repo.Repository
- changed := false
trustModel := repo_model.ToTrustModel(form.TrustModel)
if trustModel != repo.TrustModel {
repo.TrustModel = trustModel
- changed = true
- }
-
- if changed {
- if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
- ctx.ServerError("UpdateRepository", err)
+ if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "trust_model"); err != nil {
+ ctx.ServerError("UpdateRepositoryColsNoAutoTime", err)
return
}
+ log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
}
- log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
}
func handleSettingsPostAdmin(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.RepoSettingForm)
- repo := ctx.Repo.Repository
if !ctx.Doer.IsAdmin {
ctx.HTTPError(http.StatusForbidden)
return
}
+ repo := ctx.Repo.Repository
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
if repo.IsFsckEnabled != form.EnableHealthCheck {
repo.IsFsckEnabled = form.EnableHealthCheck
+ if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_fsck_enabled"); err != nil {
+ ctx.ServerError("UpdateRepositoryColsNoAutoTime", err)
+ return
+ }
+ log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
}
- if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
- ctx.ServerError("UpdateRepository", err)
- return
- }
-
- log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
}
diff --git a/routers/web/repo/setting/settings_test.go b/routers/web/repo/setting/settings_test.go
index ba6b0d1efc..15ebea888c 100644
--- a/routers/web/repo/setting/settings_test.go
+++ b/routers/web/repo/setting/settings_test.go
@@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/contexttest"
@@ -24,23 +25,8 @@ import (
"github.com/stretchr/testify/assert"
)
-func createSSHAuthorizedKeysTmpPath(t *testing.T) func() {
- tmpDir := t.TempDir()
-
- oldPath := setting.SSH.RootPath
- setting.SSH.RootPath = tmpDir
-
- return func() {
- setting.SSH.RootPath = oldPath
- }
-}
-
func TestAddReadOnlyDeployKey(t *testing.T) {
- if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
- defer deferable()
- } else {
- return
- }
+ defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "user2/repo1/settings/keys")
@@ -64,11 +50,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) {
}
func TestAddReadWriteOnlyDeployKey(t *testing.T) {
- if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
- defer deferable()
- } else {
- return
- }
+ defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
unittest.PrepareTestEnv(t)
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index d3151a86a2..006abafe57 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -185,6 +185,7 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent {
webhook_module.HookEventRepository: form.Repository,
webhook_module.HookEventPackage: form.Package,
webhook_module.HookEventStatus: form.Status,
+ webhook_module.HookEventWorkflowRun: form.WorkflowRun,
webhook_module.HookEventWorkflowJob: form.WorkflowJob,
},
BranchFilter: form.BranchFilter,
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 2f01434684..d9ff90568d 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -59,60 +59,63 @@ const (
)
type fileInfo struct {
- isTextFile bool
- isLFSFile bool
- fileSize int64
- lfsMeta *lfs.Pointer
- st typesniffer.SniffedType
+ fileSize int64
+ lfsMeta *lfs.Pointer
+ st typesniffer.SniffedType
}
-func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte, io.ReadCloser, *fileInfo, error) {
- dataRc, err := blob.DataAsync()
+func (fi *fileInfo) isLFSFile() bool {
+ return fi.lfsMeta != nil && fi.lfsMeta.Oid != ""
+}
+
+func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []byte, dataRc io.ReadCloser, fi *fileInfo, err error) {
+ dataRc, err = blob.DataAsync()
if err != nil {
return nil, nil, nil, err
}
- buf := make([]byte, 1024)
+ const prefetchSize = lfs.MetaFileMaxSize
+
+ buf = make([]byte, prefetchSize)
n, _ := util.ReadAtMost(dataRc, buf)
buf = buf[:n]
- st := typesniffer.DetectContentType(buf)
- isTextFile := st.IsText()
+ fi = &fileInfo{fileSize: blob.Size(), st: typesniffer.DetectContentType(buf)}
// FIXME: what happens when README file is an image?
- if !isTextFile || !setting.LFS.StartServer {
- return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
+ if !fi.st.IsText() || !setting.LFS.StartServer {
+ return buf, dataRc, fi, nil
}
pointer, _ := lfs.ReadPointerFromBuffer(buf)
- if !pointer.IsValid() { // fallback to plain file
- return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
+ if !pointer.IsValid() { // fallback to a plain file
+ return buf, dataRc, fi, nil
}
meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid)
- if err != nil { // fallback to plain file
+ if err != nil { // fallback to a plain file
log.Warn("Unable to access LFS pointer %s in repo %d: %v", pointer.Oid, repoID, err)
- return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
+ return buf, dataRc, fi, nil
}
- dataRc.Close()
-
+ // close the old dataRc and open the real LFS target
+ _ = dataRc.Close()
dataRc, err = lfs.ReadMetaObject(pointer)
if err != nil {
return nil, nil, nil, err
}
- buf = make([]byte, 1024)
+ buf = make([]byte, prefetchSize)
n, err = util.ReadAtMost(dataRc, buf)
if err != nil {
- dataRc.Close()
- return nil, nil, nil, err
+ _ = dataRc.Close()
+ return nil, nil, fi, err
}
buf = buf[:n]
-
- st = typesniffer.DetectContentType(buf)
-
- return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil
+ fi.st = typesniffer.DetectContentType(buf)
+ fi.fileSize = blob.Size()
+ fi.lfsMeta = &meta.Pointer
+ return buf, dataRc, fi, nil
}
func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
@@ -131,7 +134,7 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
ctx.Data["LatestCommitVerification"] = verification
ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit)
- statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll)
+ statuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll)
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
@@ -394,9 +397,10 @@ func Forks(ctx *context.Context) {
}
pager := context.NewPagination(int(total), pageSize, page, 5)
+ ctx.Data["ShowRepoOwnerAvatar"] = true
+ ctx.Data["ShowRepoOwnerOnList"] = true
ctx.Data["Page"] = pager
-
- ctx.Data["Forks"] = forks
+ ctx.Data["Repos"] = forks
ctx.HTML(http.StatusOK, tplForks)
}
diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go
index 3df6051975..2d5bddd939 100644
--- a/routers/web/repo/view_file.go
+++ b/routers/web/repo/view_file.go
@@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
issue_service "code.gitea.io/gitea/services/issue"
@@ -40,7 +41,128 @@ func prepareLatestCommitInfo(ctx *context.Context) bool {
return loadLatestCommitData(ctx, commit)
}
-func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
+func prepareFileViewLfsAttrs(ctx *context.Context) (*attribute.Attributes, bool) {
+ attrsMap, err := attribute.CheckAttributes(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, attribute.CheckAttributeOpts{
+ Filenames: []string{ctx.Repo.TreePath},
+ Attributes: []string{attribute.LinguistGenerated, attribute.LinguistVendored, attribute.LinguistLanguage, attribute.GitlabLanguage},
+ })
+ if err != nil {
+ ctx.ServerError("attribute.CheckAttributes", err)
+ return nil, false
+ }
+ attrs := attrsMap[ctx.Repo.TreePath]
+ if attrs == nil {
+ // this case shouldn't happen, just in case.
+ setting.PanicInDevOrTesting("no attributes found for %s", ctx.Repo.TreePath)
+ attrs = attribute.NewAttributes()
+ }
+ ctx.Data["IsVendored"], ctx.Data["IsGenerated"] = attrs.GetVendored().Value(), attrs.GetGenerated().Value()
+ return attrs, true
+}
+
+func handleFileViewRenderMarkup(ctx *context.Context, filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte, utf8Reader io.Reader) bool {
+ markupType := markup.DetectMarkupTypeByFileName(filename)
+ if markupType == "" {
+ markupType = markup.DetectRendererType(filename, sniffedType, prefetchBuf)
+ }
+ if markupType == "" {
+ return false
+ }
+
+ ctx.Data["HasSourceRenderedToggle"] = true
+
+ if ctx.FormString("display") == "source" {
+ return false
+ }
+
+ ctx.Data["MarkupType"] = markupType
+ metas := ctx.Repo.Repository.ComposeRepoFileMetas(ctx)
+ metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
+ rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
+ CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
+ CurrentTreePath: path.Dir(ctx.Repo.TreePath),
+ }).
+ WithMarkupType(markupType).
+ WithRelativePath(ctx.Repo.TreePath).
+ WithMetas(metas)
+
+ var err error
+ ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, utf8Reader)
+ if err != nil {
+ ctx.ServerError("Render", err)
+ return true
+ }
+ // to prevent iframe from loading third-party url
+ ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
+ return true
+}
+
+func handleFileViewRenderSource(ctx *context.Context, filename string, attrs *attribute.Attributes, fInfo *fileInfo, utf8Reader io.Reader) bool {
+ if ctx.FormString("display") == "rendered" || !fInfo.st.IsRepresentableAsText() {
+ return false
+ }
+
+ if !fInfo.st.IsText() {
+ if ctx.FormString("display") == "" {
+ // not text but representable as text, e.g. SVG
+ // since there is no "display" is specified, let other renders to handle
+ return false
+ }
+ ctx.Data["HasSourceRenderedToggle"] = true
+ }
+
+ buf, _ := io.ReadAll(utf8Reader)
+ // The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
+ // empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line;
+ // Gitea uses the definition (like most modern editors):
+ // empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines;
+ // When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL.
+ // To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines.
+ // This NumLines is only used for the display on the UI: "xxx lines"
+ if len(buf) == 0 {
+ ctx.Data["NumLines"] = 0
+ } else {
+ ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
+ }
+
+ language := attrs.GetLanguage().Value()
+ fileContent, lexerName, err := highlight.File(filename, language, buf)
+ ctx.Data["LexerName"] = lexerName
+ if err != nil {
+ log.Error("highlight.File failed, fallback to plain text: %v", err)
+ fileContent = highlight.PlainText(buf)
+ }
+ status := &charset.EscapeStatus{}
+ statuses := make([]*charset.EscapeStatus, len(fileContent))
+ for i, line := range fileContent {
+ statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
+ status = status.Or(statuses[i])
+ }
+ ctx.Data["EscapeStatus"] = status
+ ctx.Data["FileContent"] = fileContent
+ ctx.Data["LineEscapeStatus"] = statuses
+ return true
+}
+
+func handleFileViewRenderImage(ctx *context.Context, fInfo *fileInfo, prefetchBuf []byte) bool {
+ if !fInfo.st.IsImage() {
+ return false
+ }
+ if fInfo.st.IsSvgImage() && !setting.UI.SVG.Enabled {
+ return false
+ }
+ if fInfo.st.IsSvgImage() {
+ ctx.Data["HasSourceRenderedToggle"] = true
+ } else {
+ img, _, err := image.DecodeConfig(bytes.NewReader(prefetchBuf))
+ if err == nil { // ignore the error for the formats that are not supported by image.DecodeConfig
+ ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height)
+ }
+ }
+ return true
+}
+
+func prepareFileView(ctx *context.Context, entry *git.TreeEntry) {
ctx.Data["IsViewFile"] = true
ctx.Data["HideRepoInfo"] = true
@@ -86,11 +208,8 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
}
}
- isDisplayingSource := ctx.FormString("display") == "source"
- isDisplayingRendered := !isDisplayingSource
-
// Don't call any other repository functions depends on git.Repository until the dataRc closed to
- // avoid create unnecessary temporary cat file.
+ // avoid creating an unnecessary temporary cat file.
buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
if err != nil {
ctx.ServerError("getFileReader", err)
@@ -98,226 +217,94 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
}
defer dataRc.Close()
- if fInfo.isLFSFile {
+ if fInfo.isLFSFile() {
ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
}
- isRepresentableAsText := fInfo.st.IsRepresentableAsText()
- if !isRepresentableAsText {
- // If we can't show plain text, always try to render.
- isDisplayingSource = false
- isDisplayingRendered = true
+ if !prepareFileViewEditorButtons(ctx) {
+ return
}
- ctx.Data["IsLFSFile"] = fInfo.isLFSFile
+
+ ctx.Data["IsLFSFile"] = fInfo.isLFSFile()
ctx.Data["FileSize"] = fInfo.fileSize
- ctx.Data["IsTextFile"] = fInfo.isTextFile
- ctx.Data["IsRepresentableAsText"] = isRepresentableAsText
- ctx.Data["IsDisplayingSource"] = isDisplayingSource
- ctx.Data["IsDisplayingRendered"] = isDisplayingRendered
+ ctx.Data["IsRepresentableAsText"] = fInfo.st.IsRepresentableAsText()
ctx.Data["IsExecutable"] = entry.IsExecutable()
+ ctx.Data["CanCopyContent"] = fInfo.st.IsRepresentableAsText() || fInfo.st.IsImage()
- isTextSource := fInfo.isTextFile || isDisplayingSource
- ctx.Data["IsTextSource"] = isTextSource
- if isTextSource {
- ctx.Data["CanCopyContent"] = true
- }
-
- // Check LFS Lock
- lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
- ctx.Data["LFSLock"] = lfsLock
- if err != nil {
- ctx.ServerError("GetTreePathLock", err)
+ attrs, ok := prepareFileViewLfsAttrs(ctx)
+ if !ok {
return
}
- if lfsLock != nil {
- u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
- if err != nil {
- ctx.ServerError("GetTreePathLock", err)
- return
- }
- ctx.Data["LFSLockOwner"] = u.Name
- ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink()
- ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
- }
- // Assume file is not editable first.
- if fInfo.isLFSFile {
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
- } else if !isRepresentableAsText {
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
- }
-
- // read all needed attributes which will be used later
- // there should be no performance different between reading 2 or 4 here
- attrsMap, err := attribute.CheckAttributes(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, attribute.CheckAttributeOpts{
- Filenames: []string{ctx.Repo.TreePath},
- Attributes: []string{attribute.LinguistGenerated, attribute.LinguistVendored, attribute.LinguistLanguage, attribute.GitlabLanguage},
- })
- if err != nil {
- ctx.ServerError("attribute.CheckAttributes", err)
- return
- }
- attrs := attrsMap[ctx.Repo.TreePath]
- if attrs == nil {
- // this case shouldn't happen, just in case.
- setting.PanicInDevOrTesting("no attributes found for %s", ctx.Repo.TreePath)
- attrs = attribute.NewAttributes()
- }
+ // TODO: in the future maybe we need more accurate flags, for example:
+ // * IsRepresentableAsText: some files are text, some are not
+ // * IsRenderableXxx: some files are rendered by backend "markup" engine, some are rendered by frontend (pdf, 3d)
+ // * DefaultViewMode: when there is no "display" query parameter, which view mode should be used by default, source or rendered
+ utf8Reader := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
switch {
- case isRepresentableAsText:
- if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
- ctx.Data["IsFileTooLarge"] = true
- break
- }
-
- if fInfo.st.IsSvgImage() {
- ctx.Data["IsImageFile"] = true
- ctx.Data["CanCopyContent"] = true
- ctx.Data["HasSourceRenderedToggle"] = true
- }
-
- rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
-
- shouldRenderSource := ctx.FormString("display") == "source"
- readmeExist := util.IsReadmeFileName(blob.Name())
- ctx.Data["ReadmeExist"] = readmeExist
-
- markupType := markup.DetectMarkupTypeByFileName(blob.Name())
- if markupType == "" {
- markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf))
- }
- if markupType != "" {
- ctx.Data["HasSourceRenderedToggle"] = true
- }
- if markupType != "" && !shouldRenderSource {
- ctx.Data["IsMarkup"] = true
- ctx.Data["MarkupType"] = markupType
- metas := ctx.Repo.Repository.ComposeRepoFileMetas(ctx)
- metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
- rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
- CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
- CurrentTreePath: path.Dir(ctx.Repo.TreePath),
- }).
- WithMarkupType(markupType).
- WithRelativePath(ctx.Repo.TreePath).
- WithMetas(metas)
-
- ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
- if err != nil {
- ctx.ServerError("Render", err)
- return
- }
- // to prevent iframe load third-party url
- ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
- } else {
- buf, _ := io.ReadAll(rd)
-
- // The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
- // empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line;
- // Gitea uses the definition (like most modern editors):
- // empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines;
- // When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL.
- // To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines.
- // This NumLines is only used for the display on the UI: "xxx lines"
- if len(buf) == 0 {
- ctx.Data["NumLines"] = 0
- } else {
- ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
- }
-
- language := attrs.GetLanguage().Value()
- fileContent, lexerName, err := highlight.File(blob.Name(), language, buf)
- ctx.Data["LexerName"] = lexerName
- if err != nil {
- log.Error("highlight.File failed, fallback to plain text: %v", err)
- fileContent = highlight.PlainText(buf)
- }
- status := &charset.EscapeStatus{}
- statuses := make([]*charset.EscapeStatus, len(fileContent))
- for i, line := range fileContent {
- statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
- status = status.Or(statuses[i])
- }
- ctx.Data["EscapeStatus"] = status
- ctx.Data["FileContent"] = fileContent
- ctx.Data["LineEscapeStatus"] = statuses
- }
- if !fInfo.isLFSFile {
- if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
- if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
- ctx.Data["CanEditFile"] = false
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
- } else {
- ctx.Data["CanEditFile"] = true
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
- }
- } else if !ctx.Repo.RefFullName.IsBranch() {
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
- } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
- }
- }
-
- case fInfo.st.IsPDF():
- ctx.Data["IsPDFFile"] = true
+ case fInfo.fileSize >= setting.UI.MaxDisplayFileSize:
+ ctx.Data["IsFileTooLarge"] = true
+ case handleFileViewRenderMarkup(ctx, entry.Name(), fInfo.st, buf, utf8Reader):
+ // it also sets ctx.Data["FileContent"] and more
+ ctx.Data["IsMarkup"] = true
+ case handleFileViewRenderSource(ctx, entry.Name(), attrs, fInfo, utf8Reader):
+ // it also sets ctx.Data["FileContent"] and more
+ ctx.Data["IsDisplayingSource"] = true
+ case handleFileViewRenderImage(ctx, fInfo, buf):
+ ctx.Data["IsImageFile"] = true
case fInfo.st.IsVideo():
ctx.Data["IsVideoFile"] = true
case fInfo.st.IsAudio():
ctx.Data["IsAudioFile"] = true
- case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()):
- ctx.Data["IsImageFile"] = true
- ctx.Data["CanCopyContent"] = true
default:
- if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
- ctx.Data["IsFileTooLarge"] = true
- break
- }
+ // unable to render anything, show the "view raw" or let frontend handle it
+ }
+}
- // TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
- // It is used by "external renders", markupRender will execute external programs to get rendered content.
- if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType != "" {
- rd := io.MultiReader(bytes.NewReader(buf), dataRc)
- ctx.Data["IsMarkup"] = true
- ctx.Data["MarkupType"] = markupType
-
- rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
- CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
- CurrentTreePath: path.Dir(ctx.Repo.TreePath),
- }).
- WithMarkupType(markupType).
- WithRelativePath(ctx.Repo.TreePath)
-
- ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
- if err != nil {
- ctx.ServerError("Render", err)
- return
- }
- }
+func prepareFileViewEditorButtons(ctx *context.Context) bool {
+ // archived or mirror repository, the buttons should not be shown
+ if !ctx.Repo.Repository.CanEnableEditor() {
+ return true
}
- ctx.Data["IsVendored"], ctx.Data["IsGenerated"] = attrs.GetVendored().Value(), attrs.GetGenerated().Value()
+ // The buttons should not be shown if it's not a branch
+ if !ctx.Repo.RefFullName.IsBranch() {
+ ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
+ ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
+ return true
+ }
- if fInfo.st.IsImage() && !fInfo.st.IsSvgImage() {
- img, _, err := image.DecodeConfig(bytes.NewReader(buf))
- if err == nil {
- // There are Image formats go can't decode
- // Instead of throwing an error in that case, we show the size only when we can decode
- ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height)
- }
+ if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
+ ctx.Data["CanEditFile"] = true
+ ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
+ ctx.Data["CanDeleteFile"] = true
+ ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
+ return true
}
- if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
- if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
- ctx.Data["CanDeleteFile"] = false
- ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
- } else {
- ctx.Data["CanDeleteFile"] = true
- ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file")
+ lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
+ ctx.Data["LFSLock"] = lfsLock
+ if err != nil {
+ ctx.ServerError("GetTreePathLock", err)
+ return false
+ }
+ if lfsLock != nil {
+ u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
+ if err != nil {
+ ctx.ServerError("GetTreePathLock", err)
+ return false
}
- } else if !ctx.Repo.RefFullName.IsBranch() {
- ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
- } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
- ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
+ ctx.Data["LFSLockOwner"] = u.Name
+ ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink()
+ ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
}
+
+ // it's a lfs file and the user is not the owner of the lock
+ isLFSLocked := lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID
+ ctx.Data["CanEditFile"] = !isLFSLocked
+ ctx.Data["EditFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.edit_this_file"))
+ ctx.Data["CanDeleteFile"] = !isLFSLocked
+ ctx.Data["DeleteFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.delete_this_file"))
+ return true
}
diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go
index 3b053821ee..8ed9179290 100644
--- a/routers/web/repo/view_home.go
+++ b/routers/web/repo/view_home.go
@@ -21,6 +21,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
giturl "code.gitea.io/gitea/modules/git/url"
+ "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
@@ -261,6 +262,10 @@ func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status re
func handleRepoEmptyOrBroken(ctx *context.Context) {
showEmpty := true
+ if ctx.Repo.GitRepo == nil {
+ // in case the repo really exists and works, but the status was incorrectly marked as "broken", we need to open and check it again
+ ctx.Repo.GitRepo, _ = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
+ }
if ctx.Repo.GitRepo != nil {
reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty()
if err != nil {
@@ -334,7 +339,7 @@ func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) {
if entry.IsDir() {
prepareToRenderDirectory(ctx)
} else {
- prepareToRenderFile(ctx, entry)
+ prepareFileView(ctx, entry)
}
}
}
@@ -396,10 +401,8 @@ func Home(ctx *context.Context) {
return
}
- prepareHomeTreeSideBarSwitch(ctx)
-
title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
- if len(ctx.Repo.Repository.Description) > 0 {
+ if ctx.Repo.Repository.Description != "" {
title += ": " + ctx.Repo.Repository.Description
}
ctx.Data["Title"] = title
@@ -412,6 +415,8 @@ func Home(ctx *context.Context) {
return
}
+ prepareHomeTreeSideBarSwitch(ctx)
+
// get the current git entry which doer user is currently looking at.
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go
index 459cf0a616..a34de06e8e 100644
--- a/routers/web/repo/view_readme.go
+++ b/routers/web/repo/view_readme.go
@@ -150,7 +150,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
}
ctx.Data["RawFileLink"] = ""
- ctx.Data["ReadmeInList"] = true
+ ctx.Data["ReadmeInList"] = path.Join(subfolder, readmeFile.Name()) // the relative path to the readme file to the current tree path
ctx.Data["ReadmeExist"] = true
ctx.Data["FileIsSymlink"] = readmeFile.IsLink()
@@ -161,24 +161,23 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
}
defer dataRc.Close()
- ctx.Data["FileIsText"] = fInfo.isTextFile
- ctx.Data["FileTreePath"] = path.Join(subfolder, readmeFile.Name())
+ ctx.Data["FileIsText"] = fInfo.st.IsText()
+ ctx.Data["FileTreePath"] = path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())
ctx.Data["FileSize"] = fInfo.fileSize
- ctx.Data["IsLFSFile"] = fInfo.isLFSFile
+ ctx.Data["IsLFSFile"] = fInfo.isLFSFile()
- if fInfo.isLFSFile {
+ if fInfo.isLFSFile() {
filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name()))
ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64))
}
- if !fInfo.isTextFile {
+ if !fInfo.st.IsText() {
return
}
if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
// Pretend that this is a normal text file to display 'This file is too large to be shown'
ctx.Data["IsFileTooLarge"] = true
- ctx.Data["IsTextFile"] = true
return
}
@@ -212,7 +211,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale)
}
- if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
+ if !fInfo.isLFSFile() && ctx.Repo.Repository.CanEnableEditor() {
ctx.Data["CanEditReadmeFile"] = true
}
}
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index d70760bc36..69858c9692 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -7,6 +7,7 @@ package repo
import (
"bytes"
gocontext "context"
+ "html/template"
"io"
"net/http"
"net/url"
@@ -61,9 +62,9 @@ func MustEnableWiki(ctx *context.Context) {
return
}
- unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki)
+ repoUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki)
if err == nil {
- ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL)
+ ctx.Redirect(repoUnit.ExternalWikiConfig().ExternalWikiURL)
return
}
}
@@ -95,7 +96,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error)
}
func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) {
- wikiGitRepo, errGitRepo := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
+ wikiGitRepo, errGitRepo := gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository.WikiStorageRepo())
if errGitRepo != nil {
ctx.ServerError("OpenRepository", errGitRepo)
return nil, nil, errGitRepo
@@ -109,7 +110,7 @@ func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, err
return wikiGitRepo, nil, errBranch
}
// update the default branch in the database
- errDb := repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, DefaultWikiBranch: gitRepoDefaultBranch}, "default_wiki_branch")
+ errDb := repo_model.UpdateRepositoryColsNoAutoTime(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, DefaultWikiBranch: gitRepoDefaultBranch}, "default_wiki_branch")
if errDb != nil {
return wikiGitRepo, nil, errDb
}
@@ -178,23 +179,17 @@ func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName wiki_
}
func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
- wikiRepo, commit, err := findWikiRepoCommit(ctx)
+ wikiGitRepo, commit, err := findWikiRepoCommit(ctx)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
if !git.IsErrNotExist(err) {
ctx.ServerError("GetBranchCommit", err)
}
return nil, nil
}
- // Get page list.
+ // get the wiki pages list.
entries, err := commit.ListEntries()
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("ListEntries", err)
return nil, nil
}
@@ -208,9 +203,6 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
if repo_model.IsErrWikiInvalidFileName(err) {
continue
}
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("WikiFilenameToName", err)
return nil, nil
} else if wikiName == "_Sidebar" || wikiName == "_Footer" {
@@ -249,58 +241,26 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
ctx.Redirect(util.URLJoin(ctx.Repo.RepoLink, "wiki/raw", string(pageName)))
}
if entry == nil || ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
return nil, nil
}
- // get filecontent
+ // get page content
data := wikiContentsByEntry(ctx, entry)
if ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
return nil, nil
}
- var sidebarContent []byte
- if !isSideBar {
- sidebarContent, _, _, _ = wikiContentsByName(ctx, commit, "_Sidebar")
- if ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- return nil, nil
- }
- } else {
- sidebarContent = data
- }
-
- var footerContent []byte
- if !isFooter {
- footerContent, _, _, _ = wikiContentsByName(ctx, commit, "_Footer")
- if ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- return nil, nil
- }
- } else {
- footerContent = data
- }
-
rctx := renderhelper.NewRenderContextRepoWiki(ctx, ctx.Repo.Repository)
- buf := &strings.Builder{}
- renderFn := func(data []byte) (escaped *charset.EscapeStatus, output string, err error) {
+ renderFn := func(data []byte) (escaped *charset.EscapeStatus, output template.HTML, err error) {
+ buf := &strings.Builder{}
markupRd, markupWr := io.Pipe()
defer markupWr.Close()
done := make(chan struct{})
go func() {
// We allow NBSP here this is rendered
escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.RuneNBSP)
- output = buf.String()
+ output = template.HTML(buf.String())
buf.Reset()
close(done)
}()
@@ -311,75 +271,61 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
return escaped, output, err
}
- ctx.Data["EscapeStatus"], ctx.Data["content"], err = renderFn(data)
+ ctx.Data["EscapeStatus"], ctx.Data["WikiContentHTML"], err = renderFn(data)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("Render", err)
return nil, nil
}
if rctx.SidebarTocNode != nil {
- sb := &strings.Builder{}
- err = markdown.SpecializedMarkdown(rctx).Renderer().Render(sb, nil, rctx.SidebarTocNode)
- if err != nil {
+ sb := strings.Builder{}
+ if err = markdown.SpecializedMarkdown(rctx).Renderer().Render(&sb, nil, rctx.SidebarTocNode); err != nil {
log.Error("Failed to render wiki sidebar TOC: %v", err)
- } else {
- ctx.Data["sidebarTocContent"] = sb.String()
}
+ ctx.Data["WikiSidebarTocHTML"] = templates.SanitizeHTML(sb.String())
}
if !isSideBar {
- buf.Reset()
- ctx.Data["sidebarEscapeStatus"], ctx.Data["sidebarContent"], err = renderFn(sidebarContent)
+ sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar")
+ if ctx.Written() {
+ return nil, nil
+ }
+ ctx.Data["WikiSidebarEscapeStatus"], ctx.Data["WikiSidebarHTML"], err = renderFn(sidebarContent)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("Render", err)
return nil, nil
}
- ctx.Data["sidebarPresent"] = sidebarContent != nil
- } else {
- ctx.Data["sidebarPresent"] = false
}
if !isFooter {
- buf.Reset()
- ctx.Data["footerEscapeStatus"], ctx.Data["footerContent"], err = renderFn(footerContent)
+ footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer")
+ if ctx.Written() {
+ return nil, nil
+ }
+ ctx.Data["WikiFooterEscapeStatus"], ctx.Data["WikiFooterHTML"], err = renderFn(footerContent)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("Render", err)
return nil, nil
}
- ctx.Data["footerPresent"] = footerContent != nil
- } else {
- ctx.Data["footerPresent"] = false
}
// get commit count - wiki revisions
- commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
+ commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
ctx.Data["CommitCount"] = commitsCount
- return wikiRepo, entry
+ return wikiGitRepo, entry
}
func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
- wikiRepo, commit, err := findWikiRepoCommit(ctx)
+ wikiGitRepo, commit, err := findWikiRepoCommit(ctx)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
if !git.IsErrNotExist(err) {
ctx.ServerError("GetBranchCommit", err)
}
return nil, nil
}
- // get requested pagename
+ // get requested page name
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*"))
if len(pageName) == 0 {
pageName = "Home"
@@ -394,53 +340,35 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
- // lookup filename in wiki - get filecontent, gitTree entry , real filename
- data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
+ // lookup filename in wiki - get page content, gitTree entry , real filename
+ _, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
if noEntry {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages")
}
if entry == nil || ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
return nil, nil
}
- ctx.Data["content"] = string(data)
- ctx.Data["sidebarPresent"] = false
- ctx.Data["sidebarContent"] = ""
- ctx.Data["footerPresent"] = false
- ctx.Data["footerContent"] = ""
-
// get commit count - wiki revisions
- commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
+ commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
ctx.Data["CommitCount"] = commitsCount
// get page
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
// get Commit Count
- commitsHistory, err := wikiRepo.CommitsByFileAndRange(
+ commitsHistory, err := wikiGitRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
Revision: ctx.Repo.Repository.DefaultWikiBranch,
File: pageFilename,
Page: page,
})
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("CommitsByFileAndRange", err)
return nil, nil
}
ctx.Data["Commits"], err = git_service.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("ConvertFromGitCommit", err)
return nil, nil
}
@@ -449,16 +377,11 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
- return wikiRepo, entry
+ return wikiGitRepo, entry
}
func renderEditPage(ctx *context.Context) {
- wikiRepo, commit, err := findWikiRepoCommit(ctx)
- defer func() {
- if wikiRepo != nil {
- _ = wikiRepo.Close()
- }
- }()
+ _, commit, err := findWikiRepoCommit(ctx)
if err != nil {
if !git.IsErrNotExist(err) {
ctx.ServerError("GetBranchCommit", err)
@@ -466,7 +389,7 @@ func renderEditPage(ctx *context.Context) {
return
}
- // get requested pagename
+ // get requested page name
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*"))
if len(pageName) == 0 {
pageName = "Home"
@@ -490,17 +413,13 @@ func renderEditPage(ctx *context.Context) {
return
}
- // get filecontent
+ // get wiki page content
data := wikiContentsByEntry(ctx, entry)
if ctx.Written() {
return
}
- ctx.Data["content"] = string(data)
- ctx.Data["sidebarPresent"] = false
- ctx.Data["sidebarContent"] = ""
- ctx.Data["footerPresent"] = false
- ctx.Data["footerContent"] = ""
+ ctx.Data["WikiEditContent"] = string(data)
}
// WikiPost renders post of wiki page
@@ -562,12 +481,7 @@ func Wiki(ctx *context.Context) {
return
}
- wikiRepo, entry := renderViewPage(ctx)
- defer func() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- }()
+ wikiGitRepo, entry := renderViewPage(ctx)
if ctx.Written() {
return
}
@@ -583,7 +497,7 @@ func Wiki(ctx *context.Context) {
ctx.Data["FormatWarning"] = ext + " rendering is not supported at the moment. Rendered as Markdown."
}
// Get last change information.
- lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
+ lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath)
if err != nil {
ctx.ServerError("GetCommitByPath", err)
return
@@ -603,13 +517,7 @@ func WikiRevision(ctx *context.Context) {
return
}
- wikiRepo, entry := renderRevisionPage(ctx)
- defer func() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- }()
-
+ wikiGitRepo, entry := renderRevisionPage(ctx)
if ctx.Written() {
return
}
@@ -621,7 +529,7 @@ func WikiRevision(ctx *context.Context) {
// Get last change information.
wikiPath := entry.Name()
- lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
+ lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath)
if err != nil {
ctx.ServerError("GetCommitByPath", err)
return
@@ -641,12 +549,7 @@ func WikiPages(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.wiki.pages")
ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
- wikiRepo, commit, err := findWikiRepoCommit(ctx)
- defer func() {
- if wikiRepo != nil {
- _ = wikiRepo.Close()
- }
- }()
+ _, commit, err := findWikiRepoCommit(ctx)
if err != nil {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
return
@@ -700,13 +603,7 @@ func WikiPages(ctx *context.Context) {
// WikiRaw outputs raw blob requested by user (image for example)
func WikiRaw(ctx *context.Context) {
- wikiRepo, commit, err := findWikiRepoCommit(ctx)
- defer func() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- }()
-
+ _, commit, err := findWikiRepoCommit(ctx)
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound(nil)
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index b5dfa9f856..59bf6ed79b 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -164,7 +164,7 @@ func TestEditWiki(t *testing.T) {
EditWiki(ctx)
assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, "Home", ctx.Data["Title"])
- assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"])
+ assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["WikiEditContent"])
ctx, _ = contexttest.MockContext(t, "user2/repo1/wiki/jpeg.jpg?action=_edit")
ctx.SetPathParam("*", "jpeg.jpg")
@@ -245,7 +245,12 @@ func TestDefaultWikiBranch(t *testing.T) {
assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repoWithNoWiki, "main"))
// repo with wiki
- assert.NoError(t, repo_model.UpdateRepositoryCols(db.DefaultContext, &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"}))
+ assert.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(
+ db.DefaultContext,
+ &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"},
+ "default_wiki_branch",
+ ),
+ )
ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki")
ctx.SetPathParam("*", "Home")
diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go
index a642cfd66d..648f8046a4 100644
--- a/routers/web/shared/actions/runners.go
+++ b/routers/web/shared/actions/runners.go
@@ -108,10 +108,7 @@ func Runners(ctx *context.Context) {
return
}
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
opts := actions_model.FindRunnerOptions{
ListOptions: db.ListOptions{
@@ -179,10 +176,7 @@ func RunnersEdit(ctx *context.Context) {
return
}
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
runnerID := ctx.PathParamInt64("runnerid")
ownerID := rCtx.OwnerID
diff --git a/routers/web/shared/label/label.go b/routers/web/shared/label/label.go
new file mode 100644
index 0000000000..6968a318c4
--- /dev/null
+++ b/routers/web/shared/label/label.go
@@ -0,0 +1,26 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package label
+
+import (
+ "code.gitea.io/gitea/modules/label"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+)
+
+func GetLabelEditForm(ctx *context.Context) *forms.CreateLabelForm {
+ form := web.GetForm(ctx).(*forms.CreateLabelForm)
+ if ctx.HasError() {
+ ctx.JSONError(ctx.Data["ErrorMsg"].(string))
+ return nil
+ }
+ var err error
+ form.Color, err = label.NormalizeColor(form.Color)
+ if err != nil {
+ ctx.JSONError(ctx.Tr("repo.issues.label_color_invalid"))
+ return nil
+ }
+ return form
+}
diff --git a/routers/web/shared/packages/packages.go b/routers/web/shared/packages/packages.go
index 3d1795b42c..a18dedf89c 100644
--- a/routers/web/shared/packages/packages.go
+++ b/routers/web/shared/packages/packages.go
@@ -8,7 +8,6 @@ import (
"net/http"
"time"
- "code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
@@ -159,12 +158,18 @@ func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) {
PackageID: p.ID,
IsInternal: optional.Some(false),
Sort: packages_model.SortCreatedDesc,
- Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200),
})
if err != nil {
ctx.ServerError("SearchVersions", err)
return
}
+ if pcr.KeepCount > 0 {
+ if pcr.KeepCount < len(pvs) {
+ pvs = pvs[pcr.KeepCount:]
+ } else {
+ pvs = nil
+ }
+ }
for _, pv := range pvs {
if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil {
ctx.ServerError("ShouldBeSkipped", err)
@@ -177,7 +182,6 @@ func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) {
if pcr.MatchFullName {
toMatch = p.LowerName + "/" + pv.LowerVersion
}
-
if pcr.KeepPatternMatcher != nil && pcr.KeepPatternMatcher.MatchString(toMatch) {
continue
}
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index 48a5d58ea4..2bd0abc4c0 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -47,13 +47,12 @@ func prepareContextForProfileBigAvatar(ctx *context.Context) {
ctx.Data["RenderedDescription"] = content
}
- showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
orgs, err := db.Find[organization.Organization](ctx, organization.FindOrgOptions{
- UserID: ctx.ContextUser.ID,
- IncludePrivate: showPrivate,
+ UserID: ctx.ContextUser.ID,
+ IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, ctx.ContextUser),
ListOptions: db.ListOptions{
Page: 1,
- // query one more results (without a separate counting) to see whether we need to add the "show more orgs" link
+ // query one more result (without a separate counting) to see whether we need to add the "show more orgs" link
PageSize: setting.UI.User.OrgPagingNum + 1,
},
})
@@ -165,7 +164,7 @@ func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult
}
func loadHeaderCount(ctx *context.Context) error {
- repoCount, err := repo_model.CountRepository(ctx, &repo_model.SearchRepoOptions{
+ repoCount, err := repo_model.CountRepository(ctx, repo_model.SearchRepoOptions{
Actor: ctx.Doer,
OwnerID: ctx.ContextUser.ID,
Private: ctx.IsSigned,
diff --git a/routers/web/user/code.go b/routers/web/user/code.go
index f2153c6d54..11579c40a6 100644
--- a/routers/web/user/code.go
+++ b/routers/web/user/code.go
@@ -5,6 +5,7 @@ package user
import (
"net/http"
+ "slices"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
@@ -86,14 +87,7 @@ func CodeSearch(ctx *context.Context) {
loadRepoIDs := make([]int64, 0, len(searchResults))
for _, result := range searchResults {
- var find bool
- for _, id := range loadRepoIDs {
- if id == result.RepoID {
- find = true
- break
- }
- }
- if !find {
+ if !slices.Contains(loadRepoIDs, result.RepoID) {
loadRepoIDs = append(loadRepoIDs, result.RepoID)
}
}
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index 77f9cb8cca..b53a3daedb 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -176,7 +176,7 @@ func Milestones(ctx *context.Context) {
}
var (
- userRepoCond = repo_model.SearchRepositoryCondition(&repoOpts) // all repo condition user could visit
+ userRepoCond = repo_model.SearchRepositoryCondition(repoOpts) // all repo condition user could visit
repoCond = userRepoCond
repoIDs []int64
@@ -197,7 +197,7 @@ func Milestones(ctx *context.Context) {
reposQuery = reposQuery[1 : len(reposQuery)-1]
// for each ID (delimiter ",") add to int to repoIDs
- for _, rID := range strings.Split(reposQuery, ",") {
+ for rID := range strings.SplitSeq(reposQuery, ",") {
// Ensure nonempty string entries
if rID != "" && rID != "0" {
rIDint64, err := strconv.ParseInt(rID, 10, 64)
@@ -242,7 +242,7 @@ func Milestones(ctx *context.Context) {
return
}
- showRepos, _, err := repo_model.SearchRepositoryByCondition(ctx, &repoOpts, userRepoCond, false)
+ showRepos, _, err := repo_model.SearchRepositoryByCondition(ctx, repoOpts, userRepoCond, false)
if err != nil {
ctx.ServerError("SearchRepositoryByCondition", err)
return
@@ -461,7 +461,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// As team:
// - Team org's owns the repository.
// - Team has read permission to repository.
- repoOpts := &repo_model.SearchRepoOptions{
+ repoOpts := repo_model.SearchRepoOptions{
Actor: ctx.Doer,
OwnerID: ctxUser.ID,
Private: true,
@@ -520,10 +520,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
opts.IsClosed = optional.Some(isShowClosed)
// Make sure page number is at least 1. Will be posted to ctx.Data.
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
opts.Paginator = &db.ListOptions{
Page: page,
PageSize: setting.UI.IssuePagingNum,
diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go
index 2a8a8812e3..68ad79b11e 100644
--- a/routers/web/user/home_test.go
+++ b/routers/web/user/home_test.go
@@ -28,7 +28,7 @@ func TestArchivedIssues(t *testing.T) {
ctx.Req.Form.Set("state", "open")
// Assume: User 30 has access to two Repos with Issues, one of the Repos being archived.
- repos, _, _ := repo_model.GetUserRepositories(db.DefaultContext, &repo_model.SearchRepoOptions{Actor: ctx.Doer})
+ repos, _, _ := repo_model.GetUserRepositories(db.DefaultContext, repo_model.SearchRepoOptions{Actor: ctx.Doer})
assert.Len(t, repos, 3)
IsArchived := make(map[int64]bool)
NumIssues := make(map[int64]int)
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index 89f3c6956f..610a9b8076 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -203,10 +203,7 @@ func NotificationPurgePost(ctx *context.Context) {
// NotificationSubscriptions returns the list of subscribed issues
func NotificationSubscriptions(ctx *context.Context) {
- page := ctx.FormInt("page")
- if page < 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
sortType := ctx.FormString("sort")
ctx.Data["SortType"] = sortType
@@ -287,16 +284,8 @@ func NotificationSubscriptions(ctx *context.Context) {
ctx.Data["CommitLastStatus"] = lastStatus
ctx.Data["CommitStatuses"] = commitStatuses
ctx.Data["Issues"] = issues
-
ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, "")
- commitStatus, err := pull_service.GetIssuesLastCommitStatus(ctx, issues)
- if err != nil {
- ctx.ServerError("GetIssuesLastCommitStatus", err)
- return
- }
- ctx.Data["CommitStatus"] = commitStatus
-
approvalCounts, err := issues.GetApprovalCounts(ctx)
if err != nil {
ctx.ServerError("ApprovalCounts", err)
@@ -339,10 +328,7 @@ func NotificationSubscriptions(ctx *context.Context) {
// NotificationWatching returns the list of watching repos
func NotificationWatching(ctx *context.Context) {
- page := ctx.FormInt("page")
- if page < 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
keyword := ctx.FormTrim("q")
ctx.Data["Keyword"] = keyword
@@ -390,7 +376,7 @@ func NotificationWatching(ctx *context.Context) {
private := ctx.FormOptionalBool("private")
ctx.Data["IsPrivate"] = private
- repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index e96f5a04ea..216acdf927 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -4,6 +4,8 @@
package user
import (
+ gocontext "context"
+ "errors"
"net/http"
"net/url"
@@ -20,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/optional"
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
arch_module "code.gitea.io/gitea/modules/packages/arch"
+ container_module "code.gitea.io/gitea/modules/packages/container"
debian_module "code.gitea.io/gitea/modules/packages/debian"
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
"code.gitea.io/gitea/modules/setting"
@@ -31,6 +34,7 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
packages_service "code.gitea.io/gitea/services/packages"
+ container_service "code.gitea.io/gitea/services/packages/container"
)
const (
@@ -46,10 +50,7 @@ func ListPackages(ctx *context.Context) {
ctx.ServerError("RenderUserOrgHeader", err)
return
}
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
query := ctx.FormTrim("q")
packageType := ctx.FormTrim("type")
@@ -165,6 +166,24 @@ func RedirectToLastVersion(ctx *context.Context) {
ctx.Redirect(pd.VersionWebLink())
}
+func viewPackageContainerImage(ctx gocontext.Context, pd *packages_model.PackageDescriptor, digest string) (*container_module.Metadata, error) {
+ manifestBlob, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
+ OwnerID: pd.Owner.ID,
+ Image: pd.Package.LowerName,
+ Digest: digest,
+ })
+ if err != nil {
+ return nil, err
+ }
+ manifestReader, err := packages_service.OpenBlobStream(manifestBlob.Blob)
+ if err != nil {
+ return nil, err
+ }
+ defer manifestReader.Close()
+ _, _, metadata, err := container_service.ParseManifestMetadata(ctx, manifestReader, pd.Owner.ID, pd.Package.LowerName)
+ return metadata, err
+}
+
// ViewPackageVersion displays a single package version
func ViewPackageVersion(ctx *context.Context) {
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
@@ -172,6 +191,7 @@ func ViewPackageVersion(ctx *context.Context) {
return
}
+ versionSub := ctx.PathParam("version_sub")
pd := ctx.Package.Descriptor
ctx.Data["Title"] = pd.Package.Name
ctx.Data["IsPackagesPage"] = true
@@ -260,21 +280,30 @@ func ViewPackageVersion(ctx *context.Context) {
ctx.Data["Groups"] = util.Sorted(groups.Values())
ctx.Data["Architectures"] = util.Sorted(architectures.Values())
- }
-
- var (
- total int64
- pvs []*packages_model.PackageVersion
- )
- switch pd.Package.Type {
case packages_model.TypeContainer:
- pvs, total, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{
+ imageMetadata := pd.Metadata
+ if versionSub != "" {
+ imageMetadata, err = viewPackageContainerImage(ctx, pd, versionSub)
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.NotFound(nil)
+ return
+ } else if err != nil {
+ ctx.ServerError("viewPackageContainerImage", err)
+ return
+ }
+ }
+ ctx.Data["ContainerImageMetadata"] = imageMetadata
+ }
+ var pvs []*packages_model.PackageVersion
+ var pvsTotal int64
+ if pd.Package.Type == packages_model.TypeContainer {
+ pvs, pvsTotal, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{
Paginator: db.NewAbsoluteListOptions(0, 5),
PackageID: pd.Package.ID,
IsTagged: true,
})
- default:
- pvs, total, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
+ } else {
+ pvs, pvsTotal, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
Paginator: db.NewAbsoluteListOptions(0, 5),
PackageID: pd.Package.ID,
IsInternal: optional.Some(false),
@@ -284,9 +313,8 @@ func ViewPackageVersion(ctx *context.Context) {
ctx.ServerError("", err)
return
}
-
ctx.Data["LatestVersions"] = pvs
- ctx.Data["TotalVersionCount"] = total
+ ctx.Data["TotalVersionCount"] = pvsTotal
ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin()
@@ -320,10 +348,7 @@ func ListPackageVersions(ctx *context.Context) {
return
}
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
pagination := &db.ListOptions{
PageSize: setting.UI.PackagesPagingNum,
Page: page,
@@ -407,7 +432,7 @@ func PackageSettings(ctx *context.Context) {
ctx.Data["IsPackagesPage"] = true
ctx.Data["PackageDescriptor"] = pd
- repos, _, _ := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
+ repos, _, _ := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
Actor: pd.Owner,
Private: true,
})
@@ -488,9 +513,9 @@ func DownloadPackageFile(ctx *context.Context) {
return
}
- s, u, _, err := packages_service.GetPackageFileStream(ctx, pf)
+ s, u, _, err := packages_service.OpenFileForDownload(ctx, pf)
if err != nil {
- ctx.ServerError("GetPackageFileStream", err)
+ ctx.ServerError("OpenFileForDownload", err)
return
}
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index ee19665109..d7052914b6 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -63,21 +63,9 @@ func userProfile(ctx *context.Context) {
ctx.Data["Title"] = ctx.ContextUser.DisplayName()
ctx.Data["PageIsUserProfile"] = true
- // prepare heatmap data
- if setting.Service.EnableUserHeatmap {
- data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer)
- if err != nil {
- ctx.ServerError("GetUserHeatmapDataByUser", err)
- return
- }
- ctx.Data["HeatmapData"] = data
- ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
- }
-
profileDbRepo, profileReadmeBlob := shared_user.FindOwnerProfileReadme(ctx, ctx.Doer)
- showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
- prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileReadmeBlob)
+ prepareUserProfileTabData(ctx, profileDbRepo, profileReadmeBlob)
// prepare the user nav header data after "prepareUserProfileTabData" to avoid re-querying the NumFollowers & NumFollowing
// because ctx.Data["NumFollowers"] and "NumFollowing" logic duplicates in both of them
@@ -90,7 +78,7 @@ func userProfile(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplProfile)
}
-func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDbRepo *repo_model.Repository, profileReadme *git.Blob) {
+func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.Repository, profileReadme *git.Blob) {
// if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page
// if there is not a profile readme, the overview tab should be treated as the repositories tab
tab := ctx.FormString("tab")
@@ -173,8 +161,20 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
ctx.Data["Cards"] = following
total = int(numFollowing)
case "activity":
+ // prepare heatmap data
+ if setting.Service.EnableUserHeatmap {
+ data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer)
+ if err != nil {
+ ctx.ServerError("GetUserHeatmapDataByUser", err)
+ return
+ }
+ ctx.Data["HeatmapData"] = data
+ ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
+ }
+
date := ctx.FormString("date")
pagingNum = setting.UI.FeedPagingNum
+ showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
items, count, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{
RequestedUser: ctx.ContextUser,
Actor: ctx.Doer,
@@ -197,7 +197,8 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
total = int(count)
case "stars":
ctx.Data["PageIsProfileStarList"] = true
- repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ ctx.Data["ShowRepoOwnerOnList"] = true
+ repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: pagingNum,
Page: page,
@@ -224,7 +225,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
total = int(count)
case "watching":
- repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: pagingNum,
Page: page,
@@ -265,8 +266,8 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
}
case "organizations":
orgs, count, err := db.FindAndCount[organization.Organization](ctx, organization.FindOrgOptions{
- UserID: ctx.ContextUser.ID,
- IncludePrivate: showPrivate,
+ UserID: ctx.ContextUser.ID,
+ IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, ctx.ContextUser),
ListOptions: db.ListOptions{
Page: page,
PageSize: pagingNum,
@@ -279,7 +280,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
ctx.Data["Cards"] = orgs
total = int(count)
default: // default to "repositories"
- repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: pagingNum,
Page: page,
diff --git a/routers/web/user/search.go b/routers/web/user/search.go
index be5eee90a9..9acb9694d7 100644
--- a/routers/web/user/search.go
+++ b/routers/web/user/search.go
@@ -16,7 +16,7 @@ import (
// SearchCandidates searches candidate users for dropdown list
func SearchCandidates(ctx *context.Context) {
- users, _, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
+ users, _, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"),
Type: user_model.UserTypeIndividual,
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index 7577036a55..98995cd69c 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/typesniffer"
@@ -206,8 +207,8 @@ func Organization(ctx *context.Context) {
PageSize: setting.UI.Admin.UserPagingNum,
Page: ctx.FormInt("page"),
},
- UserID: ctx.Doer.ID,
- IncludePrivate: ctx.IsSigned,
+ UserID: ctx.Doer.ID,
+ IncludeVisibility: structs.VisibleTypePrivate,
}
if opts.Page <= 0 {
@@ -284,7 +285,7 @@ func Repos(ctx *context.Context) {
return
}
- userRepos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
+ userRepos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
Actor: ctxUser,
Private: true,
ListOptions: db.ListOptions{
@@ -309,7 +310,7 @@ func Repos(ctx *context.Context) {
ctx.Data["Dirs"] = repoNames
ctx.Data["ReposMap"] = repos
} else {
- repos, count64, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts})
+ repos, count64, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts})
if err != nil {
ctx.ServerError("GetUserRepositories", err)
return
diff --git a/routers/web/web.go b/routers/web/web.go
index bd850baec0..1039f9e739 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -964,7 +964,8 @@ func registerWebRoutes(m *web.Router) {
addSettingsVariablesRoutes()
}, actions.MustEnableActions)
- m.Methods("GET,POST", "/delete", org.SettingsDelete)
+ m.Post("/rename", web.Bind(forms.RenameOrgForm{}), org.SettingsRenamePost)
+ m.Post("/delete", org.SettingsDeleteOrgPost)
m.Group("/packages", func() {
m.Get("", org.Packages)
@@ -1012,6 +1013,7 @@ func registerWebRoutes(m *web.Router) {
m.Get("/versions", user.ListPackageVersions)
m.Group("/{version}", func() {
m.Get("", user.ViewPackageVersion)
+ m.Get("/{version_sub}", user.ViewPackageVersion)
m.Get("/files/{fileid}", user.DownloadPackageFile)
m.Group("/settings", func() {
m.Get("", user.PackageSettings)
@@ -1029,7 +1031,7 @@ func registerWebRoutes(m *web.Router) {
m.Get("", org.Projects)
m.Get("/{id}", org.ViewProject)
}, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true))
- m.Group("", func() { //nolint:dupl
+ m.Group("", func() { //nolint:dupl // duplicates lines 1421-1441
m.Get("/new", org.RenderNewProject)
m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
m.Group("/{id}", func() {
@@ -1251,7 +1253,8 @@ func registerWebRoutes(m *web.Router) {
m.Post("/add", web.Bind(forms.AddTimeManuallyForm{}), repo.AddTimeManually)
m.Post("/{timeid}/delete", repo.DeleteTime)
m.Group("/stopwatch", func() {
- m.Post("/toggle", repo.IssueStopwatch)
+ m.Post("/start", repo.IssueStartStopwatch)
+ m.Post("/stop", repo.IssueStopStopwatch)
m.Post("/cancel", repo.CancelStopwatch)
})
})
@@ -1311,26 +1314,38 @@ func registerWebRoutes(m *web.Router) {
}, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived())
// end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones
- m.Group("/{username}/{reponame}", func() { // repo code
+ m.Group("/{username}/{reponame}", func() { // repo code (at least "code reader")
m.Group("", func() {
m.Group("", func() {
- m.Post("/_preview/*", web.Bind(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost)
- m.Combo("/_edit/*").Get(repo.EditFile).
- Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
- m.Combo("/_new/*").Get(repo.NewFile).
- Post(web.Bind(forms.EditRepoFileForm{}), repo.NewFilePost)
- m.Combo("/_delete/*").Get(repo.DeleteFile).
- Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
- m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile).
- Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost)
- m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch).
- Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
- m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick).
- Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost)
- }, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData)
+ // "GET" requests only need "code reader" permission, "POST" requests need "code writer" permission.
+ // Because reader can "fork and edit"
+ canWriteToBranch := context.CanWriteToBranch()
+ m.Post("/_preview/*", repo.DiffPreviewPost) // read-only, fine with "code reader"
+ m.Post("/_fork/*", repo.ForkToEditPost) // read-only, fork to own repo, fine with "code reader"
+
+ // the path params are used in PrepareCommitFormOptions to construct the correct form action URL
+ m.Combo("/{editor_action:_edit}/*").
+ Get(repo.EditFile).
+ Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
+ m.Combo("/{editor_action:_new}/*").
+ Get(repo.EditFile).
+ Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
+ m.Combo("/{editor_action:_delete}/*").
+ Get(repo.DeleteFile).
+ Post(web.Bind(forms.DeleteRepoFileForm{}), canWriteToBranch, repo.DeleteFilePost)
+ m.Combo("/{editor_action:_upload}/*", repo.MustBeAbleToUpload).
+ Get(repo.UploadFile).
+ Post(web.Bind(forms.UploadRepoFileForm{}), canWriteToBranch, repo.UploadFilePost)
+ m.Combo("/{editor_action:_diffpatch}/*").
+ Get(repo.NewDiffPatch).
+ Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.NewDiffPatchPost)
+ m.Combo("/{editor_action:_cherrypick}/{sha:([a-f0-9]{7,64})}/*").
+ Get(repo.CherryPick).
+ Post(web.Bind(forms.CherryPickForm{}), canWriteToBranch, repo.CherryPickPost)
+ }, context.RepoRefByType(git.RefTypeBranch), repo.WebGitOperationCommonData)
m.Group("", func() {
m.Post("/upload-file", repo.UploadFileToServer)
- m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
+ m.Post("/upload-remove", repo.RemoveUploadFileFromServer)
}, repo.MustBeAbleToUpload, reqRepoCodeWriter)
}, repo.MustBeEditable, context.RepoMustNotBeArchived())
@@ -1403,7 +1418,7 @@ func registerWebRoutes(m *web.Router) {
m.Group("/{username}/{reponame}/projects", func() {
m.Get("", repo.Projects)
m.Get("/{id}", repo.ViewProject)
- m.Group("", func() { //nolint:dupl
+ m.Group("", func() { //nolint:dupl // duplicates lines 1034-1054
m.Get("/new", repo.RenderNewProject)
m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
m.Group("/{id}", func() {
@@ -1445,8 +1460,10 @@ func registerWebRoutes(m *web.Router) {
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
m.Get("/logs", actions.Logs)
})
+ m.Get("/workflow", actions.ViewWorkflowFile)
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
+ m.Post("/delete", reqRepoActionsWriter, actions.Delete)
m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView)
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)