From cddd19efc8c2ccbcd57e17811e3adebee7d49d60 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 29 Mar 2025 13:26:41 +0800
Subject: [PATCH] Add anonymous access support for private/unlisted
 repositories (#34051)

Follow #33127

Fix #8649, fix #639

This is a complete solution. A repo unit could be set to:

* Anonymous read (non-signed-in user)
* Everyone read (signed-in user)
* Everyone write (wiki-only)
---
 models/perm/access/repo_permission.go      |  12 +-
 models/repo/repo_unit.go                   |   6 +
 options/locale/locale_en-US.ini            |   6 +-
 routers/web/repo/setting/public_access.go  | 155 +++++++++++++++
 routers/web/repo/setting/setting.go        | 134 +++++--------
 routers/web/web.go                         |   2 +
 services/forms/repo_form.go                |  13 +-
 templates/repo/header.tmpl                 |   5 +-
 templates/repo/navbar.tmpl                 |   1 +
 templates/repo/settings/navbar.tmpl        |   5 +
 templates/repo/settings/options.tmpl       |  28 ---
 templates/repo/settings/public_access.tmpl |  36 ++++
 tests/integration/repo_test.go             | 213 +++++++++++++--------
 13 files changed, 410 insertions(+), 206 deletions(-)
 create mode 100644 routers/web/repo/setting/public_access.go
 create mode 100644 templates/repo/settings/public_access.tmpl

diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go
index 20ede7fee2..f42c96bbe2 100644
--- a/models/perm/access/repo_permission.go
+++ b/models/perm/access/repo_permission.go
@@ -15,6 +15,7 @@ import (
 	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 )
 
@@ -50,7 +51,7 @@ func (p *Permission) HasAnyUnitAccess() bool {
 	return p.AccessMode >= perm_model.AccessModeRead
 }
 
-func (p *Permission) HasAnyUnitAccessOrPublicAccess() bool {
+func (p *Permission) HasAnyUnitPublicAccess() bool {
 	for _, v := range p.anonymousAccessMode {
 		if v >= perm_model.AccessModeRead {
 			return true
@@ -61,7 +62,11 @@ func (p *Permission) HasAnyUnitAccessOrPublicAccess() bool {
 			return true
 		}
 	}
-	return p.HasAnyUnitAccess()
+	return false
+}
+
+func (p *Permission) HasAnyUnitAccessOrPublicAccess() bool {
+	return p.HasAnyUnitPublicAccess() || p.HasAnyUnitAccess()
 }
 
 // HasUnits returns true if the permission contains attached units
@@ -188,6 +193,9 @@ func (p *Permission) LogString() string {
 }
 
 func applyPublicAccessPermission(unitType unit.Type, accessMode perm_model.AccessMode, modeMap *map[unit.Type]perm_model.AccessMode) {
+	if setting.Repository.ForcePrivate {
+		return
+	}
 	if accessMode >= perm_model.AccessModeRead && accessMode > (*modeMap)[unitType] {
 		if *modeMap == nil {
 			*modeMap = make(map[unit.Type]perm_model.AccessMode)
diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go
index 93b0dcab31..e83a3dc8c2 100644
--- a/models/repo/repo_unit.go
+++ b/models/repo/repo_unit.go
@@ -342,3 +342,9 @@ func UpdateRepoUnit(ctx context.Context, unit *RepoUnit) error {
 	_, err := db.GetEngine(ctx).ID(unit.ID).Update(unit)
 	return err
 }
+
+func UpdateRepoUnitPublicAccess(ctx context.Context, unit *RepoUnit) error {
+	_, err := db.GetEngine(ctx).Where("repo_id=? AND `type`=?", unit.RepoID, unit.Type).
+		Cols("anonymous_access_mode", "everyone_access_mode").Update(unit)
+	return err
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index b2e079c696..252044da16 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -926,6 +926,9 @@ permission_not_set = Not set
 permission_no_access = No Access
 permission_read = Read
 permission_write = Read and Write
+permission_anonymous_read = Anonymous Read
+permission_everyone_read = Everyone Read
+permission_everyone_write = Everyone Write
 access_token_desc = Selected token permissions limit authorization only to the corresponding <a %s>API</a> routes. Read the <a %s>documentation</a> for more information.
 at_least_one_permission = You must select at least one permission to create a token
 permissions_list = Permissions:
@@ -1138,6 +1141,7 @@ transfer.no_permission_to_reject = You do not have permission to reject this tra
 
 desc.private = Private
 desc.public = Public
+desc.public_access = Public Access
 desc.template = Template
 desc.internal = Internal
 desc.archived = Archived
@@ -2133,6 +2137,7 @@ contributors.contribution_type.deletions = Deletions
 settings = Settings
 settings.desc = Settings is where you can manage the settings for the repository
 settings.options = Repository
+settings.public_access = Public Access
 settings.collaboration = Collaborators
 settings.collaboration.admin = Administrator
 settings.collaboration.write = Write
@@ -2179,7 +2184,6 @@ settings.advanced_settings = Advanced Settings
 settings.wiki_desc = Enable Repository Wiki
 settings.use_internal_wiki = Use Built-In Wiki
 settings.default_wiki_branch_name = Default Wiki Branch Name
-settings.default_permission_everyone_access = Default access permission for all signed-in users:
 settings.failed_to_change_default_wiki_branch = Failed to change the default wiki branch.
 settings.use_external_wiki = Use External Wiki
 settings.external_wiki_url = External Wiki URL
diff --git a/routers/web/repo/setting/public_access.go b/routers/web/repo/setting/public_access.go
new file mode 100644
index 0000000000..368d34294a
--- /dev/null
+++ b/routers/web/repo/setting/public_access.go
@@ -0,0 +1,155 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+	"net/http"
+	"slices"
+	"strconv"
+
+	"code.gitea.io/gitea/models/perm"
+	"code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unit"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/templates"
+	"code.gitea.io/gitea/services/context"
+)
+
+const tplRepoSettingsPublicAccess templates.TplName = "repo/settings/public_access"
+
+func parsePublicAccessMode(permission string, allowed []string) (ret struct {
+	AnonymousAccessMode, EveryoneAccessMode perm.AccessMode
+},
+) {
+	ret.AnonymousAccessMode = perm.AccessModeNone
+	ret.EveryoneAccessMode = perm.AccessModeNone
+
+	// if site admin forces repositories to be private, then do not allow any other access mode,
+	// otherwise the "force private" setting would be bypassed
+	if setting.Repository.ForcePrivate {
+		return ret
+	}
+	if !slices.Contains(allowed, permission) {
+		return ret
+	}
+	switch permission {
+	case paAnonymousRead:
+		ret.AnonymousAccessMode = perm.AccessModeRead
+	case paEveryoneRead:
+		ret.EveryoneAccessMode = perm.AccessModeRead
+	case paEveryoneWrite:
+		ret.EveryoneAccessMode = perm.AccessModeWrite
+	}
+	return ret
+}
+
+const (
+	paNotSet        = "not-set"
+	paAnonymousRead = "anonymous-read"
+	paEveryoneRead  = "everyone-read"
+	paEveryoneWrite = "everyone-write"
+)
+
+type repoUnitPublicAccess struct {
+	UnitType          unit.Type
+	FormKey           string
+	DisplayName       string
+	PublicAccessTypes []string
+	UnitPublicAccess  string
+}
+
+func repoUnitPublicAccesses(ctx *context.Context) []*repoUnitPublicAccess {
+	accesses := []*repoUnitPublicAccess{
+		{
+			UnitType:          unit.TypeCode,
+			DisplayName:       ctx.Locale.TrString("repo.code"),
+			PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+		},
+		{
+			UnitType:          unit.TypeIssues,
+			DisplayName:       ctx.Locale.TrString("issues"),
+			PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+		},
+		{
+			UnitType:          unit.TypePullRequests,
+			DisplayName:       ctx.Locale.TrString("pull_requests"),
+			PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+		},
+		{
+			UnitType:          unit.TypeReleases,
+			DisplayName:       ctx.Locale.TrString("repo.releases"),
+			PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+		},
+		{
+			UnitType:          unit.TypeWiki,
+			DisplayName:       ctx.Locale.TrString("repo.wiki"),
+			PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead, paEveryoneWrite},
+		},
+		{
+			UnitType:          unit.TypeProjects,
+			DisplayName:       ctx.Locale.TrString("repo.projects"),
+			PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+		},
+		{
+			UnitType:          unit.TypePackages,
+			DisplayName:       ctx.Locale.TrString("repo.packages"),
+			PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+		},
+		{
+			UnitType:          unit.TypeActions,
+			DisplayName:       ctx.Locale.TrString("repo.actions"),
+			PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+		},
+	}
+	for _, ua := range accesses {
+		ua.FormKey = "repo-unit-access-" + strconv.Itoa(int(ua.UnitType))
+		for _, u := range ctx.Repo.Repository.Units {
+			if u.Type == ua.UnitType {
+				ua.UnitPublicAccess = paNotSet
+				switch {
+				case u.EveryoneAccessMode == perm.AccessModeWrite:
+					ua.UnitPublicAccess = paEveryoneWrite
+				case u.EveryoneAccessMode == perm.AccessModeRead:
+					ua.UnitPublicAccess = paEveryoneRead
+				case u.AnonymousAccessMode == perm.AccessModeRead:
+					ua.UnitPublicAccess = paAnonymousRead
+				}
+				break
+			}
+		}
+	}
+	return slices.DeleteFunc(accesses, func(ua *repoUnitPublicAccess) bool {
+		return ua.UnitPublicAccess == ""
+	})
+}
+
+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.Flash.Error(ctx.Tr("form.repository_force_private"), true)
+	}
+	ctx.HTML(http.StatusOK, tplRepoSettingsPublicAccess)
+}
+
+func PublicAccessPost(ctx *context.Context) {
+	accesses := repoUnitPublicAccesses(ctx)
+	for _, ua := range accesses {
+		formVal := ctx.FormString(ua.FormKey)
+		parsed := parsePublicAccessMode(formVal, ua.PublicAccessTypes)
+		err := repo.UpdateRepoUnitPublicAccess(ctx, &repo.RepoUnit{
+			RepoID:              ctx.Repo.Repository.ID,
+			Type:                ua.UnitType,
+			AnonymousAccessMode: parsed.AnonymousAccessMode,
+			EveryoneAccessMode:  parsed.EveryoneAccessMode,
+		})
+		if err != nil {
+			ctx.ServerError("UpdateRepoUnitPublicAccess", err)
+			return
+		}
+	}
+	ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+	ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/public_access")
+}
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index e8da443b67..a0edb1e11a 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -13,7 +13,6 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/organization"
-	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
 	unit_model "code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
@@ -37,6 +36,8 @@ import (
 	mirror_service "code.gitea.io/gitea/services/mirror"
 	repo_service "code.gitea.io/gitea/services/repository"
 	wiki_service "code.gitea.io/gitea/services/wiki"
+
+	"xorm.io/xorm/convert"
 )
 
 const (
@@ -48,15 +49,6 @@ const (
 	tplDeployKeys      templates.TplName = "repo/settings/deploy_keys"
 )
 
-func parseEveryoneAccessMode(permission string, allowed ...perm.AccessMode) perm.AccessMode {
-	// if site admin forces repositories to be private, then do not allow any other access mode,
-	// otherwise the "force private" setting would be bypassed
-	if setting.Repository.ForcePrivate {
-		return perm.AccessModeNone
-	}
-	return perm.ParseAccessMode(permission, allowed...)
-}
-
 // SettingsCtxData is a middleware that sets all the general context data for the
 // settings template.
 func SettingsCtxData(ctx *context.Context) {
@@ -504,6 +496,17 @@ func handleSettingsPostPushMirrorAdd(ctx *context.Context) {
 	ctx.Redirect(repo.Link() + "/settings")
 }
 
+func newRepoUnit(repo *repo_model.Repository, unitType unit_model.Type, config convert.Conversion) repo_model.RepoUnit {
+	repoUnit := repo_model.RepoUnit{RepoID: repo.ID, Type: unitType, Config: config}
+	for _, u := range repo.Units {
+		if u.Type == unitType {
+			repoUnit.EveryoneAccessMode = u.EveryoneAccessMode
+			repoUnit.AnonymousAccessMode = u.AnonymousAccessMode
+		}
+	}
+	return repoUnit
+}
+
 func handleSettingsPostAdvanced(ctx *context.Context) {
 	form := web.GetForm(ctx).(*forms.RepoSettingForm)
 	repo := ctx.Repo.Repository
@@ -521,11 +524,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
 	}
 
 	if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() {
-		units = append(units, repo_model.RepoUnit{
-			RepoID:             repo.ID,
-			Type:               unit_model.TypeCode,
-			EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultCodeEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead),
-		})
+		units = append(units, newRepoUnit(repo, unit_model.TypeCode, nil))
 	} else if !unit_model.TypeCode.UnitGlobalDisabled() {
 		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode)
 	}
@@ -537,21 +536,12 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
 			return
 		}
 
-		units = append(units, repo_model.RepoUnit{
-			RepoID: repo.ID,
-			Type:   unit_model.TypeExternalWiki,
-			Config: &repo_model.ExternalWikiConfig{
-				ExternalWikiURL: form.ExternalWikiURL,
-			},
-		})
+		units = append(units, newRepoUnit(repo, unit_model.TypeExternalWiki, &repo_model.ExternalWikiConfig{
+			ExternalWikiURL: form.ExternalWikiURL,
+		}))
 		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
 	} else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() {
-		units = append(units, repo_model.RepoUnit{
-			RepoID:             repo.ID,
-			Type:               unit_model.TypeWiki,
-			Config:             new(repo_model.UnitConfig),
-			EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultWikiEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead, perm.AccessModeWrite),
-		})
+		units = append(units, newRepoUnit(repo, unit_model.TypeWiki, new(repo_model.UnitConfig)))
 		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
 	} else {
 		if !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
@@ -580,28 +570,19 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
 			ctx.Redirect(repo.Link() + "/settings")
 			return
 		}
