mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-25 20:42:46 +01:00
Merge pull request '[FEAT] Repository flags' (#2079) from algernon/forgejo:f/instance-flags into forgejo-dependency
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2079 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
commit
78287806e2
15 changed files with 610 additions and 0 deletions
|
@ -46,6 +46,8 @@ var migrations = []*Migration{
|
||||||
NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable),
|
NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable),
|
||||||
// v3 -> v4
|
// v3 -> v4
|
||||||
NewMigration("Add default_permissions to repo_unit", forgejo_v1_22.AddDefaultPermissionsToRepoUnit),
|
NewMigration("Add default_permissions to repo_unit", forgejo_v1_22.AddDefaultPermissionsToRepoUnit),
|
||||||
|
// v4 -> v5
|
||||||
|
NewMigration("create the forgejo_repo_flag table", forgejo_v1_22.CreateRepoFlagTable),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||||
|
|
22
models/forgejo_migrations/v1_22/v5.go
Normal file
22
models/forgejo_migrations/v1_22/v5.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_22 //nolint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RepoFlag struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RepoID int64 `xorm:"UNIQUE(s) INDEX"`
|
||||||
|
Name string `xorm:"UNIQUE(s) INDEX"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RepoFlag) TableName() string {
|
||||||
|
return "forgejo_repo_flag"
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateRepoFlagTable(x *xorm.Engine) error {
|
||||||
|
return x.Sync(new(RepoFlag))
|
||||||
|
}
|
102
models/repo/repo_flags.go
Normal file
102
models/repo/repo_flags.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RepoFlag represents a single flag against a repository
|
||||||
|
type RepoFlag struct { //revive:disable-line:exported
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RepoID int64 `xorm:"UNIQUE(s) INDEX"`
|
||||||
|
Name string `xorm:"UNIQUE(s) INDEX"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
db.RegisterModel(new(RepoFlag))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName provides the real table name
|
||||||
|
func (RepoFlag) TableName() string {
|
||||||
|
return "forgejo_repo_flag"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFlags returns the array of flags on the repo.
|
||||||
|
func (repo *Repository) ListFlags(ctx context.Context) ([]RepoFlag, error) {
|
||||||
|
var flags []RepoFlag
|
||||||
|
err := db.GetEngine(ctx).Table(&RepoFlag{}).Where("repo_id = ?", repo.ID).Find(&flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return flags, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFlagged returns whether a repo has any flags or not
|
||||||
|
func (repo *Repository) IsFlagged(ctx context.Context) bool {
|
||||||
|
has, _ := db.Exist[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID})
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFlag returns a single RepoFlag based on its name
|
||||||
|
func (repo *Repository) GetFlag(ctx context.Context, flagName string) (bool, *RepoFlag, error) {
|
||||||
|
flag, has, err := db.Get[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID, "name": flagName})
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
return has, flag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasFlag returns true if a repo has a given flag, false otherwise
|
||||||
|
func (repo *Repository) HasFlag(ctx context.Context, flagName string) bool {
|
||||||
|
has, _ := db.Exist[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID, "name": flagName})
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFlag adds a new flag to the repo
|
||||||
|
func (repo *Repository) AddFlag(ctx context.Context, flagName string) error {
|
||||||
|
return db.Insert(ctx, RepoFlag{
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Name: flagName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteFlag removes a flag from the repo
|
||||||
|
func (repo *Repository) DeleteFlag(ctx context.Context, flagName string) (int64, error) {
|
||||||
|
return db.DeleteByBean(ctx, &RepoFlag{RepoID: repo.ID, Name: flagName})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceAllFlags replaces all flags of a repo with a new set
|
||||||
|
func (repo *Repository) ReplaceAllFlags(ctx context.Context, flagNames []string) error {
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
if err := db.DeleteBeans(ctx, &RepoFlag{RepoID: repo.ID}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(flagNames) == 0 {
|
||||||
|
return committer.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
var flags []RepoFlag
|
||||||
|
for _, name := range flagNames {
|
||||||
|
flags = append(flags, RepoFlag{
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := db.Insert(ctx, &flags); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return committer.Commit()
|
||||||
|
}
|
114
models/repo/repo_flags_test.go
Normal file
114
models/repo/repo_flags_test.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRepositoryFlags(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
||||||
|
|
||||||
|
// ********************
|
||||||
|
// ** NEGATIVE TESTS **
|
||||||
|
// ********************
|
||||||
|
|
||||||
|
// Unless we add flags, the repo has none
|
||||||
|
flags, err := repo.ListFlags(db.DefaultContext)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, flags)
|
||||||
|
|
||||||
|
// If the repo has no flags, it is not flagged
|
||||||
|
flagged := repo.IsFlagged(db.DefaultContext)
|
||||||
|
assert.False(t, flagged)
|
||||||
|
|
||||||
|
// Trying to find a flag when there is none
|
||||||
|
has := repo.HasFlag(db.DefaultContext, "foo")
|
||||||
|
assert.False(t, has)
|
||||||
|
|
||||||
|
// Trying to retrieve a non-existent flag indicates not found
|
||||||
|
has, _, err = repo.GetFlag(db.DefaultContext, "foo")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, has)
|
||||||
|
|
||||||
|
// Deleting a non-existent flag fails
|
||||||
|
deleted, err := repo.DeleteFlag(db.DefaultContext, "no-such-flag")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(0), deleted)
|
||||||
|
|
||||||
|
// ********************
|
||||||
|
// ** POSITIVE TESTS **
|
||||||
|
// ********************
|
||||||
|
|
||||||
|
// Adding a flag works
|
||||||
|
err = repo.AddFlag(db.DefaultContext, "foo")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Adding it again fails
|
||||||
|
err = repo.AddFlag(db.DefaultContext, "foo")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Listing flags includes the one we added
|
||||||
|
flags, err = repo.ListFlags(db.DefaultContext)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, flags, 1)
|
||||||
|
assert.Equal(t, "foo", flags[0].Name)
|
||||||
|
|
||||||
|
// With a flag added, the repo is flagged
|
||||||
|
flagged = repo.IsFlagged(db.DefaultContext)
|
||||||
|
assert.True(t, flagged)
|
||||||
|
|
||||||
|
// The flag can be found
|
||||||
|
has = repo.HasFlag(db.DefaultContext, "foo")
|
||||||
|
assert.True(t, has)
|
||||||
|
|
||||||
|
// Added flag can be retrieved
|
||||||
|
_, flag, err := repo.GetFlag(db.DefaultContext, "foo")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "foo", flag.Name)
|
||||||
|
|
||||||
|
// Deleting a flag works
|
||||||
|
deleted, err = repo.DeleteFlag(db.DefaultContext, "foo")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), deleted)
|
||||||
|
|
||||||
|
// The list is now empty
|
||||||
|
flags, err = repo.ListFlags(db.DefaultContext)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, flags)
|
||||||
|
|
||||||
|
// Replacing an empty list works
|
||||||
|
err = repo.ReplaceAllFlags(db.DefaultContext, []string{"bar"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// The repo is now flagged with "bar"
|
||||||
|
has = repo.HasFlag(db.DefaultContext, "bar")
|
||||||
|
assert.True(t, has)
|
||||||
|
|
||||||
|
// Replacing a tag set with another works
|
||||||
|
err = repo.ReplaceAllFlags(db.DefaultContext, []string{"baz", "quux"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// The repo now has two tags
|
||||||
|
flags, err = repo.ListFlags(db.DefaultContext)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, flags, 2)
|
||||||
|
assert.Equal(t, "baz", flags[0].Name)
|
||||||
|
assert.Equal(t, "quux", flags[1].Name)
|
||||||
|
|
||||||
|
// Replacing flags with an empty set deletes all flags
|
||||||
|
err = repo.ReplaceAllFlags(db.DefaultContext, []string{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// The repo is now unflagged
|
||||||
|
flagged = repo.IsFlagged(db.DefaultContext)
|
||||||
|
assert.False(t, flagged)
|
||||||
|
}
|
|
@ -112,6 +112,9 @@ var (
|
||||||
Wiki []string
|
Wiki []string
|
||||||
DefaultTrustModel string
|
DefaultTrustModel string
|
||||||
} `ini:"repository.signing"`
|
} `ini:"repository.signing"`
|
||||||
|
|
||||||
|
SettableFlags []string
|
||||||
|
EnableFlags bool
|
||||||
}{
|
}{
|
||||||
DetectedCharsetsOrder: []string{
|
DetectedCharsetsOrder: []string{
|
||||||
"UTF-8",
|
"UTF-8",
|
||||||
|
@ -267,6 +270,8 @@ var (
|
||||||
Wiki: []string{"never"},
|
Wiki: []string{"never"},
|
||||||
DefaultTrustModel: "collaborator",
|
DefaultTrustModel: "collaborator",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
EnableFlags: false,
|
||||||
}
|
}
|
||||||
RepoRootPath string
|
RepoRootPath string
|
||||||
ScriptType = "bash"
|
ScriptType = "bash"
|
||||||
|
@ -369,4 +374,6 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
|
||||||
log.Error("Unrecognised repository download or clone method: %s", method)
|
log.Error("Unrecognised repository download or clone method: %s", method)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Repository.EnableFlags = sec.Key("ENABLE_FLAGS").MustBool()
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,9 @@ func NewFuncMap() template.FuncMap {
|
||||||
"AppDomain": func() string { // documented in mail-templates.md
|
"AppDomain": func() string { // documented in mail-templates.md
|
||||||
return setting.Domain
|
return setting.Domain
|
||||||
},
|
},
|
||||||
|
"RepoFlagsEnabled": func() bool {
|
||||||
|
return setting.Repository.EnableFlags
|
||||||
|
},
|
||||||
"AssetVersion": func() string {
|
"AssetVersion": func() string {
|
||||||
return setting.AssetVersion
|
return setting.AssetVersion
|
||||||
},
|
},
|
||||||
|
|
|
@ -937,6 +937,12 @@ visibility.private_tooltip = Visible only to members of organizations you have j
|
||||||
[repo]
|
[repo]
|
||||||
rss.must_be_on_branch = You must be on a branch to have an RSS feed.
|
rss.must_be_on_branch = You must be on a branch to have an RSS feed.
|
||||||
|
|
||||||
|
admin.manage_flags = Manage flags
|
||||||
|
admin.enabled_flags = Flags enabled for the repository:
|
||||||
|
admin.update_flags = Update flags
|
||||||
|
admin.failed_to_replace_flags = Failed to replace repository flags
|
||||||
|
admin.flags_replaced = Repository flags replaced
|
||||||
|
|
||||||
new_repo_helper = A repository contains all project files, including revision history. Already hosting one elsewhere? <a href="%s">Migrate repository.</a>
|
new_repo_helper = A repository contains all project files, including revision history. Already hosting one elsewhere? <a href="%s">Migrate repository.</a>
|
||||||
owner = Owner
|
owner = Owner
|
||||||
owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit.
|
owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit.
|
||||||
|
|
49
routers/web/repo/flags/manage.go
Normal file
49
routers/web/repo/flags/manage.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package flags
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tplRepoFlags base.TplName = "repo/flags"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Manage(ctx *context.Context) {
|
||||||
|
ctx.Data["IsRepoFlagsPage"] = true
|
||||||
|
ctx.Data["Title"] = ctx.Tr("repo.admin.manage_flags")
|
||||||
|
|
||||||
|
flags := map[string]bool{}
|
||||||
|
for _, f := range setting.Repository.SettableFlags {
|
||||||
|
flags[f] = false
|
||||||
|
}
|
||||||
|
repoFlags, _ := ctx.Repo.Repository.ListFlags(ctx)
|
||||||
|
for _, f := range repoFlags {
|
||||||
|
flags[f.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["Flags"] = flags
|
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, tplRepoFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ManagePost(ctx *context.Context) {
|
||||||
|
newFlags := ctx.FormStrings("flags")
|
||||||
|
|
||||||
|
err := ctx.Repo.Repository.ReplaceAllFlags(ctx, newFlags)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.admin.failed_to_replace_flags"))
|
||||||
|
log.Error("Error replacing repository flags for repo %d: %v", ctx.Repo.Repository.ID, err)
|
||||||
|
} else {
|
||||||
|
ctx.Flash.Success(ctx.Tr("repo.admin.flags_replaced"))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Redirect(ctx.Repo.Repository.HTMLURL() + "/flags")
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ import (
|
||||||
"code.gitea.io/gitea/routers/web/repo"
|
"code.gitea.io/gitea/routers/web/repo"
|
||||||
"code.gitea.io/gitea/routers/web/repo/actions"
|
"code.gitea.io/gitea/routers/web/repo/actions"
|
||||||
"code.gitea.io/gitea/routers/web/repo/badges"
|
"code.gitea.io/gitea/routers/web/repo/badges"
|
||||||
|
repo_flags "code.gitea.io/gitea/routers/web/repo/flags"
|
||||||
repo_setting "code.gitea.io/gitea/routers/web/repo/setting"
|
repo_setting "code.gitea.io/gitea/routers/web/repo/setting"
|
||||||
"code.gitea.io/gitea/routers/web/user"
|
"code.gitea.io/gitea/routers/web/user"
|
||||||
user_setting "code.gitea.io/gitea/routers/web/user/setting"
|
user_setting "code.gitea.io/gitea/routers/web/user/setting"
|
||||||
|
@ -1572,6 +1573,13 @@ func registerRoutes(m *web.Route) {
|
||||||
gitHTTPRouters(m)
|
gitHTTPRouters(m)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if setting.Repository.EnableFlags {
|
||||||
|
m.Group("/{username}/{reponame}/flags", func() {
|
||||||
|
m.Get("", repo_flags.Manage)
|
||||||
|
m.Post("", repo_flags.ManagePost)
|
||||||
|
}, adminReq, context.RepoAssignment, context.UnitTypes())
|
||||||
|
}
|
||||||
// ***** END: Repository *****
|
// ***** END: Repository *****
|
||||||
|
|
||||||
m.Group("/notifications", func() {
|
m.Group("/notifications", func() {
|
||||||
|
|
0
templates/custom/repo_flag_banners.tmpl
Normal file
0
templates/custom/repo_flag_banners.tmpl
Normal file
8
templates/repo/admin_flags.tmpl
Normal file
8
templates/repo/admin_flags.tmpl
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{{if .Repository.IsFlagged $.Context}}
|
||||||
|
<div class="ui info message" style="text-align: left">
|
||||||
|
<strong>{{ctx.Locale.Tr "repo.admin.enabled_flags"}}</strong>
|
||||||
|
{{range .Repository.ListFlags $.Context}}
|
||||||
|
<span class="ui label">{{.Name}}</span>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
33
templates/repo/flags.tmpl
Normal file
33
templates/repo/flags.tmpl
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{{template "base/head" .}}
|
||||||
|
<div role="main" aria-label="{{.Title}}" class="page-content repository">
|
||||||
|
{{template "repo/header" .}}
|
||||||
|
<div class="ui container">
|
||||||
|
{{template "base/alert" .}}
|
||||||
|
<div class="user-main-content twelve wide column">
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{ctx.Locale.Tr "repo.admin.manage_flags"}}
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<form class="ui form" action="{{.Link}}" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<strong>{{ctx.Locale.Tr "repo.admin.enabled_flags"}}</strong>
|
||||||
|
<div class="ui segment gt-pl-4">
|
||||||
|
{{range $flag, $checked := .Flags}}
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui checkbox{{if $checked}} checked{{end}}">
|
||||||
|
<input name="flags" type="checkbox" value="{{$flag}}" {{if $checked}}checked{{end}}>
|
||||||
|
<label>{{$flag}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<button class="ui primary button">{{ctx.Locale.Tr "repo.admin.update_flags"}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "base/footer" .}}
|
|
@ -227,6 +227,12 @@
|
||||||
|
|
||||||
{{template "custom/extra_tabs" .}}
|
{{template "custom/extra_tabs" .}}
|
||||||
|
|
||||||
|
{{if and RepoFlagsEnabled .SignedUser.IsAdmin}}
|
||||||
|
<a class="{{if .IsRepoFlagsPage}}active {{end}}item" href="{{.RepoLink}}/flags">
|
||||||
|
{{svg "octicon-milestone"}} {{ctx.Locale.Tr "repo.admin.manage_flags"}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{if .Permission.IsAdmin}}
|
{{if .Permission.IsAdmin}}
|
||||||
<a class="{{if .PageIsRepoSettings}}active {{end}}right item" href="{{.RepoLink}}/settings">
|
<a class="{{if .PageIsRepoSettings}}active {{end}}right item" href="{{.RepoLink}}/settings">
|
||||||
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
|
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
|
||||||
|
|
|
@ -52,6 +52,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{if RepoFlagsEnabled}}
|
||||||
|
{{template "custom/repo_flag_banners" .}}
|
||||||
|
{{if .SignedUser.IsAdmin}}
|
||||||
|
{{template "repo/admin_flags" .}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{if .Repository.IsArchived}}
|
{{if .Repository.IsArchived}}
|
||||||
<div class="ui warning message gt-text-center">
|
<div class="ui warning message gt-text-center">
|
||||||
{{if .Repository.ArchivedUnix.IsZero}}
|
{{if .Repository.ArchivedUnix.IsZero}}
|
||||||
|
|
242
tests/integration/repo_flags_test.go
Normal file
242
tests/integration/repo_flags_test.go
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
"code.gitea.io/gitea/routers"
|
||||||
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRepositoryFlagsUIDisabled(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
defer test.MockVariableValue(&setting.Repository.EnableFlags, false)()
|
||||||
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
|
|
||||||
|
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
|
||||||
|
session := loginUser(t, admin.Name)
|
||||||
|
|
||||||
|
// With the repo flags feature disabled, the /flags route is 404
|
||||||
|
req := NewRequest(t, "GET", "/user2/repo1/flags")
|
||||||
|
session.MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
|
||||||
|
// With the repo flags feature disabled, the "Modify flags" tab does not
|
||||||
|
// appear for instance admins
|
||||||
|
req = NewRequest(t, "GET", "/user2/repo1")
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
|
flagsLinkCount := doc.Find(fmt.Sprintf(`a[href="%s/flags"]`, "/user2/repo1")).Length()
|
||||||
|
assert.Equal(t, 0, flagsLinkCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepositoryFlagsUI(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
defer test.MockVariableValue(&setting.Repository.EnableFlags, true)()
|
||||||
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||||
|
|
||||||
|
// *******************
|
||||||
|
// ** Preparations **
|
||||||
|
// *******************
|
||||||
|
flaggedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
unflaggedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
|
|
||||||
|
// **************
|
||||||
|
// ** Helpers **
|
||||||
|
// **************
|
||||||
|
|
||||||
|
adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}).Name
|
||||||
|
flaggedOwner := "user2"
|
||||||
|
flaggedRepoURLStr := "/user2/repo1"
|
||||||
|
unflaggedOwner := "user5"
|
||||||
|
unflaggedRepoURLStr := "/user5/repo4"
|
||||||
|
otherUser := "user4"
|
||||||
|
|
||||||
|
ensureFlags := func(repo *repo_model.Repository, flags []string) func() {
|
||||||
|
repo.ReplaceAllFlags(db.DefaultContext, flags)
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
repo.ReplaceAllFlags(db.DefaultContext, flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests:
|
||||||
|
// - Presence of the link
|
||||||
|
// - Number of flags listed in the admin-only message box
|
||||||
|
// - Whether there's a link to /user/repo/flags
|
||||||
|
// - Whether /user/repo/flags is OK or Forbidden
|
||||||
|
assertFlagAccessAndCount := func(t *testing.T, user, repoURL string, hasAccess bool, expectedFlagCount int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var expectedLinkCount int
|
||||||
|
var expectedStatus int
|
||||||
|
if hasAccess {
|
||||||
|
expectedLinkCount = 1
|
||||||
|
expectedStatus = http.StatusOK
|
||||||
|
} else {
|
||||||
|
expectedLinkCount = 0
|
||||||
|
if user != "" {
|
||||||
|
expectedStatus = http.StatusForbidden
|
||||||
|
} else {
|
||||||
|
expectedStatus = http.StatusSeeOther
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp *httptest.ResponseRecorder
|
||||||
|
var session *TestSession
|
||||||
|
req := NewRequest(t, "GET", repoURL)
|
||||||
|
if user != "" {
|
||||||
|
session = loginUser(t, user)
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
} else {
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
}
|
||||||
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
flagsLinkCount := doc.Find(fmt.Sprintf(`a[href="%s/flags"]`, repoURL)).Length()
|
||||||
|
assert.Equal(t, expectedLinkCount, flagsLinkCount)
|
||||||
|
|
||||||
|
flagCount := doc.Find(".ui.info.message .ui.label").Length()
|
||||||
|
assert.Equal(t, expectedFlagCount, flagCount)
|
||||||
|
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/flags", repoURL))
|
||||||
|
if user != "" {
|
||||||
|
session.MakeRequest(t, req, expectedStatus)
|
||||||
|
} else {
|
||||||
|
MakeRequest(t, req, expectedStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensures that given a repo owner and a repo:
|
||||||
|
// - An instance admin has access to flags, and sees the list on the repo home
|
||||||
|
// - A repo admin does not have access to either, and does not see the list
|
||||||
|
// - A passer by has no access to either, and does not see the list
|
||||||
|
runTests := func(t *testing.T, ownerUser, repoURL string, expectedFlagCount int) {
|
||||||
|
t.Run("as instance admin", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
assertFlagAccessAndCount(t, adminUser, repoURL, true, expectedFlagCount)
|
||||||
|
})
|
||||||
|
t.Run("as owner", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
assertFlagAccessAndCount(t, ownerUser, repoURL, false, 0)
|
||||||
|
})
|
||||||
|
t.Run("as other user", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
assertFlagAccessAndCount(t, otherUser, repoURL, false, 0)
|
||||||
|
})
|
||||||
|
t.Run("as non-logged in user", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
assertFlagAccessAndCount(t, "", repoURL, false, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************
|
||||||
|
// ** The tests themselves **
|
||||||
|
// **************************
|
||||||
|
t.Run("unflagged repo", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer ensureFlags(unflaggedRepo, []string{})()
|
||||||
|
|
||||||
|
runTests(t, unflaggedOwner, unflaggedRepoURLStr, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("flagged repo", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer ensureFlags(flaggedRepo, []string{"test-flag"})()
|
||||||
|
|
||||||
|
runTests(t, flaggedOwner, flaggedRepoURLStr, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("modifying flags", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
session := loginUser(t, adminUser)
|
||||||
|
flaggedRepoManageURL := fmt.Sprintf("%s/flags", flaggedRepoURLStr)
|
||||||
|
unflaggedRepoManageURL := fmt.Sprintf("%s/flags", unflaggedRepoURLStr)
|
||||||
|
|
||||||
|
assertUIFlagStates := func(t *testing.T, url string, flagStates map[string]bool) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", url)
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
doc := NewHTMLParser(t, resp.Body)
|
||||||
|
flagBoxes := doc.Find(`input[name="flags"]`)
|
||||||
|
assert.Equal(t, len(flagStates), flagBoxes.Length())
|
||||||
|
|
||||||
|
for name, state := range flagStates {
|
||||||
|
_, checked := doc.Find(fmt.Sprintf(`input[value="%s"]`, name)).Attr("checked")
|
||||||
|
assert.Equal(t, state, checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("flag presence on the UI", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer ensureFlags(flaggedRepo, []string{"test-flag"})()
|
||||||
|
|
||||||
|
assertUIFlagStates(t, flaggedRepoManageURL, map[string]bool{"test-flag": true})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("setting.Repository.SettableFlags is respected", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer test.MockVariableValue(&setting.Repository.SettableFlags, []string{"featured", "no-license"})()
|
||||||
|
defer ensureFlags(flaggedRepo, []string{"test-flag"})()
|
||||||
|
|
||||||
|
assertUIFlagStates(t, flaggedRepoManageURL, map[string]bool{
|
||||||
|
"test-flag": true,
|
||||||
|
"featured": false,
|
||||||
|
"no-license": false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("removing flags", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer ensureFlags(flaggedRepo, []string{"test-flag"})()
|
||||||
|
|
||||||
|
flagged := flaggedRepo.IsFlagged(db.DefaultContext)
|
||||||
|
assert.True(t, flagged)
|
||||||
|
|
||||||
|
req := NewRequestWithValues(t, "POST", flaggedRepoManageURL, map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, flaggedRepoManageURL),
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
|
||||||
|
flagged = flaggedRepo.IsFlagged(db.DefaultContext)
|
||||||
|
assert.False(t, flagged)
|
||||||
|
|
||||||
|
assertUIFlagStates(t, flaggedRepoManageURL, map[string]bool{})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adding flags", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer ensureFlags(unflaggedRepo, []string{})()
|
||||||
|
|
||||||
|
flagged := unflaggedRepo.IsFlagged(db.DefaultContext)
|
||||||
|
assert.False(t, flagged)
|
||||||
|
|
||||||
|
req := NewRequestWithValues(t, "POST", unflaggedRepoManageURL, map[string]string{
|
||||||
|
"_csrf": GetCSRF(t, session, unflaggedRepoManageURL),
|
||||||
|
"flags": "test-flag",
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
|
||||||
|
assertUIFlagStates(t, unflaggedRepoManageURL, map[string]bool{"test-flag": true})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in a new issue