Files
gitea/services/convert/repository.go
Myers Carpenter 9e031eb3df 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>
2026-04-29 20:47:52 +08:00

269 lines
11 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package convert
import (
"context"
"time"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
)
// ToRepo converts a Repository to api.Repository
func ToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission) *api.Repository {
return innerToRepo(ctx, repo, permissionInRepo, false)
}
func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission, isParent bool) *api.Repository {
var parent *api.Repository
if !permissionInRepo.HasUnits() && permissionInRepo.AccessMode > perm.AccessModeNone {
// If units is empty, it means that it's a hard-coded permission, like access_model.Permission{AccessMode: perm.AccessModeAdmin}
// So we need to load units for the repo, otherwise UnitAccessMode will just return perm.AccessModeNone.
// TODO: this logic is still not right (because unit modes are not correctly prepared)
// the caller should prepare a proper "permission" before calling this function.
_ = repo.LoadUnits(ctx) // the error is not important, so ignore it
permissionInRepo.SetUnitsWithDefaultAccessMode(repo.Units, permissionInRepo.AccessMode)
}
// TODO: ideally we should pass "doer" into "ToRepo" to make CloneLink could generate user-related links
// And passing "doer" in will also fix other FIXMEs in this file.
cloneLink := repo.CloneLinkGeneral(ctx) // no doer at the moment
permission := &api.Permission{
Admin: permissionInRepo.AccessMode >= perm.AccessModeAdmin,
Push: permissionInRepo.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeWrite,
Pull: permissionInRepo.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeRead,
}
if !isParent {
err := repo.GetBaseRepo(ctx)
if err != nil {
return nil
}
if repo.BaseRepo != nil {
// FIXME: The permission of the parent repo is not correct.
// It's the permission of the current repo, so it's probably different from the parent repo.
// But there isn't a good way to get the permission of the parent repo, because the doer is not passed in.
// Use the permission of the current repo to keep the behavior consistent with the old API.
// Maybe the right way is setting the permission of the parent repo to nil, empty is better than wrong.
parent = innerToRepo(ctx, repo.BaseRepo, permissionInRepo, true)
}
}
// check enabled/disabled units
hasIssues := false
var externalTracker *api.ExternalTracker
var internalTracker *api.InternalTracker
if unit, err := repo.GetUnit(ctx, unit_model.TypeIssues); err == nil {
config := unit.IssuesConfig()
hasIssues = true
internalTracker = &api.InternalTracker{
EnableTimeTracker: config.EnableTimetracker,
AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime,
EnableIssueDependencies: config.EnableDependencies,
}
} else if unit, err := repo.GetUnit(ctx, unit_model.TypeExternalTracker); err == nil {
config := unit.ExternalTrackerConfig()
hasIssues = true
externalTracker = &api.ExternalTracker{
ExternalTrackerURL: config.ExternalTrackerURL,
ExternalTrackerFormat: config.ExternalTrackerFormat,
ExternalTrackerStyle: config.ExternalTrackerStyle,
ExternalTrackerRegexpPattern: config.ExternalTrackerRegexpPattern,
}
}
hasWiki := false
var externalWiki *api.ExternalWiki
if _, err := repo.GetUnit(ctx, unit_model.TypeWiki); err == nil {
hasWiki = true
} else if unit, err := repo.GetUnit(ctx, unit_model.TypeExternalWiki); err == nil {
hasWiki = true
config := unit.ExternalWikiConfig()
externalWiki = &api.ExternalWiki{
ExternalWikiURL: config.ExternalWikiURL,
}
}
hasPullRequests := false
ignoreWhitespaceConflicts := false
allowMerge := false
allowRebase := false
allowRebaseMerge := false
allowSquash := false
allowFastForwardOnly := false
allowRebaseUpdate := false
allowManualMerge := true
autodetectManualMerge := false
defaultDeleteBranchAfterMerge := false
defaultMergeStyle := repo_model.MergeStyleMerge
defaultAllowMaintainerEdit := false
defaultTargetBranch := ""
if unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests); err == nil {
config := unit.PullRequestsConfig()
hasPullRequests = true
ignoreWhitespaceConflicts = config.IgnoreWhitespaceConflicts
allowMerge = config.AllowMerge
allowRebase = config.AllowRebase
allowRebaseMerge = config.AllowRebaseMerge
allowSquash = config.AllowSquash
allowFastForwardOnly = config.AllowFastForwardOnly
allowRebaseUpdate = config.AllowRebaseUpdate
allowManualMerge = config.AllowManualMerge
autodetectManualMerge = config.AutodetectManualMerge
defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
defaultMergeStyle = config.DefaultMergeStyle
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
defaultTargetBranch = config.DefaultTargetBranch
}
hasProjects := false
projectsMode := repo_model.ProjectsModeAll
if unit, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
hasProjects = true
config := unit.ProjectsConfig()
projectsMode = config.ProjectsMode
}
hasCode := repo.UnitEnabled(ctx, unit_model.TypeCode)
hasReleases := repo.UnitEnabled(ctx, unit_model.TypeReleases)
hasPackages := repo.UnitEnabled(ctx, unit_model.TypePackages)
hasActions := repo.UnitEnabled(ctx, unit_model.TypeActions)
if err := repo.LoadOwner(ctx); err != nil {
return nil
}
numReleases, _ := db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{
IncludeDrafts: false,
IncludeTags: false,
RepoID: repo.ID,
})
branchCount, err := git_model.CountBranches(ctx, repo.ID, false)
if err != nil {
log.Error("CountBranches [%d]: %v", repo.ID, err)
}
mirrorInterval := ""
var mirrorUpdated time.Time
if repo.IsMirror {
pullMirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID)
if err == nil {
mirrorInterval = pullMirror.Interval.String()
mirrorUpdated = pullMirror.UpdatedUnix.AsTime()
}
}
var transfer *api.RepoTransfer
if repo.Status == repo_model.RepositoryPendingTransfer {
t, err := repo_model.GetPendingRepositoryTransfer(ctx, repo)
if err != nil && !repo_model.IsErrNoPendingTransfer(err) {
log.Warn("GetPendingRepositoryTransfer: %v", err)
} else {
if err := t.LoadAttributes(ctx); err != nil {
log.Warn("LoadAttributes of RepoTransfer: %v", err)
} else {
transfer = ToRepoTransfer(ctx, t)
}
}
}
var language string
if repo.PrimaryLanguage != nil {
language = repo.PrimaryLanguage.Language
}
repoLicenses, err := repo_model.GetRepoLicenses(ctx, repo)
if err != nil {
return nil
}
repoAPIURL := repo.APIURL()
return &api.Repository{
ID: repo.ID,
Owner: ToUserWithAccessMode(ctx, repo.Owner, permissionInRepo.AccessMode),
Name: repo.Name,
FullName: repo.FullName(),
Description: repo.Description,
Private: repo.IsPrivate,
Template: repo.IsTemplate,
Empty: repo.IsEmpty,
Archived: repo.IsArchived,
Size: int(repo.Size / 1024),
Fork: repo.IsFork,
Parent: parent,
Mirror: repo.IsMirror,
HTMLURL: repo.HTMLURL(ctx),
URL: repoAPIURL,
SSHURL: cloneLink.SSH,
CloneURL: cloneLink.HTTPS,
OriginalURL: repo.SanitizedOriginalURL(),
Website: repo.Website,
Language: language,
LanguagesURL: repoAPIURL + "/languages",
Stars: repo.NumStars,
Forks: repo.NumForks,
Watchers: repo.NumWatches,
BranchCount: int(branchCount),
OpenIssues: repo.NumOpenIssues,
OpenPulls: repo.NumOpenPulls,
Releases: int(numReleases),
DefaultBranch: repo.DefaultBranch,
Created: repo.CreatedUnix.AsTime(),
Updated: repo.UpdatedUnix.AsTime(),
ArchivedAt: repo.ArchivedUnix.AsTime(),
Permissions: permission,
HasCode: hasCode,
HasIssues: hasIssues,
ExternalTracker: externalTracker,
InternalTracker: internalTracker,
HasWiki: hasWiki,
HasProjects: hasProjects,
ProjectsMode: string(projectsMode),
HasReleases: hasReleases,
HasPackages: hasPackages,
HasActions: hasActions,
ExternalWiki: externalWiki,
HasPullRequests: hasPullRequests,
IgnoreWhitespaceConflicts: ignoreWhitespaceConflicts,
AllowMerge: allowMerge,
AllowRebase: allowRebase,
AllowRebaseMerge: allowRebaseMerge,
AllowSquash: allowSquash,
AllowFastForwardOnly: allowFastForwardOnly,
AllowRebaseUpdate: allowRebaseUpdate,
AllowManualMerge: allowManualMerge,
AutodetectManualMerge: autodetectManualMerge,
DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
DefaultMergeStyle: string(defaultMergeStyle),
DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit,
DefaultTargetBranch: defaultTargetBranch,
AvatarURL: repo.AvatarLink(ctx),
Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
MirrorInterval: mirrorInterval,
MirrorUpdated: mirrorUpdated,
RepoTransfer: transfer,
Topics: util.SliceNilAsEmpty(repo.Topics),
ObjectFormatName: api.ObjectFormatName(repo.ObjectFormatName),
Licenses: util.SliceNilAsEmpty(repoLicenses.StringList()),
}
}
// ToRepoTransfer convert a models.RepoTransfer to a structs.RepeTransfer
func ToRepoTransfer(ctx context.Context, t *repo_model.RepoTransfer) *api.RepoTransfer {
teams, _ := ToTeams(ctx, t.Teams, false)
return &api.RepoTransfer{
Doer: ToUser(ctx, t.Doer, nil),
Recipient: ToUser(ctx, t.Recipient, nil),
Teams: teams,
}
}