mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-16 14:35:09 +00:00
feat(org): add team visibility so org members can discover teams (#37680)
Closes #37670. Today, org members in Gitea only see teams they're a member of. In larger orgs that hurts onboarding and discoverability — there's no way to look up which team owns what without asking around. GitHub solves this with a per-team visibility setting; this PR brings the same model to Gitea. ## What changes - Every team gets a `visibility` setting: - `private` *(default)* — only team members and org owners can see the team. Same as today's behavior. - `limited` — listable by any member of the organization. Members and the repos the team has access to are visible too. Non-org-members still see nothing. - `public` — listable by any signed-in user. - The Owners team visibility is fixed and cannot be changed via settings. - Existing teams default to `private`, so this is a no-op for anyone who doesn't change anything. ## API - `Team`, `CreateTeamOption`, `EditTeamOption` all gain a `visibility` field (string enum: `private` | `limited` | `public`). - `GET /orgs/{org}/teams` and `/orgs/{org}/teams/search` now apply the same visibility rules as the web UI: - site admins and org owners still see every team - other org members see their own teams plus any `limited` or `public` team - `private` teams are no longer leaked through these endpoints - Swagger/OpenAPI specs regenerated. ## UI View from admin2 (not an owner): <img width="1669" height="726" src="https://github.com/user-attachments/assets/daf4bccb-644b-4426-b178-71963aeaf73b" /> View from admin (owner): <img width="2559" height="863" src="https://github.com/user-attachments/assets/4f22cebc-e9df-4fd2-8ed4-724d31fadb7a" /> --------- Signed-off-by: bircni <bircni@icloud.com> Co-authored-by: TheFox0x7 <thefox0x7@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
+123
-18
@@ -6,6 +6,7 @@ package openapi3gen
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/modules/json"
|
||||
@@ -25,10 +26,12 @@ var rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`)
|
||||
// Gitea-specific post-processing: file-schema fixups, URI formats,
|
||||
// deprecated flags, and shared-enum extraction.
|
||||
//
|
||||
// astEnumMap is a value-set-key → Go-type-name map (built by
|
||||
// ScanSwaggerEnumTypes). If a shared enum in the spec has no entry in the
|
||||
// map, Convert returns an error — no fallback naming.
|
||||
func Convert(swaggerJSON []byte, astEnumMap map[string]string) (*openapi3.T, error) {
|
||||
// astEnumMap is a value-set-key → Go-type-name(s) map (built by
|
||||
// ScanSwaggerEnumTypes). When a value set is shared by multiple Go types,
|
||||
// per-property disambiguation uses the x-go-enum-desc extension. If a shared
|
||||
// enum in the spec has no matching entry, Convert returns an error — no
|
||||
// fallback naming.
|
||||
func Convert(swaggerJSON []byte, astEnumMap map[string][]string) (*openapi3.T, error) {
|
||||
var swagger2 openapi2.T
|
||||
if err := json.Unmarshal(swaggerJSON, &swagger2); err != nil {
|
||||
return nil, fmt.Errorf("parsing swagger 2.0: %w", err)
|
||||
@@ -176,12 +179,24 @@ type enumUsage struct {
|
||||
// If the derived enum name collides with an existing component schema, or
|
||||
// no // swagger:enum annotation matches the value set, generation aborts
|
||||
// with an actionable error — there are no silent fallbacks.
|
||||
func extractSharedEnums(doc *openapi3.T, astEnumMap map[string]string) error {
|
||||
func extractSharedEnums(doc *openapi3.T, astEnumMap map[string][]string) error {
|
||||
if doc.Components == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
enumGroups := map[string][]enumUsage{}
|
||||
type groupKey struct {
|
||||
valueSet string
|
||||
typeName string
|
||||
}
|
||||
enumGroups := map[groupKey][]enumUsage{}
|
||||
groupOrder := []groupKey{} // deterministic iteration
|
||||
|
||||
addUsage := func(key groupKey, u enumUsage) {
|
||||
if _, seen := enumGroups[key]; !seen {
|
||||
groupOrder = append(groupOrder, key)
|
||||
}
|
||||
enumGroups[key] = append(enumGroups[key], u)
|
||||
}
|
||||
|
||||
for schemaName, schemaRef := range doc.Components.Schemas {
|
||||
if schemaRef.Value == nil {
|
||||
@@ -192,24 +207,31 @@ func extractSharedEnums(doc *openapi3.T, astEnumMap map[string]string) error {
|
||||
continue
|
||||
}
|
||||
if len(propRef.Value.Enum) > 1 && propRef.Value.Type.Is("string") {
|
||||
key := EnumKey(propRef.Value.Enum)
|
||||
enumGroups[key] = append(enumGroups[key], enumUsage{schemaName, propName, propRef, false})
|
||||
key := groupKey{
|
||||
valueSet: EnumKey(propRef.Value.Enum),
|
||||
typeName: extractEnumTypeName(propRef.Value, astEnumMap),
|
||||
}
|
||||
addUsage(key, enumUsage{schemaName, propName, propRef, false})
|
||||
}
|
||||
if propRef.Value.Type.Is("array") && propRef.Value.Items != nil &&
|
||||
propRef.Value.Items.Value != nil && propRef.Value.Items.Ref == "" &&
|
||||
len(propRef.Value.Items.Value.Enum) > 1 && propRef.Value.Items.Value.Type.Is("string") {
|
||||
key := EnumKey(propRef.Value.Items.Value.Enum)
|
||||
enumGroups[key] = append(enumGroups[key], enumUsage{schemaName, propName, propRef, true})
|
||||
key := groupKey{
|
||||
valueSet: EnumKey(propRef.Value.Items.Value.Enum),
|
||||
typeName: extractEnumTypeName(propRef.Value.Items.Value, astEnumMap),
|
||||
}
|
||||
addUsage(key, enumUsage{schemaName, propName, propRef, true})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key, usages := range enumGroups {
|
||||
for _, key := range groupOrder {
|
||||
usages := enumGroups[key]
|
||||
if len(usages) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
enumName, err := deriveEnumName(key, usages, astEnumMap)
|
||||
enumName, err := deriveEnumName(key.valueSet, key.typeName, usages, astEnumMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -257,12 +279,17 @@ func extractSharedEnums(doc *openapi3.T, astEnumMap map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// deriveEnumName looks up a shared enum's Go type name from astEnumMap by
|
||||
// value-set key. If no annotation matches, returns an error identifying the
|
||||
// offending properties and the fix.
|
||||
func deriveEnumName(key string, usages []enumUsage, astEnumMap map[string]string) (string, error) {
|
||||
if name, ok := astEnumMap[key]; ok {
|
||||
return name, nil
|
||||
// deriveEnumName looks up a shared enum's Go type name. If typeName is
|
||||
// non-empty (because we recovered it from x-go-enum-desc), it is used
|
||||
// directly. Otherwise the value-set must map to exactly one known type. On
|
||||
// failure, returns an error identifying the offending properties.
|
||||
func deriveEnumName(key, typeName string, usages []enumUsage, astEnumMap map[string][]string) (string, error) {
|
||||
if typeName != "" {
|
||||
return typeName, nil
|
||||
}
|
||||
names := astEnumMap[key]
|
||||
if len(names) == 1 {
|
||||
return names[0], nil
|
||||
}
|
||||
|
||||
props := map[string]bool{}
|
||||
@@ -273,9 +300,87 @@ func deriveEnumName(key string, usages []enumUsage, astEnumMap map[string]string
|
||||
for p := range props {
|
||||
propList = append(propList, p)
|
||||
}
|
||||
if len(names) > 1 {
|
||||
return "", fmt.Errorf(
|
||||
"value-set %q is shared by multiple swagger:enum types %v and could not be disambiguated for properties: %v; "+
|
||||
"ensure go-swagger emits x-go-enum-desc for those properties",
|
||||
key, names, propList,
|
||||
)
|
||||
}
|
||||
return "", fmt.Errorf(
|
||||
"no swagger:enum annotation matches value-set %q used by %d properties: %v; "+
|
||||
"fix by adding a named string type with // swagger:enum to modules/structs or modules/commitstatus",
|
||||
key, len(usages), propList,
|
||||
)
|
||||
}
|
||||
|
||||
// extractEnumTypeName recovers the Go type name a schema's enum came from by
|
||||
// parsing the property's x-go-enum-desc extension. go-swagger emits one line
|
||||
// per value as "<value> <ConstName>[ <free text>]"; the type is the longest
|
||||
// common prefix of the const names, narrowed to the candidate set in
|
||||
// astEnumMap. Returns "" if extraction is inconclusive.
|
||||
func extractEnumTypeName(s *openapi3.Schema, astEnumMap map[string][]string) string {
|
||||
if s == nil || s.Extensions == nil {
|
||||
return ""
|
||||
}
|
||||
raw, ok := s.Extensions["x-go-enum-desc"]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
desc, ok := raw.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
candidates := astEnumMap[EnumKey(s.Enum)]
|
||||
if len(candidates) == 0 {
|
||||
return ""
|
||||
}
|
||||
// Collect the const names (second whitespace-separated field per line).
|
||||
var consts []string
|
||||
for line := range strings.SplitSeq(desc, "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
consts = append(consts, fields[1])
|
||||
}
|
||||
}
|
||||
if len(consts) == 0 {
|
||||
return ""
|
||||
}
|
||||
// A candidate matches when it is a prefix of every const name AND the
|
||||
// first character after the prefix is an uppercase ASCII letter — this
|
||||
// rejects e.g. "Alpha" matching "Alphabet" (suffix "bet" starts lower)
|
||||
// while still accepting both "Alpha" and "AlphaPlus" against "AlphaPlusX"
|
||||
// (both prefixes valid). The most specific (longest) wins; ties return
|
||||
// "" so deriveEnumName surfaces the ambiguity rather than silently
|
||||
// picking a winner.
|
||||
ordered := append([]string(nil), candidates...)
|
||||
sort.Slice(ordered, func(i, j int) bool { return len(ordered[i]) > len(ordered[j]) })
|
||||
var matches []string
|
||||
for _, name := range ordered {
|
||||
ok := true
|
||||
for _, c := range consts {
|
||||
if !strings.HasPrefix(c, name) {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
suffix := c[len(name):]
|
||||
// Empty suffix means the const name exactly equals the type name — valid exact match.
|
||||
// A non-empty suffix must begin with an uppercase letter to reject incidental
|
||||
// prefix matches (e.g. "Alpha" should not match "Alphabet").
|
||||
if len(suffix) > 0 && (suffix[0] < 'A' || suffix[0] > 'Z') {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
matches = append(matches, name)
|
||||
}
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(matches) > 1 && len(matches[0]) == len(matches[1]) {
|
||||
return ""
|
||||
}
|
||||
return matches[0]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user