-		units = append(units, repo_model.RepoUnit{
-			RepoID: repo.ID,
-			Type:   unit_model.TypeExternalTracker,
-			Config: &repo_model.ExternalTrackerConfig{
-				ExternalTrackerURL:           form.ExternalTrackerURL,
-				ExternalTrackerFormat:        form.TrackerURLFormat,
-				ExternalTrackerStyle:         form.TrackerIssueStyle,
-				ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern,
-			},
-		})
+		units = append(units, newRepoUnit(repo, unit_model.TypeExternalTracker, &repo_model.ExternalTrackerConfig{
+			ExternalTrackerURL:           form.ExternalTrackerURL,
+			ExternalTrackerFormat:        form.TrackerURLFormat,
+			ExternalTrackerStyle:         form.TrackerIssueStyle,
+			ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern,
+		}))
 		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
 	} else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() {
-		units = append(units, repo_model.RepoUnit{
-			RepoID: repo.ID,
-			Type:   unit_model.TypeIssues,
-			Config: &repo_model.IssuesConfig{
-				EnableTimetracker:                form.EnableTimetracker,
-				AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
-				EnableDependencies:               form.EnableIssueDependencies,
-			},
-			EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultIssuesEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead),
-		})
+		units = append(units, newRepoUnit(repo, unit_model.TypeIssues, &repo_model.IssuesConfig{
+			EnableTimetracker:                form.EnableTimetracker,
+			AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
+			EnableDependencies:               form.EnableIssueDependencies,
+		}))
 		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
 	} else {
 		if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
@@ -613,63 +594,46 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
 	}
 
 	if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
