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)}} -
+ {{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)}} + +{{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) + }) + }) }) }