feat(org): org-level tag protection, layered with per-repo protected tags (#727) #729
@@ -5,6 +5,7 @@
|
||||
### Added
|
||||
- Org branch protection: repositories now show the inherited organization rules read-only in their Branch Protection settings, with an expandable detail (direct push, force-push, branch deletion, merge restrictions, required approvals, status checks, protected files, and whitelisted teams) — like GitHub surfaces org rulesets in a repo (#727)
|
||||
- Org branch protection: org-level rules can now also protect against branch deletion (`enable_delete` + delete allowlist teams), mirroring the per-repo delete allowlist (#727)
|
||||
- Org-level tag protection: protect tag patterns org-wide (e.g. `v*`) with a team allowlist, layered on top of each repo's own protected tags — a tag is controllable only if allowed at both levels (fail-closed). API at `/orgs/{org}/tag_protections`; enforced at the git push/delete hook and the release create/delete paths; shown read-only in the repo Tag settings (#727)
|
||||
- Code security scanner: pattern-based detection of SQL injection, XSS, command injection, path traversal, insecure deserialization, hardcoded credentials, and weak cryptography across Go/PHP/Python/JS/TS (#552)
|
||||
- Cascade merge: auto-create PRs to downstream branches after merge with configurable rules per repo (#460)
|
||||
- Issue status presets: 4 built-in templates (default, software-development, support-tickets, bug-tracking) with API + web UI (#507)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
||||
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
||||
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// OrgProtectedTag represents an org-level tag protection rule. It cascades to all
|
||||
// repositories in the organization and layers on top of each repo's own protected
|
||||
// tags (a tag is controllable only if allowed at both levels). Org rules reference
|
||||
// teams only (like OrgProtectedBranch). See issue #727.
|
||||
type OrgProtectedTag struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"UNIQUE(s) index"`
|
||||
NamePattern string `xorm:"UNIQUE(s)"`
|
||||
AllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(OrgProtectedTag))
|
||||
}
|
||||
|
||||
// ToProtectedTag converts an org-level tag rule into a repo-scoped ProtectedTag so
|
||||
// the standard name-matching and allowlist logic can be reused. Org rules are
|
||||
// team-only, so the user allowlist is left empty.
|
||||
func (o *OrgProtectedTag) ToProtectedTag() *ProtectedTag {
|
||||
return &ProtectedTag{
|
||||
NamePattern: o.NamePattern,
|
||||
AllowlistTeamIDs: o.AllowlistTeamIDs,
|
||||
}
|
||||
}
|
||||
|
||||
// GetOrgProtectedTagByID retrieves a single org tag rule by org ID and rule ID.
|
||||
func GetOrgProtectedTagByID(ctx context.Context, orgID, id int64) (*OrgProtectedTag, error) {
|
||||
rule, exist, err := db.Get[OrgProtectedTag](ctx, builder.Eq{"org_id": orgID, "id": id})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !exist {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
// GetOrgProtectedTagByNamePattern retrieves a single org tag rule by its pattern.
|
||||
func GetOrgProtectedTagByNamePattern(ctx context.Context, orgID int64, pattern string) (*OrgProtectedTag, error) {
|
||||
rule, exist, err := db.Get[OrgProtectedTag](ctx, builder.Eq{"org_id": orgID, "name_pattern": pattern})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !exist {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
// FindOrgProtectedTags loads all org-level tag protection rules for an organization.
|
||||
func FindOrgProtectedTags(ctx context.Context, orgID int64) ([]*OrgProtectedTag, error) {
|
||||
var rules []*OrgProtectedTag
|
||||
err := db.GetEngine(ctx).Where("org_id = ?", orgID).Asc("created_unix").Find(&rules)
|
||||
return rules, err
|
||||
}
|
||||
|
||||
// CreateOrgProtectedTag creates a new org-level tag protection rule.
|
||||
func CreateOrgProtectedTag(ctx context.Context, rule *OrgProtectedTag) error {
|
||||
if _, err := db.GetEngine(ctx).Insert(rule); err != nil {
|
||||
return fmt.Errorf("Insert OrgProtectedTag: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateOrgProtectedTag updates an existing org-level tag protection rule.
|
||||
func UpdateOrgProtectedTag(ctx context.Context, rule *OrgProtectedTag) error {
|
||||
if _, err := db.GetEngine(ctx).ID(rule.ID).AllCols().Update(rule); err != nil {
|
||||
return fmt.Errorf("Update OrgProtectedTag: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteOrgProtectedTag deletes an org-level tag protection rule.
|
||||
func DeleteOrgProtectedTag(ctx context.Context, orgID, id int64) error {
|
||||
affected, err := db.GetEngine(ctx).Where("org_id = ? AND id = ?", orgID, id).Delete(new(OrgProtectedTag))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected == 0 {
|
||||
return fmt.Errorf("org tag protection rule ID(%d) not found", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsUserAllowedToControlTagInRepo layers org-level tag rules on top of a repo's own
|
||||
// protected tags: the user must be allowed by BOTH levels (most-restrictive). The
|
||||
// repo decision is evaluated first (using the already-loaded repoTags); if it
|
||||
// allows and the owner is an organization, the org-level rules must also allow.
|
||||
func IsUserAllowedToControlTagInRepo(ctx context.Context, repoTags []*ProtectedTag, repo *repo_model.Repository, tagName string, userID int64) (bool, error) {
|
||||
allowed, err := IsUserAllowedToControlTag(ctx, repoTags, tagName, userID)
|
||||
if err != nil || !allowed {
|
||||
return allowed, err
|
||||
}
|
||||
|
||||
owner, err := user_model.GetUserByID(ctx, repo.OwnerID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !owner.IsOrganization() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
orgRules, err := FindOrgProtectedTags(ctx, owner.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(orgRules) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
orgTags := make([]*ProtectedTag, len(orgRules))
|
||||
for i, r := range orgRules {
|
||||
orgTags[i] = r.ToProtectedTag()
|
||||
}
|
||||
return IsUserAllowedToControlTag(ctx, orgTags, tagName, userID)
|
||||
}
|
||||
@@ -440,6 +440,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(360, "Add delete allowlist to protected branch", v1_27.AddDeleteAllowlistToProtectedBranch),
|
||||
newMigration(361, "Add cascade merge rule table", v1_27.AddCascadeMergeRuleTable),
|
||||
newMigration(362, "Add delete allowlist to org protected branch", v1_27.AddDeleteAllowlistToOrgProtectedBranch),
|
||||
newMigration(363, "Add org protected tag table", v1_27.AddOrgProtectedTagTable),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1_27
|
||||
|
||||
import (
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// AddOrgProtectedTagTable creates the org-level tag protection table. Org tag rules
|
||||
// cascade to all repositories in the organization and layer on top of each repo's
|
||||
// own protected tags. See issue #727.
|
||||
func AddOrgProtectedTagTable(x *xorm.Engine) error {
|
||||
type OrgProtectedTag struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"UNIQUE(s) index"`
|
||||
NamePattern string `xorm:"UNIQUE(s)"`
|
||||
AllowlistTeamIDs []int64 `xorm:"JSON TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
return x.Sync(new(OrgProtectedTag))
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs
|
||||
|
||||
import "time"
|
||||
|
||||
// OrgTagProtection represents an org-level tag protection rule
|
||||
type OrgTagProtection struct {
|
||||
ID int64 `json:"id"`
|
||||
OrgID int64 `json:"org_id"`
|
||||
NamePattern string `json:"name_pattern"`
|
||||
WhitelistTeams []string `json:"whitelist_teams"`
|
||||
// swagger:strfmt date-time
|
||||
Created time.Time `json:"created_at"`
|
||||
// swagger:strfmt date-time
|
||||
Updated time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// CreateOrgTagProtectionOption options for creating an org-level tag protection
|
||||
type CreateOrgTagProtectionOption struct {
|
||||
NamePattern string `json:"name_pattern" binding:"Required"`
|
||||
WhitelistTeams []string `json:"whitelist_teams"`
|
||||
}
|
||||
|
||||
// EditOrgTagProtectionOption options for editing an org-level tag protection
|
||||
type EditOrgTagProtectionOption struct {
|
||||
NamePattern *string `json:"name_pattern"`
|
||||
WhitelistTeams []string `json:"whitelist_teams"`
|
||||
}
|
||||
@@ -2433,6 +2433,9 @@
|
||||
"repo.settings.org_protected_branch.block_outdated": "Block on outdated branch",
|
||||
"repo.settings.org_protected_branch.block_rejected": "Block on rejected reviews",
|
||||
"repo.settings.org_protected_branch.block_admin": "Block admin merge override",
|
||||
"repo.settings.org_protected_tag": "Organization Tag Protection",
|
||||
"repo.settings.org_protected_tag_desc": "These tag protection rules are defined by the organization and are enforced on top of this repository's own rules. They cannot be edited here.",
|
||||
"repo.settings.org_protected_tag.read_only": "Read-only",
|
||||
"repo.settings.protected_branch_can_push": "Allow push?",
|
||||
"repo.settings.protected_branch_can_push_yes": "You can push",
|
||||
"repo.settings.protected_branch_can_push_no": "You cannot push",
|
||||
|
||||
@@ -1823,6 +1823,16 @@ func Routes() *web.Router {
|
||||
})
|
||||
}, reqToken(), reqOrgOwnership())
|
||||
|
||||
m.Group("/tag_protections", func() {
|
||||
m.Combo("").Get(org.ListOrgTagProtections).
|
||||
Post(bind(api.CreateOrgTagProtectionOption{}), org.CreateOrgTagProtection)
|
||||
m.Group("/{id}", func() {
|
||||
m.Get("", org.GetOrgTagProtection)
|
||||
m.Patch("", bind(api.EditOrgTagProtectionOption{}), org.EditOrgTagProtection)
|
||||
m.Delete("", org.DeleteOrgTagProtection)
|
||||
})
|
||||
}, reqToken(), reqOrgOwnership())
|
||||
|
||||
m.Group("/blocks", func() {
|
||||
m.Get("", org.ListBlocks)
|
||||
m.Group("/{username}", func() {
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
git_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/git"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
|
||||
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/web"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
)
|
||||
|
||||
// toAPIOrgTagProtection converts an org tag rule to its API representation.
|
||||
func toAPIOrgTagProtection(ctx *context.APIContext, rule *git_model.OrgProtectedTag) *api.OrgTagProtection {
|
||||
teams, err := organization.FindOrgTeams(ctx, rule.OrgID)
|
||||
if err != nil {
|
||||
teams = nil
|
||||
}
|
||||
teamNamesByID := make(map[int64]string, len(teams))
|
||||
for _, t := range teams {
|
||||
teamNamesByID[t.ID] = t.Name
|
||||
}
|
||||
names := make([]string, 0, len(rule.AllowlistTeamIDs))
|
||||
for _, id := range rule.AllowlistTeamIDs {
|
||||
if name, ok := teamNamesByID[id]; ok {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
return &api.OrgTagProtection{
|
||||
ID: rule.ID,
|
||||
OrgID: rule.OrgID,
|
||||
NamePattern: rule.NamePattern,
|
||||
WhitelistTeams: names,
|
||||
Created: rule.CreatedUnix.AsTime(),
|
||||
Updated: rule.UpdatedUnix.AsTime(),
|
||||
}
|
||||
}
|
||||
|
||||
// ListOrgTagProtections list org-level tag protection rules
|
||||
func ListOrgTagProtections(ctx *context.APIContext) {
|
||||
rules, err := git_model.FindOrgProtectedTags(ctx, ctx.Org.Organization.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
apiRules := make([]*api.OrgTagProtection, len(rules))
|
||||
for i, rule := range rules {
|
||||
apiRules[i] = toAPIOrgTagProtection(ctx, rule)
|
||||
}
|
||||
ctx.JSON(http.StatusOK, apiRules)
|
||||
}
|
||||
|
||||
// GetOrgTagProtection get a specific org-level tag protection rule
|
||||
func GetOrgTagProtection(ctx *context.APIContext) {
|
||||
rule, err := git_model.GetOrgProtectedTagByID(ctx, ctx.Org.Organization.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if rule == nil {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, toAPIOrgTagProtection(ctx, rule))
|
||||
}
|
||||
|
||||
// CreateOrgTagProtection create an org-level tag protection rule
|
||||
func CreateOrgTagProtection(ctx *context.APIContext) {
|
||||
form := web.GetForm(ctx).(*api.CreateOrgTagProtectionOption)
|
||||
orgID := ctx.Org.Organization.ID
|
||||
|
||||
existing, err := git_model.GetOrgProtectedTagByNamePattern(ctx, orgID, form.NamePattern)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if existing != nil {
|
||||
ctx.APIError(http.StatusForbidden, "org tag protection rule already exists for this pattern")
|
||||
return
|
||||
}
|
||||
|
||||
teams, ok := resolveTeamIDs(ctx, orgID, form.WhitelistTeams)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
rule := &git_model.OrgProtectedTag{
|
||||
OrgID: orgID,
|
||||
NamePattern: form.NamePattern,
|
||||
AllowlistTeamIDs: teams,
|
||||
}
|
||||
if err := git_model.CreateOrgProtectedTag(ctx, rule); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusCreated, toAPIOrgTagProtection(ctx, rule))
|
||||
}
|
||||
|
||||
// EditOrgTagProtection edit an org-level tag protection rule
|
||||
func EditOrgTagProtection(ctx *context.APIContext) {
|
||||
form := web.GetForm(ctx).(*api.EditOrgTagProtectionOption)
|
||||
orgID := ctx.Org.Organization.ID
|
||||
|
||||
rule, err := git_model.GetOrgProtectedTagByID(ctx, orgID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if rule == nil {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if form.NamePattern != nil {
|
||||
rule.NamePattern = *form.NamePattern
|
||||
}
|
||||
if form.WhitelistTeams != nil {
|
||||
ids, ok := resolveTeamIDs(ctx, orgID, form.WhitelistTeams)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rule.AllowlistTeamIDs = ids
|
||||
}
|
||||
|
||||
if err := git_model.UpdateOrgProtectedTag(ctx, rule); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, toAPIOrgTagProtection(ctx, rule))
|
||||
}
|
||||
|
||||
// DeleteOrgTagProtection delete an org-level tag protection rule
|
||||
func DeleteOrgTagProtection(ctx *context.APIContext) {
|
||||
orgID := ctx.Org.Organization.ID
|
||||
rule, err := git_model.GetOrgProtectedTagByID(ctx, orgID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if rule == nil {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
if err := git_model.DeleteOrgProtectedTag(ctx, orgID, rule.ID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -468,7 +468,7 @@ func preReceiveTag(ctx *preReceiveContext, refFullName git.RefName) {
|
||||
ctx.gotProtectedTags = true
|
||||
}
|
||||
|
||||
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, ctx.protectedTags, tagName, ctx.opts.UserID)
|
||||
isAllowed, err := git_model.IsUserAllowedToControlTagInRepo(ctx, ctx.protectedTags, ctx.Repo.Repository, tagName, ctx.opts.UserID)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
|
||||
@@ -138,6 +138,13 @@ func DeleteProtectedTagPost(ctx *context.Context) {
|
||||
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/tags")
|
||||
}
|
||||
|
||||
// orgProtectedTagView is a read-only presentation of an org-level tag rule for the
|
||||
// repo settings page, with allowlist team IDs resolved to names.
|
||||
type orgProtectedTagView struct {
|
||||
Rule *git_model.OrgProtectedTag
|
||||
Teams string
|
||||
}
|
||||
|
||||
func setTagsContext(ctx *context.Context) error {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.tags")
|
||||
ctx.Data["PageIsSettingsTags"] = true
|
||||
@@ -163,6 +170,36 @@ func setTagsContext(ctx *context.Context) error {
|
||||
return err
|
||||
}
|
||||
ctx.Data["Teams"] = teams
|
||||
|
||||
// Surface the organization's tag protection rules read-only, so admins can see
|
||||
// the org "floor" layered on top of this repo's own protected tags (#727).
|
||||
orgRules, err := git_model.FindOrgProtectedTags(ctx, ctx.Repo.Owner.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindOrgProtectedTags", err)
|
||||
return err
|
||||
}
|
||||
if len(orgRules) > 0 {
|
||||
allTeams, err := organization.FindOrgTeams(ctx, ctx.Repo.Owner.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindOrgTeams", err)
|
||||
return err
|
||||
}
|
||||
teamNames := make(map[int64]string, len(allTeams))
|
||||
for _, t := range allTeams {
|
||||
teamNames[t.ID] = t.Name
|
||||
}
|
||||
views := make([]*orgProtectedTagView, len(orgRules))
|
||||
for i, r := range orgRules {
|
||||
names := make([]string, 0, len(r.AllowlistTeamIDs))
|
||||
for _, id := range r.AllowlistTeamIDs {
|
||||
if n, ok := teamNames[id]; ok {
|
||||
names = append(names, n)
|
||||
}
|
||||
}
|
||||
views[i] = &orgProtectedTagView{Rule: r, Teams: strings.Join(names, ", ")}
|
||||
}
|
||||
ctx.Data["OrgProtectedTags"] = views
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -92,7 +92,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel
|
||||
|
||||
// Trim '--' prefix to prevent command line argument vulnerability.
|
||||
rel.TagName = strings.TrimPrefix(rel.TagName, "--")
|
||||
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, rel.PublisherID)
|
||||
isAllowed, err := git_model.IsUserAllowedToControlTagInRepo(ctx, protectedTags, rel.Repo, rel.TagName, rel.PublisherID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -439,7 +439,7 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetProtectedTags: %w", err)
|
||||
}
|
||||
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, doer.ID)
|
||||
isAllowed, err := git_model.IsUserAllowedToControlTagInRepo(ctx, protectedTags, repo, rel.TagName, doer.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -116,6 +116,30 @@
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{if .OrgProtectedTags}}
|
||||
<h5 class="ui top attached header tw-mt-4">
|
||||
{{svg "octicon-organization" 14}} {{ctx.Locale.Tr "repo.settings.org_protected_tag"}}
|
||||
</h5>
|
||||
<div class="ui attached segment">
|
||||
<p class="tw-mb-3">{{ctx.Locale.Tr "repo.settings.org_protected_tag_desc"}}</p>
|
||||
<table class="ui single line table">
|
||||
<thead>
|
||||
<th>{{ctx.Locale.Tr "repo.settings.tags.protection.pattern"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.settings.tags.protection.allowed"}}</th>
|
||||
<th class="tw-text-right">{{ctx.Locale.Tr "repo.settings.org_protected_tag.read_only"}}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .OrgProtectedTags}}
|
||||
<tr>
|
||||
<td><pre>{{.Rule.NamePattern}}</pre></td>
|
||||
<td>{{if .Teams}}{{.Teams}}{{else}}{{ctx.Locale.Tr "repo.settings.tags.protection.allowed.noone"}}{{end}}</td>
|
||||
<td class="tw-text-right"><span class="text grey">{{svg "octicon-lock" 14}}</span></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user