-		units = append(units, repo_model.RepoUnit{
-			RepoID: repo.ID,
-			Type:   unit_model.TypeProjects,
-			Config: &repo_model.ProjectsConfig{
-				ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
-			},
-		})
+		units = append(units, newRepoUnit(repo, unit_model.TypeProjects, &repo_model.ProjectsConfig{
+			ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
+		}))
 	} else if !unit_model.TypeProjects.UnitGlobalDisabled() {
 		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
 	}
 
 	if form.EnableReleases && !unit_model.TypeReleases.UnitGlobalDisabled() {
-		units = append(units, repo_model.RepoUnit{
-			RepoID: repo.ID,
-			Type:   unit_model.TypeReleases,
-		})
+		units = append(units, newRepoUnit(repo, unit_model.TypeReleases, nil))
 	} else if !unit_model.TypeReleases.UnitGlobalDisabled() {
 		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases)
 	}
 
 	if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() {
-		units = append(units, repo_model.RepoUnit{
-			RepoID: repo.ID,
-			Type:   unit_model.TypePackages,
-		})
+		units = append(units, newRepoUnit(repo, unit_model.TypePackages, nil))
 	} else if !unit_model.TypePackages.UnitGlobalDisabled() {
 		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages)
 	}
 
 	if form.EnableActions && !unit_model.TypeActions.UnitGlobalDisabled() {
-		units = append(units, repo_model.RepoUnit{
-			RepoID: repo.ID,
-			Type:   unit_model.TypeActions,
-		})
+		units = append(units, newRepoUnit(repo, unit_model.TypeActions, nil))
 	} else if !unit_model.TypeActions.UnitGlobalDisabled() {
 		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions)
 	}
 
 	if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() {
-		units = append(units, repo_model.RepoUnit{
-			RepoID: repo.ID,
-			Type:   unit_model.TypePullRequests,
-			Config: &repo_model.PullRequestsConfig{
-				IgnoreWhitespaceConflicts:     form.PullsIgnoreWhitespace,
-				AllowMerge:                    form.PullsAllowMerge,
-				AllowRebase:                   form.PullsAllowRebase,
-				AllowRebaseMerge:              form.PullsAllowRebaseMerge,
-				AllowSquash:                   form.PullsAllowSquash,
-				AllowFastForwardOnly:          form.PullsAllowFastForwardOnly,
-				AllowManualMerge:              form.PullsAllowManualMerge,
-				AutodetectManualMerge:         form.EnableAutodetectManualMerge,
-				AllowRebaseUpdate:             form.PullsAllowRebaseUpdate,
-				DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
-				DefaultMergeStyle:             repo_model.MergeStyle(form.PullsDefaultMergeStyle),
-				DefaultAllowMaintainerEdit:    form.DefaultAllowMaintainerEdit,
-			},
-		})
+		units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, &repo_model.PullRequestsConfig{
+			IgnoreWhitespaceConflicts:     form.PullsIgnoreWhitespace,
+			AllowMerge:                    form.PullsAllowMerge,
+			AllowRebase:                   form.PullsAllowRebase,
+			AllowRebaseMerge:              form.PullsAllowRebaseMerge,
+			AllowSquash:                   form.PullsAllowSquash,
+			AllowFastForwardOnly:          form.PullsAllowFastForwardOnly,
+			AllowManualMerge:              form.PullsAllowManualMerge,
+			AutodetectManualMerge:         form.EnableAutodetectManualMerge,
+			AllowRebaseUpdate:             form.PullsAllowRebaseUpdate,
+			DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
+			DefaultMergeStyle:             repo_model.MergeStyle(form.PullsDefaultMergeStyle),
+			DefaultAllowMaintainerEdit:    form.DefaultAllowMaintainerEdit,
+		}))
 	} else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
 		deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
 	}
