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)
pull/34054/head
wxiaoguang 2025-03-29 13:26:41 +08:00 committed by GitHub
parent 49899070cd
commit cddd19efc8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 410 additions and 206 deletions

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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")
}

View File

@ -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{
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{
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{
units = append(units, newRepoUnit(repo, unit_model.TypeIssues, &repo_model.IssuesConfig{
EnableTimetracker: form.EnableTimetracker,
AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
EnableDependencies: form.EnableIssueDependencies,
},
EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultIssuesEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead),
})
}))
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
} else {
if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
@ -613,49 +594,33 @@ 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{
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{
units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, &repo_model.PullRequestsConfig{
IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
AllowMerge: form.PullsAllowMerge,
AllowRebase: form.PullsAllowRebase,
@ -668,8 +633,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle),
DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit,
},
})
}))
} else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
}

View File

@ -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)

View File

@ -111,16 +111,13 @@ type RepoSettingForm struct {
// Advanced settings
EnableCode bool
DefaultCodeEveryoneAccess string
EnableWiki bool
EnableExternalWiki bool
DefaultWikiBranch string
DefaultWikiEveryoneAccess string
ExternalWikiURL string
EnableIssues bool
DefaultIssuesEveryoneAccess string
EnableExternalTracker bool
ExternalTrackerURL string
TrackerURLFormat string

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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" .}}

View File

@ -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,9 +61,9 @@ 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)
@ -96,32 +116,63 @@ func testViewRepo(t *testing.T) {
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")
MakeRequest(t, req, http.StatusNotFound)
t.Run("OrgMemberAccess", func(t *testing.T) {
req = NewRequest(t, "GET", "/org3/repo3")
session := loginUser(t, "user4")
session.MakeRequest(t, req, http.StatusOK)
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")