From 0ea021c8c94a11372c0edf6ed5f9705da8750e01 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Sun, 25 Feb 2024 11:58:23 +0100 Subject: [PATCH] Allow instance-wide disabling of forking For small, personal self-hosted instances with no user signups, the fork button is just a noise. This patch allows disabling them like stars can be disabled too. Disabling forks does not only remove the buttons from the web UI, it also disables the routes that could be used to create forks. Fixes #2441. Signed-off-by: Gergely Nagy --- custom/conf/app.example.ini | 3 ++ modules/context/context.go | 1 + modules/setting/repository.go | 2 ++ modules/structs/settings.go | 1 + routers/api/v1/api.go | 6 ++-- routers/api/v1/settings/settings.go | 1 + routers/web/web.go | 18 ++++++---- templates/explore/repo_list.tmpl | 4 ++- templates/explore/repo_search.tmpl | 6 ++-- templates/repo/header.tmpl | 51 ++--------------------------- templates/repo/header_fork.tmpl | 50 ++++++++++++++++++++++++++++ templates/swagger/v1_json.tmpl | 4 +++ tests/integration/api_fork_test.go | 29 ++++++++++++++++ tests/integration/repo_fork_test.go | 46 ++++++++++++++++++++++++++ 14 files changed, 162 insertions(+), 60 deletions(-) create mode 100644 templates/repo/header_fork.tmpl diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 1b53732b1b..dc1843097f 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -991,6 +991,9 @@ LEVEL = Info ;; Disable stars feature. ;DISABLE_STARS = false ;; +;; Disable repository forking. +;DISABLE_FORKS = false +;; ;; The default branch name of new repositories ;DEFAULT_BRANCH = main ;; diff --git a/modules/context/context.go b/modules/context/context.go index 66732eaa8a..a06ebfb0dc 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -198,6 +198,7 @@ func Contexter() func(next http.Handler) http.Handler { // FIXME: do we really always need these setting? There should be someway to have to avoid having to always set these ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations ctx.Data["DisableStars"] = setting.Repository.DisableStars + ctx.Data["DisableForks"] = setting.Repository.DisableForks ctx.Data["EnableActions"] = setting.Actions.Enabled ctx.Data["ManifestData"] = setting.ManifestData diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 4ab566b7ff..34eff196b8 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -50,6 +50,7 @@ var ( PrefixArchiveFiles bool DisableMigrations bool DisableStars bool `ini:"DISABLE_STARS"` + DisableForks bool DefaultBranch string AllowAdoptionOfUnadoptedRepositories bool AllowDeleteOfUnadoptedRepositories bool @@ -172,6 +173,7 @@ var ( PrefixArchiveFiles: true, DisableMigrations: false, DisableStars: false, + DisableForks: false, DefaultBranch: "main", AllowForkWithoutMaximumLimit: true, diff --git a/modules/structs/settings.go b/modules/structs/settings.go index e48b1a493d..b127b58462 100644 --- a/modules/structs/settings.go +++ b/modules/structs/settings.go @@ -9,6 +9,7 @@ type GeneralRepoSettings struct { HTTPGitDisabled bool `json:"http_git_disabled"` MigrationsDisabled bool `json:"migrations_disabled"` StarsDisabled bool `json:"stars_disabled"` + ForksDisabled bool `json:"forks_disabled"` TimeTrackingDisabled bool `json:"time_tracking_disabled"` LFSDisabled bool `json:"lfs_disabled"` } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 1babccb650..38c0c01a0a 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1161,8 +1161,10 @@ func Routes() *web.Route { 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.Combo("/forks").Get(repo.ListForks). - Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork) + if !setting.Repository.DisableForks { + m.Combo("/forks").Get(repo.ListForks). + Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork) + } m.Group("/branches", func() { m.Get("", repo.ListBranches) m.Get("/*", repo.GetBranch) diff --git a/routers/api/v1/settings/settings.go b/routers/api/v1/settings/settings.go index 02bda1309d..957b839e66 100644 --- a/routers/api/v1/settings/settings.go +++ b/routers/api/v1/settings/settings.go @@ -61,6 +61,7 @@ func GetGeneralRepoSettings(ctx *context.APIContext) { HTTPGitDisabled: setting.Repository.DisableHTTPGit, MigrationsDisabled: setting.Repository.DisableMigrations, StarsDisabled: setting.Repository.DisableStars, + ForksDisabled: setting.Repository.DisableForks, TimeTrackingDisabled: !setting.Service.EnableTimetracking, LFSDisabled: !setting.LFS.StartServer, }) diff --git a/routers/web/web.go b/routers/web/web.go index 0684b2ac82..06ad3490aa 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -968,7 +968,9 @@ func registerRoutes(m *web.Route) { m.Post("/create", web.Bind(forms.CreateRepoForm{}), repo.CreatePost) m.Get("/migrate", repo.Migrate) m.Post("/migrate", web.Bind(forms.MigrateRepoForm{}), repo.MigratePost) - m.Get("/fork/{repoid}", context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader, repo.ForkByID) + if !setting.Repository.DisableForks { + m.Get("/fork/{repoid}", context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader, repo.ForkByID) + } m.Get("/search", repo.SearchRepo) }, reqSignIn) @@ -1148,8 +1150,10 @@ func registerRoutes(m *web.Route) { // Grouping for those endpoints that do require authentication m.Group("/{username}/{reponame}", func() { - m.Combo("/fork", reqRepoCodeReader).Get(repo.Fork). - Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost) + if !setting.Repository.DisableForks { + m.Combo("/fork", reqRepoCodeReader).Get(repo.Fork). + Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost) + } m.Group("/issues", func() { m.Group("/new", func() { m.Combo("").Get(context.RepoRef(), repo.NewIssue). @@ -1560,9 +1564,11 @@ func registerRoutes(m *web.Route) { m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.Home) }, repo.SetEditorconfigIfExists) - m.Group("", func() { - m.Get("/forks", repo.Forks) - }, context.RepoRef(), reqRepoCodeReader) + if !setting.Repository.DisableForks { + m.Group("", func() { + m.Get("/forks", repo.Forks) + }, context.RepoRef(), reqRepoCodeReader) + } m.Get("/commit/{sha:([a-f0-9]{4,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff) }, ignSignIn, context.RepoAssignment, context.UnitTypes()) diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl index c51dcaa3ff..848afd305c 100644 --- a/templates/explore/repo_list.tmpl +++ b/templates/explore/repo_list.tmpl @@ -39,7 +39,9 @@ {{if not $.DisableStars}} {{svg "octicon-star" 16}}{{.NumStars}} {{end}} - {{svg "octicon-git-branch" 16}}{{.NumForks}} + {{if not $.DisableForks}} + {{svg "octicon-git-branch" 16}}{{.NumForks}} + {{end}} {{$description := .DescriptionHTML $.Context}} diff --git a/templates/explore/repo_search.tmpl b/templates/explore/repo_search.tmpl index eaf2e7a090..573163d554 100644 --- a/templates/explore/repo_search.tmpl +++ b/templates/explore/repo_search.tmpl @@ -29,8 +29,10 @@ {{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}} {{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}} {{end}} - {{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}} - {{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}} + {{if not .DisableForks}} + {{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}} + {{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}} + {{end}} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 2a3167f982..ed377e9d18 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -62,55 +62,8 @@ {{if not $.DisableStars}} {{template "repo/star_unstar" $}} {{end}} - {{if and (not .IsEmpty) ($.Permission.CanRead $.UnitTypeCode)}} -
- - {{svg "octicon-repo-forked"}}{{ctx.Locale.Tr "repo.fork"}} - - - - {{CountFmt .NumForks}} - -
+ {{if not $.DisableForks}} + {{template "repo/header_fork" $}} {{end}} {{end}} diff --git a/templates/repo/header_fork.tmpl b/templates/repo/header_fork.tmpl new file mode 100644 index 0000000000..5bce9e0f14 --- /dev/null +++ b/templates/repo/header_fork.tmpl @@ -0,0 +1,50 @@ +{{if and (not .IsEmpty) ($.Permission.CanRead $.UnitTypeCode)}} +
+ + {{svg "octicon-repo-forked"}}{{ctx.Locale.Tr "repo.fork"}} + + + + {{CountFmt .NumForks}} + +
+{{end}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 0b330a89ee..18ab544415 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -20565,6 +20565,10 @@ "description": "GeneralRepoSettings contains global repository settings exposed by API", "type": "object", "properties": { + "forks_disabled": { + "type": "boolean", + "x-go-name": "ForksDisabled" + }, "http_git_disabled": { "type": "boolean", "x-go-name": "HTTPGitDisabled" diff --git a/tests/integration/api_fork_test.go b/tests/integration/api_fork_test.go index 7c231415a3..87d2a10152 100644 --- a/tests/integration/api_fork_test.go +++ b/tests/integration/api_fork_test.go @@ -1,13 +1,18 @@ // Copyright 2017 The Gogs Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT package integration import ( "net/http" + "net/url" "testing" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/routers" "code.gitea.io/gitea/tests" ) @@ -16,3 +21,27 @@ func TestCreateForkNoLogin(t *testing.T) { req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{}) MakeRequest(t, req, http.StatusUnauthorized) } + +func TestAPIDisabledForkRepo(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + defer test.MockVariableValue(&setting.Repository.DisableForks, true)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + t.Run("fork listing", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks") + MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("forking", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + session := loginUser(t, "user5") + token := getTokenForLoggedInUser(t, session) + + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{}).AddTokenAuth(token) + session.MakeRequest(t, req, http.StatusNotFound) + }) + }) +} diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go index c6e3fed7a9..6c0cdc4339 100644 --- a/tests/integration/repo_fork_test.go +++ b/tests/integration/repo_fork_test.go @@ -1,4 +1,5 @@ // Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. // SPDX-License-Identifier: MIT package integration @@ -14,6 +15,9 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "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/routers" repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" @@ -119,6 +123,48 @@ func TestRepoFork(t *testing.T) { session.MakeRequest(t, req, http.StatusNotFound) }) }) + + t.Run("DISABLE_FORKS", func(t *testing.T) { + defer test.MockVariableValue(&setting.Repository.DisableForks, true)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + t.Run("fork button not present", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // The "Fork" button should not appear on the repo home + req := NewRequest(t, "GET", "/user2/repo1") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + htmlDoc.AssertElement(t, "[href=/user2/repo1/fork]", false) + }) + + t.Run("forking by URL", func(t *testing.T) { + t.Run("by name", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Forking by URL should be Not Found + req := NewRequest(t, "GET", "/user2/repo1/fork") + session.MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("by legacy URL", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Forking by legacy URL should be Not Found + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // user2/repo1 + req := NewRequestf(t, "GET", "/repo/fork/%d", repo.ID) + session.MakeRequest(t, req, http.StatusNotFound) + }) + }) + + t.Run("fork listing", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Listing the forks should be Not Found, too + req := NewRequest(t, "GET", "/user2/repo1/forks") + MakeRequest(t, req, http.StatusNotFound) + }) + }) }) }