Files
gitea/services/convert/convert.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

886 lines
30 KiB
Go

// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package convert
import (
"bytes"
"context"
"fmt"
"net/url"
"path"
"strconv"
"strings"
"time"
actions_model "code.gitea.io/gitea/models/actions"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/gitdiff"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"github.com/nektos/act/pkg/model"
)
// ToEmail convert models.EmailAddress to api.Email
func ToEmail(email *user_model.EmailAddress) *api.Email {
return &api.Email{
Email: email.Email,
Verified: email.IsActivated,
Primary: email.IsPrimary,
}
}
// ToEmail convert models.EmailAddress to api.Email
func ToEmailSearch(email *user_model.SearchEmailResult) *api.Email {
return &api.Email{
Email: email.Email,
Verified: email.IsActivated,
Primary: email.IsPrimary,
UserID: email.UID,
UserName: email.Name,
}
}
// ToBranch convert a git.Commit and git.Branch to an api.Branch
func ToBranch(ctx context.Context, repo *repo_model.Repository, branchName string, c *git.Commit, bp *git_model.ProtectedBranch, user *user_model.User, isRepoAdmin bool) (*api.Branch, error) {
if bp == nil {
var hasPerm bool
var canPush bool
var err error
if user != nil {
hasPerm, err = access_model.HasAccessUnit(ctx, user, repo, unit.TypeCode, perm.AccessModeWrite)
if err != nil {
return nil, err
}
perms, err := access_model.GetIndividualUserRepoPermission(ctx, repo, user)
if err != nil {
return nil, err
}
canPush = issues_model.CanMaintainerWriteToBranch(ctx, perms, branchName, user)
}
return &api.Branch{
Name: branchName,
Commit: ToPayloadCommit(ctx, repo, c),
Protected: false,
RequiredApprovals: 0,
EnableStatusCheck: false,
StatusCheckContexts: []string{},
UserCanPush: canPush,
UserCanMerge: hasPerm,
}, nil
}
branch := &api.Branch{
Name: branchName,
Commit: ToPayloadCommit(ctx, repo, c),
Protected: true,
RequiredApprovals: bp.RequiredApprovals,
EnableStatusCheck: bp.EnableStatusCheck,
StatusCheckContexts: bp.StatusCheckContexts,
}
if isRepoAdmin {
branch.EffectiveBranchProtectionName = bp.RuleName
}
if user != nil {
permission, err := access_model.GetIndividualUserRepoPermission(ctx, repo, user)
if err != nil {
return nil, err
}
bp.Repo = repo
branch.UserCanPush = bp.CanUserPush(ctx, user)
branch.UserCanMerge = git_model.IsUserMergeWhitelisted(ctx, bp, user.ID, permission)
}
return branch, nil
}
// getWhitelistEntities returns the names of the entities that are in the whitelist
func getWhitelistEntities[T *user_model.User | *organization.Team](entities []T, whitelistIDs []int64) []string {
whitelistUserIDsSet := container.SetOf(whitelistIDs...)
whitelistNames := make([]string, 0)
for _, entity := range entities {
switch v := any(entity).(type) {
case *user_model.User:
if whitelistUserIDsSet.Contains(v.ID) {
whitelistNames = append(whitelistNames, v.Name)
}
case *organization.Team:
if whitelistUserIDsSet.Contains(v.ID) {
whitelistNames = append(whitelistNames, v.Name)
}
}
}
return whitelistNames
}
// ToBranchProtection convert a ProtectedBranch to api.BranchProtection
func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo *repo_model.Repository) *api.BranchProtection {
readers, err := access_model.GetUsersWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests)
if err != nil {
log.Error("GetRepoReaders: %v", err)
}
pushWhitelistUsernames := getWhitelistEntities(readers, bp.WhitelistUserIDs)
forcePushAllowlistUsernames := getWhitelistEntities(readers, bp.ForcePushAllowlistUserIDs)
mergeWhitelistUsernames := getWhitelistEntities(readers, bp.MergeWhitelistUserIDs)
approvalsWhitelistUsernames := getWhitelistEntities(readers, bp.ApprovalsWhitelistUserIDs)
teamReaders, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.Owner.ID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil {
log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err)
}
pushWhitelistTeams := getWhitelistEntities(teamReaders, bp.WhitelistTeamIDs)
forcePushAllowlistTeams := getWhitelistEntities(teamReaders, bp.ForcePushAllowlistTeamIDs)
mergeWhitelistTeams := getWhitelistEntities(teamReaders, bp.MergeWhitelistTeamIDs)
approvalsWhitelistTeams := getWhitelistEntities(teamReaders, bp.ApprovalsWhitelistTeamIDs)
branchName := ""
if !git_model.IsRuleNameSpecial(bp.RuleName) {
branchName = bp.RuleName
}
return &api.BranchProtection{
BranchName: branchName,
RuleName: bp.RuleName,
Priority: bp.Priority,
EnablePush: bp.CanPush,
EnablePushWhitelist: bp.EnableWhitelist,
PushWhitelistUsernames: pushWhitelistUsernames,
PushWhitelistTeams: pushWhitelistTeams,
PushWhitelistDeployKeys: bp.WhitelistDeployKeys,
EnableForcePush: bp.CanForcePush,
EnableForcePushAllowlist: bp.EnableForcePushAllowlist,
ForcePushAllowlistUsernames: forcePushAllowlistUsernames,
ForcePushAllowlistTeams: forcePushAllowlistTeams,
ForcePushAllowlistDeployKeys: bp.ForcePushAllowlistDeployKeys,
EnableMergeWhitelist: bp.EnableMergeWhitelist,
MergeWhitelistUsernames: mergeWhitelistUsernames,
MergeWhitelistTeams: mergeWhitelistTeams,
EnableStatusCheck: bp.EnableStatusCheck,
StatusCheckContexts: bp.StatusCheckContexts,
RequiredApprovals: bp.RequiredApprovals,
EnableApprovalsWhitelist: bp.EnableApprovalsWhitelist,
ApprovalsWhitelistUsernames: approvalsWhitelistUsernames,
ApprovalsWhitelistTeams: approvalsWhitelistTeams,
BlockOnRejectedReviews: bp.BlockOnRejectedReviews,
BlockOnOfficialReviewRequests: bp.BlockOnOfficialReviewRequests,
BlockOnOutdatedBranch: bp.BlockOnOutdatedBranch,
DismissStaleApprovals: bp.DismissStaleApprovals,
IgnoreStaleApprovals: bp.IgnoreStaleApprovals,
RequireSignedCommits: bp.RequireSignedCommits,
ProtectedFilePatterns: bp.ProtectedFilePatterns,
UnprotectedFilePatterns: bp.UnprotectedFilePatterns,
BlockAdminMergeOverride: bp.BlockAdminMergeOverride,
Created: bp.CreatedUnix.AsTime(),
Updated: bp.UpdatedUnix.AsTime(),
}
}
// ToTag convert a git.Tag to an api.Tag
func ToTag(repo *repo_model.Repository, t *git.Tag) *api.Tag {
tarballURL := repo.HTMLURL() + "/archive/" + url.PathEscape(t.Name+".tar.gz")
zipballURL := repo.HTMLURL() + "/archive/" + url.PathEscape(t.Name+".zip")
// Archive URLs are "" if the download feature is disabled
if setting.Repository.DisableDownloadSourceArchives {
tarballURL = ""
zipballURL = ""
}
return &api.Tag{
Name: t.Name,
Message: strings.TrimSpace(t.Message),
ID: t.ID.String(),
Commit: ToCommitMeta(repo, t),
ZipballURL: zipballURL,
TarballURL: tarballURL,
}
}
// ToActionTask convert an actions_model.ActionTask to an api.ActionTask
func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.ActionTask, error) {
// don't need Steps here, only need to load job and its run
if err := t.LoadJob(ctx); err != nil {
return nil, err
}
if err := t.Job.LoadRun(ctx); err != nil {
return nil, err
}
if err := t.Job.Run.LoadRepo(ctx); err != nil {
return nil, err
}
return &api.ActionTask{
ID: t.ID,
Name: t.Job.Name,
HeadBranch: t.Job.Run.PrettyRef(),
HeadSHA: t.Job.CommitSHA,
RunNumber: t.Job.Run.Index,
Event: t.Job.Run.TriggerEvent,
DisplayTitle: t.Job.Run.Title,
Status: t.Status.String(),
WorkflowID: t.Job.Run.WorkflowID,
URL: httplib.MakeAbsoluteURL(ctx, t.Job.Run.Link()),
CreatedAt: t.Created.AsLocalTime(),
UpdatedAt: t.Updated.AsLocalTime(),
RunStartedAt: t.Started.AsLocalTime(),
}, nil
}
func ToActionWorkflowRun(ctx context.Context, run *actions_model.ActionRun, attempt *actions_model.ActionRunAttempt) (_ *api.ActionWorkflowRun, err error) {
if err := run.LoadRepo(ctx); err != nil {
return nil, err
}
if err := run.LoadTriggerUser(ctx); err != nil {
return nil, err
}
if attempt == nil {
attempt, _, err = run.GetLatestAttempt(ctx)
if err != nil {
return nil, err
}
}
runAttempt := int64(0)
status, conclusion := ToActionsStatus(run.Status)
startedAt := run.Started.AsLocalTime()
completedAt := run.Stopped.AsLocalTime()
actor := run.TriggerUser // The username of the user that triggered the initial workflow run.
triggerUser := run.TriggerUser // The username of the user that initiated the workflow run. If the workflow run is a re-run, this value may differ from actor.
// previousAttemptURL is the value of ActionWorkflowRun.PreviousAttemptURL, which is declared as *string without `omitempty` on purpose:
// a nil value must still appear in the JSON body as `"previous_attempt_url": null`, matching GitHub's Actions API.
var previousAttemptURL *string
if attempt != nil {
attempt.Run = run
if err := attempt.LoadAttributes(ctx); err != nil {
return nil, err
}
runAttempt = attempt.Attempt
status, conclusion = ToActionsStatus(attempt.Status)
startedAt = attempt.Started.AsLocalTime()
completedAt = attempt.Stopped.AsLocalTime()
triggerUser = attempt.TriggerUser
if attempt.Attempt > 1 {
previousAttemptURL = new(fmt.Sprintf("%s/actions/runs/%d/attempts/%d", run.Repo.APIURL(ctx), run.ID, attempt.Attempt-1))
}
}
return &api.ActionWorkflowRun{
ID: run.ID,
URL: fmt.Sprintf("%s/actions/runs/%d", run.Repo.APIURL(ctx), run.ID),
PreviousAttemptURL: previousAttemptURL,
HTMLURL: run.HTMLURL(ctx),
RunNumber: run.Index,
RunAttempt: runAttempt,
StartedAt: startedAt,
CompletedAt: completedAt,
Event: run.TriggerEvent,
DisplayTitle: run.Title,
HeadBranch: git.RefName(run.Ref).BranchName(),
HeadSha: run.CommitSHA,
Status: status,
Conclusion: conclusion,
Path: fmt.Sprintf("%s@%s", run.WorkflowID, run.Ref),
Repository: ToRepo(ctx, run.Repo, access_model.Permission{AccessMode: perm.AccessModeNone}),
TriggerActor: ToUser(ctx, triggerUser, nil),
Actor: ToUser(ctx, actor, nil),
}, nil
}
func ToWorkflowRunAction(status actions_model.Status) string {
var action string
switch status {
case actions_model.StatusWaiting, actions_model.StatusBlocked:
action = "requested"
case actions_model.StatusRunning:
action = "in_progress"
}
if status.IsDone() {
action = "completed"
}
return action
}
func ToActionsStatus(status actions_model.Status) (string, string) {
var action string
var conclusion string
switch status {
// This is a naming conflict of the webhook between Gitea and GitHub Actions
case actions_model.StatusWaiting:
action = "queued"
case actions_model.StatusBlocked:
action = "waiting"
case actions_model.StatusRunning:
action = "in_progress"
}
if status.IsDone() {
action = "completed"
switch status {
case actions_model.StatusSuccess:
conclusion = "success"
case actions_model.StatusCancelled:
conclusion = "cancelled"
case actions_model.StatusFailure:
conclusion = "failure"
case actions_model.StatusSkipped:
conclusion = "skipped"
}
}
return action, conclusion
}
// ToActionWorkflowJob convert a actions_model.ActionRunJob to an api.ActionWorkflowJob
// task is optional and can be nil
func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task *actions_model.ActionTask, job *actions_model.ActionRunJob) (*api.ActionWorkflowJob, error) {
err := job.LoadAttributes(ctx)
if err != nil {
return nil, err
}
status, conclusion := ToActionsStatus(job.Status)
var runnerID int64
var runnerName string
var steps []*api.ActionWorkflowStep
if effectiveTaskID := job.EffectiveTaskID(); effectiveTaskID != 0 {
if task == nil {
task, _, err = db.GetByID[actions_model.ActionTask](ctx, effectiveTaskID)
if err != nil {
return nil, err
}
}
if task != nil {
if task.Steps == nil {
task.Steps, err = actions_model.GetTaskStepsByTaskID(ctx, task.ID)
if err != nil {
return nil, err
}
task.Steps = util.SliceNilAsEmpty(task.Steps)
}
runnerID = task.RunnerID
if runner, ok, _ := db.GetByID[actions_model.ActionRunner](ctx, runnerID); ok {
runnerName = runner.Name
}
for i, step := range task.Steps {
stepStatus, stepConclusion := ToActionsStatus(job.Status)
steps = append(steps, &api.ActionWorkflowStep{
Name: step.Name,
Number: int64(i),
Status: stepStatus,
Conclusion: stepConclusion,
StartedAt: step.Started.AsTime().UTC(),
CompletedAt: step.Stopped.AsTime().UTC(),
})
}
}
}
return &api.ActionWorkflowJob{
ID: job.ID,
// missing api endpoint for this location
URL: fmt.Sprintf("%s/actions/jobs/%d", repo.APIURL(ctx), job.ID),
HTMLURL: fmt.Sprintf("%s/jobs/%d", job.Run.HTMLURL(ctx), job.ID),
RunID: job.RunID,
// Missing api endpoint for this location, artifacts are available under a nested url
RunURL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(ctx), job.RunID),
Name: job.Name,
Labels: job.RunsOn,
RunAttempt: job.Attempt,
HeadSha: job.Run.CommitSHA,
HeadBranch: git.RefName(job.Run.Ref).BranchName(),
Status: status,
Conclusion: conclusion,
RunnerID: runnerID,
RunnerName: runnerName,
Steps: util.SliceNilAsEmpty(steps),
CreatedAt: job.Created.AsTime().UTC(),
StartedAt: job.Started.AsTime().UTC(),
CompletedAt: job.Stopped.AsTime().UTC(),
}, nil
}
func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, refName git.RefName, folder string, entry *git.TreeEntry) *api.ActionWorkflow {
cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions)
cfg := cfgUnit.ActionsConfig()
workflowURL := fmt.Sprintf("%s/actions/workflows/%s", repo.APIURL(), util.PathEscapeSegments(entry.Name()))
workflowRepoURL := fmt.Sprintf("%s/src/commit/%s/%s/%s", repo.HTMLURL(ctx), commit.ID.String(), util.PathEscapeSegments(folder), util.PathEscapeSegments(entry.Name()))
if refWebLinkPath := refName.RefWebLinkPath(); refWebLinkPath != "" {
workflowRepoURL = fmt.Sprintf("%s/src/%s/%s/%s", repo.HTMLURL(ctx), refWebLinkPath, util.PathEscapeSegments(folder), util.PathEscapeSegments(entry.Name()))
}
badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", repo.HTMLURL(ctx), util.PathEscapeSegments(entry.Name()), url.QueryEscape(repo.DefaultBranch))
// See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow
// State types:
// - active
// - deleted
// - disabled_fork
// - disabled_inactivity
// - disabled_manually
state := "active"
if cfg.IsWorkflowDisabled(entry.Name()) {
state = "disabled_manually"
}
// The CreatedAt and UpdatedAt fields currently reflect the timestamp of the latest commit, which can later be refined
// by retrieving the first and last commits for the file history. The first commit would indicate the creation date,
// while the last commit would represent the modification date. The DeletedAt could be determined by identifying
// the last commit where the file existed. However, this implementation has not been done here yet, as it would likely
// cause a significant performance degradation.
createdAt := commit.Author.When
updatedAt := commit.Author.When
content, err := actions.GetContentFromEntry(entry)
name := entry.Name()
if err == nil {
workflow, err := model.ReadWorkflow(bytes.NewReader(content))
if err == nil {
// Only use the name when specified in the workflow file
if workflow.Name != "" {
name = workflow.Name
}
} else {
log.Error("getActionWorkflowEntry: Failed to parse workflow: %v", err)
}
} else {
log.Error("getActionWorkflowEntry: Failed to get content from entry: %v", err)
}
return &api.ActionWorkflow{
ID: entry.Name(),
Name: name,
Path: path.Join(folder, entry.Name()),
State: state,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
URL: workflowURL,
HTMLURL: workflowRepoURL,
BadgeURL: badgeURL,
}
}
func ListActionWorkflows(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository) ([]*api.ActionWorkflow, error) {
defaultBranchCommit, err := gitrepo.GetBranchCommit(repo.DefaultBranch)
if err != nil {
return nil, err
}
folder, entries, err := actions.ListWorkflows(defaultBranchCommit)
if err != nil {
return nil, err
}
workflows := make([]*api.ActionWorkflow, len(entries))
for i, entry := range entries {
workflows[i] = getActionWorkflowEntry(ctx, repo, defaultBranchCommit, git.RefNameFromBranch(repo.DefaultBranch), folder, entry)
}
return workflows, nil
}
func GetActionWorkflow(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository, workflowID string) (*api.ActionWorkflow, error) {
defaultBranchCommit, err := gitrepo.GetBranchCommit(repo.DefaultBranch)
if err != nil {
return nil, err
}
return getActionWorkflowFromCommit(ctx, repo, defaultBranchCommit, git.RefNameFromBranch(repo.DefaultBranch), workflowID)
}
func GetActionWorkflowByRef(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository, workflowID string, ref git.RefName) (*api.ActionWorkflow, error) {
if ref == "" {
return nil, util.NewNotExistErrorf("workflow %q not found", workflowID)
}
refCommitID, err := gitrepo.GetRefCommitID(ref.String())
if err != nil {
return nil, err
}
refCommit, err := gitrepo.GetCommit(refCommitID)
if err != nil {
return nil, err
}
return getActionWorkflowFromCommit(ctx, repo, refCommit, ref, workflowID)
}
func getActionWorkflowFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, refName git.RefName, workflowID string) (*api.ActionWorkflow, error) {
folder, entries, err := actions.ListWorkflows(commit)
if err != nil {
return nil, err
}
for _, entry := range entries {
if entry.Name() == workflowID {
return getActionWorkflowEntry(ctx, repo, commit, refName, folder, entry), nil
}
}
return nil, util.NewNotExistErrorf("workflow %q not found", workflowID)
}
// ToActionArtifact convert a actions_model.ActionArtifact to an api.ActionArtifact
func ToActionArtifact(repo *repo_model.Repository, art *actions_model.ActionArtifact) (*api.ActionArtifact, error) {
url := fmt.Sprintf("%s/actions/artifacts/%d", repo.APIURL(), art.ID)
return &api.ActionArtifact{
ID: art.ID,
Name: art.ArtifactName,
SizeInBytes: art.FileSize,
Expired: art.Status == actions_model.ArtifactStatusExpired,
URL: url,
ArchiveDownloadURL: url + "/zip",
CreatedAt: art.CreatedUnix.AsLocalTime(),
UpdatedAt: art.UpdatedUnix.AsLocalTime(),
ExpiresAt: art.ExpiredUnix.AsLocalTime(),
WorkflowRun: &api.ActionWorkflowRun{
ID: art.RunID,
RepositoryID: art.RepoID,
HeadSha: art.CommitSHA,
},
}, nil
}
func ToActionRunner(ctx context.Context, runner *actions_model.ActionRunner) *api.ActionRunner {
status := runner.Status()
apiStatus := "offline"
if runner.IsOnline() {
apiStatus = "online"
}
labels := make([]*api.ActionRunnerLabel, len(runner.AgentLabels))
for i, label := range runner.AgentLabels {
labels[i] = &api.ActionRunnerLabel{
ID: int64(i),
Name: label,
Type: "custom",
}
}
return &api.ActionRunner{
ID: runner.ID,
Name: runner.Name,
Status: apiStatus,
Busy: status == runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE,
Disabled: runner.IsDisabled,
Ephemeral: runner.Ephemeral,
Labels: labels,
}
}
// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification {
verif := asymkey_service.ParseCommitWithSignature(ctx, c)
commitVerification := &api.PayloadCommitVerification{
Verified: verif.Verified,
Reason: verif.Reason,
}
if c.Signature != nil {
commitVerification.Signature = c.Signature.Signature
commitVerification.Payload = c.Signature.Payload
}
if verif.SigningUser != nil {
commitVerification.Signer = &api.PayloadUser{
UserName: verif.SigningUser.Name,
Name: verif.SigningUser.DisplayName(),
Email: verif.SigningEmail, // Use the email from the signature, not from the user profile
}
}
return commitVerification
}
// ToPublicKey convert asymkey_model.PublicKey to api.PublicKey
func ToPublicKey(apiLink string, key *asymkey_model.PublicKey) *api.PublicKey {
return &api.PublicKey{
ID: key.ID,
Key: key.Content,
URL: fmt.Sprintf("%s%d", apiLink, key.ID),
Title: key.Name,
Fingerprint: key.Fingerprint,
Created: key.CreatedUnix.AsTime(),
Updated: key.UpdatedUnix.AsTime(),
}
}
// ToGPGKey converts models.GPGKey to api.GPGKey
func ToGPGKey(key *asymkey_model.GPGKey) *api.GPGKey {
subkeys := make([]*api.GPGKey, len(key.SubsKey))
for id, k := range key.SubsKey {
subkeys[id] = &api.GPGKey{
ID: k.ID,
PrimaryKeyID: k.PrimaryKeyID,
KeyID: k.KeyID,
PublicKey: k.Content,
Created: k.CreatedUnix.AsTime(),
Expires: k.ExpiredUnix.AsTime(),
CanSign: k.CanSign,
CanEncryptComms: k.CanEncryptComms,
CanEncryptStorage: k.CanEncryptStorage,
CanCertify: k.CanSign,
Verified: k.Verified,
}
}
emails := make([]*api.GPGKeyEmail, len(key.Emails))
for i, e := range key.Emails {
emails[i] = ToGPGKeyEmail(e)
}
return &api.GPGKey{
ID: key.ID,
PrimaryKeyID: key.PrimaryKeyID,
KeyID: key.KeyID,
PublicKey: key.Content,
Created: key.CreatedUnix.AsTime(),
Expires: key.ExpiredUnix.AsTime(),
Emails: emails,
SubsKey: subkeys,
CanSign: key.CanSign,
CanEncryptComms: key.CanEncryptComms,
CanEncryptStorage: key.CanEncryptStorage,
CanCertify: key.CanSign,
Verified: key.Verified,
}
}
// ToGPGKeyEmail convert models.EmailAddress to api.GPGKeyEmail
func ToGPGKeyEmail(email *user_model.EmailAddress) *api.GPGKeyEmail {
return &api.GPGKeyEmail{
Email: email.Email,
Verified: email.IsActivated,
}
}
// ToGitHook convert git.Hook to api.GitHook
func ToGitHook(h *git.Hook) *api.GitHook {
return &api.GitHook{
Name: h.Name(),
IsActive: h.IsActive,
Content: h.Content,
}
}
// ToDeployKey convert asymkey_model.DeployKey to api.DeployKey
func ToDeployKey(apiLink string, key *asymkey_model.DeployKey) *api.DeployKey {
return &api.DeployKey{
ID: key.ID,
KeyID: key.KeyID,
Key: key.Content,
Fingerprint: key.Fingerprint,
URL: fmt.Sprintf("%s%d", apiLink, key.ID),
Title: key.Name,
Created: key.CreatedUnix.AsTime(),
ReadOnly: key.Mode == perm.AccessModeRead, // All deploy keys are read-only.
}
}
// ToOrganization convert user_model.User to api.Organization
func ToOrganization(ctx context.Context, org *organization.Organization) *api.Organization {
return &api.Organization{
ID: org.ID,
AvatarURL: org.AsUser().AvatarLink(ctx),
Name: org.Name,
UserName: org.Name,
FullName: org.FullName,
Email: org.Email,
Description: org.Description,
Website: org.Website,
Location: org.Location,
Visibility: api.UserVisibility(org.Visibility.String()),
RepoAdminChangeTeamAccess: org.RepoAdminChangeTeamAccess,
}
}
// ToTeam convert models.Team to api.Team
func ToTeam(ctx context.Context, team *organization.Team, loadOrg ...bool) (*api.Team, error) {
teams, err := ToTeams(ctx, []*organization.Team{team}, len(loadOrg) != 0 && loadOrg[0])
if err != nil || len(teams) == 0 {
return nil, err
}
return teams[0], nil
}
// ToTeams convert models.Team list to api.Team list
func ToTeams(ctx context.Context, teams []*organization.Team, loadOrgs bool) ([]*api.Team, error) {
cache := make(map[int64]*api.Organization)
apiTeams := make([]*api.Team, 0, len(teams))
for _, t := range teams {
if err := t.LoadUnits(ctx); err != nil {
return nil, err
}
apiTeam := &api.Team{
ID: t.ID,
Name: t.Name,
Description: t.Description,
IncludesAllRepositories: t.IncludesAllRepositories,
CanCreateOrgRepo: t.CanCreateOrgRepo,
Permission: api.AccessLevelName(t.AccessMode.ToString()),
Units: t.GetUnitNames(),
UnitsMap: t.GetUnitsMap(),
}
if loadOrgs {
apiOrg, ok := cache[t.OrgID]
if !ok {
org, err := organization.GetOrgByID(ctx, t.OrgID)
if err != nil {
return nil, err
}
apiOrg = ToOrganization(ctx, org)
cache[t.OrgID] = apiOrg
}
apiTeam.Organization = apiOrg
}
apiTeams = append(apiTeams, apiTeam)
}
return apiTeams, nil
}
// ToAnnotatedTag convert git.Tag to api.AnnotatedTag
func ToAnnotatedTag(ctx context.Context, repo *repo_model.Repository, t *git.Tag, c *git.Commit) *api.AnnotatedTag {
return &api.AnnotatedTag{
Tag: t.Name,
SHA: t.ID.String(),
Object: ToAnnotatedTagObject(repo, c),
Message: t.Message,
URL: repo.APIURL() + "/git/tags/" + t.ID.String(),
Tagger: ToCommitUser(t.Tagger),
Verification: ToVerification(ctx, c),
}
}
// ToAnnotatedTagObject convert a git.Commit to an api.AnnotatedTagObject
func ToAnnotatedTagObject(repo *repo_model.Repository, commit *git.Commit) *api.AnnotatedTagObject {
return &api.AnnotatedTagObject{
SHA: commit.ID.String(),
Type: string(git.ObjectCommit),
URL: repo.APIURL() + "/git/commits/" + commit.ID.String(),
}
}
// ToTagProtection convert a git.ProtectedTag to an api.TagProtection
func ToTagProtection(ctx context.Context, pt *git_model.ProtectedTag, repo *repo_model.Repository) *api.TagProtection {
readers, err := access_model.GetUsersWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests)
if err != nil {
log.Error("GetRepoReaders: %v", err)
}
whitelistUsernames := getWhitelistEntities(readers, pt.AllowlistUserIDs)
teamReaders, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.Owner.ID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil {
log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err)
}
whitelistTeams := getWhitelistEntities(teamReaders, pt.AllowlistTeamIDs)
return &api.TagProtection{
ID: pt.ID,
NamePattern: pt.NamePattern,
WhitelistUsernames: whitelistUsernames,
WhitelistTeams: whitelistTeams,
Created: pt.CreatedUnix.AsTime(),
Updated: pt.UpdatedUnix.AsTime(),
}
}
// ToTopicResponse convert from models.Topic to api.TopicResponse
func ToTopicResponse(topic *repo_model.Topic) *api.TopicResponse {
return &api.TopicResponse{
ID: topic.ID,
Name: topic.Name,
RepoCount: topic.RepoCount,
Created: topic.CreatedUnix.AsTime(),
Updated: topic.UpdatedUnix.AsTime(),
}
}
// ToOAuth2Application convert from auth.OAuth2Application to api.OAuth2Application
func ToOAuth2Application(app *auth.OAuth2Application) *api.OAuth2Application {
return &api.OAuth2Application{
ID: app.ID,
Name: app.Name,
ClientID: app.ClientID,
ClientSecret: app.ClientSecret,
ConfidentialClient: app.ConfidentialClient,
SkipSecondaryAuthorization: app.SkipSecondaryAuthorization,
RedirectURIs: app.RedirectURIs,
Created: app.CreatedUnix.AsTime(),
}
}
// ToLFSLock convert a LFSLock to api.LFSLock
func ToLFSLock(ctx context.Context, l *git_model.LFSLock) *api.LFSLock {
_, u, err := user_model.GetPossibleUserByID(ctx, l.OwnerID)
if err != nil {
return nil
}
return &api.LFSLock{
ID: strconv.FormatInt(l.ID, 10),
Path: l.Path,
LockedAt: l.Created.Round(time.Second),
Owner: &api.LFSLockOwner{
Name: u.Name,
},
}
}
// ToChangedFile convert a gitdiff.DiffFile to api.ChangedFile
func ToChangedFile(f *gitdiff.DiffFile, repo *repo_model.Repository, commit string) *api.ChangedFile {
status := "changed"
previousFilename := ""
if f.IsDeleted {
status = "deleted"
} else if f.IsCreated {
status = "added"
} else if f.IsRenamed && f.Type == gitdiff.DiffFileCopy {
status = "copied"
} else if f.IsRenamed && f.Type == gitdiff.DiffFileRename {
status = "renamed"
previousFilename = f.OldName
} else if f.Addition == 0 && f.Deletion == 0 {
status = "unchanged"
}
file := &api.ChangedFile{
Filename: f.GetDiffFileName(),
Status: status,
Additions: f.Addition,
Deletions: f.Deletion,
Changes: f.Addition + f.Deletion,
PreviousFilename: previousFilename,
HTMLURL: fmt.Sprint(repo.HTMLURL(), "/src/commit/", commit, "/", util.PathEscapeSegments(f.GetDiffFileName())),
ContentsURL: fmt.Sprint(repo.APIURL(), "/contents/", util.PathEscapeSegments(f.GetDiffFileName()), "?ref=", commit),
RawURL: fmt.Sprint(repo.HTMLURL(), "/raw/commit/", commit, "/", util.PathEscapeSegments(f.GetDiffFileName())),
}
return file
}