diff --git a/routers/web/web.go b/routers/web/web.go
index a7593c34a6..4b8cfd81f3 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1078,6 +1078,8 @@ func registerRoutes(m *web.Router) {
 		m.Post("/avatar", web.Bind(forms.AvatarForm{}), repo_setting.SettingsAvatar)
 		m.Post("/avatar/delete", repo_setting.SettingsDeleteAvatar)
 
+		m.Combo("/public_access").Get(repo_setting.PublicAccess).Post(repo_setting.PublicAccessPost)
+
 		m.Group("/collaboration", func() {
 			m.Combo("").Get(repo_setting.Collaboration).Post(repo_setting.CollaborationPost)
 			m.Post("/access_mode", repo_setting.ChangeCollaborationAccessMode)
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 1366d30b1f..d20220b784 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -110,17 +110,14 @@ type RepoSettingForm struct {
 	EnablePrune            bool
 
 	// Advanced settings
-	EnableCode                bool
-	DefaultCodeEveryoneAccess string
+	EnableCode bool
 
-	EnableWiki                bool
-	EnableExternalWiki        bool
-	DefaultWikiBranch         string
-	DefaultWikiEveryoneAccess string
-	ExternalWikiURL           string
+	EnableWiki         bool
+	EnableExternalWiki bool
+	DefaultWikiBranch  string
+	ExternalWikiURL    string
 
 	EnableIssues                          bool
-	DefaultIssuesEveryoneAccess           string
 	EnableExternalTracker                 bool
 	ExternalTrackerURL                    string
 	TrackerURLFormat                      string
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index 8c2a0da8d0..c7c53b4f5d 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -25,6 +25,9 @@
 							<div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.internal"}}">{{svg "octicon-shield-lock" 18}}</div>
 						{{end}}
 					{{end}}
+					{{if $.Permission.HasAnyUnitPublicAccess}}
+						<span class="ui basic orange label">{{ctx.Locale.Tr "repo.desc.public_access"}}</span>
+					{{end}}
 					{{if .IsTemplate}}
 						<span class="ui basic label not-mobile">{{ctx.Locale.Tr "repo.desc.template"}}</span>
 						<div class="repo-icon only-mobile" data-tooltip-content="{{ctx.Locale.Tr "repo.desc.template"}}">{{svg "octicon-repo-template" 18}}</div>
@@ -208,7 +211,7 @@
 						</a>
 					{{end}}
 
-					{{if and (.Permission.CanReadAny ctx.Consts.RepoUnitTypePullRequests ctx.Consts.RepoUnitTypeIssues ctx.Consts.RepoUnitTypeReleases) (not .IsEmptyRepo)}}
+					{{if and (.Permission.CanReadAny ctx.Consts.RepoUnitTypePullRequests ctx.Consts.RepoUnitTypeIssues ctx.Consts.RepoUnitTypeReleases ctx.Consts.RepoUnitTypeCode) (not .IsEmptyRepo)}}
 						<a class="{{if .PageIsActivity}}active {{end}}item" href="{{.RepoLink}}/activity">
 							{{svg "octicon-pulse"}} {{ctx.Locale.Tr "repo.activity"}}
 						</a>
diff --git a/templates/repo/navbar.tmpl b/templates/repo/navbar.tmpl
index d6e9b1b8d7..e004c5254b 100644
--- a/templates/repo/navbar.tmpl
+++ b/templates/repo/navbar.tmpl
@@ -1,6 +1,7 @@
 {{$canReadCode := $.Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
 
 <div class="ui fluid vertical menu">
+	{{/* the default activity page "pulse" could work with any permission: code, issue, pr, release*/}}
 	<a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity">
 		{{ctx.Locale.Tr "repo.activity.navbar.pulse"}}
 	</a>
diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl
index 3e127ccbb3..3dd86d1f6a 100644
--- a/templates/repo/settings/navbar.tmpl
+++ b/templates/repo/settings/navbar.tmpl
@@ -4,6 +4,11 @@
 		<a class="{{if .PageIsSettingsOptions}}active {{end}}item" href="{{.RepoLink}}/settings">
 			{{ctx.Locale.Tr "repo.settings.options"}}
 		</a>
+		{{if or .Repository.IsPrivate .Permission.HasAnyUnitPublicAccess}}
+		<a class="{{if .PageIsSettingsPublicAccess}}active {{end}}item" href="{{.RepoLink}}/settings/public_access">
+			{{ctx.Locale.Tr "repo.settings.public_access"}}
+		</a>
+		{{end}}
 		<a class="{{if .PageIsSettingsCollaboration}}active {{end}}item" href="{{.RepoLink}}/settings/collaboration">
 			{{ctx.Locale.Tr "repo.settings.collaboration"}}
 		</a>
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index aade734a1d..202be3fce7 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -310,15 +310,6 @@
 						<input class="enable-system" name="enable_code" type="checkbox"{{if $isCodeEnabled}} checked{{end}}>
 						<label>{{ctx.Locale.Tr "repo.code.desc"}}</label>
 					</div>
-					<div class="inline field tw-pl-4">
-						{{$unitCode := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeCode}}
-						<label>{{ctx.Locale.Tr "repo.settings.default_permission_everyone_access"}}</label>
-						<select name="default_code_everyone_access" class="ui selection dropdown">
-							{{/* everyone access mode is different from others, none means it is unset and won't be applied */}}
-							<option value="none" {{Iif (eq $unitCode.EveryoneAccessMode 0) "selected"}}>{{ctx.Locale.Tr "settings.permission_not_set"}}</option>
-							<option value="read" {{Iif (eq $unitCode.EveryoneAccessMode 1) "selected"}}>{{ctx.Locale.Tr "settings.permission_read"}}</option>
-						</select>
-					</div>
 				</div>
 
 				{{$isInternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeWiki}}
@@ -346,16 +337,6 @@
 							<label>{{ctx.Locale.Tr "repo.settings.default_wiki_branch_name"}}</label>
 							<input name="default_wiki_branch" value="{{.Repository.DefaultWikiBranch}}">
 						</div>
-						<div class="inline field">
-							{{$unitInternalWiki := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki}}
-							<label>{{ctx.Locale.Tr "repo.settings.default_permission_everyone_access"}}</label>
-							<select name="default_wiki_everyone_access" class="ui selection dropdown">
-								{{/* everyone access mode is different from others, none means it is unset and won't be applied */}}
-								<option value="none" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 0) "selected"}}>{{ctx.Locale.Tr "settings.permission_not_set"}}</option>
-								<option value="read" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 1) "selected"}}>{{ctx.Locale.Tr "settings.permission_read"}}</option>
-								<option value="write" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 2) "selected"}}>{{ctx.Locale.Tr "settings.permission_write"}}</option>
-							</select>
-						</div>
 					</div>
 					<div class="field">
 						<div class="ui radio checkbox{{if $isExternalWikiGlobalDisabled}} disabled{{end}}"{{if $isExternalWikiGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
@@ -391,15 +372,6 @@
 						</div>
 					</div>
 					<div class="field tw-pl-4 {{if (.Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeExternalTracker)}}disabled{{end}}" id="internal_issue_box">
-						<div class="inline field">
-							{{$unitIssue := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeIssues}}
-							<label>{{ctx.Locale.Tr "repo.settings.default_permission_everyone_access"}}</label>
-							<select name="default_issues_everyone_access" class="ui selection dropdown">
-								{{/* everyone access mode is different from others, none means it is unset and won't be applied */}}
-								<option value="none" {{Iif (eq $unitIssue.EveryoneAccessMode 0) "selected"}}>{{ctx.Locale.Tr "settings.permission_not_set"}}</option>
-								<option value="read" {{Iif (eq $unitIssue.EveryoneAccessMode 1) "selected"}}>{{ctx.Locale.Tr "settings.permission_read"}}</option>
-							</select>
-						</div>
 						{{if .Repository.CanEnableTimetracker}}
 							<div class="field">
 								<div class="ui checkbox">
diff --git a/templates/repo/settings/public_access.tmpl b/templates/repo/settings/public_access.tmpl
new file mode 100644
index 0000000000..5c80796931
--- /dev/null
+++ b/templates/repo/settings/public_access.tmpl
@@ -0,0 +1,36 @@
+{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings")}}
+<div class="repo-setting-content">
+	{{$paNotSet := "not-set"}}
+	{{$paAnonymousRead := "anonymous-read"}}
+	{{$paEveryoneRead := "everyone-read"}}
+	{{$paEveryoneWrite := "everyone-write"}}
+	<form class="ui form" method="post">
+		{{.CsrfTokenHtml}}
+		<table class="ui table unstackable tw-my-2">
+			<tr>
+				<th></th>
+				<th>{{ctx.Locale.Tr "settings.permission_not_set"}}</th>
+				<th>{{ctx.Locale.Tr "settings.permission_anonymous_read"}}</th>
+				<th>{{ctx.Locale.Tr "settings.permission_everyone_read"}}</th>
+				<th>{{ctx.Locale.Tr "settings.permission_everyone_write"}}</th>
+			</tr>
+			{{range $ua := .RepoUnitPublicAccesses}}
+				<tr>
+					<td>{{$ua.DisplayName}}</td>
+					<td class="tw-text-center"><label><input type="radio" name="{{$ua.FormKey}}" value="{{$paNotSet}}" {{Iif (eq $paNotSet $ua.UnitPublicAccess) "checked"}}></label></td>
+					<td class="tw-text-center"><label><input type="radio" name="{{$ua.FormKey}}" value="{{$paAnonymousRead}}" {{Iif (eq $paAnonymousRead $ua.UnitPublicAccess) "checked"}}></label></td>
+					<td class="tw-text-center"><label><input type="radio" name="{{$ua.FormKey}}" value="{{$paEveryoneRead}}" {{Iif (eq $paEveryoneRead $ua.UnitPublicAccess) "checked"}}></label></td>
+					<td class="tw-text-center">
+						{{if SliceUtils.Contains $ua.PublicAccessTypes $paEveryoneWrite}}
+							<label><input type="radio" name="{{$ua.FormKey}}" value="{{$paEveryoneWrite}}" {{Iif (eq $paEveryoneWrite $ua.UnitPublicAccess) "checked"}}></label>
+						{{else}}
+							-
+						{{end}}
+					</td>
+				</tr>
+			{{end}}
+		</table>
+		<button class="ui primary button {{if .GlobalForcePrivate}}disabled{{end}}">{{ctx.Locale.Tr "repo.settings.update_settings"}}</button>
+	</form>
+</div>
+{{template "repo/settings/layout_footer" .}}
diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go
index a5728ffcbd..9a3cb988a6 100644
--- a/tests/integration/repo_test.go
+++ b/tests/integration/repo_test.go
@@ -7,10 +7,12 @@ import (
 	"fmt"
 	"net/http"
 	"path"
+	"strconv"
 	"strings"
 	"testing"
 	"time"
 
+	"code.gitea.io/gitea/models/unit"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/tests"
@@ -19,8 +21,26 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestViewRepo(t *testing.T) {
+func TestRepoView(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
+	t.Run("ViewRepoPublic", testViewRepoPublic)
+	t.Run("ViewRepoWithCache", testViewRepoWithCache)
+	t.Run("ViewRepoPrivate", testViewRepoPrivate)
+	t.Run("ViewRepo1CloneLinkAnonymous", testViewRepo1CloneLinkAnonymous)
+	t.Run("ViewRepo1CloneLinkAuthorized", testViewRepo1CloneLinkAuthorized)
+	t.Run("ViewRepoWithSymlinks", testViewRepoWithSymlinks)
+	t.Run("ViewFileInRepo", testViewFileInRepo)
+	t.Run("BlameFileInRepo", testBlameFileInRepo)
+	t.Run("ViewRepoDirectory", testViewRepoDirectory)
+	t.Run("ViewRepoDirectoryReadme", testViewRepoDirectoryReadme)
+	t.Run("MarkDownReadmeImage", testMarkDownReadmeImage)
+	t.Run("MarkDownReadmeImageSubfolder", testMarkDownReadmeImageSubfolder)
+	t.Run("GeneratedSourceLink", testGeneratedSourceLink)
+	t.Run("ViewCommit", testViewCommit)
+}
+
+func testViewRepoPublic(t *testing.T) {
+	defer tests.PrintCurrentTest(t)()
 
 	session := loginUser(t, "user2")
 
@@ -41,87 +61,118 @@ func TestViewRepo(t *testing.T) {
 	session.MakeRequest(t, req, http.StatusNotFound)
 }
 
-func testViewRepo(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
+func testViewRepoWithCache(t *testing.T) {
+	defer tests.PrintCurrentTest(t)()
+	testView := func(t *testing.T) {
+		req := NewRequest(t, "GET", "/org3/repo3")
+		session := loginUser(t, "user2")
+		resp := session.MakeRequest(t, req, http.StatusOK)
 
-	req := NewRequest(t, "GET", "/org3/repo3")
-	session := loginUser(t, "user2")
-	resp := session.MakeRequest(t, req, http.StatusOK)
+		htmlDoc := NewHTMLParser(t, resp.Body)
+		files := htmlDoc.doc.Find("#repo-files-table .repo-file-item")
 
-	htmlDoc := NewHTMLParser(t, resp.Body)
-	files := htmlDoc.doc.Find("#repo-files-table .repo-file-item")
+		type file struct {
+			fileName   string
+			commitID   string
+			commitMsg  string
+			commitTime string
+		}
 
-	type file struct {
-		fileName   string
-		commitID   string
-		commitMsg  string
-		commitTime string
-	}
+		var items []file
 
-	var items []file
+		files.Each(func(i int, s *goquery.Selection) {
+			tds := s.Find(".repo-file-cell")
+			var f file
+			tds.Each(func(i int, s *goquery.Selection) {
+				if i == 0 {
+					f.fileName = strings.TrimSpace(s.Text())
+				} else if i == 1 {
+					a := s.Find("a")
+					f.commitMsg = strings.TrimSpace(a.Text())
+					l, _ := a.Attr("href")
+					f.commitID = path.Base(l)
+				}
+			})
 
-	files.Each(func(i int, s *goquery.Selection) {
-		tds := s.Find(".repo-file-cell")
-		var f file
-		tds.Each(func(i int, s *goquery.Selection) {
-			if i == 0 {
-				f.fileName = strings.TrimSpace(s.Text())
-			} else if i == 1 {
-				a := s.Find("a")
-				f.commitMsg = strings.TrimSpace(a.Text())
-				l, _ := a.Attr("href")
-				f.commitID = path.Base(l)
-			}
+			// convert "2017-06-14 21:54:21 +0800" to "Wed, 14 Jun 2017 13:54:21 UTC"
+			htmlTimeString, _ := s.Find("relative-time").Attr("datetime")
+			htmlTime, _ := time.Parse(time.RFC3339, htmlTimeString)
+			f.commitTime = htmlTime.In(time.Local).Format(time.RFC1123)
+			items = append(items, f)
 		})
 
-		// convert "2017-06-14 21:54:21 +0800" to "Wed, 14 Jun 2017 13:54:21 UTC"
-		htmlTimeString, _ := s.Find("relative-time").Attr("datetime")
-		htmlTime, _ := time.Parse(time.RFC3339, htmlTimeString)
-		f.commitTime = htmlTime.In(time.Local).Format(time.RFC1123)
-		items = append(items, f)
-	})
+		commitT := time.Date(2017, time.June, 14, 13, 54, 21, 0, time.UTC).In(time.Local).Format(time.RFC1123)
+		assert.EqualValues(t, []file{
+			{
+				fileName:   "doc",
+				commitID:   "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6",
+				commitMsg:  "init project",
+				commitTime: commitT,
+			},
+			{
+				fileName:   "README.md",
+				commitID:   "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6",
+				commitMsg:  "init project",
+				commitTime: commitT,
+			},
+		}, items)
+	}
 
-	commitT := time.Date(2017, time.June, 14, 13, 54, 21, 0, time.UTC).In(time.Local).Format(time.RFC1123)
-	assert.EqualValues(t, []file{
-		{
-			fileName:   "doc",
-			commitID:   "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6",
-			commitMsg:  "init project",
-			commitTime: commitT,
-		},
-		{
-			fileName:   "README.md",
-			commitID:   "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6",
-			commitMsg:  "init project",
-			commitTime: commitT,
-		},
-	}, items)
-}
-
-func TestViewRepo2(t *testing.T) {
+	// FIXME: these test don't seem quite right, no enough assert
 	// no last commit cache
-	testViewRepo(t)
-
+	testView(t)
 	// enable last commit cache for all repositories
 	oldCommitsCount := setting.CacheService.LastCommit.CommitsCount
 	setting.CacheService.LastCommit.CommitsCount = 0
 	// first view will not hit the cache
-	testViewRepo(t)
+	testView(t)
 	// second view will hit the cache
-	testViewRepo(t)
+	testView(t)
 	setting.CacheService.LastCommit.CommitsCount = oldCommitsCount
 }
 
-func TestViewRepo3(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
+func testViewRepoPrivate(t *testing.T) {
+	defer tests.PrintCurrentTest(t)()
 
 	req := NewRequest(t, "GET", "/org3/repo3")
-	session := loginUser(t, "user4")
-	session.MakeRequest(t, req, http.StatusOK)
+	MakeRequest(t, req, http.StatusNotFound)
+
+	t.Run("OrgMemberAccess", func(t *testing.T) {
+		req = NewRequest(t, "GET", "/org3/repo3")
+		session := loginUser(t, "user4")
+		resp := session.MakeRequest(t, req, http.StatusOK)
+		assert.Contains(t, resp.Body.String(), `<div id="repo-files-table"`)
+	})
+
+	t.Run("PublicAccess-AnonymousAccess", func(t *testing.T) {
+		session := loginUser(t, "user1")
+
+		// set unit code to "anonymous read"
+		req = NewRequestWithValues(t, "POST", "/org3/repo3/settings/public_access", map[string]string{
+			"_csrf": GetUserCSRFToken(t, session),
+			"repo-unit-access-" + strconv.Itoa(int(unit.TypeCode)): "anonymous-read",
+		})
+		session.MakeRequest(t, req, http.StatusSeeOther)
+
+		// try to "anonymous read" (ok)
+		req = NewRequest(t, "GET", "/org3/repo3")
+		resp := MakeRequest(t, req, http.StatusOK)
+		assert.Contains(t, resp.Body.String(), `<span class="ui basic orange label">Public Access</span>`)
+
+		// remove "anonymous read"
+		req = NewRequestWithValues(t, "POST", "/org3/repo3/settings/public_access", map[string]string{
+			"_csrf": GetUserCSRFToken(t, session),
+		})
+		session.MakeRequest(t, req, http.StatusSeeOther)
+
+		// try to "anonymous read" (not found)
+		req = NewRequest(t, "GET", "/org3/repo3")
+		MakeRequest(t, req, http.StatusNotFound)
+	})
 }
 
-func TestViewRepo1CloneLinkAnonymous(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
+func testViewRepo1CloneLinkAnonymous(t *testing.T) {
+	defer tests.PrintCurrentTest(t)()
 
 	req := NewRequest(t, "GET", "/user2/repo1")
 	resp := MakeRequest(t, req, http.StatusOK)
@@ -139,8 +190,8 @@ func TestViewRepo1CloneLinkAnonymous(t *testing.T) {
 	assert.Equal(t, "tea clone user2/repo1", link)
 }
 
-func TestViewRepo1CloneLinkAuthorized(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
+func testViewRepo1CloneLinkAuthorized(t *testing.T) {
+	defer tests.PrintCurrentTest(t)()
 
 	session := loginUser(t, "user2")
 
@@ -162,8 +213,8 @@ func TestViewRepo1CloneLinkAuthorized(t *testing.T) {
 	assert.Equal(t, "tea clone user2/repo1", link)
 }
 
-func TestViewRepoWithSymlinks(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
+func testViewRepoWithSymlinks(t *testing.T) {
+	defer tests.PrintCurrentTest(t)()
 	defer test.MockVariableValue(&setting.UI.FileIconTheme, "basic")()
 	session := loginUser(t, "user2")
 
@@ -186,8 +237,8 @@ func TestViewRepoWithSymlinks(t *testing.T) {
 }
 
 // TestViewFileInRepo repo description, topics and summary should not be displayed when viewing a file
-func TestViewFileInRepo(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
+func testViewFileInRepo(t *testing.T) {
+	defer tests.PrintCurrentTest(t)()
 
 	session := loginUser(t, "user2")
 
@@ -205,8 +256,8 @@ func TestViewFileInRepo(t *testing.T) {
 }
 
 // TestBlameFileInRepo repo description, topics and summary should not be displayed when running blame on a file
-func TestBlameFileInRepo(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
+func testBlameFileInRepo(t *testing.T) {
+	defer tests.PrintCurrentTest(t)()
 
 	session := loginUser(t, "user2")
 
@@ -224,8 +275,8 @@ func TestBlameFileInRepo(t *testing.T) {
 }
 
 // TestViewRepoDirectory repo description, topics and summary should not be displayed when within a directory
-func TestViewRepoDirectory(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
+func testViewRepoDirectory(t *testing.T) {
+	defer tests.PrintCurrentTest(t)()
 
 	session := loginUser(t, "user2")
 
@@ -246,8 +297,8 @@ func TestViewRepoDirectory(t *testing.T) {
 }
 
 // ensure that the all the different ways to find and render a README work
-func TestViewRepoDirectoryReadme(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
+func testViewRepoDirectoryReadme(t *testing.T) {
+	defer tests.PrintCurrentTest(t)()
 
 	// there are many combinations:
 	// - READMEs can be .md, .txt, or have no extension
@@ -353,8 +404,8 @@ func TestViewRepoDirectoryReadme(t *testing.T) {
 	missing("symlink-loop", "/user2/readme-test/src/branch/symlink-loop/")
 }
 
-func TestMarkDownReadmeImage(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
+func testMarkDownReadmeImage(t *testing.T) {
+	defer tests.PrintCurrentTest(t)()
 
 	session := loginUser(t, "user2")
 
@@ -375,8 +426,8 @@ func TestMarkDownReadmeImage(t *testing.T) {
 	assert.Equal(t, "/user2/repo1/media/branch/home-md-img-check/test-fake-img.jpg", src)
 }
 
-func TestMarkDownReadmeImageSubfolder(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
+func testMarkDownReadmeImageSubfolder(t *testing.T) {
+	defer tests.PrintCurrentTest(t)()
 
 	session := loginUser(t, "user2")
 
@@ -398,8 +449,8 @@ func TestMarkDownReadmeImageSubfolder(t *testing.T) {
 	assert.Equal(t, "/user2/repo1/media/branch/sub-home-md-img-check/docs/test-fake-img.jpg", src)
 }
 
-func TestGeneratedSourceLink(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
+func testGeneratedSourceLink(t *testing.T) {
+	defer tests.PrintCurrentTest(t)()
 
 	t.Run("Rendered file", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
@@ -434,8 +485,8 @@ func TestGeneratedSourceLink(t *testing.T) {
 	})
 }
 
-func TestViewCommit(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
+func testViewCommit(t *testing.T) {
+	defer tests.PrintCurrentTest(t)()
 
 	req := NewRequest(t, "GET", "/user2/repo1/commit/0123456789012345678901234567890123456789")
 	req.Header.Add("Accept", "text/html")