Merge pull request 'Allow instance-wide disabling of forking' (#2445) from algernon/forgejo:f/disable-forks into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2445 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
commit
649ca2b230
@ -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
|
||||
;;
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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())
|
||||
|
||||
|
@ -39,7 +39,9 @@
|
||||
{{if not $.DisableStars}}
|
||||
<a class="text grey flex-text-inline" href="{{.Link}}/stars">{{svg "octicon-star" 16}}{{.NumStars}}</a>
|
||||
{{end}}
|
||||
<a class="text grey flex-text-inline" href="{{.Link}}/forks">{{svg "octicon-git-branch" 16}}{{.NumForks}}</a>
|
||||
{{if not $.DisableForks}}
|
||||
<a class="text grey flex-text-inline" href="{{.Link}}/forks">{{svg "octicon-git-branch" 16}}{{.NumForks}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{$description := .DescriptionHTML $.Context}}
|
||||
|
@ -29,8 +29,10 @@
|
||||
<a class="{{if eq .SortType "moststars"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=moststars&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</a>
|
||||
<a class="{{if eq .SortType "feweststars"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=feweststars&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</a>
|
||||
{{end}}
|
||||
<a class="{{if eq .SortType "mostforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=mostforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</a>
|
||||
<a class="{{if eq .SortType "fewestforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=fewestforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</a>
|
||||
{{if not .DisableForks}}
|
||||
<a class="{{if eq .SortType "mostforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=mostforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</a>
|
||||
<a class="{{if eq .SortType "fewestforks"}}active {{end}}item" href="{{$.Link}}?tab={{$.TabName}}&sort=fewestforks&q={{$.Keyword}}&language={{$.Language}}">{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -62,55 +62,8 @@
|
||||
{{if not $.DisableStars}}
|
||||
{{template "repo/star_unstar" $}}
|
||||
{{end}}
|
||||
{{if and (not .IsEmpty) ($.Permission.CanRead $.UnitTypeCode)}}
|
||||
<div class="ui labeled button
|
||||
{{if or (not $.IsSigned) (and (not $.CanSignedUserFork) (not $.UserAndOrgForks))}}
|
||||
disabled
|
||||
{{end}}"
|
||||
{{if not $.IsSigned}}
|
||||
data-tooltip-content="{{ctx.Locale.Tr "repo.fork_guest_user"}}"
|
||||
{{else if and (not $.CanSignedUserFork) (not $.UserAndOrgForks)}}
|
||||
data-tooltip-content="{{ctx.Locale.Tr "repo.fork_from_self"}}"
|
||||
{{end}}
|
||||
>
|
||||
<a class="ui compact{{if $.ShowForkModal}} show-modal{{end}} small basic button"
|
||||
{{if not $.CanSignedUserFork}}
|
||||
{{if gt (len $.UserAndOrgForks) 1}}
|
||||
data-modal="#fork-repo-modal"
|
||||
{{else if eq (len $.UserAndOrgForks) 1}}
|
||||
href="{{AppSubUrl}}/{{(index $.UserAndOrgForks 0).FullName}}"
|
||||
{{/*else is not required here, because the button shouldn't link to any site if you can't create a fork*/}}
|
||||
{{end}}
|
||||
{{else if not $.UserAndOrgForks}}
|
||||
href="{{$.RepoLink}}/fork"
|
||||
{{else}}
|
||||
data-modal="#fork-repo-modal"
|
||||
{{end}}
|
||||
>
|
||||
{{svg "octicon-repo-forked"}}<span class="text">{{ctx.Locale.Tr "repo.fork"}}</span>
|
||||
</a>
|
||||
<div class="ui small modal" id="fork-repo-modal">
|
||||
<div class="header">
|
||||
{{ctx.Locale.Tr "repo.already_forked" .Name}}
|
||||
</div>
|
||||
<div class="content gt-text-left">
|
||||
<div class="ui list">
|
||||
{{range $.UserAndOrgForks}}
|
||||
<div class="ui item gt-py-3">
|
||||
<a href="{{.Link}}">{{svg "octicon-repo-forked" 16 "gt-mr-3"}}{{.FullName}}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if $.CanSignedUserFork}}
|
||||
<div class="divider"></div>
|
||||
<a href="{{$.RepoLink}}/fork">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<a class="ui basic label" href="{{.Link}}/forks">
|
||||
{{CountFmt .NumForks}}
|
||||
</a>
|
||||
</div>
|
||||
{{if not $.DisableForks}}
|
||||
{{template "repo/header_fork" $}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
50
templates/repo/header_fork.tmpl
Normal file
50
templates/repo/header_fork.tmpl
Normal file
@ -0,0 +1,50 @@
|
||||
{{if and (not .IsEmpty) ($.Permission.CanRead $.UnitTypeCode)}}
|
||||
<div class="ui labeled button
|
||||
{{if or (not $.IsSigned) (and (not $.CanSignedUserFork) (not $.UserAndOrgForks))}}
|
||||
disabled
|
||||
{{end}}"
|
||||
{{if not $.IsSigned}}
|
||||
data-tooltip-content="{{ctx.Locale.Tr "repo.fork_guest_user"}}"
|
||||
{{else if and (not $.CanSignedUserFork) (not $.UserAndOrgForks)}}
|
||||
data-tooltip-content="{{ctx.Locale.Tr "repo.fork_from_self"}}"
|
||||
{{end}}
|
||||
>
|
||||
<a class="ui compact{{if $.ShowForkModal}} show-modal{{end}} small basic button"
|
||||
{{if not $.CanSignedUserFork}}
|
||||
{{if gt (len $.UserAndOrgForks) 1}}
|
||||
data-modal="#fork-repo-modal"
|
||||
{{else if eq (len $.UserAndOrgForks) 1}}
|
||||
href="{{AppSubUrl}}/{{(index $.UserAndOrgForks 0).FullName}}"
|
||||
{{/*else is not required here, because the button shouldn't link to any site if you can't create a fork*/}}
|
||||
{{end}}
|
||||
{{else if not $.UserAndOrgForks}}
|
||||
href="{{$.RepoLink}}/fork"
|
||||
{{else}}
|
||||
data-modal="#fork-repo-modal"
|
||||
{{end}}
|
||||
>
|
||||
{{svg "octicon-repo-forked"}}<span class="text">{{ctx.Locale.Tr "repo.fork"}}</span>
|
||||
</a>
|
||||
<div class="ui small modal" id="fork-repo-modal">
|
||||
<div class="header">
|
||||
{{ctx.Locale.Tr "repo.already_forked" .Name}}
|
||||
</div>
|
||||
<div class="content gt-text-left">
|
||||
<div class="ui list">
|
||||
{{range $.UserAndOrgForks}}
|
||||
<div class="ui item gt-py-3">
|
||||
<a href="{{.Link}}">{{svg "octicon-repo-forked" 16 "gt-mr-3"}}{{.FullName}}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if $.CanSignedUserFork}}
|
||||
<div class="divider"></div>
|
||||
<a href="{{$.RepoLink}}/fork">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<a class="ui basic label" href="{{.Link}}/forks">
|
||||
{{CountFmt .NumForks}}
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
4
templates/swagger/v1_json.tmpl
generated
4
templates/swagger/v1_json.tmpl
generated
@ -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"
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user