feat(repo): enforce dot-prefixed repos as always-private system repos #77

Merged
jmiller merged 1 commits from feature/system-repo-private into main 2026-05-13 00:16:13 +00:00
10 changed files with 52 additions and 14 deletions
+6
View File
@@ -303,6 +303,12 @@ func (repo *Repository) IsBeingCreated() bool {
return repo.IsBeingMigrated()
}
// IsSystemRepo returns true when the repository name starts with "."
// System repositories are always private and cannot be made public.
func (repo *Repository) IsSystemRepo() bool {
return strings.HasPrefix(repo.Name, ".")
}
// IsBroken indicates that repository is broken
func (repo *Repository) IsBroken() bool {
return repo.Status == RepositoryBroken
+1
View File
@@ -2496,6 +2496,7 @@
"repo.settings.visibility.success": "Repository visibility changed.",
"repo.settings.visibility.error": "An error occurred while trying to change the repo visibility.",
"repo.settings.visibility.fork_error": "Can't change the visibility of a forked repo.",
"repo.settings.visibility.system_repo_private": "System repositories (dot-prefixed names) are always private and cannot be made public.",
"repo.settings.archive.button": "Archive Repo",
"repo.settings.archive.header": "Archive This Repo",
"repo.settings.archive.text": "Archiving the repo will make it entirely read-only. It will be hidden from the dashboard. Nobody (not even you!) will be able to make new commits, or open any issues or pull requests.",
+2 -2
View File
@@ -139,7 +139,7 @@ func Migrate(ctx *context.APIContext) {
CloneAddr: remoteAddr,
RepoName: form.RepoName,
Description: form.Description,
Private: form.Private || setting.Repository.ForcePrivate,
Private: form.Private || setting.Repository.ForcePrivate || strings.HasPrefix(form.RepoName, "."),
Mirror: form.Mirror,
LFS: form.LFS,
LFSEndpoint: form.LFSEndpoint,
@@ -174,7 +174,7 @@ func Migrate(ctx *context.APIContext) {
Description: opts.Description,
OriginalURL: form.CloneAddr,
GitServiceType: gitServiceType,
IsPrivate: opts.Private || setting.Repository.ForcePrivate,
IsPrivate: opts.Private || setting.Repository.ForcePrivate || strings.HasPrefix(opts.RepoName, "."),
IsMirror: opts.Mirror,
Status: repo_model.RepositoryBeingMigrated,
}, false)
+10 -1
View File
@@ -253,6 +253,9 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
return
}
// System repos (dot-prefixed names) are always private.
isPrivate := opt.Private || setting.Repository.ForcePrivate || strings.HasPrefix(opt.Name, ".")
repo, err := repo_service.CreateRepository(ctx, ctx.Doer, owner, repo_service.CreateRepoOptions{
Name: opt.Name,
Description: opt.Description,
@@ -260,7 +263,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
Gitignores: opt.Gitignores,
License: opt.License,
Readme: opt.Readme,
IsPrivate: opt.Private || setting.Repository.ForcePrivate,
IsPrivate: isPrivate,
AutoInit: opt.AutoInit,
DefaultBranch: opt.DefaultBranch,
TrustModel: repo_model.ToTrustModel(opt.TrustModel),
@@ -700,6 +703,12 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
}
visibilityChanged = repo.IsPrivate != *opts.Private
// System repos (dot-prefixed) cannot be made public, regardless of user role.
if visibilityChanged && !*opts.Private && repo.IsSystemRepo() {
err := errors.New("system repositories (dot-prefixed) cannot be made public")
ctx.APIError(http.StatusForbidden, err)
return err
}
// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.Doer.IsAdmin {
err := errors.New("cannot change private repository to public")
+9 -4
View File
@@ -210,10 +210,15 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
// 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()
if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {
ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to change visibility"})
// System repos (dot-prefixed) are always private — ignore attempts to make them public.
if !isPrivate.Value() && repo.IsSystemRepo() {
log.Warn("Ignoring push option to make system repo %s public", repo.FullName())
} else {
// TODO: it needs to do more work
repo.IsPrivate = isPrivate.Value()
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() && repo.IsTemplate != isTemplate.Value() {
+1 -1
View File
@@ -210,7 +210,7 @@ func MigratePost(ctx *context.Context) {
CloneAddr: remoteAddr,
RepoName: form.RepoName,
Description: form.Description,
Private: form.Private || setting.Repository.ForcePrivate,
Private: form.Private || setting.Repository.ForcePrivate || strings.HasPrefix(form.RepoName, "."),
Mirror: form.Mirror,
LFS: form.LFS,
LFSEndpoint: form.LFSEndpoint,
+2 -2
View File
@@ -243,7 +243,7 @@ func CreatePost(ctx *context.Context) {
opts := repo_service.GenerateRepoOptions{
Name: form.RepoName,
Description: form.Description,
Private: form.Private || setting.Repository.ForcePrivate,
Private: form.Private || setting.Repository.ForcePrivate || strings.HasPrefix(form.RepoName, "."),
GitContent: form.GitContent,
Topics: form.Topics,
GitHooks: form.GitHooks,
@@ -282,7 +282,7 @@ func CreatePost(ctx *context.Context) {
IssueLabels: form.IssueLabels,
License: form.License,
Readme: form.Readme,
IsPrivate: form.Private || setting.Repository.ForcePrivate,
IsPrivate: form.Private || setting.Repository.ForcePrivate || strings.HasPrefix(form.RepoName, "."),
DefaultBranch: form.DefaultBranch,
AutoInit: form.AutoInit,
IsTemplate: form.Template,
+10 -2
View File
@@ -127,14 +127,22 @@ func repoUnitPublicAccesses(ctx *context.Context) []*repoUnitPublicAccess {
func PublicAccess(ctx *context.Context) {
ctx.Data["PageIsSettingsPublicAccess"] = true
ctx.Data["RepoUnitPublicAccesses"] = repoUnitPublicAccesses(ctx)
ctx.Data["GlobalForcePrivate"] = setting.Repository.ForcePrivate
if setting.Repository.ForcePrivate {
ctx.Data["GlobalForcePrivate"] = setting.Repository.ForcePrivate || ctx.Repo.Repository.IsSystemRepo()
if ctx.Repo.Repository.IsSystemRepo() {
ctx.Flash.Error(ctx.Tr("repo.settings.visibility.system_repo_private"), true)
} else if setting.Repository.ForcePrivate {
ctx.Flash.Error(ctx.Tr("form.repository_force_private"), true)
}
ctx.HTML(http.StatusOK, tplRepoSettingsPublicAccess)
}
func PublicAccessPost(ctx *context.Context) {
// System repos (dot-prefixed) cannot have public access on any unit.
if ctx.Repo.Repository.IsSystemRepo() {
ctx.Flash.Error(ctx.Tr("repo.settings.visibility.system_repo_private"), true)
ctx.Redirect(ctx.Repo.RepoLink + "/settings/public_access")
return
}
accesses := repoUnitPublicAccesses(ctx)
for _, ua := range accesses {
formVal := ctx.FormString(ua.FormKey)
+7 -2
View File
@@ -54,7 +54,7 @@ const (
func SettingsCtxData(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.options")
ctx.Data["PageIsSettingsOptions"] = true
ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate || ctx.Repo.Repository.IsSystemRepo()
ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush
@@ -103,7 +103,7 @@ func Settings(ctx *context.Context) {
// SettingsPost response for changes of a repository
func SettingsPost(ctx *context.Context) {
ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate || ctx.Repo.Repository.IsSystemRepo()
ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush
@@ -1008,6 +1008,11 @@ func handleSettingsPostVisibility(ctx *context.Context) {
private := ctx.FormOptionalBool("private").ValueOrDefault(true) // default to true for privacy & safety
// System repos (dot-prefixed) cannot be made public, regardless of user role.
if !private && repo.IsSystemRepo() {
ctx.JSONError(ctx.Tr("repo.settings.visibility.system_repo_private"))
return
}
// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
if !private && setting.Repository.ForcePrivate && !ctx.Doer.IsAdmin {
ctx.JSONError(ctx.Tr("form.repository_force_private"))
+4
View File
@@ -123,6 +123,10 @@ func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibili
}
func MakeRepoPrivate(ctx context.Context, repo *repo_model.Repository, private bool) (err error) {
// System repos (dot-prefixed) are always private and cannot be made public.
if !private && repo.IsSystemRepo() {
return errors.New("system repositories (dot-prefixed) cannot be made public")
}
return db.WithTx(ctx, func(ctx context.Context) error {
repo.IsPrivate = private
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil {