mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-06 08:26:41 -04:00
Serve OpenAPI 3.0 spec at /openapi.v1.json (#37038)
Add a build-time conversion step that transforms the existing Swagger 2.0 spec into an OpenAPI 3.0 spec. The OAS3 spec is served alongside the existing Swagger 2.0 spec, enabling API clients that require OAS3 to generate code directly from Gitea's API. This is not to be an answer to how gitea handles OAS3 long term, but a way to use what we have to move a step forward. --------- Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
+1
-1
@@ -18,7 +18,7 @@ indent_style = tab
|
||||
[templates/custom/*.tmpl]
|
||||
insert_final_newline = false
|
||||
|
||||
[templates/swagger/v1_json.tmpl]
|
||||
[templates/swagger/*_json.tmpl]
|
||||
indent_style = space
|
||||
insert_final_newline = false
|
||||
|
||||
|
||||
@@ -151,6 +151,7 @@ ESLINT_CONCURRENCY ?= 2
|
||||
SWAGGER_SPEC := templates/swagger/v1_json.tmpl
|
||||
SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json
|
||||
SWAGGER_EXCLUDE := code.gitea.io/sdk
|
||||
OPENAPI3_SPEC := templates/swagger/v1_openapi3_json.tmpl
|
||||
|
||||
TEST_MYSQL_HOST ?= mysql:3306
|
||||
TEST_MYSQL_DBNAME ?= testgitea
|
||||
@@ -233,7 +234,7 @@ TAGS_PREREQ := $(TAGS_EVIDENCE)
|
||||
endif
|
||||
|
||||
.PHONY: generate-swagger
|
||||
generate-swagger: $(SWAGGER_SPEC) ## generate the swagger spec from code comments
|
||||
generate-swagger: $(SWAGGER_SPEC) $(OPENAPI3_SPEC) ## generate the swagger spec from code comments
|
||||
|
||||
$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT)
|
||||
$(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)'
|
||||
@@ -255,6 +256,21 @@ swagger-validate: ## check if the swagger spec is valid
|
||||
$(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)'
|
||||
@$(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)' # remove the prefix slash from basePath
|
||||
|
||||
.PHONY: generate-openapi3
|
||||
generate-openapi3: $(OPENAPI3_SPEC) ## generate the OpenAPI 3.0 spec from the Swagger 2.0 spec
|
||||
|
||||
$(OPENAPI3_SPEC): $(SWAGGER_SPEC) build/generate-openapi.go $(wildcard build/openapi3gen/*.go)
|
||||
$(GO) run build/generate-openapi.go
|
||||
|
||||
.PHONY: openapi3-check
|
||||
openapi3-check: generate-openapi3
|
||||
@diff=$$(git diff --color=always '$(OPENAPI3_SPEC)'); \
|
||||
if [ -n "$$diff" ]; then \
|
||||
echo "Please run 'make generate-openapi3' and commit the result:"; \
|
||||
printf "%s" "$${diff}"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: checks
|
||||
checks: checks-frontend checks-backend ## run various consistency checks
|
||||
|
||||
@@ -262,7 +278,7 @@ checks: checks-frontend checks-backend ## run various consistency checks
|
||||
checks-frontend: lockfile-check svg-check ## check frontend files
|
||||
|
||||
.PHONY: checks-backend
|
||||
checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check ## check backend files
|
||||
checks-backend: tidy-check swagger-check openapi3-check fmt-check swagger-validate security-check ## check backend files
|
||||
|
||||
.PHONY: lint
|
||||
lint: lint-frontend lint-backend lint-spell ## lint everything
|
||||
|
||||
Generated
+40
File diff suppressed because one or more lines are too long
@@ -0,0 +1,97 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// generate-openapi converts Gitea's Swagger 2.0 spec into an OpenAPI 3.0 spec.
|
||||
//
|
||||
// Gitea generates a Swagger 2.0 spec from code annotations (make generate-swagger).
|
||||
// This tool converts it to OAS3 so that SDK generators and tools that require
|
||||
// OAS3 (e.g. progenitor for Rust) can consume it directly. The conversion also
|
||||
// deduplicates inline enum definitions into named schema components, producing
|
||||
// cleaner SDK output with proper enum types instead of anonymous strings.
|
||||
//
|
||||
// Run: go run build/generate-openapi.go
|
||||
// Output: templates/swagger/v1_openapi3_json.tmpl
|
||||
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/build/openapi3gen"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
const (
|
||||
swaggerSpecPath = "templates/swagger/v1_json.tmpl"
|
||||
openapi3OutPath = "templates/swagger/v1_openapi3_json.tmpl"
|
||||
|
||||
appSubUrlVar = "{{.SwaggerAppSubUrl}}"
|
||||
appVerVar = "{{.SwaggerAppVer}}"
|
||||
|
||||
appSubUrlPlaceholder = "GITEA_APP_SUB_URL_PLACEHOLDER"
|
||||
appVerPlaceholder = "0.0.0-gitea-placeholder"
|
||||
)
|
||||
|
||||
var (
|
||||
appSubUrlRe = regexp.MustCompile(regexp.QuoteMeta(appSubUrlVar))
|
||||
appVerRe = regexp.MustCompile(regexp.QuoteMeta(appVerVar))
|
||||
|
||||
enumScanDirs = []string{
|
||||
"modules/structs",
|
||||
"modules/commitstatus",
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
astEnumMap, err := openapi3gen.ScanSwaggerEnumTypes(enumScanDirs)
|
||||
if err != nil {
|
||||
log.Fatalf("scanning swagger:enum annotations: %v", err)
|
||||
}
|
||||
names := make([]string, 0, len(astEnumMap))
|
||||
for _, n := range astEnumMap {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
fmt.Fprintf(os.Stderr, "discovered %d swagger:enum types: %s\n", len(names), strings.Join(names, ", "))
|
||||
|
||||
data, err := os.ReadFile(swaggerSpecPath)
|
||||
if err != nil {
|
||||
log.Fatalf("reading swagger spec: %v", err)
|
||||
}
|
||||
|
||||
cleaned := appSubUrlRe.ReplaceAll(data, []byte(appSubUrlPlaceholder))
|
||||
cleaned = appVerRe.ReplaceAll(cleaned, []byte(appVerPlaceholder))
|
||||
|
||||
oas3, err := openapi3gen.Convert(cleaned, astEnumMap)
|
||||
if err != nil {
|
||||
log.Fatalf("converting to openapi 3.0: %v", err)
|
||||
}
|
||||
|
||||
oas3.Servers = openapi3.Servers{
|
||||
{URL: appSubUrlPlaceholder + "/api/v1"},
|
||||
}
|
||||
|
||||
out, err := json.MarshalIndent(oas3, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("marshaling openapi 3.0: %v", err)
|
||||
}
|
||||
|
||||
result := strings.ReplaceAll(string(out), appSubUrlPlaceholder, appSubUrlVar)
|
||||
result = strings.ReplaceAll(result, appVerPlaceholder, appVerVar)
|
||||
result = strings.TrimSpace(result)
|
||||
|
||||
if err := os.WriteFile(openapi3OutPath, []byte(result), 0o644); err != nil {
|
||||
log.Fatalf("writing openapi 3.0 spec: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Generated %s\n", openapi3OutPath)
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package openapi3gen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi2"
|
||||
"github.com/getkin/kin-openapi/openapi2conv"
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
// rxDeprecated matches "deprecated" as a word at the start of a description
|
||||
// or preceded by whitespace/punctuation that indicates a leading marker (e.g.
|
||||
// "Deprecated: true", "deprecated (use X instead)"). Rejects negated phrases
|
||||
// like "not deprecated" or "previously deprecated, now supported".
|
||||
var rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`)
|
||||
|
||||
// Convert parses a Swagger 2.0 spec and returns an OAS3 spec, applying
|
||||
// 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) {
|
||||
var swagger2 openapi2.T
|
||||
if err := json.Unmarshal(swaggerJSON, &swagger2); err != nil {
|
||||
return nil, fmt.Errorf("parsing swagger 2.0: %w", err)
|
||||
}
|
||||
|
||||
oas3, err := openapi2conv.ToV3(&swagger2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting to openapi 3.0: %w", err)
|
||||
}
|
||||
|
||||
fixFileSchemas(oas3)
|
||||
addURIFormats(oas3)
|
||||
addDeprecatedFlags(oas3)
|
||||
if err := extractSharedEnums(oas3, astEnumMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return oas3, nil
|
||||
}
|
||||
|
||||
func fixFileSchemas(doc *openapi3.T) {
|
||||
for _, pathItem := range doc.Paths.Map() {
|
||||
for _, op := range []*openapi3.Operation{
|
||||
pathItem.Get, pathItem.Post, pathItem.Put, pathItem.Patch,
|
||||
pathItem.Delete, pathItem.Head, pathItem.Options, pathItem.Trace,
|
||||
} {
|
||||
if op == nil {
|
||||
continue
|
||||
}
|
||||
for _, resp := range op.Responses.Map() {
|
||||
if resp.Value == nil {
|
||||
continue
|
||||
}
|
||||
for _, mediaType := range resp.Value.Content {
|
||||
fixSchema(mediaType.Schema)
|
||||
}
|
||||
}
|
||||
if op.RequestBody != nil && op.RequestBody.Value != nil {
|
||||
for _, mediaType := range op.RequestBody.Value.Content {
|
||||
fixSchema(mediaType.Schema)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fixSchema rewrites any "type: file" schemas to the OAS3 equivalent
|
||||
// (type: string, format: binary), recursing into Properties, Items, and
|
||||
// AllOf/OneOf/AnyOf/Not branches. $ref nodes are skipped so shared schemas
|
||||
// are rewritten exactly once when visited through their declaration.
|
||||
func fixSchema(ref *openapi3.SchemaRef) {
|
||||
if ref == nil || ref.Value == nil || ref.Ref != "" {
|
||||
return
|
||||
}
|
||||
s := ref.Value
|
||||
if s.Type.Is("file") {
|
||||
s.Type = &openapi3.Types{"string"}
|
||||
s.Format = "binary"
|
||||
}
|
||||
for _, p := range s.Properties {
|
||||
fixSchema(p)
|
||||
}
|
||||
fixSchema(s.Items)
|
||||
for _, sub := range s.AllOf {
|
||||
fixSchema(sub)
|
||||
}
|
||||
for _, sub := range s.OneOf {
|
||||
fixSchema(sub)
|
||||
}
|
||||
for _, sub := range s.AnyOf {
|
||||
fixSchema(sub)
|
||||
}
|
||||
fixSchema(s.Not)
|
||||
}
|
||||
|
||||
// addURIFormats sets format: uri on string properties whose names indicate
|
||||
// they hold URLs. This information is lost in Swagger 2.0 but is valuable
|
||||
// for code generators.
|
||||
func addURIFormats(doc *openapi3.T) {
|
||||
if doc.Components == nil {
|
||||
return
|
||||
}
|
||||
for _, schemaRef := range doc.Components.Schemas {
|
||||
if schemaRef.Value == nil {
|
||||
continue
|
||||
}
|
||||
for propName, propRef := range schemaRef.Value.Properties {
|
||||
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
|
||||
continue
|
||||
}
|
||||
prop := propRef.Value
|
||||
if !prop.Type.Is("string") || prop.Format != "" {
|
||||
continue
|
||||
}
|
||||
if isURLProperty(propName) {
|
||||
prop.Format = "uri"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isURLProperty(name string) bool {
|
||||
if strings.HasSuffix(name, "_url") {
|
||||
return true
|
||||
}
|
||||
switch name {
|
||||
case "url", "html_url", "clone_url":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// addDeprecatedFlags sets deprecated: true on schema properties whose
|
||||
// description starts with a "deprecated" marker (e.g. "Deprecated: true"
|
||||
// or "deprecated (use X instead)"). Does not match negated phrases.
|
||||
func addDeprecatedFlags(doc *openapi3.T) {
|
||||
if doc.Components == nil {
|
||||
return
|
||||
}
|
||||
for _, schemaRef := range doc.Components.Schemas {
|
||||
if schemaRef.Value == nil {
|
||||
continue
|
||||
}
|
||||
for _, propRef := range schemaRef.Value.Properties {
|
||||
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
|
||||
continue
|
||||
}
|
||||
if rxDeprecated.MatchString(propRef.Value.Description) {
|
||||
propRef.Value.Deprecated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type enumUsage struct {
|
||||
schemaName string
|
||||
propName string
|
||||
propRef *openapi3.SchemaRef
|
||||
inItems bool
|
||||
}
|
||||
|
||||
// extractSharedEnums finds identical enum arrays used by multiple schema
|
||||
// properties, creates a standalone named schema for each, and replaces
|
||||
// the inline enums with $ref pointers.
|
||||
//
|
||||
// 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 {
|
||||
if doc.Components == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
enumGroups := map[string][]enumUsage{}
|
||||
|
||||
for schemaName, schemaRef := range doc.Components.Schemas {
|
||||
if schemaRef.Value == nil {
|
||||
continue
|
||||
}
|
||||
for propName, propRef := range schemaRef.Value.Properties {
|
||||
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
|
||||
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})
|
||||
}
|
||||
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})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key, usages := range enumGroups {
|
||||
if len(usages) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
enumName, err := deriveEnumName(key, usages, astEnumMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, exists := doc.Components.Schemas[enumName]; exists {
|
||||
return fmt.Errorf("enum name collision: %s already exists as a component schema", enumName)
|
||||
}
|
||||
|
||||
var enumValues []any
|
||||
if usages[0].inItems {
|
||||
enumValues = usages[0].propRef.Value.Items.Value.Enum
|
||||
} else {
|
||||
enumValues = usages[0].propRef.Value.Enum
|
||||
}
|
||||
|
||||
doc.Components.Schemas[enumName] = &openapi3.SchemaRef{
|
||||
Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"string"},
|
||||
Enum: enumValues,
|
||||
},
|
||||
}
|
||||
|
||||
ref := "#/components/schemas/" + enumName
|
||||
|
||||
for _, usage := range usages {
|
||||
if usage.inItems {
|
||||
usage.propRef.Value.Items = &openapi3.SchemaRef{Ref: ref}
|
||||
} else {
|
||||
old := usage.propRef.Value
|
||||
if old.Description == "" && !old.Deprecated && old.Format == "" {
|
||||
usage.propRef.Ref = ref
|
||||
usage.propRef.Value = nil
|
||||
} else {
|
||||
usage.propRef.Value = &openapi3.Schema{
|
||||
AllOf: openapi3.SchemaRefs{
|
||||
{Ref: ref},
|
||||
},
|
||||
Description: old.Description,
|
||||
Deprecated: old.Deprecated,
|
||||
Format: old.Format,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
props := map[string]bool{}
|
||||
for _, u := range usages {
|
||||
props[fmt.Sprintf("%s.%s", u.schemaName, u.propName)] = true
|
||||
}
|
||||
propList := make([]string, 0, len(props))
|
||||
for p := range props {
|
||||
propList = append(propList, p)
|
||||
}
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package openapi3gen
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
)
|
||||
|
||||
func TestDeriveEnumName_hit(t *testing.T) {
|
||||
key := EnumKey([]any{"red", "green", "blue"})
|
||||
astMap := map[string]string{key: "Color"}
|
||||
usages := []enumUsage{{schemaName: "Paint", propName: "color"}}
|
||||
got, err := deriveEnumName(key, usages, astMap)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != "Color" {
|
||||
t.Fatalf("got %q, want %q", got, "Color")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveEnumName_miss(t *testing.T) {
|
||||
key := EnumKey([]any{"x", "y"})
|
||||
usages := []enumUsage{{schemaName: "Thing", propName: "kind"}}
|
||||
_, err := deriveEnumName(key, usages, map[string]string{})
|
||||
if err == nil {
|
||||
t.Fatal("expected miss error, got nil")
|
||||
}
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, "Thing.kind") {
|
||||
t.Fatalf("error %q should list the missing usage", msg)
|
||||
}
|
||||
if !strings.Contains(msg, "swagger:enum") {
|
||||
t.Fatalf("error %q should hint at the fix", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractSharedEnums_usesASTMap(t *testing.T) {
|
||||
doc := &openapi3.T{
|
||||
Components: &openapi3.Components{
|
||||
Schemas: openapi3.Schemas{
|
||||
"A": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"object"},
|
||||
Properties: openapi3.Schemas{
|
||||
"color": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"string"},
|
||||
Enum: []any{"red", "green", "blue"},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
"B": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"object"},
|
||||
Properties: openapi3.Schemas{
|
||||
"color": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"string"},
|
||||
Enum: []any{"red", "green", "blue"},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
astMap := map[string]string{EnumKey([]any{"red", "green", "blue"}): "Color"}
|
||||
if err := extractSharedEnums(doc, astMap); err != nil {
|
||||
t.Fatalf("extractSharedEnums: %v", err)
|
||||
}
|
||||
if _, ok := doc.Components.Schemas["Color"]; !ok {
|
||||
t.Fatalf("expected Color schema to be extracted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixFileSchemas_recursesIntoNested(t *testing.T) {
|
||||
fileType := func() *openapi3.SchemaRef {
|
||||
return &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"file"}}}
|
||||
}
|
||||
doc := &openapi3.T{
|
||||
Paths: openapi3.NewPaths(),
|
||||
}
|
||||
doc.Paths.Set("/upload", &openapi3.PathItem{
|
||||
Post: &openapi3.Operation{
|
||||
RequestBody: &openapi3.RequestBodyRef{
|
||||
Value: &openapi3.RequestBody{
|
||||
Content: openapi3.Content{
|
||||
"multipart/form-data": {
|
||||
Schema: &openapi3.SchemaRef{Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"object"},
|
||||
Properties: openapi3.Schemas{
|
||||
"attachment": fileType(),
|
||||
"items": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"array"},
|
||||
Items: fileType(),
|
||||
}},
|
||||
"alt": {Value: &openapi3.Schema{
|
||||
AllOf: openapi3.SchemaRefs{fileType()},
|
||||
}},
|
||||
"one": {Value: &openapi3.Schema{
|
||||
OneOf: openapi3.SchemaRefs{fileType()},
|
||||
}},
|
||||
"any": {Value: &openapi3.Schema{
|
||||
AnyOf: openapi3.SchemaRefs{fileType()},
|
||||
}},
|
||||
"not": {Value: &openapi3.Schema{
|
||||
Not: fileType(),
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Responses: openapi3.NewResponses(),
|
||||
},
|
||||
})
|
||||
|
||||
fixFileSchemas(doc)
|
||||
|
||||
props := doc.Paths.Value("/upload").Post.RequestBody.Value.Content["multipart/form-data"].Schema.Value.Properties
|
||||
if !props["attachment"].Value.Type.Is("string") || props["attachment"].Value.Format != "binary" {
|
||||
t.Errorf("nested property not fixed: %+v", props["attachment"].Value)
|
||||
}
|
||||
if !props["items"].Value.Items.Value.Type.Is("string") || props["items"].Value.Items.Value.Format != "binary" {
|
||||
t.Errorf("array items not fixed: %+v", props["items"].Value.Items.Value)
|
||||
}
|
||||
if !props["alt"].Value.AllOf[0].Value.Type.Is("string") || props["alt"].Value.AllOf[0].Value.Format != "binary" {
|
||||
t.Errorf("allOf branch not fixed: %+v", props["alt"].Value.AllOf[0].Value)
|
||||
}
|
||||
if !props["one"].Value.OneOf[0].Value.Type.Is("string") || props["one"].Value.OneOf[0].Value.Format != "binary" {
|
||||
t.Errorf("oneOf branch not fixed: %+v", props["one"].Value.OneOf[0].Value)
|
||||
}
|
||||
if !props["any"].Value.AnyOf[0].Value.Type.Is("string") || props["any"].Value.AnyOf[0].Value.Format != "binary" {
|
||||
t.Errorf("anyOf branch not fixed: %+v", props["any"].Value.AnyOf[0].Value)
|
||||
}
|
||||
if !props["not"].Value.Not.Value.Type.Is("string") || props["not"].Value.Not.Value.Format != "binary" {
|
||||
t.Errorf("not branch not fixed: %+v", props["not"].Value.Not.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractSharedEnums_missReturnsError(t *testing.T) {
|
||||
doc := &openapi3.T{
|
||||
Components: &openapi3.Components{
|
||||
Schemas: openapi3.Schemas{
|
||||
"A": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"object"},
|
||||
Properties: openapi3.Schemas{
|
||||
"color": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"string"},
|
||||
Enum: []any{"red", "green"},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
"B": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"object"},
|
||||
Properties: openapi3.Schemas{
|
||||
"color": {Value: &openapi3.Schema{
|
||||
Type: &openapi3.Types{"string"},
|
||||
Enum: []any{"red", "green"},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := extractSharedEnums(doc, map[string]string{}); err == nil {
|
||||
t.Fatal("expected miss error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package openapi3gen converts Gitea's Swagger 2.0 spec to an OpenAPI 3.0
|
||||
// spec. It discovers Go enum type names by scanning swagger:enum annotations
|
||||
// in the source tree, then names extracted shared-enum schemas accordingly.
|
||||
package openapi3gen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EnumKey returns a canonical key for a set of enum values: values are
|
||||
// stringified, sorted, and joined with "|". Used to match enum value sets
|
||||
// across spec properties and scanned Go type declarations.
|
||||
func EnumKey(values []any) string {
|
||||
strs := make([]string, len(values))
|
||||
for i, v := range values {
|
||||
strs[i] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
sort.Strings(strs)
|
||||
return strings.Join(strs, "|")
|
||||
}
|
||||
|
||||
var rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`)
|
||||
|
||||
// ScanSwaggerEnumTypes walks .go files under each dir and returns a map from
|
||||
// a canonical value-set key (see EnumKey) to the Go type name declared with
|
||||
// // swagger:enum TypeName.
|
||||
//
|
||||
// Returns an error on parse failure, on an annotation for a type whose
|
||||
// constants can't be extracted, or on value-set collisions between two
|
||||
// different enum types.
|
||||
func ScanSwaggerEnumTypes(dirs []string) (map[string]string, error) {
|
||||
fset := token.NewFileSet()
|
||||
parsed := []*ast.File{}
|
||||
|
||||
for _, dir := range dirs {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading %s: %w", dir, err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(entry.Name(), "_test.go") {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dir, entry.Name())
|
||||
file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
parsed = append(parsed, file)
|
||||
}
|
||||
}
|
||||
|
||||
enumTypes := map[string]string{} // typeName → "" (presence marker)
|
||||
enumValues := map[string][]any{} // typeName → values
|
||||
|
||||
// Pass 1: collect every // swagger:enum TypeName declaration.
|
||||
for _, file := range parsed {
|
||||
for _, decl := range file.Decls {
|
||||
gd, ok := decl.(*ast.GenDecl)
|
||||
if !ok || gd.Tok != token.TYPE {
|
||||
continue
|
||||
}
|
||||
if err := collectEnumType(gd, enumTypes); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", fset.Position(gd.Pos()).Filename, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: collect const values; now every annotated type is visible.
|
||||
for _, file := range parsed {
|
||||
for _, decl := range file.Decls {
|
||||
gd, ok := decl.(*ast.GenDecl)
|
||||
if !ok || gd.Tok != token.CONST {
|
||||
continue
|
||||
}
|
||||
collectEnumValues(gd, enumTypes, enumValues)
|
||||
}
|
||||
}
|
||||
|
||||
result := map[string]string{}
|
||||
for typeName := range enumTypes {
|
||||
values, ok := enumValues[typeName]
|
||||
if !ok || len(values) == 0 {
|
||||
return nil, fmt.Errorf("swagger:enum %s has no const block with typed string values", typeName)
|
||||
}
|
||||
key := EnumKey(values)
|
||||
if existing, ok := result[key]; ok && existing != typeName {
|
||||
return nil, fmt.Errorf("swagger:enum value-set collision: %s and %s both use %q", existing, typeName, key)
|
||||
}
|
||||
result[key] = typeName
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// collectEnumType scans a `type` GenDecl for // swagger:enum annotations,
|
||||
// handling both the lone form (`// swagger:enum Foo\n type Foo string`)
|
||||
// where the comment group is attached to the GenDecl, and the grouped form:
|
||||
//
|
||||
// type (
|
||||
// // swagger:enum Foo
|
||||
// Foo string
|
||||
// )
|
||||
//
|
||||
// where the comment group is attached to each TypeSpec. Caveat: Go's parser
|
||||
// only attaches a CommentGroup when it is immediately adjacent to the decl.
|
||||
// A blank line (not a `//` continuation line) between the comment and the
|
||||
// declaration drops the Doc, so annotations MUST sit directly above their
|
||||
// type. All current annotated files obey this — the rule is noted here so
|
||||
// a future edit that inserts a blank line fails fast rather than silently.
|
||||
func collectEnumType(gd *ast.GenDecl, enumTypes map[string]string) error {
|
||||
if err := registerEnumAnnotation(gd.Doc, gd.Specs, enumTypes); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, spec := range gd.Specs {
|
||||
ts, ok := spec.(*ast.TypeSpec)
|
||||
if !ok || ts.Doc == nil {
|
||||
continue
|
||||
}
|
||||
if err := registerEnumAnnotation(ts.Doc, []ast.Spec{ts}, enumTypes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerEnumAnnotation(doc *ast.CommentGroup, specs []ast.Spec, enumTypes map[string]string) error {
|
||||
if doc == nil {
|
||||
return nil
|
||||
}
|
||||
matches := rxSwaggerEnum.FindStringSubmatch(doc.Text())
|
||||
if len(matches) < 2 {
|
||||
return nil
|
||||
}
|
||||
annotated := matches[1]
|
||||
for _, spec := range specs {
|
||||
ts, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ts.Name.Name == annotated {
|
||||
enumTypes[annotated] = ""
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("swagger:enum %s: no type declaration with that name in the same decl group; check for a typo", annotated)
|
||||
}
|
||||
|
||||
func collectEnumValues(gd *ast.GenDecl, enumTypes map[string]string, enumValues map[string][]any) {
|
||||
for _, spec := range gd.Specs {
|
||||
vs, ok := spec.(*ast.ValueSpec)
|
||||
if !ok || vs.Type == nil {
|
||||
continue
|
||||
}
|
||||
ident, ok := vs.Type.(*ast.Ident)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, isEnum := enumTypes[ident.Name]; !isEnum {
|
||||
continue
|
||||
}
|
||||
for _, val := range vs.Values {
|
||||
lit, ok := val.(*ast.BasicLit)
|
||||
if !ok || lit.Kind != token.STRING {
|
||||
continue
|
||||
}
|
||||
unquoted, err := strconv.Unquote(lit.Value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
enumValues[ident.Name] = append(enumValues[ident.Name], unquoted)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package openapi3gen
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnumKey_sortsAndJoins(t *testing.T) {
|
||||
key := EnumKey([]any{"b", "a", "c"})
|
||||
if key != "a|b|c" {
|
||||
t.Fatalf("EnumKey = %q, want %q", key, "a|b|c")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumKey_handlesNonStringValues(t *testing.T) {
|
||||
key := EnumKey([]any{2, 1, 3})
|
||||
if key != "1|2|3" {
|
||||
t.Fatalf("EnumKey = %q, want %q", key, "1|2|3")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSwaggerEnumTypes_basic(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := `package fixture
|
||||
|
||||
// Color is a primary color.
|
||||
// swagger:enum Color
|
||||
type Color string
|
||||
|
||||
const (
|
||||
ColorRed Color = "red"
|
||||
ColorGreen Color = "green"
|
||||
ColorBlue Color = "blue"
|
||||
)
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "color.go"), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := ScanSwaggerEnumTypes([]string{dir})
|
||||
if err != nil {
|
||||
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
|
||||
}
|
||||
wantKey := EnumKey([]any{"red", "green", "blue"})
|
||||
if got[wantKey] != "Color" {
|
||||
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Color")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSwaggerEnumTypes_orphanAnnotation(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := `package fixture
|
||||
|
||||
// swagger:enum Sttype
|
||||
type StateType string
|
||||
|
||||
const (
|
||||
StateOpen StateType = "open"
|
||||
)
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "typo.go"), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := ScanSwaggerEnumTypes([]string{dir})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for annotation referencing a non-matching type name")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Sttype") {
|
||||
t.Fatalf("error %q should mention the typo'd name Sttype", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSwaggerEnumTypes_collision(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := `package fixture
|
||||
|
||||
// swagger:enum Alpha
|
||||
type Alpha string
|
||||
const (
|
||||
AlphaX Alpha = "x"
|
||||
AlphaY Alpha = "y"
|
||||
)
|
||||
|
||||
// swagger:enum Beta
|
||||
type Beta string
|
||||
const (
|
||||
BetaX Beta = "x"
|
||||
BetaY Beta = "y"
|
||||
)
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "dup.go"), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := ScanSwaggerEnumTypes([]string{dir})
|
||||
if err == nil {
|
||||
t.Fatal("expected collision error, got nil")
|
||||
}
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, "Alpha") || !strings.Contains(msg, "Beta") {
|
||||
t.Fatalf("error %q should mention both Alpha and Beta", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSwaggerEnumTypes_parseFailure(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "bad.go"), []byte("package fixture\nfunc Foo() {"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := ScanSwaggerEnumTypes([]string{dir})
|
||||
if err == nil {
|
||||
t.Fatal("expected parse error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSwaggerEnumTypes_annotationWithoutConsts(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := `package fixture
|
||||
|
||||
// swagger:enum Lonely
|
||||
type Lonely string
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "lonely.go"), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err := ScanSwaggerEnumTypes([]string{dir})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for annotation without consts")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Lonely") {
|
||||
t.Fatalf("error %q should mention Lonely", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSwaggerEnumTypes_constsAndTypeInDifferentFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// Name ordering: `a_consts.go` < `b_type.go`, so readdir returns consts first.
|
||||
// Old single-pass scanner would miss the values; two-pass must not.
|
||||
constsSrc := `package fixture
|
||||
|
||||
const (
|
||||
HueA Hue = "a"
|
||||
HueB Hue = "b"
|
||||
)
|
||||
`
|
||||
typeSrc := `package fixture
|
||||
|
||||
// swagger:enum Hue
|
||||
type Hue string
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "a_consts.go"), []byte(constsSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "b_type.go"), []byte(typeSrc), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := ScanSwaggerEnumTypes([]string{dir})
|
||||
if err != nil {
|
||||
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
|
||||
}
|
||||
wantKey := EnumKey([]any{"a", "b"})
|
||||
if got[wantKey] != "Hue" {
|
||||
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Hue")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSwaggerEnumTypes_constsBeforeType(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := `package fixture
|
||||
|
||||
const (
|
||||
ShadeDark Shade = "dark"
|
||||
ShadeLight Shade = "light"
|
||||
)
|
||||
|
||||
// swagger:enum Shade
|
||||
type Shade string
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "shade.go"), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := ScanSwaggerEnumTypes([]string{dir})
|
||||
if err != nil {
|
||||
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
|
||||
}
|
||||
wantKey := EnumKey([]any{"dark", "light"})
|
||||
if got[wantKey] != "Shade" {
|
||||
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Shade")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanSwaggerEnumTypes_groupedTypeDecl(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
src := `package fixture
|
||||
|
||||
type (
|
||||
// swagger:enum Color
|
||||
Color string
|
||||
// swagger:enum Shade
|
||||
Shade string
|
||||
)
|
||||
|
||||
const (
|
||||
ColorRed Color = "red"
|
||||
ColorBlue Color = "blue"
|
||||
)
|
||||
|
||||
const (
|
||||
ShadeDark Shade = "dark"
|
||||
ShadeLight Shade = "light"
|
||||
)
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(dir, "grouped.go"), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := ScanSwaggerEnumTypes([]string{dir})
|
||||
if err != nil {
|
||||
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
|
||||
}
|
||||
colorKey := EnumKey([]any{"red", "blue"})
|
||||
shadeKey := EnumKey([]any{"dark", "light"})
|
||||
if got[colorKey] != "Color" {
|
||||
t.Fatalf("Color: map[%q] = %q, want %q", colorKey, got[colorKey], "Color")
|
||||
}
|
||||
if got[shadeKey] != "Shade" {
|
||||
t.Fatalf("Shade: map[%q] = %q, want %q", shadeKey, got[shadeKey], "Shade")
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ require (
|
||||
github.com/ethantkoenig/rupture v1.0.1
|
||||
github.com/felixge/fgprof v0.9.5
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/getkin/kin-openapi v0.134.0
|
||||
github.com/gliderlabs/ssh v0.3.8
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-chi/cors v1.2.2
|
||||
@@ -192,6 +193,8 @@ require (
|
||||
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
@@ -232,15 +235,19 @@ require (
|
||||
github.com/minio/minlz v1.1.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
|
||||
github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.2.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.8 // indirect
|
||||
github.com/olekukonko/tablewriter v1.1.4 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
@@ -260,6 +267,7 @@ require (
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/unknwon/com v1.0.1 // indirect
|
||||
github.com/woodsbury/decimal128 v1.3.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
|
||||
@@ -275,6 +275,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/getkin/kin-openapi v0.134.0 h1:/L5+1+kfe6dXh8Ot/wqiTgUkjOIEJiC0bbYVziHB8rU=
|
||||
github.com/getkin/kin-openapi v0.134.0/go.mod h1:wK6ZLG/VgoETO9pcLJ/VmAtIcl/DNlMayNTb716EUxE=
|
||||
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0=
|
||||
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
@@ -310,6 +312,10 @@ github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZR
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
|
||||
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI=
|
||||
@@ -547,6 +553,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
|
||||
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
|
||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||
@@ -565,6 +573,10 @@ github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsR
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c h1:7ACFcSaQsrWtrH4WHHfUqE1C+f8r2uv8KGaW0jTNjus=
|
||||
github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c/go.mod h1:JKox4Gszkxt57kj27u7rvi7IFoIULvCZHUsBTUmQM/s=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b h1:vivRhVUAa9t1q0Db4ZmezBP8pWQWnXHFokZj0AOea2g=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||
@@ -591,6 +603,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
@@ -700,6 +714,8 @@ github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77ro
|
||||
github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ=
|
||||
github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
@@ -712,6 +728,8 @@ github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZ
|
||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
|
||||
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
||||
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
|
||||
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
|
||||
@@ -615,6 +615,7 @@ var (
|
||||
"sitemap.xml", // search engine sitemap
|
||||
"ssh_info", // agit info
|
||||
"swagger.v1.json",
|
||||
"openapi3.v1.json",
|
||||
|
||||
"ghost", // reserved name for deleted users (id: -1)
|
||||
"gitea-actions", // gitea builtin user (id: -2)
|
||||
|
||||
@@ -30,7 +30,7 @@ type CreateUserOption struct {
|
||||
// Whether the user has restricted access privileges
|
||||
Restricted *bool `json:"restricted"`
|
||||
// User visibility level: public, limited, or private
|
||||
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
|
||||
Visibility UserVisibility `json:"visibility" binding:"In(,public,limited,private)"`
|
||||
|
||||
// For explicitly setting the user creation timestamp. Useful when users are
|
||||
// migrated from other systems. When omitted, the user's creation timestamp
|
||||
@@ -79,5 +79,5 @@ type EditUserOption struct {
|
||||
// Whether the user has restricted access privileges
|
||||
Restricted *bool `json:"restricted"`
|
||||
// User visibility level: public, limited, or private
|
||||
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
|
||||
Visibility UserVisibility `json:"visibility" binding:"In(,public,limited,private)"`
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ type Organization struct {
|
||||
// The location of the organization
|
||||
Location string `json:"location"`
|
||||
// The visibility level of the organization (public, limited, private)
|
||||
Visibility string `json:"visibility"`
|
||||
Visibility UserVisibility `json:"visibility"`
|
||||
// Whether repository administrators can change team access
|
||||
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
|
||||
// username of the organization
|
||||
@@ -60,8 +60,7 @@ type CreateOrgOption struct {
|
||||
// The location of the organization
|
||||
Location string `json:"location" binding:"MaxSize(50)"`
|
||||
// possible values are `public` (default), `limited` or `private`
|
||||
// enum: ["public","limited","private"]
|
||||
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
|
||||
Visibility UserVisibility `json:"visibility" binding:"In(,public,limited,private)"`
|
||||
// Whether repository administrators can change team access
|
||||
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
|
||||
}
|
||||
@@ -79,8 +78,7 @@ type EditOrgOption struct {
|
||||
// The location of the organization
|
||||
Location *string `json:"location" binding:"MaxSize(50)"`
|
||||
// possible values are `public`, `limited` or `private`
|
||||
// enum: ["public","limited","private"]
|
||||
Visibility *string `json:"visibility" binding:"In(,public,limited,private)"`
|
||||
Visibility *UserVisibility `json:"visibility" binding:"In(,public,limited,private)"`
|
||||
// Whether repository administrators can change team access
|
||||
RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"`
|
||||
}
|
||||
|
||||
@@ -15,9 +15,8 @@ type Team struct {
|
||||
// The organization that the team belongs to
|
||||
Organization *Organization `json:"organization"`
|
||||
// Whether the team has access to all repositories in the organization
|
||||
IncludesAllRepositories bool `json:"includes_all_repositories"`
|
||||
// enum: ["none","read","write","admin","owner"]
|
||||
Permission string `json:"permission"`
|
||||
IncludesAllRepositories bool `json:"includes_all_repositories"`
|
||||
Permission AccessLevelName `json:"permission"`
|
||||
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"]
|
||||
// Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions.
|
||||
Units []string `json:"units"`
|
||||
@@ -34,9 +33,8 @@ type CreateTeamOption struct {
|
||||
// The description of the team
|
||||
Description string `json:"description" binding:"MaxSize(255)"`
|
||||
// Whether the team has access to all repositories in the organization
|
||||
IncludesAllRepositories bool `json:"includes_all_repositories"`
|
||||
// enum: ["read","write","admin"]
|
||||
Permission string `json:"permission"`
|
||||
IncludesAllRepositories bool `json:"includes_all_repositories"`
|
||||
Permission RepoWritePermission `json:"permission"`
|
||||
// example: ["repo.actions","repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.ext_wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"]
|
||||
// Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions.
|
||||
Units []string `json:"units"`
|
||||
@@ -53,9 +51,8 @@ type EditTeamOption struct {
|
||||
// The description of the team
|
||||
Description *string `json:"description" binding:"MaxSize(255)"`
|
||||
// Whether the team has access to all repositories in the organization
|
||||
IncludesAllRepositories *bool `json:"includes_all_repositories"`
|
||||
// enum: ["read","write","admin"]
|
||||
Permission string `json:"permission"`
|
||||
IncludesAllRepositories *bool `json:"includes_all_repositories"`
|
||||
Permission RepoWritePermission `json:"permission"`
|
||||
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"]
|
||||
// Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions.
|
||||
Units []string `json:"units"`
|
||||
|
||||
+11
-4
@@ -8,6 +8,15 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ObjectFormatName is the git hash algorithm used by a repository.
|
||||
// swagger:enum ObjectFormatName
|
||||
type ObjectFormatName string
|
||||
|
||||
const (
|
||||
ObjectFormatSHA1 ObjectFormatName = "sha1"
|
||||
ObjectFormatSHA256 ObjectFormatName = "sha256"
|
||||
)
|
||||
|
||||
// Permission represents a set of permissions
|
||||
type Permission struct {
|
||||
Admin bool `json:"admin"` // Admin indicates if the user is an administrator of the repository.
|
||||
@@ -114,8 +123,7 @@ type Repository struct {
|
||||
Internal bool `json:"internal"`
|
||||
MirrorInterval string `json:"mirror_interval"`
|
||||
// ObjectFormatName of the underlying git repository
|
||||
// enum: ["sha1","sha256"]
|
||||
ObjectFormatName string `json:"object_format_name"`
|
||||
ObjectFormatName ObjectFormatName `json:"object_format_name"`
|
||||
// swagger:strfmt date-time
|
||||
MirrorUpdated time.Time `json:"mirror_updated"`
|
||||
RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"`
|
||||
@@ -153,8 +161,7 @@ type CreateRepoOption struct {
|
||||
// enum: ["default","collaborator","committer","collaboratorcommitter"]
|
||||
TrustModel string `json:"trust_model"`
|
||||
// ObjectFormatName of the underlying git repository, empty string for default (sha1)
|
||||
// enum: ["sha1","sha256"]
|
||||
ObjectFormatName string `json:"object_format_name" binding:"MaxSize(6)"`
|
||||
ObjectFormatName ObjectFormatName `json:"object_format_name" binding:"MaxSize(6)"`
|
||||
}
|
||||
|
||||
// EditRepoOption options when editing a repository's properties
|
||||
|
||||
@@ -3,17 +3,41 @@
|
||||
|
||||
package structs
|
||||
|
||||
// RepoWritePermission is a permission level callers may grant to a team or
|
||||
// collaborator on input. Output fields use AccessLevelName instead.
|
||||
// swagger:enum RepoWritePermission
|
||||
type RepoWritePermission string
|
||||
|
||||
const (
|
||||
RepoWritePermissionRead RepoWritePermission = "read"
|
||||
RepoWritePermissionWrite RepoWritePermission = "write"
|
||||
RepoWritePermissionAdmin RepoWritePermission = "admin"
|
||||
)
|
||||
|
||||
// AccessLevelName is the string rendering of a perm.AccessMode produced on
|
||||
// API responses. Callers must not send these values; use RepoWritePermission
|
||||
// on input.
|
||||
// swagger:enum AccessLevelName
|
||||
type AccessLevelName string
|
||||
|
||||
const (
|
||||
AccessLevelNameNone AccessLevelName = "none"
|
||||
AccessLevelNameRead AccessLevelName = "read"
|
||||
AccessLevelNameWrite AccessLevelName = "write"
|
||||
AccessLevelNameAdmin AccessLevelName = "admin"
|
||||
AccessLevelNameOwner AccessLevelName = "owner"
|
||||
)
|
||||
|
||||
// AddCollaboratorOption options when adding a user as a collaborator of a repository
|
||||
type AddCollaboratorOption struct {
|
||||
// enum: ["read","write","admin"]
|
||||
// Permission level to grant the collaborator
|
||||
Permission *string `json:"permission"`
|
||||
Permission *RepoWritePermission `json:"permission"`
|
||||
}
|
||||
|
||||
// RepoCollaboratorPermission to get repository permission for a collaborator
|
||||
type RepoCollaboratorPermission struct {
|
||||
// Permission level of the collaborator
|
||||
Permission string `json:"permission"`
|
||||
Permission AccessLevelName `json:"permission"`
|
||||
// RoleName is the name of the permission role
|
||||
RoleName string `json:"role_name"`
|
||||
// User information of the collaborator
|
||||
|
||||
@@ -51,7 +51,7 @@ type User struct {
|
||||
// the user's description
|
||||
Description string `json:"description"`
|
||||
// User visibility level option: public, limited, private
|
||||
Visibility string `json:"visibility"`
|
||||
Visibility UserVisibility `json:"visibility"`
|
||||
|
||||
// user counts
|
||||
Followers int `json:"followers_count"`
|
||||
|
||||
@@ -56,3 +56,14 @@ func ExtractKeysFromMapString(in map[string]VisibleType) (keys []string) {
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// UserVisibility defines the visibility level of a user or organization as
|
||||
// rendered in API payloads. The DB representation is VisibleType (int).
|
||||
// swagger:enum UserVisibility
|
||||
type UserVisibility string
|
||||
|
||||
const (
|
||||
UserVisibilityPublic UserVisibility = "public"
|
||||
UserVisibilityLimited UserVisibility = "limited"
|
||||
UserVisibilityPrivate UserVisibility = "private"
|
||||
)
|
||||
|
||||
@@ -48,7 +48,7 @@ func CreateOrg(ctx *context.APIContext) {
|
||||
|
||||
visibility := api.VisibleTypePublic
|
||||
if form.Visibility != "" {
|
||||
visibility = api.VisibilityModes[form.Visibility]
|
||||
visibility = api.VisibilityModes[string(form.Visibility)]
|
||||
}
|
||||
|
||||
org := &organization.Organization{
|
||||
|
||||
@@ -123,7 +123,7 @@ func CreateUser(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if form.Visibility != "" {
|
||||
visibility := api.VisibilityModes[form.Visibility]
|
||||
visibility := api.VisibilityModes[string(form.Visibility)]
|
||||
overwriteDefault.Visibility = &visibility
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ func EditUser(ctx *context.APIContext) {
|
||||
Description: optional.FromPtr(form.Description),
|
||||
IsActive: optional.FromPtr(form.Active),
|
||||
IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin),
|
||||
Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
|
||||
Visibility: optional.FromMapLookup(api.VisibilityModes, string(form.Visibility)),
|
||||
AllowGitHook: optional.FromPtr(form.AllowGitHook),
|
||||
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
|
||||
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
|
||||
|
||||
@@ -258,7 +258,7 @@ func Create(ctx *context.APIContext) {
|
||||
|
||||
visibility := api.VisibleTypePublic
|
||||
if form.Visibility != "" {
|
||||
visibility = api.VisibilityModes[form.Visibility]
|
||||
visibility = api.VisibilityModes[string(form.Visibility)]
|
||||
}
|
||||
|
||||
org := &organization.Organization{
|
||||
@@ -402,7 +402,7 @@ func Edit(ctx *context.APIContext) {
|
||||
Description: optional.FromPtr(form.Description),
|
||||
Website: optional.FromPtr(form.Website),
|
||||
Location: optional.FromPtr(form.Location),
|
||||
Visibility: optional.FromMapLookup(api.VisibilityModes, optional.FromPtr(form.Visibility).Value()),
|
||||
Visibility: optional.FromMapLookup(api.VisibilityModes, string(optional.FromPtr(form.Visibility).Value())),
|
||||
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
|
||||
}
|
||||
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
|
||||
|
||||
@@ -210,7 +210,7 @@ func CreateTeam(ctx *context.APIContext) {
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
form := web.GetForm(ctx).(*api.CreateTeamOption)
|
||||
teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin)
|
||||
teamPermission := perm.ParseAccessMode(string(form.Permission), perm.AccessModeNone, perm.AccessModeAdmin)
|
||||
team := &organization.Team{
|
||||
OrgID: ctx.Org.Organization.ID,
|
||||
Name: form.Name,
|
||||
@@ -224,7 +224,7 @@ func CreateTeam(ctx *context.APIContext) {
|
||||
if len(form.UnitsMap) > 0 {
|
||||
attachTeamUnitsMap(team, form.UnitsMap)
|
||||
} else if len(form.Units) > 0 {
|
||||
unitPerm := perm.ParseAccessMode(form.Permission, perm.AccessModeRead, perm.AccessModeWrite)
|
||||
unitPerm := perm.ParseAccessMode(string(form.Permission), perm.AccessModeRead, perm.AccessModeWrite)
|
||||
attachTeamUnits(team, unitPerm, form.Units)
|
||||
} else {
|
||||
ctx.APIErrorInternal(errors.New("units permission should not be empty"))
|
||||
@@ -298,7 +298,7 @@ func EditTeam(ctx *context.APIContext) {
|
||||
isAuthChanged := false
|
||||
isIncludeAllChanged := false
|
||||
if !team.IsOwnerTeam() && len(form.Permission) != 0 {
|
||||
teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin)
|
||||
teamPermission := perm.ParseAccessMode(string(form.Permission), perm.AccessModeNone, perm.AccessModeAdmin)
|
||||
if team.AccessMode != teamPermission {
|
||||
isAuthChanged = true
|
||||
team.AccessMode = teamPermission
|
||||
@@ -314,7 +314,7 @@ func EditTeam(ctx *context.APIContext) {
|
||||
if len(form.UnitsMap) > 0 {
|
||||
attachTeamUnitsMap(team, form.UnitsMap)
|
||||
} else if len(form.Units) > 0 {
|
||||
unitPerm := perm.ParseAccessMode(form.Permission, perm.AccessModeRead, perm.AccessModeWrite)
|
||||
unitPerm := perm.ParseAccessMode(string(form.Permission), perm.AccessModeRead, perm.AccessModeWrite)
|
||||
attachTeamUnits(team, unitPerm, form.Units)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -181,7 +181,7 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) {
|
||||
|
||||
p := perm.AccessModeWrite
|
||||
if form.Permission != nil {
|
||||
p = perm.ParseAccessMode(*form.Permission, perm.AccessModeRead, perm.AccessModeWrite, perm.AccessModeAdmin)
|
||||
p = perm.ParseAccessMode(string(*form.Permission), perm.AccessModeRead, perm.AccessModeWrite, perm.AccessModeAdmin)
|
||||
}
|
||||
|
||||
if err := repo_service.AddOrUpdateCollaborator(ctx, ctx.Repo.Repository, collaborator, p); err != nil {
|
||||
|
||||
@@ -262,7 +262,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
|
||||
DefaultBranch: opt.DefaultBranch,
|
||||
TrustModel: repo_model.ToTrustModel(opt.TrustModel),
|
||||
IsTemplate: opt.Template,
|
||||
ObjectFormatName: opt.ObjectFormatName,
|
||||
ObjectFormatName: string(opt.ObjectFormatName),
|
||||
})
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoAlreadyExist(err) {
|
||||
|
||||
@@ -16,3 +16,10 @@ func SwaggerV1Json(ctx *context.Context) {
|
||||
ctx.Data["SwaggerAppSubUrl"] = setting.AppSubURL // it is JS-safe
|
||||
ctx.JSONTemplate("swagger/v1_json")
|
||||
}
|
||||
|
||||
// OpenAPI3Json render OpenAPI 3.0 json (auto-converted from Swagger 2.0)
|
||||
func OpenAPI3Json(ctx *context.Context) {
|
||||
ctx.Data["SwaggerAppVer"] = template.HTML(template.JSEscapeString(setting.AppVer))
|
||||
ctx.Data["SwaggerAppSubUrl"] = setting.AppSubURL // it is JS-safe
|
||||
ctx.JSONTemplate("swagger/v1_openapi3_json")
|
||||
}
|
||||
|
||||
@@ -1751,6 +1751,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
|
||||
if setting.API.EnableSwagger {
|
||||
m.Get("/swagger.v1.json", SwaggerV1Json)
|
||||
m.Get("/openapi3.v1.json", OpenAPI3Json)
|
||||
}
|
||||
|
||||
if !setting.IsProd || setting.IsInE2eTesting() {
|
||||
|
||||
@@ -711,7 +711,7 @@ func ToOrganization(ctx context.Context, org *organization.Organization) *api.Or
|
||||
Description: org.Description,
|
||||
Website: org.Website,
|
||||
Location: org.Location,
|
||||
Visibility: org.Visibility.String(),
|
||||
Visibility: api.UserVisibility(org.Visibility.String()),
|
||||
RepoAdminChangeTeamAccess: org.RepoAdminChangeTeamAccess,
|
||||
}
|
||||
}
|
||||
@@ -740,7 +740,7 @@ func ToTeams(ctx context.Context, teams []*organization.Team, loadOrgs bool) ([]
|
||||
Description: t.Description,
|
||||
IncludesAllRepositories: t.IncludesAllRepositories,
|
||||
CanCreateOrgRepo: t.CanCreateOrgRepo,
|
||||
Permission: t.AccessMode.ToString(),
|
||||
Permission: api.AccessLevelName(t.AccessMode.ToString()),
|
||||
Units: t.GetUnitNames(),
|
||||
UnitsMap: t.GetUnitsMap(),
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
||||
MirrorUpdated: mirrorUpdated,
|
||||
RepoTransfer: transfer,
|
||||
Topics: util.SliceNilAsEmpty(repo.Topics),
|
||||
ObjectFormatName: repo.ObjectFormatName,
|
||||
ObjectFormatName: api.ObjectFormatName(repo.ObjectFormatName),
|
||||
Licenses: util.SliceNilAsEmpty(repoLicenses.StringList()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func toUser(ctx context.Context, user *user_model.User, signed, authed bool) *ap
|
||||
StarredRepos: user.NumStars,
|
||||
}
|
||||
|
||||
result.Visibility = user.Visibility.String()
|
||||
result.Visibility = api.UserVisibility(user.Visibility.String())
|
||||
|
||||
// hide primary email if API caller is anonymous or user keep email private
|
||||
if signed && (!user.KeepEmailPrivate || authed) {
|
||||
@@ -104,7 +104,7 @@ func User2UserSettings(user *user_model.User) api.UserSettings {
|
||||
func ToUserAndPermission(ctx context.Context, user, doer *user_model.User, accessMode perm.AccessMode) api.RepoCollaboratorPermission {
|
||||
return api.RepoCollaboratorPermission{
|
||||
User: ToUser(ctx, user, doer),
|
||||
Permission: accessMode.ToString(),
|
||||
Permission: api.AccessLevelName(accessMode.ToString()),
|
||||
RoleName: accessMode.ToString(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ func TestUser_ToUser(t *testing.T) {
|
||||
|
||||
apiUser = toUser(t.Context(), user1, false, false)
|
||||
assert.False(t, apiUser.IsAdmin)
|
||||
assert.Equal(t, api.VisibleTypePublic.String(), apiUser.Visibility)
|
||||
assert.Equal(t, api.UserVisibilityPublic, apiUser.Visibility)
|
||||
|
||||
user31 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31, IsAdmin: false, Visibility: api.VisibleTypePrivate})
|
||||
|
||||
apiUser = toUser(t.Context(), user31, true, true)
|
||||
assert.False(t, apiUser.IsAdmin)
|
||||
assert.Equal(t, api.VisibleTypePrivate.String(), apiUser.Visibility)
|
||||
assert.Equal(t, api.UserVisibilityPrivate, apiUser.Visibility)
|
||||
}
|
||||
|
||||
Generated
+50
-9
@@ -22299,12 +22299,14 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"permission": {
|
||||
"description": "Permission level to grant the collaborator\nread RepoWritePermissionRead\nwrite RepoWritePermissionWrite\nadmin RepoWritePermissionAdmin",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"admin"
|
||||
],
|
||||
"x-go-enum-desc": "read RepoWritePermissionRead\nwrite RepoWritePermissionWrite\nadmin RepoWritePermissionAdmin",
|
||||
"x-go-name": "Permission"
|
||||
}
|
||||
},
|
||||
@@ -24031,13 +24033,14 @@
|
||||
"x-go-name": "UserName"
|
||||
},
|
||||
"visibility": {
|
||||
"description": "possible values are `public` (default), `limited` or `private`",
|
||||
"description": "possible values are `public` (default), `limited` or `private`\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"public",
|
||||
"limited",
|
||||
"private"
|
||||
],
|
||||
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
||||
"x-go-name": "Visibility"
|
||||
},
|
||||
"website": {
|
||||
@@ -24322,12 +24325,13 @@
|
||||
"x-go-name": "Name"
|
||||
},
|
||||
"object_format_name": {
|
||||
"description": "ObjectFormatName of the underlying git repository, empty string for default (sha1)",
|
||||
"description": "ObjectFormatName of the underlying git repository, empty string for default (sha1)\nsha1 ObjectFormatSHA1\nsha256 ObjectFormatSHA256",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"sha1",
|
||||
"sha256"
|
||||
],
|
||||
"x-go-enum-desc": "sha1 ObjectFormatSHA1\nsha256 ObjectFormatSHA256",
|
||||
"x-go-name": "ObjectFormatName"
|
||||
},
|
||||
"private": {
|
||||
@@ -24480,6 +24484,7 @@
|
||||
"write",
|
||||
"admin"
|
||||
],
|
||||
"x-go-enum-desc": "read RepoWritePermissionRead\nwrite RepoWritePermissionWrite\nadmin RepoWritePermissionAdmin",
|
||||
"x-go-name": "Permission"
|
||||
},
|
||||
"units": {
|
||||
@@ -24574,8 +24579,14 @@
|
||||
"x-go-name": "Username"
|
||||
},
|
||||
"visibility": {
|
||||
"description": "User visibility level: public, limited, or private",
|
||||
"description": "User visibility level: public, limited, or private\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"public",
|
||||
"limited",
|
||||
"private"
|
||||
],
|
||||
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
||||
"x-go-name": "Visibility"
|
||||
}
|
||||
},
|
||||
@@ -25200,13 +25211,14 @@
|
||||
"x-go-name": "RepoAdminChangeTeamAccess"
|
||||
},
|
||||
"visibility": {
|
||||
"description": "possible values are `public`, `limited` or `private`",
|
||||
"description": "possible values are `public`, `limited` or `private`\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"public",
|
||||
"limited",
|
||||
"private"
|
||||
],
|
||||
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
||||
"x-go-name": "Visibility"
|
||||
},
|
||||
"website": {
|
||||
@@ -25570,6 +25582,7 @@
|
||||
"write",
|
||||
"admin"
|
||||
],
|
||||
"x-go-enum-desc": "read RepoWritePermissionRead\nwrite RepoWritePermissionWrite\nadmin RepoWritePermissionAdmin",
|
||||
"x-go-name": "Permission"
|
||||
},
|
||||
"units": {
|
||||
@@ -25700,8 +25713,14 @@
|
||||
"x-go-name": "SourceID"
|
||||
},
|
||||
"visibility": {
|
||||
"description": "User visibility level: public, limited, or private",
|
||||
"description": "User visibility level: public, limited, or private\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"public",
|
||||
"limited",
|
||||
"private"
|
||||
],
|
||||
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
||||
"x-go-name": "Visibility"
|
||||
},
|
||||
"website": {
|
||||
@@ -27652,8 +27671,14 @@
|
||||
"x-go-name": "UserName"
|
||||
},
|
||||
"visibility": {
|
||||
"description": "The visibility level of the organization (public, limited, private)",
|
||||
"description": "The visibility level of the organization (public, limited, private)\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"public",
|
||||
"limited",
|
||||
"private"
|
||||
],
|
||||
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
||||
"x-go-name": "Visibility"
|
||||
},
|
||||
"website": {
|
||||
@@ -28637,8 +28662,16 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"permission": {
|
||||
"description": "Permission level of the collaborator",
|
||||
"description": "Permission level of the collaborator\nnone AccessLevelNameNone\nread AccessLevelNameRead\nwrite AccessLevelNameWrite\nadmin AccessLevelNameAdmin\nowner AccessLevelNameOwner",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"none",
|
||||
"read",
|
||||
"write",
|
||||
"admin",
|
||||
"owner"
|
||||
],
|
||||
"x-go-enum-desc": "none AccessLevelNameNone\nread AccessLevelNameRead\nwrite AccessLevelNameWrite\nadmin AccessLevelNameAdmin\nowner AccessLevelNameOwner",
|
||||
"x-go-name": "Permission"
|
||||
},
|
||||
"role_name": {
|
||||
@@ -28915,12 +28948,13 @@
|
||||
"x-go-name": "Name"
|
||||
},
|
||||
"object_format_name": {
|
||||
"description": "ObjectFormatName of the underlying git repository",
|
||||
"description": "ObjectFormatName of the underlying git repository\nsha1 ObjectFormatSHA1\nsha256 ObjectFormatSHA256",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"sha1",
|
||||
"sha256"
|
||||
],
|
||||
"x-go-enum-desc": "sha1 ObjectFormatSHA1\nsha256 ObjectFormatSHA256",
|
||||
"x-go-name": "ObjectFormatName"
|
||||
},
|
||||
"open_issues_count": {
|
||||
@@ -29294,6 +29328,7 @@
|
||||
"admin",
|
||||
"owner"
|
||||
],
|
||||
"x-go-enum-desc": "none AccessLevelNameNone\nread AccessLevelNameRead\nwrite AccessLevelNameWrite\nadmin AccessLevelNameAdmin\nowner AccessLevelNameOwner",
|
||||
"x-go-name": "Permission"
|
||||
},
|
||||
"units": {
|
||||
@@ -29840,8 +29875,14 @@
|
||||
"x-go-name": "StarredRepos"
|
||||
},
|
||||
"visibility": {
|
||||
"description": "User visibility level option: public, limited, private",
|
||||
"description": "User visibility level option: public, limited, private\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"public",
|
||||
"limited",
|
||||
"private"
|
||||
],
|
||||
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
|
||||
"x-go-name": "Visibility"
|
||||
},
|
||||
"website": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -96,12 +96,12 @@ func doAPIEditRepository(ctx APITestContext, editRepoOption *api.EditRepoOption,
|
||||
|
||||
func doAPIAddCollaborator(ctx APITestContext, username string, mode perm.AccessMode) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
permission := "read"
|
||||
permission := api.RepoWritePermissionRead
|
||||
|
||||
if mode == perm.AccessModeAdmin {
|
||||
permission = "admin"
|
||||
permission = api.RepoWritePermissionAdmin
|
||||
} else if mode > perm.AccessModeRead {
|
||||
permission = "write"
|
||||
permission = api.RepoWritePermissionWrite
|
||||
}
|
||||
addCollaboratorOption := &api.AddCollaboratorOption{
|
||||
Permission: &permission,
|
||||
|
||||
@@ -127,7 +127,7 @@ func testAPIOrgGeneral(t *testing.T) {
|
||||
apiOrgList := DecodeJSON(t, resp, []*api.Organization{})
|
||||
assert.Len(t, apiOrgList, 13)
|
||||
assert.Equal(t, "Limited Org 36", apiOrgList[1].FullName)
|
||||
assert.Equal(t, "limited", apiOrgList[1].Visibility)
|
||||
assert.Equal(t, api.UserVisibilityLimited, apiOrgList[1].Visibility)
|
||||
|
||||
// accessing without a token will return only public orgs
|
||||
req = NewRequest(t, "GET", "/api/v1/orgs")
|
||||
@@ -136,7 +136,7 @@ func testAPIOrgGeneral(t *testing.T) {
|
||||
apiOrgList = DecodeJSON(t, resp, []*api.Organization{})
|
||||
assert.Len(t, apiOrgList, 9)
|
||||
assert.Equal(t, "org 17", apiOrgList[0].FullName)
|
||||
assert.Equal(t, "public", apiOrgList[0].Visibility)
|
||||
assert.Equal(t, api.UserVisibilityPublic, apiOrgList[0].Visibility)
|
||||
})
|
||||
|
||||
t.Run("OrgEdit", func(t *testing.T) {
|
||||
@@ -148,7 +148,7 @@ func testAPIOrgGeneral(t *testing.T) {
|
||||
Description: new("new description"),
|
||||
Website: new("https://org3-new-website.example.com"),
|
||||
Location: new("new location"),
|
||||
Visibility: new("limited"),
|
||||
Visibility: new(api.UserVisibilityLimited),
|
||||
Email: new("org3-new-email@example.com"),
|
||||
}
|
||||
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org3Edit).AddTokenAuth(user1Token)
|
||||
@@ -178,7 +178,7 @@ func testAPIOrgGeneral(t *testing.T) {
|
||||
|
||||
t.Run("OrgEditInvalidVisibility", func(t *testing.T) {
|
||||
org := api.EditOrgOption{
|
||||
Visibility: new("invalid-visibility"),
|
||||
Visibility: new(api.UserVisibility("invalid-visibility")),
|
||||
}
|
||||
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
|
||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
|
||||
|
||||
repoPermission := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
|
||||
|
||||
assert.Equal(t, "owner", repoPermission.Permission)
|
||||
assert.Equal(t, api.AccessLevelNameOwner, repoPermission.Permission)
|
||||
})
|
||||
|
||||
t.Run("CollaboratorWithReadAccess", func(t *testing.T) {
|
||||
@@ -50,7 +50,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
|
||||
|
||||
repoPermission := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
|
||||
|
||||
assert.Equal(t, "read", repoPermission.Permission)
|
||||
assert.Equal(t, api.AccessLevelNameRead, repoPermission.Permission)
|
||||
})
|
||||
|
||||
t.Run("CollaboratorWithWriteAccess", func(t *testing.T) {
|
||||
@@ -62,7 +62,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
|
||||
|
||||
repoPermission := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
|
||||
|
||||
assert.Equal(t, "write", repoPermission.Permission)
|
||||
assert.Equal(t, api.AccessLevelNameWrite, repoPermission.Permission)
|
||||
})
|
||||
|
||||
t.Run("CollaboratorWithAdminAccess", func(t *testing.T) {
|
||||
@@ -74,7 +74,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
|
||||
|
||||
repoPermission := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
|
||||
|
||||
assert.Equal(t, "admin", repoPermission.Permission)
|
||||
assert.Equal(t, api.AccessLevelNameAdmin, repoPermission.Permission)
|
||||
})
|
||||
|
||||
t.Run("CollaboratorNotFound", func(t *testing.T) {
|
||||
@@ -101,7 +101,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
|
||||
|
||||
repoPermission := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
|
||||
|
||||
assert.Equal(t, "read", repoPermission.Permission)
|
||||
assert.Equal(t, api.AccessLevelNameRead, repoPermission.Permission)
|
||||
|
||||
t.Run("CollaboratorCanReadOwnPermission", func(t *testing.T) {
|
||||
session := loginUser(t, user5.Name)
|
||||
@@ -112,7 +112,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
|
||||
|
||||
repoCollPerm := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
|
||||
|
||||
assert.Equal(t, "read", repoCollPerm.Permission)
|
||||
assert.Equal(t, api.AccessLevelNameRead, repoCollPerm.Permission)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -128,7 +128,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
|
||||
|
||||
repoPermission := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
|
||||
|
||||
assert.Equal(t, "read", repoPermission.Permission)
|
||||
assert.Equal(t, api.AccessLevelNameRead, repoPermission.Permission)
|
||||
})
|
||||
|
||||
t.Run("RepoAdminCanQueryACollaboratorsPermissions", func(t *testing.T) {
|
||||
@@ -144,6 +144,6 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
|
||||
|
||||
repoPermission := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
|
||||
|
||||
assert.Equal(t, "read", repoPermission.Permission)
|
||||
assert.Equal(t, api.AccessLevelNameRead, repoPermission.Permission)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ func TestAPIRepoTeams(t *testing.T) {
|
||||
assert.Equal(t, "Owners", teams[0].Name)
|
||||
assert.True(t, teams[0].CanCreateOrgRepo)
|
||||
assert.True(t, util.SliceSortedEqual(unit.AllUnitKeyNames(), teams[0].Units), "%v == %v", unit.AllUnitKeyNames(), teams[0].Units)
|
||||
assert.Equal(t, "owner", teams[0].Permission)
|
||||
assert.Equal(t, api.AccessLevelNameOwner, teams[0].Permission)
|
||||
|
||||
assert.Equal(t, "test_team", teams[1].Name)
|
||||
assert.False(t, teams[1].CanCreateOrgRepo)
|
||||
assert.Equal(t, []string{"repo.issues"}, teams[1].Units)
|
||||
assert.Equal(t, "write", teams[1].Permission)
|
||||
assert.Equal(t, api.AccessLevelNameWrite, teams[1].Permission)
|
||||
}
|
||||
|
||||
// IsTeam
|
||||
|
||||
@@ -75,9 +75,9 @@ func TestAPITeam(t *testing.T) {
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
apiTeam = DecodeJSON(t, resp, &api.Team{})
|
||||
checkTeamResponse(t, "CreateTeam1", apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
|
||||
"none", teamToCreate.Units, nil)
|
||||
api.AccessLevelNameNone, teamToCreate.Units, nil)
|
||||
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
|
||||
"none", teamToCreate.Units, nil)
|
||||
api.AccessLevelNameNone, teamToCreate.Units, nil)
|
||||
teamID := apiTeam.ID
|
||||
|
||||
// Edit team.
|
||||
@@ -96,9 +96,9 @@ func TestAPITeam(t *testing.T) {
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiTeam = DecodeJSON(t, resp, &api.Team{})
|
||||
checkTeamResponse(t, "EditTeam1", apiTeam, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
|
||||
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
|
||||
api.AccessLevelName(teamToEdit.Permission), unit.AllUnitKeyNames(), nil)
|
||||
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
|
||||
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
|
||||
api.AccessLevelName(teamToEdit.Permission), unit.AllUnitKeyNames(), nil)
|
||||
|
||||
// Edit team Description only
|
||||
editDescription = "first team"
|
||||
@@ -108,9 +108,9 @@ func TestAPITeam(t *testing.T) {
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiTeam = DecodeJSON(t, resp, &api.Team{})
|
||||
checkTeamResponse(t, "EditTeam1_DescOnly", apiTeam, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
|
||||
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
|
||||
api.AccessLevelName(teamToEdit.Permission), unit.AllUnitKeyNames(), nil)
|
||||
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
|
||||
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
|
||||
api.AccessLevelName(teamToEdit.Permission), unit.AllUnitKeyNames(), nil)
|
||||
|
||||
// Read team.
|
||||
teamRead := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
|
||||
@@ -120,7 +120,7 @@ func TestAPITeam(t *testing.T) {
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiTeam = DecodeJSON(t, resp, &api.Team{})
|
||||
checkTeamResponse(t, "ReadTeam1", apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories,
|
||||
teamRead.AccessMode.ToString(), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
|
||||
api.AccessLevelName(teamRead.AccessMode.ToString()), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
|
||||
|
||||
// Delete team.
|
||||
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d", teamID).
|
||||
@@ -142,9 +142,9 @@ func TestAPITeam(t *testing.T) {
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
apiTeam = DecodeJSON(t, resp, &api.Team{})
|
||||
checkTeamResponse(t, "CreateTeam2", apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
|
||||
"none", nil, teamToCreate.UnitsMap)
|
||||
api.AccessLevelNameNone, nil, teamToCreate.UnitsMap)
|
||||
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
|
||||
"none", nil, teamToCreate.UnitsMap)
|
||||
api.AccessLevelNameNone, nil, teamToCreate.UnitsMap)
|
||||
teamID = apiTeam.ID
|
||||
|
||||
// Edit team.
|
||||
@@ -163,9 +163,9 @@ func TestAPITeam(t *testing.T) {
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiTeam = DecodeJSON(t, resp, &api.Team{})
|
||||
checkTeamResponse(t, "EditTeam2", apiTeam, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
|
||||
"none", nil, teamToEdit.UnitsMap)
|
||||
api.AccessLevelNameNone, nil, teamToEdit.UnitsMap)
|
||||
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
|
||||
"none", nil, teamToEdit.UnitsMap)
|
||||
api.AccessLevelNameNone, nil, teamToEdit.UnitsMap)
|
||||
|
||||
// Edit team Description only
|
||||
editDescription = "second team"
|
||||
@@ -175,9 +175,9 @@ func TestAPITeam(t *testing.T) {
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiTeam = DecodeJSON(t, resp, &api.Team{})
|
||||
checkTeamResponse(t, "EditTeam2_DescOnly", apiTeam, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
|
||||
"none", nil, teamToEdit.UnitsMap)
|
||||
api.AccessLevelNameNone, nil, teamToEdit.UnitsMap)
|
||||
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
|
||||
"none", nil, teamToEdit.UnitsMap)
|
||||
api.AccessLevelNameNone, nil, teamToEdit.UnitsMap)
|
||||
|
||||
// Read team.
|
||||
teamRead = unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
|
||||
@@ -187,7 +187,7 @@ func TestAPITeam(t *testing.T) {
|
||||
apiTeam = DecodeJSON(t, resp, &api.Team{})
|
||||
assert.NoError(t, teamRead.LoadUnits(t.Context()))
|
||||
checkTeamResponse(t, "ReadTeam2", apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories,
|
||||
teamRead.AccessMode.ToString(), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
|
||||
api.AccessLevelName(teamRead.AccessMode.ToString()), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
|
||||
|
||||
// Delete team.
|
||||
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d", teamID).
|
||||
@@ -227,7 +227,7 @@ func TestAPITeam(t *testing.T) {
|
||||
unittest.AssertNotExistsBean(t, &organization.Team{ID: teamID})
|
||||
}
|
||||
|
||||
func checkTeamResponse(t *testing.T, testName string, apiTeam *api.Team, name, description string, includesAllRepositories bool, permission string, units []string, unitsMap map[string]string) {
|
||||
func checkTeamResponse(t *testing.T, testName string, apiTeam *api.Team, name, description string, includesAllRepositories bool, permission api.AccessLevelName, units []string, unitsMap map[string]string) {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
assert.Equal(t, name, apiTeam.Name, "name")
|
||||
assert.Equal(t, description, apiTeam.Description, "description")
|
||||
@@ -244,7 +244,7 @@ func checkTeamResponse(t *testing.T, testName string, apiTeam *api.Team, name, d
|
||||
})
|
||||
}
|
||||
|
||||
func checkTeamBean(t *testing.T, id int64, name, description string, includesAllRepositories bool, permission string, units []string, unitsMap map[string]string) {
|
||||
func checkTeamBean(t *testing.T, id int64, name, description string, includesAllRepositories bool, permission api.AccessLevelName, units []string, unitsMap map[string]string) {
|
||||
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: id})
|
||||
assert.NoError(t, team.LoadUnits(t.Context()), "LoadUnits")
|
||||
apiTeam, err := convert.ToTeam(t.Context(), team)
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestAPIUserSearchLoggedIn(t *testing.T) {
|
||||
for _, user := range results.Data {
|
||||
assert.Contains(t, user.UserName, query)
|
||||
assert.NotEmpty(t, user.Email)
|
||||
assert.Equal(t, "public", user.Visibility)
|
||||
assert.Equal(t, api.UserVisibilityPublic, user.Visibility)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func TestAPIUserSearchAdminLoggedInUserHidden(t *testing.T) {
|
||||
for _, user := range results.Data {
|
||||
assert.Contains(t, user.UserName, query)
|
||||
assert.NotEmpty(t, user.Email)
|
||||
assert.Equal(t, "private", user.Visibility)
|
||||
assert.Equal(t, api.UserVisibilityPrivate, user.Visibility)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -192,17 +192,21 @@ func testRenameInvalidUsername(t *testing.T) {
|
||||
}
|
||||
|
||||
func testRenameReservedUsername(t *testing.T) {
|
||||
reservedUsernames := []string{
|
||||
// ".", "..", ".well-known", // The names are not only reserved but also invalid
|
||||
// ".", "..", ".well-known" are also reserved but invalid as form input.
|
||||
reservedNames := []string{
|
||||
"api",
|
||||
"openapi3.v1.json",
|
||||
"swagger.v1.json",
|
||||
}
|
||||
patternNotAllowedNames := []string{
|
||||
"name.keys",
|
||||
}
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
locale := translation.NewLocale("en-US")
|
||||
for _, reservedUsername := range reservedUsernames {
|
||||
check := func(name, msgKey string) {
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": reservedUsername,
|
||||
"name": name,
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
})
|
||||
@@ -212,12 +216,14 @@ func testRenameReservedUsername(t *testing.T) {
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
actualMsg := strings.TrimSpace(htmlDoc.doc.Find(".ui.negative.message").Text())
|
||||
expectedMsg := locale.TrString("user.form.name_reserved", reservedUsername)
|
||||
if strings.Contains(reservedUsername, ".") {
|
||||
expectedMsg = locale.TrString("user.form.name_pattern_not_allowed", reservedUsername)
|
||||
}
|
||||
assert.Equal(t, expectedMsg, actualMsg)
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: reservedUsername})
|
||||
assert.Equal(t, locale.TrString(msgKey, name), actualMsg)
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: name})
|
||||
}
|
||||
for _, name := range reservedNames {
|
||||
check(name, "user.form.name_reserved")
|
||||
}
|
||||
for _, name := range patternNotAllowedNames {
|
||||
check(name, "user.form.name_pattern_not_allowed")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user