From cddd19efc8c2ccbcd57e17811e3adebee7d49d60 Mon Sep 17 00:00:00 2001 From: wxiaoguang 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 API routes. Read the documentation 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 @@
{{svg "octicon-shield-lock" 18}}
{{end}} {{end}} + {{if $.Permission.HasAnyUnitPublicAccess}} + {{ctx.Locale.Tr "repo.desc.public_access"}} + {{end}} {{if .IsTemplate}} {{ctx.Locale.Tr "repo.desc.template"}}
{{svg "octicon-repo-template" 18}}
@@ -208,7 +211,7 @@ {{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)}} {{svg "octicon-pulse"}} {{ctx.Locale.Tr "repo.activity"}} 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}} -
- {{$unitCode := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeCode}} - - -
{{$isInternalWikiEnabled := .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeWiki}} @@ -346,16 +337,6 @@ -
- {{$unitInternalWiki := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki}} - - -
@@ -391,15 +372,6 @@
-
- {{$unitIssue := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeIssues}} - - -
{{if .Repository.CanEnableTimetracker}}
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")}} +
+ {{$paNotSet := "not-set"}} + {{$paAnonymousRead := "anonymous-read"}} + {{$paEveryoneRead := "everyone-read"}} + {{$paEveryoneWrite := "everyone-write"}} +
+ {{.CsrfTokenHtml}} + + + + + + + + + {{range $ua := .RepoUnitPublicAccesses}} + + + + + + + + {{end}} +
{{ctx.Locale.Tr "settings.permission_not_set"}}{{ctx.Locale.Tr "settings.permission_anonymous_read"}}{{ctx.Locale.Tr "settings.permission_everyone_read"}}{{ctx.Locale.Tr "settings.permission_everyone_write"}}
{{$ua.DisplayName}} + {{if SliceUtils.Contains $ua.PublicAccessTypes $paEveryoneWrite}} + + {{else}} + - + {{end}} +
+ +
+
+{{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(), `
Public Access`) + + // 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")