summaryrefslogtreecommitdiffstats
path: root/services/convert
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-12-12 23:57:56 +0100
commite68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch)
tree97775d6c13b0f416af55314eb6a89ef792474615 /services/convert
parentInitial commit. (diff)
downloadforgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz
forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r--services/convert/activity.go52
-rw-r--r--services/convert/attachment.go63
-rw-r--r--services/convert/convert.go510
-rw-r--r--services/convert/git_commit.go228
-rw-r--r--services/convert/git_commit_test.go42
-rw-r--r--services/convert/issue.go288
-rw-r--r--services/convert/issue_comment.go187
-rw-r--r--services/convert/issue_test.go58
-rw-r--r--services/convert/main_test.go16
-rw-r--r--services/convert/mirror.go27
-rw-r--r--services/convert/notification.go98
-rw-r--r--services/convert/package.go53
-rw-r--r--services/convert/pull.go261
-rw-r--r--services/convert/pull_review.go139
-rw-r--r--services/convert/pull_test.go78
-rw-r--r--services/convert/quota.go185
-rw-r--r--services/convert/release.go35
-rw-r--r--services/convert/release_test.go29
-rw-r--r--services/convert/repository.go254
-rw-r--r--services/convert/secret.go18
-rw-r--r--services/convert/status.go65
-rw-r--r--services/convert/user.go113
-rw-r--r--services/convert/user_test.go41
-rw-r--r--services/convert/utils.go44
-rw-r--r--services/convert/utils_test.go39
-rw-r--r--services/convert/wiki.go45
26 files changed, 2968 insertions, 0 deletions
diff --git a/services/convert/activity.go b/services/convert/activity.go
new file mode 100644
index 0000000..01fef73
--- /dev/null
+++ b/services/convert/activity.go
@@ -0,0 +1,52 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+
+ activities_model "code.gitea.io/gitea/models/activities"
+ perm_model "code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+func ToActivity(ctx context.Context, ac *activities_model.Action, doer *user_model.User) *api.Activity {
+ p, err := access_model.GetUserRepoPermission(ctx, ac.Repo, doer)
+ if err != nil {
+ log.Error("GetUserRepoPermission[%d]: %v", ac.RepoID, err)
+ p.AccessMode = perm_model.AccessModeNone
+ }
+
+ result := &api.Activity{
+ ID: ac.ID,
+ UserID: ac.UserID,
+ OpType: ac.OpType.String(),
+ ActUserID: ac.ActUserID,
+ ActUser: ToUser(ctx, ac.ActUser, doer),
+ RepoID: ac.RepoID,
+ Repo: ToRepo(ctx, ac.Repo, p),
+ RefName: ac.RefName,
+ IsPrivate: ac.IsPrivate,
+ Content: ac.Content,
+ Created: ac.CreatedUnix.AsTime(),
+ }
+
+ if ac.Comment != nil {
+ result.CommentID = ac.CommentID
+ result.Comment = ToAPIComment(ctx, ac.Repo, ac.Comment)
+ }
+
+ return result
+}
+
+func ToActivities(ctx context.Context, al activities_model.ActionList, doer *user_model.User) []*api.Activity {
+ result := make([]*api.Activity, 0, len(al))
+ for _, ac := range al {
+ result = append(result, ToActivity(ctx, ac, doer))
+ }
+ return result
+}
diff --git a/services/convert/attachment.go b/services/convert/attachment.go
new file mode 100644
index 0000000..d632c94
--- /dev/null
+++ b/services/convert/attachment.go
@@ -0,0 +1,63 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ repo_model "code.gitea.io/gitea/models/repo"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+func WebAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachment) string {
+ if attach.ExternalURL != "" {
+ return attach.ExternalURL
+ }
+
+ return attach.DownloadURL()
+}
+
+func APIAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachment) string {
+ return attach.DownloadURL()
+}
+
+// ToAttachment converts models.Attachment to api.Attachment for API usage
+func ToAttachment(repo *repo_model.Repository, a *repo_model.Attachment) *api.Attachment {
+ return toAttachment(repo, a, WebAssetDownloadURL)
+}
+
+// ToAPIAttachment converts models.Attachment to api.Attachment for API usage
+func ToAPIAttachment(repo *repo_model.Repository, a *repo_model.Attachment) *api.Attachment {
+ return toAttachment(repo, a, APIAssetDownloadURL)
+}
+
+// toAttachment converts models.Attachment to api.Attachment for API usage
+func toAttachment(repo *repo_model.Repository, a *repo_model.Attachment, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) *api.Attachment {
+ var typeName string
+ if a.ExternalURL != "" {
+ typeName = "external"
+ } else {
+ typeName = "attachment"
+ }
+ return &api.Attachment{
+ ID: a.ID,
+ Name: a.Name,
+ Created: a.CreatedUnix.AsTime(),
+ DownloadCount: a.DownloadCount,
+ Size: a.Size,
+ UUID: a.UUID,
+ DownloadURL: getDownloadURL(repo, a), // for web request json and api request json, return different download urls
+ Type: typeName,
+ }
+}
+
+func ToAPIAttachments(repo *repo_model.Repository, attachments []*repo_model.Attachment) []*api.Attachment {
+ return toAttachments(repo, attachments, APIAssetDownloadURL)
+}
+
+func toAttachments(repo *repo_model.Repository, attachments []*repo_model.Attachment, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) []*api.Attachment {
+ converted := make([]*api.Attachment, 0, len(attachments))
+ for _, attachment := range attachments {
+ converted = append(converted, toAttachment(repo, attachment, getDownloadURL))
+ }
+ return converted
+}
diff --git a/services/convert/convert.go b/services/convert/convert.go
new file mode 100644
index 0000000..7a09449
--- /dev/null
+++ b/services/convert/convert.go
@@ -0,0 +1,510 @@
+// Copyright 2015 The Gogs Authors. All rights reserved.
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+ "fmt"
+ "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"
+ 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/container"
+ "code.gitea.io/gitea/modules/git"
+ "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"
+ "code.gitea.io/gitea/services/gitdiff"
+)
+
+// 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.GetUserRepoPermission(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.GetUserRepoPermission(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.GetRepoReaders(ctx, repo)
+ if err != nil {
+ log.Error("GetRepoReaders: %v", err)
+ }
+
+ pushWhitelistUsernames := getWhitelistEntities(readers, bp.WhitelistUserIDs)
+ mergeWhitelistUsernames := getWhitelistEntities(readers, bp.MergeWhitelistUserIDs)
+ approvalsWhitelistUsernames := getWhitelistEntities(readers, bp.ApprovalsWhitelistUserIDs)
+
+ teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead)
+ if err != nil {
+ log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err)
+ }
+
+ pushWhitelistTeams := getWhitelistEntities(teamReaders, bp.WhitelistTeamIDs)
+ 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,
+ EnablePush: bp.CanPush,
+ EnablePushWhitelist: bp.EnableWhitelist,
+ PushWhitelistUsernames: pushWhitelistUsernames,
+ PushWhitelistTeams: pushWhitelistTeams,
+ PushWhitelistDeployKeys: bp.WhitelistDeployKeys,
+ 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,
+ ApplyToAdmins: bp.ApplyToAdmins,
+ 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 {
+ return &api.Tag{
+ Name: t.Name,
+ Message: strings.TrimSpace(t.Message),
+ ID: t.ID.String(),
+ Commit: ToCommitMeta(repo, t),
+ ZipballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".zip"),
+ TarballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".tar.gz"),
+ ArchiveDownloadCount: t.ArchiveDownloadCount,
+ }
+}
+
+// ToActionTask convert a actions_model.ActionTask to an api.ActionTask
+func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.ActionTask, error) {
+ if err := t.LoadAttributes(ctx); err != nil {
+ return nil, err
+ }
+
+ url := strings.TrimSuffix(setting.AppURL, "/") + t.GetRunLink()
+
+ 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: url,
+ CreatedAt: t.Created.AsLocalTime(),
+ UpdatedAt: t.Updated.AsLocalTime(),
+ RunStartedAt: t.Started.AsLocalTime(),
+ }, nil
+}
+
+// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
+func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification {
+ verif := asymkey_model.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{
+ Name: verif.SigningUser.Name,
+ Email: verif.SigningUser.Email,
+ }
+ }
+ 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(),
+ }
+}
+
+// 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: 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: t.AccessMode.String(),
+ 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: util.URLJoin(repo.APIURL(), "git/tags", t.ID.String()),
+ Tagger: ToCommitUser(t.Tagger),
+ Verification: ToVerification(ctx, c),
+ ArchiveDownloadCount: t.ArchiveDownloadCount,
+ }
+}
+
+// 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: util.URLJoin(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.GetRepoReaders(ctx, repo)
+ if err != nil {
+ log.Error("GetRepoReaders: %v", err)
+ }
+
+ whitelistUsernames := getWhitelistEntities(readers, pt.AllowlistUserIDs)
+
+ teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead)
+ 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,
+ 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.GetUserByID(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
+}
diff --git a/services/convert/git_commit.go b/services/convert/git_commit.go
new file mode 100644
index 0000000..e0efcdd
--- /dev/null
+++ b/services/convert/git_commit.go
@@ -0,0 +1,228 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+ "net/url"
+ "time"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ ctx "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/gitdiff"
+)
+
+// ToCommitUser convert a git.Signature to an api.CommitUser
+func ToCommitUser(sig *git.Signature) *api.CommitUser {
+ return &api.CommitUser{
+ Identity: api.Identity{
+ Name: sig.Name,
+ Email: sig.Email,
+ },
+ Date: sig.When.UTC().Format(time.RFC3339),
+ }
+}
+
+// ToCommitMeta convert a git.Tag to an api.CommitMeta
+func ToCommitMeta(repo *repo_model.Repository, tag *git.Tag) *api.CommitMeta {
+ return &api.CommitMeta{
+ SHA: tag.Object.String(),
+ URL: util.URLJoin(repo.APIURL(), "git/commits", tag.ID.String()),
+ Created: tag.Tagger.When,
+ }
+}
+
+// ToPayloadCommit convert a git.Commit to api.PayloadCommit
+func ToPayloadCommit(ctx context.Context, repo *repo_model.Repository, c *git.Commit) *api.PayloadCommit {
+ authorUsername := ""
+ if author, err := user_model.GetUserByEmail(ctx, c.Author.Email); err == nil {
+ authorUsername = author.Name
+ } else if !user_model.IsErrUserNotExist(err) {
+ log.Error("GetUserByEmail: %v", err)
+ }
+
+ committerUsername := ""
+ if committer, err := user_model.GetUserByEmail(ctx, c.Committer.Email); err == nil {
+ committerUsername = committer.Name
+ } else if !user_model.IsErrUserNotExist(err) {
+ log.Error("GetUserByEmail: %v", err)
+ }
+
+ return &api.PayloadCommit{
+ ID: c.ID.String(),
+ Message: c.Message(),
+ URL: util.URLJoin(repo.HTMLURL(), "commit", c.ID.String()),
+ Author: &api.PayloadUser{
+ Name: c.Author.Name,
+ Email: c.Author.Email,
+ UserName: authorUsername,
+ },
+ Committer: &api.PayloadUser{
+ Name: c.Committer.Name,
+ Email: c.Committer.Email,
+ UserName: committerUsername,
+ },
+ Timestamp: c.Author.When,
+ Verification: ToVerification(ctx, c),
+ }
+}
+
+type ToCommitOptions struct {
+ Stat bool
+ Verification bool
+ Files bool
+}
+
+func ParseCommitOptions(ctx *ctx.APIContext) ToCommitOptions {
+ return ToCommitOptions{
+ Stat: ctx.FormString("stat") == "" || ctx.FormBool("stat"),
+ Files: ctx.FormString("files") == "" || ctx.FormBool("files"),
+ Verification: ctx.FormString("verification") == "" || ctx.FormBool("verification"),
+ }
+}
+
+// ToCommit convert a git.Commit to api.Commit
+func ToCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, userCache map[string]*user_model.User, opts ToCommitOptions) (*api.Commit, error) {
+ var apiAuthor, apiCommitter *api.User
+
+ // Retrieve author and committer information
+
+ var cacheAuthor *user_model.User
+ var ok bool
+ if userCache == nil {
+ cacheAuthor = (*user_model.User)(nil)
+ ok = false
+ } else {
+ cacheAuthor, ok = userCache[commit.Author.Email]
+ }
+
+ if ok {
+ apiAuthor = ToUser(ctx, cacheAuthor, nil)
+ } else {
+ author, err := user_model.GetUserByEmail(ctx, commit.Author.Email)
+ if err != nil && !user_model.IsErrUserNotExist(err) {
+ return nil, err
+ } else if err == nil {
+ apiAuthor = ToUser(ctx, author, nil)
+ if userCache != nil {
+ userCache[commit.Author.Email] = author
+ }
+ }
+ }
+
+ var cacheCommitter *user_model.User
+ if userCache == nil {
+ cacheCommitter = (*user_model.User)(nil)
+ ok = false
+ } else {
+ cacheCommitter, ok = userCache[commit.Committer.Email]
+ }
+
+ if ok {
+ apiCommitter = ToUser(ctx, cacheCommitter, nil)
+ } else {
+ committer, err := user_model.GetUserByEmail(ctx, commit.Committer.Email)
+ if err != nil && !user_model.IsErrUserNotExist(err) {
+ return nil, err
+ } else if err == nil {
+ apiCommitter = ToUser(ctx, committer, nil)
+ if userCache != nil {
+ userCache[commit.Committer.Email] = committer
+ }
+ }
+ }
+
+ // Retrieve parent(s) of the commit
+ apiParents := make([]*api.CommitMeta, commit.ParentCount())
+ for i := 0; i < commit.ParentCount(); i++ {
+ sha, _ := commit.ParentID(i)
+ apiParents[i] = &api.CommitMeta{
+ URL: repo.APIURL() + "/git/commits/" + url.PathEscape(sha.String()),
+ SHA: sha.String(),
+ }
+ }
+
+ res := &api.Commit{
+ CommitMeta: &api.CommitMeta{
+ URL: repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String()),
+ SHA: commit.ID.String(),
+ Created: commit.Committer.When,
+ },
+ HTMLURL: repo.HTMLURL() + "/commit/" + url.PathEscape(commit.ID.String()),
+ RepoCommit: &api.RepoCommit{
+ URL: repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String()),
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: commit.Author.Name,
+ Email: commit.Author.Email,
+ },
+ Date: commit.Author.When.Format(time.RFC3339),
+ },
+ Committer: &api.CommitUser{
+ Identity: api.Identity{
+ Name: commit.Committer.Name,
+ Email: commit.Committer.Email,
+ },
+ Date: commit.Committer.When.Format(time.RFC3339),
+ },
+ Message: commit.Message(),
+ Tree: &api.CommitMeta{
+ URL: repo.APIURL() + "/git/trees/" + url.PathEscape(commit.ID.String()),
+ SHA: commit.ID.String(),
+ Created: commit.Committer.When,
+ },
+ },
+ Author: apiAuthor,
+ Committer: apiCommitter,
+ Parents: apiParents,
+ }
+
+ // Retrieve verification for commit
+ if opts.Verification {
+ res.RepoCommit.Verification = ToVerification(ctx, commit)
+ }
+
+ // Retrieve files affected by the commit
+ if opts.Files {
+ fileStatus, err := git.GetCommitFileStatus(gitRepo.Ctx, repo.RepoPath(), commit.ID.String())
+ if err != nil {
+ return nil, err
+ }
+
+ affectedFileList := make([]*api.CommitAffectedFiles, 0, len(fileStatus.Added)+len(fileStatus.Removed)+len(fileStatus.Modified))
+ for filestatus, files := range map[string][]string{"added": fileStatus.Added, "removed": fileStatus.Removed, "modified": fileStatus.Modified} {
+ for _, filename := range files {
+ affectedFileList = append(affectedFileList, &api.CommitAffectedFiles{
+ Filename: filename,
+ Status: filestatus,
+ })
+ }
+ }
+
+ res.Files = affectedFileList
+ }
+
+ // Get diff stats for commit
+ if opts.Stat {
+ diff, err := gitdiff.GetDiff(ctx, gitRepo, &gitdiff.DiffOptions{
+ AfterCommitID: commit.ID.String(),
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ res.Stats = &api.CommitStats{
+ Total: diff.TotalAddition + diff.TotalDeletion,
+ Additions: diff.TotalAddition,
+ Deletions: diff.TotalDeletion,
+ }
+ }
+
+ return res, nil
+}
diff --git a/services/convert/git_commit_test.go b/services/convert/git_commit_test.go
new file mode 100644
index 0000000..68d1b05
--- /dev/null
+++ b/services/convert/git_commit_test.go
@@ -0,0 +1,42 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "testing"
+ "time"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/git"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestToCommitMeta(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ sha1 := git.Sha1ObjectFormat
+ signature := &git.Signature{Name: "Test Signature", Email: "test@email.com", When: time.Unix(0, 0)}
+ tag := &git.Tag{
+ Name: "Test Tag",
+ ID: sha1.EmptyObjectID(),
+ Object: sha1.EmptyObjectID(),
+ Type: "Test Type",
+ Tagger: signature,
+ Message: "Test Message",
+ }
+
+ commitMeta := ToCommitMeta(headRepo, tag)
+
+ assert.NotNil(t, commitMeta)
+ assert.EqualValues(t, &api.CommitMeta{
+ SHA: sha1.EmptyObjectID().String(),
+ URL: util.URLJoin(headRepo.APIURL(), "git/commits", sha1.EmptyObjectID().String()),
+ Created: time.Unix(0, 0),
+ }, commitMeta)
+}
diff --git a/services/convert/issue.go b/services/convert/issue.go
new file mode 100644
index 0000000..f514dc4
--- /dev/null
+++ b/services/convert/issue.go
@@ -0,0 +1,288 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+ "fmt"
+ "net/url"
+ "strings"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/label"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+func ToIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue {
+ return toIssue(ctx, doer, issue, WebAssetDownloadURL)
+}
+
+// ToAPIIssue converts an Issue to API format
+// it assumes some fields assigned with values:
+// Required - Poster, Labels,
+// Optional - Milestone, Assignee, PullRequest
+func ToAPIIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue {
+ return toIssue(ctx, doer, issue, APIAssetDownloadURL)
+}
+
+func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) *api.Issue {
+ if err := issue.LoadPoster(ctx); err != nil {
+ return &api.Issue{}
+ }
+ if err := issue.LoadRepo(ctx); err != nil {
+ return &api.Issue{}
+ }
+ if err := issue.LoadAttachments(ctx); err != nil {
+ return &api.Issue{}
+ }
+
+ apiIssue := &api.Issue{
+ ID: issue.ID,
+ Index: issue.Index,
+ Poster: ToUser(ctx, issue.Poster, doer),
+ Title: issue.Title,
+ Body: issue.Content,
+ Attachments: toAttachments(issue.Repo, issue.Attachments, getDownloadURL),
+ Ref: issue.Ref,
+ State: issue.State(),
+ IsLocked: issue.IsLocked,
+ Comments: issue.NumComments,
+ Created: issue.CreatedUnix.AsTime(),
+ Updated: issue.UpdatedUnix.AsTime(),
+ PinOrder: issue.PinOrder,
+ }
+
+ if issue.Repo != nil {
+ if err := issue.Repo.LoadOwner(ctx); err != nil {
+ return &api.Issue{}
+ }
+ apiIssue.URL = issue.APIURL(ctx)
+ apiIssue.HTMLURL = issue.HTMLURL()
+ if err := issue.LoadLabels(ctx); err != nil {
+ return &api.Issue{}
+ }
+ apiIssue.Labels = ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner)
+ apiIssue.Repo = &api.RepositoryMeta{
+ ID: issue.Repo.ID,
+ Name: issue.Repo.Name,
+ Owner: issue.Repo.OwnerName,
+ FullName: issue.Repo.FullName(),
+ }
+ }
+
+ if issue.ClosedUnix != 0 {
+ apiIssue.Closed = issue.ClosedUnix.AsTimePtr()
+ }
+
+ if err := issue.LoadMilestone(ctx); err != nil {
+ return &api.Issue{}
+ }
+ if issue.Milestone != nil {
+ apiIssue.Milestone = ToAPIMilestone(issue.Milestone)
+ }
+
+ if err := issue.LoadAssignees(ctx); err != nil {
+ return &api.Issue{}
+ }
+ if len(issue.Assignees) > 0 {
+ for _, assignee := range issue.Assignees {
+ apiIssue.Assignees = append(apiIssue.Assignees, ToUser(ctx, assignee, nil))
+ }
+ apiIssue.Assignee = ToUser(ctx, issue.Assignees[0], nil) // For compatibility, we're keeping the first assignee as `apiIssue.Assignee`
+ }
+ if issue.IsPull {
+ if err := issue.LoadPullRequest(ctx); err != nil {
+ return &api.Issue{}
+ }
+ if issue.PullRequest != nil {
+ apiIssue.PullRequest = &api.PullRequestMeta{
+ HasMerged: issue.PullRequest.HasMerged,
+ IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx),
+ }
+ if issue.PullRequest.HasMerged {
+ apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr()
+ }
+ // Add pr's html url
+ apiIssue.PullRequest.HTMLURL = issue.HTMLURL()
+ }
+ }
+ if issue.DeadlineUnix != 0 {
+ apiIssue.Deadline = issue.DeadlineUnix.AsTimePtr()
+ }
+
+ return apiIssue
+}
+
+// ToIssueList converts an IssueList to API format
+func ToIssueList(ctx context.Context, doer *user_model.User, il issues_model.IssueList) []*api.Issue {
+ result := make([]*api.Issue, len(il))
+ for i := range il {
+ result[i] = ToIssue(ctx, doer, il[i])
+ }
+ return result
+}
+
+// ToAPIIssueList converts an IssueList to API format
+func ToAPIIssueList(ctx context.Context, doer *user_model.User, il issues_model.IssueList) []*api.Issue {
+ result := make([]*api.Issue, len(il))
+ for i := range il {
+ result[i] = ToAPIIssue(ctx, doer, il[i])
+ }
+ return result
+}
+
+// ToTrackedTime converts TrackedTime to API format
+func ToTrackedTime(ctx context.Context, doer *user_model.User, t *issues_model.TrackedTime) (apiT *api.TrackedTime) {
+ apiT = &api.TrackedTime{
+ ID: t.ID,
+ IssueID: t.IssueID,
+ UserID: t.UserID,
+ Time: t.Time,
+ Created: t.Created,
+ }
+ if t.Issue != nil {
+ apiT.Issue = ToAPIIssue(ctx, doer, t.Issue)
+ }
+ if t.User != nil {
+ apiT.UserName = t.User.Name
+ }
+ return apiT
+}
+
+// ToStopWatches convert Stopwatch list to api.StopWatches
+func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.StopWatches, error) {
+ result := api.StopWatches(make([]api.StopWatch, 0, len(sws)))
+
+ issueCache := make(map[int64]*issues_model.Issue)
+ repoCache := make(map[int64]*repo_model.Repository)
+ var (
+ issue *issues_model.Issue
+ repo *repo_model.Repository
+ ok bool
+ err error
+ )
+
+ for _, sw := range sws {
+ issue, ok = issueCache[sw.IssueID]
+ if !ok {
+ issue, err = issues_model.GetIssueByID(ctx, sw.IssueID)
+ if err != nil {
+ return nil, err
+ }
+ }
+ repo, ok = repoCache[issue.RepoID]
+ if !ok {
+ repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ result = append(result, api.StopWatch{
+ Created: sw.CreatedUnix.AsTime(),
+ Seconds: sw.Seconds(),
+ Duration: sw.Duration(),
+ IssueIndex: issue.Index,
+ IssueTitle: issue.Title,
+ RepoOwnerName: repo.OwnerName,
+ RepoName: repo.Name,
+ })
+ }
+ return result, nil
+}
+
+// ToTrackedTimeList converts TrackedTimeList to API format
+func ToTrackedTimeList(ctx context.Context, doer *user_model.User, tl issues_model.TrackedTimeList) api.TrackedTimeList {
+ result := make([]*api.TrackedTime, 0, len(tl))
+ for _, t := range tl {
+ result = append(result, ToTrackedTime(ctx, doer, t))
+ }
+ return result
+}
+
+// ToLabel converts Label to API format
+func ToLabel(label *issues_model.Label, repo *repo_model.Repository, org *user_model.User) *api.Label {
+ result := &api.Label{
+ ID: label.ID,
+ Name: label.Name,
+ Exclusive: label.Exclusive,
+ Color: strings.TrimLeft(label.Color, "#"),
+ Description: label.Description,
+ IsArchived: label.IsArchived(),
+ }
+
+ labelBelongsToRepo := label.BelongsToRepo()
+
+ // calculate URL
+ if labelBelongsToRepo && repo != nil {
+ result.URL = fmt.Sprintf("%s/labels/%d", repo.APIURL(), label.ID)
+ } else { // BelongsToOrg
+ if org != nil {
+ result.URL = fmt.Sprintf("%sapi/v1/orgs/%s/labels/%d", setting.AppURL, url.PathEscape(org.Name), label.ID)
+ } else {
+ log.Error("ToLabel did not get org to calculate url for label with id '%d'", label.ID)
+ }
+ }
+
+ if labelBelongsToRepo && repo == nil {
+ log.Error("ToLabel did not get repo to calculate url for label with id '%d'", label.ID)
+ }
+
+ return result
+}
+
+// ToLabelList converts list of Label to API format
+func ToLabelList(labels []*issues_model.Label, repo *repo_model.Repository, org *user_model.User) []*api.Label {
+ result := make([]*api.Label, len(labels))
+ for i := range labels {
+ result[i] = ToLabel(labels[i], repo, org)
+ }
+ return result
+}
+
+// ToAPIMilestone converts Milestone into API Format
+func ToAPIMilestone(m *issues_model.Milestone) *api.Milestone {
+ apiMilestone := &api.Milestone{
+ ID: m.ID,
+ State: m.State(),
+ Title: m.Name,
+ Description: m.Content,
+ OpenIssues: m.NumOpenIssues,
+ ClosedIssues: m.NumClosedIssues,
+ Created: m.CreatedUnix.AsTime(),
+ Updated: m.UpdatedUnix.AsTimePtr(),
+ }
+ if m.IsClosed {
+ apiMilestone.Closed = m.ClosedDateUnix.AsTimePtr()
+ }
+ if m.DeadlineUnix.Year() < 9999 {
+ apiMilestone.Deadline = m.DeadlineUnix.AsTimePtr()
+ }
+ return apiMilestone
+}
+
+// ToLabelTemplate converts Label to API format
+func ToLabelTemplate(label *label.Label) *api.LabelTemplate {
+ result := &api.LabelTemplate{
+ Name: label.Name,
+ Exclusive: label.Exclusive,
+ Color: strings.TrimLeft(label.Color, "#"),
+ Description: label.Description,
+ }
+
+ return result
+}
+
+// ToLabelTemplateList converts list of Label to API format
+func ToLabelTemplateList(labels []*label.Label) []*api.LabelTemplate {
+ result := make([]*api.LabelTemplate, len(labels))
+ for i := range labels {
+ result[i] = ToLabelTemplate(labels[i])
+ }
+ return result
+}
diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go
new file mode 100644
index 0000000..9ec9ac7
--- /dev/null
+++ b/services/convert/issue_comment.go
@@ -0,0 +1,187 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+)
+
+// ToAPIComment converts a issues_model.Comment to the api.Comment format for API usage
+func ToAPIComment(ctx context.Context, repo *repo_model.Repository, c *issues_model.Comment) *api.Comment {
+ return &api.Comment{
+ ID: c.ID,
+ Poster: ToUser(ctx, c.Poster, nil),
+ HTMLURL: c.HTMLURL(ctx),
+ IssueURL: c.IssueURL(ctx),
+ PRURL: c.PRURL(ctx),
+ Body: c.Content,
+ Attachments: ToAPIAttachments(repo, c.Attachments),
+ Created: c.CreatedUnix.AsTime(),
+ Updated: c.UpdatedUnix.AsTime(),
+ }
+}
+
+// ToTimelineComment converts a issues_model.Comment to the api.TimelineComment format
+func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issues_model.Comment, doer *user_model.User) *api.TimelineComment {
+ err := c.LoadMilestone(ctx)
+ if err != nil {
+ log.Error("LoadMilestone: %v", err)
+ return nil
+ }
+
+ err = c.LoadAssigneeUserAndTeam(ctx)
+ if err != nil {
+ log.Error("LoadAssigneeUserAndTeam: %v", err)
+ return nil
+ }
+
+ err = c.LoadResolveDoer(ctx)
+ if err != nil {
+ log.Error("LoadResolveDoer: %v", err)
+ return nil
+ }
+
+ err = c.LoadDepIssueDetails(ctx)
+ if err != nil {
+ log.Error("LoadDepIssueDetails: %v", err)
+ return nil
+ }
+
+ err = c.LoadTime(ctx)
+ if err != nil {
+ log.Error("LoadTime: %v", err)
+ return nil
+ }
+
+ err = c.LoadLabel(ctx)
+ if err != nil {
+ log.Error("LoadLabel: %v", err)
+ return nil
+ }
+
+ if c.Content != "" {
+ if (c.Type == issues_model.CommentTypeAddTimeManual ||
+ c.Type == issues_model.CommentTypeStopTracking ||
+ c.Type == issues_model.CommentTypeDeleteTimeManual) &&
+ c.Content[0] == '|' {
+ // TimeTracking Comments from v1.21 on store the seconds instead of an formatted string
+ // so we check for the "|" delimiter and convert new to legacy format on demand
+ c.Content = util.SecToTime(c.Content[1:])
+ }
+ }
+
+ comment := &api.TimelineComment{
+ ID: c.ID,
+ Type: c.Type.String(),
+ Poster: ToUser(ctx, c.Poster, nil),
+ HTMLURL: c.HTMLURL(ctx),
+ IssueURL: c.IssueURL(ctx),
+ PRURL: c.PRURL(ctx),
+ Body: c.Content,
+ Created: c.CreatedUnix.AsTime(),
+ Updated: c.UpdatedUnix.AsTime(),
+
+ OldProjectID: c.OldProjectID,
+ ProjectID: c.ProjectID,
+
+ OldTitle: c.OldTitle,
+ NewTitle: c.NewTitle,
+
+ OldRef: c.OldRef,
+ NewRef: c.NewRef,
+
+ RefAction: c.RefAction.String(),
+ RefCommitSHA: c.CommitSHA,
+
+ ReviewID: c.ReviewID,
+
+ RemovedAssignee: c.RemovedAssignee,
+ }
+
+ if c.OldMilestone != nil {
+ comment.OldMilestone = ToAPIMilestone(c.OldMilestone)
+ }
+ if c.Milestone != nil {
+ comment.Milestone = ToAPIMilestone(c.Milestone)
+ }
+
+ if c.Time != nil {
+ err = c.Time.LoadAttributes(ctx)
+ if err != nil {
+ log.Error("Time.LoadAttributes: %v", err)
+ return nil
+ }
+
+ comment.TrackedTime = ToTrackedTime(ctx, doer, c.Time)
+ }
+
+ if c.RefIssueID != 0 {
+ issue, err := issues_model.GetIssueByID(ctx, c.RefIssueID)
+ if err != nil {
+ log.Error("GetIssueByID(%d): %v", c.RefIssueID, err)
+ return nil
+ }
+ comment.RefIssue = ToAPIIssue(ctx, doer, issue)
+ }
+
+ if c.RefCommentID != 0 {
+ com, err := issues_model.GetCommentByID(ctx, c.RefCommentID)
+ if err != nil {
+ log.Error("GetCommentByID(%d): %v", c.RefCommentID, err)
+ return nil
+ }
+ err = com.LoadPoster(ctx)
+ if err != nil {
+ log.Error("LoadPoster: %v", err)
+ return nil
+ }
+ comment.RefComment = ToAPIComment(ctx, repo, com)
+ }
+
+ if c.Label != nil {
+ var org *user_model.User
+ var repo *repo_model.Repository
+ if c.Label.BelongsToOrg() {
+ var err error
+ org, err = user_model.GetUserByID(ctx, c.Label.OrgID)
+ if err != nil {
+ log.Error("GetUserByID(%d): %v", c.Label.OrgID, err)
+ return nil
+ }
+ }
+ if c.Label.BelongsToRepo() {
+ var err error
+ repo, err = repo_model.GetRepositoryByID(ctx, c.Label.RepoID)
+ if err != nil {
+ log.Error("GetRepositoryByID(%d): %v", c.Label.RepoID, err)
+ return nil
+ }
+ }
+ comment.Label = ToLabel(c.Label, repo, org)
+ }
+
+ if c.Assignee != nil {
+ comment.Assignee = ToUser(ctx, c.Assignee, nil)
+ }
+ if c.AssigneeTeam != nil {
+ comment.AssigneeTeam, _ = ToTeam(ctx, c.AssigneeTeam)
+ }
+
+ if c.ResolveDoer != nil {
+ comment.ResolveDoer = ToUser(ctx, c.ResolveDoer, nil)
+ }
+
+ if c.DependentIssue != nil {
+ comment.DependentIssue = ToAPIIssue(ctx, doer, c.DependentIssue)
+ }
+
+ return comment
+}
diff --git a/services/convert/issue_test.go b/services/convert/issue_test.go
new file mode 100644
index 0000000..0aeb3e5
--- /dev/null
+++ b/services/convert/issue_test.go
@@ -0,0 +1,58 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestLabel_ToLabel(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: label.RepoID})
+ assert.Equal(t, &api.Label{
+ ID: label.ID,
+ Name: label.Name,
+ Color: "abcdef",
+ URL: fmt.Sprintf("%sapi/v1/repos/user2/repo1/labels/%d", setting.AppURL, label.ID),
+ }, ToLabel(label, repo, nil))
+}
+
+func TestMilestone_APIFormat(t *testing.T) {
+ milestone := &issues_model.Milestone{
+ ID: 3,
+ RepoID: 4,
+ Name: "milestoneName",
+ Content: "milestoneContent",
+ IsClosed: false,
+ NumOpenIssues: 5,
+ NumClosedIssues: 6,
+ CreatedUnix: timeutil.TimeStamp(time.Date(1999, time.January, 1, 0, 0, 0, 0, time.UTC).Unix()),
+ UpdatedUnix: timeutil.TimeStamp(time.Date(1999, time.March, 1, 0, 0, 0, 0, time.UTC).Unix()),
+ DeadlineUnix: timeutil.TimeStamp(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC).Unix()),
+ }
+ assert.Equal(t, api.Milestone{
+ ID: milestone.ID,
+ State: api.StateOpen,
+ Title: milestone.Name,
+ Description: milestone.Content,
+ OpenIssues: milestone.NumOpenIssues,
+ ClosedIssues: milestone.NumClosedIssues,
+ Created: milestone.CreatedUnix.AsTime(),
+ Updated: milestone.UpdatedUnix.AsTimePtr(),
+ Deadline: milestone.DeadlineUnix.AsTimePtr(),
+ }, *ToAPIMilestone(milestone))
+}
diff --git a/services/convert/main_test.go b/services/convert/main_test.go
new file mode 100644
index 0000000..363cc4a
--- /dev/null
+++ b/services/convert/main_test.go
@@ -0,0 +1,16 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+
+ _ "code.gitea.io/gitea/models/actions"
+)
+
+func TestMain(m *testing.M) {
+ unittest.MainTest(m)
+}
diff --git a/services/convert/mirror.go b/services/convert/mirror.go
new file mode 100644
index 0000000..85e0d1c
--- /dev/null
+++ b/services/convert/mirror.go
@@ -0,0 +1,27 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToPushMirror convert from repo_model.PushMirror and remoteAddress to api.TopicResponse
+func ToPushMirror(ctx context.Context, pm *repo_model.PushMirror) (*api.PushMirror, error) {
+ repo := pm.GetRepository(ctx)
+ return &api.PushMirror{
+ RepoName: repo.Name,
+ RemoteName: pm.RemoteName,
+ RemoteAddress: pm.RemoteAddress,
+ CreatedUnix: pm.CreatedUnix.AsTime(),
+ LastUpdateUnix: pm.LastUpdateUnix.AsTimePtr(),
+ LastError: pm.LastError,
+ Interval: pm.Interval.String(),
+ SyncOnCommit: pm.SyncOnCommit,
+ PublicKey: pm.GetPublicKey(),
+ }, nil
+}
diff --git a/services/convert/notification.go b/services/convert/notification.go
new file mode 100644
index 0000000..41063cf
--- /dev/null
+++ b/services/convert/notification.go
@@ -0,0 +1,98 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+ "net/url"
+
+ activities_model "code.gitea.io/gitea/models/activities"
+ "code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToNotificationThread convert a Notification to api.NotificationThread
+func ToNotificationThread(ctx context.Context, n *activities_model.Notification) *api.NotificationThread {
+ result := &api.NotificationThread{
+ ID: n.ID,
+ Unread: !(n.Status == activities_model.NotificationStatusRead || n.Status == activities_model.NotificationStatusPinned),
+ Pinned: n.Status == activities_model.NotificationStatusPinned,
+ UpdatedAt: n.UpdatedUnix.AsTime(),
+ URL: n.APIURL(),
+ }
+
+ // since user only get notifications when he has access to use minimal access mode
+ if n.Repository != nil {
+ result.Repository = ToRepo(ctx, n.Repository, access_model.Permission{AccessMode: perm.AccessModeRead})
+
+ // This permission is not correct and we should not be reporting it
+ for repository := result.Repository; repository != nil; repository = repository.Parent {
+ repository.Permissions = nil
+ }
+ }
+
+ // handle Subject
+ switch n.Source {
+ case activities_model.NotificationSourceIssue:
+ result.Subject = &api.NotificationSubject{Type: api.NotifySubjectIssue}
+ if n.Issue != nil {
+ result.Subject.Title = n.Issue.Title
+ result.Subject.URL = n.Issue.APIURL(ctx)
+ result.Subject.HTMLURL = n.Issue.HTMLURL()
+ result.Subject.State = n.Issue.State()
+ comment, err := n.Issue.GetLastComment(ctx)
+ if err == nil && comment != nil {
+ result.Subject.LatestCommentURL = comment.APIURL(ctx)
+ result.Subject.LatestCommentHTMLURL = comment.HTMLURL(ctx)
+ }
+ }
+ case activities_model.NotificationSourcePullRequest:
+ result.Subject = &api.NotificationSubject{Type: api.NotifySubjectPull}
+ if n.Issue != nil {
+ result.Subject.Title = n.Issue.Title
+ result.Subject.URL = n.Issue.APIURL(ctx)
+ result.Subject.HTMLURL = n.Issue.HTMLURL()
+ result.Subject.State = n.Issue.State()
+ comment, err := n.Issue.GetLastComment(ctx)
+ if err == nil && comment != nil {
+ result.Subject.LatestCommentURL = comment.APIURL(ctx)
+ result.Subject.LatestCommentHTMLURL = comment.HTMLURL(ctx)
+ }
+
+ if err := n.Issue.LoadPullRequest(ctx); err == nil &&
+ n.Issue.PullRequest != nil &&
+ n.Issue.PullRequest.HasMerged {
+ result.Subject.State = "merged"
+ }
+ }
+ case activities_model.NotificationSourceCommit:
+ url := n.Repository.HTMLURL() + "/commit/" + url.PathEscape(n.CommitID)
+ result.Subject = &api.NotificationSubject{
+ Type: api.NotifySubjectCommit,
+ Title: n.CommitID,
+ URL: url,
+ HTMLURL: url,
+ }
+ case activities_model.NotificationSourceRepository:
+ result.Subject = &api.NotificationSubject{
+ Type: api.NotifySubjectRepository,
+ Title: n.Repository.FullName(),
+ // FIXME: this is a relative URL, rather useless and inconsistent, but keeping for backwards compat
+ URL: n.Repository.Link(),
+ HTMLURL: n.Repository.HTMLURL(),
+ }
+ }
+
+ return result
+}
+
+// ToNotifications convert list of Notification to api.NotificationThread list
+func ToNotifications(ctx context.Context, nl activities_model.NotificationList) []*api.NotificationThread {
+ result := make([]*api.NotificationThread, 0, len(nl))
+ for _, n := range nl {
+ result = append(result, ToNotificationThread(ctx, n))
+ }
+ return result
+}
diff --git a/services/convert/package.go b/services/convert/package.go
new file mode 100644
index 0000000..b5fca21
--- /dev/null
+++ b/services/convert/package.go
@@ -0,0 +1,53 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/models/packages"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToPackage convert a packages.PackageDescriptor to api.Package
+func ToPackage(ctx context.Context, pd *packages.PackageDescriptor, doer *user_model.User) (*api.Package, error) {
+ var repo *api.Repository
+ if pd.Repository != nil {
+ permission, err := access_model.GetUserRepoPermission(ctx, pd.Repository, doer)
+ if err != nil {
+ return nil, err
+ }
+
+ if permission.HasAccess() {
+ repo = ToRepo(ctx, pd.Repository, permission)
+ }
+ }
+
+ return &api.Package{
+ ID: pd.Version.ID,
+ Owner: ToUser(ctx, pd.Owner, doer),
+ Repository: repo,
+ Creator: ToUser(ctx, pd.Creator, doer),
+ Type: string(pd.Package.Type),
+ Name: pd.Package.Name,
+ Version: pd.Version.Version,
+ CreatedAt: pd.Version.CreatedUnix.AsTime(),
+ HTMLURL: pd.VersionHTMLURL(),
+ }, nil
+}
+
+// ToPackageFile converts packages.PackageFileDescriptor to api.PackageFile
+func ToPackageFile(pfd *packages.PackageFileDescriptor) *api.PackageFile {
+ return &api.PackageFile{
+ ID: pfd.File.ID,
+ Size: pfd.Blob.Size,
+ Name: pfd.File.Name,
+ HashMD5: pfd.Blob.HashMD5,
+ HashSHA1: pfd.Blob.HashSHA1,
+ HashSHA256: pfd.Blob.HashSHA256,
+ HashSHA512: pfd.Blob.HashSHA512,
+ }
+}
diff --git a/services/convert/pull.go b/services/convert/pull.go
new file mode 100644
index 0000000..4ec24a8
--- /dev/null
+++ b/services/convert/pull.go
@@ -0,0 +1,261 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+ "fmt"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/cache"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/log"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToAPIPullRequest assumes following fields have been assigned with valid values:
+// Required - Issue
+// Optional - Merger
+func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) *api.PullRequest {
+ var (
+ baseBranch *git.Branch
+ headBranch *git.Branch
+ baseCommit *git.Commit
+ err error
+ )
+
+ if err = pr.Issue.LoadRepo(ctx); err != nil {
+ log.Error("pr.Issue.LoadRepo[%d]: %v", pr.ID, err)
+ return nil
+ }
+
+ apiIssue := ToAPIIssue(ctx, doer, pr.Issue)
+ if err := pr.LoadBaseRepo(ctx); err != nil {
+ log.Error("GetRepositoryById[%d]: %v", pr.ID, err)
+ return nil
+ }
+
+ if err := pr.LoadHeadRepo(ctx); err != nil {
+ log.Error("GetRepositoryById[%d]: %v", pr.ID, err)
+ return nil
+ }
+
+ var doerID int64
+ if doer != nil {
+ doerID = doer.ID
+ }
+
+ const repoDoerPermCacheKey = "repo_doer_perm_cache"
+ p, err := cache.GetWithContextCache(ctx, repoDoerPermCacheKey, fmt.Sprintf("%d_%d", pr.BaseRepoID, doerID),
+ func() (access_model.Permission, error) {
+ return access_model.GetUserRepoPermission(ctx, pr.BaseRepo, doer)
+ })
+ if err != nil {
+ log.Error("GetUserRepoPermission[%d]: %v", pr.BaseRepoID, err)
+ p.AccessMode = perm.AccessModeNone
+ }
+
+ apiPullRequest := &api.PullRequest{
+ ID: pr.ID,
+ URL: pr.Issue.HTMLURL(),
+ Index: pr.Index,
+ Poster: apiIssue.Poster,
+ Title: apiIssue.Title,
+ Body: apiIssue.Body,
+ Labels: apiIssue.Labels,
+ Milestone: apiIssue.Milestone,
+ Assignee: apiIssue.Assignee,
+ Assignees: apiIssue.Assignees,
+ State: apiIssue.State,
+ Draft: pr.IsWorkInProgress(ctx),
+ IsLocked: apiIssue.IsLocked,
+ Comments: apiIssue.Comments,
+ ReviewComments: pr.GetReviewCommentsCount(ctx),
+ HTMLURL: pr.Issue.HTMLURL(),
+ DiffURL: pr.Issue.DiffURL(),
+ PatchURL: pr.Issue.PatchURL(),
+ HasMerged: pr.HasMerged,
+ MergeBase: pr.MergeBase,
+ Mergeable: pr.Mergeable(ctx),
+ Deadline: apiIssue.Deadline,
+ Created: pr.Issue.CreatedUnix.AsTimePtr(),
+ Updated: pr.Issue.UpdatedUnix.AsTimePtr(),
+ PinOrder: apiIssue.PinOrder,
+
+ AllowMaintainerEdit: pr.AllowMaintainerEdit,
+
+ Base: &api.PRBranchInfo{
+ Name: pr.BaseBranch,
+ Ref: pr.BaseBranch,
+ RepoID: pr.BaseRepoID,
+ Repository: ToRepo(ctx, pr.BaseRepo, p),
+ },
+ Head: &api.PRBranchInfo{
+ Name: pr.HeadBranch,
+ Ref: fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index),
+ RepoID: -1,
+ },
+ }
+
+ if err = pr.LoadRequestedReviewers(ctx); err != nil {
+ log.Error("LoadRequestedReviewers[%d]: %v", pr.ID, err)
+ return nil
+ }
+ if err = pr.LoadRequestedReviewersTeams(ctx); err != nil {
+ log.Error("LoadRequestedReviewersTeams[%d]: %v", pr.ID, err)
+ return nil
+ }
+
+ for _, reviewer := range pr.RequestedReviewers {
+ apiPullRequest.RequestedReviewers = append(apiPullRequest.RequestedReviewers, ToUser(ctx, reviewer, nil))
+ }
+
+ for _, reviewerTeam := range pr.RequestedReviewersTeams {
+ convertedTeam, err := ToTeam(ctx, reviewerTeam, true)
+ if err != nil {
+ log.Error("LoadRequestedReviewersTeams[%d]: %v", pr.ID, err)
+ return nil
+ }
+
+ apiPullRequest.RequestedReviewersTeams = append(apiPullRequest.RequestedReviewersTeams, convertedTeam)
+ }
+
+ if pr.Issue.ClosedUnix != 0 {
+ apiPullRequest.Closed = pr.Issue.ClosedUnix.AsTimePtr()
+ }
+
+ gitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
+ if err != nil {
+ log.Error("OpenRepository[%s]: %v", pr.BaseRepo.RepoPath(), err)
+ return nil
+ }
+ defer gitRepo.Close()
+
+ baseBranch, err = gitRepo.GetBranch(pr.BaseBranch)
+ if err != nil && !git.IsErrBranchNotExist(err) {
+ log.Error("GetBranch[%s]: %v", pr.BaseBranch, err)
+ return nil
+ }
+
+ if err == nil {
+ baseCommit, err = baseBranch.GetCommit()
+ if err != nil && !git.IsErrNotExist(err) {
+ log.Error("GetCommit[%s]: %v", baseBranch.Name, err)
+ return nil
+ }
+
+ if err == nil {
+ apiPullRequest.Base.Sha = baseCommit.ID.String()
+ }
+ }
+
+ if pr.Flow == issues_model.PullRequestFlowAGit {
+ gitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
+ if err != nil {
+ log.Error("OpenRepository[%s]: %v", pr.GetGitRefName(), err)
+ return nil
+ }
+ defer gitRepo.Close()
+
+ apiPullRequest.Head.Sha, err = gitRepo.GetRefCommitID(pr.GetGitRefName())
+ if err != nil {
+ log.Error("GetRefCommitID[%s]: %v", pr.GetGitRefName(), err)
+ return nil
+ }
+ apiPullRequest.Head.RepoID = pr.BaseRepoID
+ apiPullRequest.Head.Repository = apiPullRequest.Base.Repository
+ apiPullRequest.Head.Name = ""
+ }
+
+ if pr.HeadRepo != nil && pr.Flow == issues_model.PullRequestFlowGithub {
+ p, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer)
+ if err != nil {
+ log.Error("GetUserRepoPermission[%d]: %v", pr.HeadRepoID, err)
+ p.AccessMode = perm.AccessModeNone
+ }
+
+ apiPullRequest.Head.RepoID = pr.HeadRepo.ID
+ apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p)
+
+ headGitRepo, err := gitrepo.OpenRepository(ctx, pr.HeadRepo)
+ if err != nil {
+ log.Error("OpenRepository[%s]: %v", pr.HeadRepo.RepoPath(), err)
+ return nil
+ }
+ defer headGitRepo.Close()
+
+ headBranch, err = headGitRepo.GetBranch(pr.HeadBranch)
+ if err != nil && !git.IsErrBranchNotExist(err) {
+ log.Error("GetBranch[%s]: %v", pr.HeadBranch, err)
+ return nil
+ }
+
+ // Outer scope variables to be used in diff calculation
+ var (
+ startCommitID string
+ endCommitID string
+ )
+
+ if git.IsErrBranchNotExist(err) {
+ headCommitID, err := headGitRepo.GetRefCommitID(apiPullRequest.Head.Ref)
+ if err != nil && !git.IsErrNotExist(err) {
+ log.Error("GetCommit[%s]: %v", pr.HeadBranch, err)
+ return nil
+ }
+ if err == nil {
+ apiPullRequest.Head.Sha = headCommitID
+ endCommitID = headCommitID
+ }
+ } else {
+ commit, err := headBranch.GetCommit()
+ if err != nil && !git.IsErrNotExist(err) {
+ log.Error("GetCommit[%s]: %v", headBranch.Name, err)
+ return nil
+ }
+ if err == nil {
+ apiPullRequest.Head.Ref = pr.HeadBranch
+ apiPullRequest.Head.Sha = commit.ID.String()
+ endCommitID = commit.ID.String()
+ }
+ }
+
+ // Calculate diff
+ startCommitID = pr.MergeBase
+
+ apiPullRequest.ChangedFiles, apiPullRequest.Additions, apiPullRequest.Deletions, err = gitRepo.GetDiffShortStat(startCommitID, endCommitID)
+ if err != nil {
+ log.Error("GetDiffShortStat: %v", err)
+ }
+ }
+
+ if len(apiPullRequest.Head.Sha) == 0 && len(apiPullRequest.Head.Ref) != 0 {
+ baseGitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
+ if err != nil {
+ log.Error("OpenRepository[%s]: %v", pr.BaseRepo.RepoPath(), err)
+ return nil
+ }
+ defer baseGitRepo.Close()
+ refs, err := baseGitRepo.GetRefsFiltered(apiPullRequest.Head.Ref)
+ if err != nil {
+ log.Error("GetRefsFiltered[%s]: %v", apiPullRequest.Head.Ref, err)
+ return nil
+ } else if len(refs) == 0 {
+ log.Error("unable to resolve PR head ref")
+ } else {
+ apiPullRequest.Head.Sha = refs[0].Object.String()
+ }
+ }
+
+ if pr.HasMerged {
+ apiPullRequest.Merged = pr.MergedUnix.AsTimePtr()
+ apiPullRequest.MergedCommitID = &pr.MergedCommitID
+ apiPullRequest.MergedBy = ToUser(ctx, pr.Merger, nil)
+ }
+
+ return apiPullRequest
+}
diff --git a/services/convert/pull_review.go b/services/convert/pull_review.go
new file mode 100644
index 0000000..f7990e7
--- /dev/null
+++ b/services/convert/pull_review.go
@@ -0,0 +1,139 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+ "strings"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToPullReview convert a review to api format
+func ToPullReview(ctx context.Context, r *issues_model.Review, doer *user_model.User) (*api.PullReview, error) {
+ if err := r.LoadAttributes(ctx); err != nil {
+ if !user_model.IsErrUserNotExist(err) {
+ return nil, err
+ }
+ r.Reviewer = user_model.NewGhostUser()
+ }
+
+ result := &api.PullReview{
+ ID: r.ID,
+ Reviewer: ToUser(ctx, r.Reviewer, doer),
+ State: api.ReviewStateUnknown,
+ Body: r.Content,
+ CommitID: r.CommitID,
+ Stale: r.Stale,
+ Official: r.Official,
+ Dismissed: r.Dismissed,
+ CodeCommentsCount: r.GetCodeCommentsCount(ctx),
+ Submitted: r.CreatedUnix.AsTime(),
+ Updated: r.UpdatedUnix.AsTime(),
+ HTMLURL: r.HTMLURL(ctx),
+ HTMLPullURL: r.Issue.HTMLURL(),
+ }
+
+ if r.ReviewerTeam != nil {
+ var err error
+ result.ReviewerTeam, err = ToTeam(ctx, r.ReviewerTeam)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ switch r.Type {
+ case issues_model.ReviewTypeApprove:
+ result.State = api.ReviewStateApproved
+ case issues_model.ReviewTypeReject:
+ result.State = api.ReviewStateRequestChanges
+ case issues_model.ReviewTypeComment:
+ result.State = api.ReviewStateComment
+ case issues_model.ReviewTypePending:
+ result.State = api.ReviewStatePending
+ case issues_model.ReviewTypeRequest:
+ result.State = api.ReviewStateRequestReview
+ }
+
+ return result, nil
+}
+
+// ToPullReviewList convert a list of review to it's api format
+func ToPullReviewList(ctx context.Context, rl []*issues_model.Review, doer *user_model.User) ([]*api.PullReview, error) {
+ result := make([]*api.PullReview, 0, len(rl))
+ for i := range rl {
+ // show pending reviews only for the user who created them
+ if rl[i].Type == issues_model.ReviewTypePending && (doer == nil || !(doer.IsAdmin || doer.ID == rl[i].ReviewerID)) {
+ continue
+ }
+ r, err := ToPullReview(ctx, rl[i], doer)
+ if err != nil {
+ return nil, err
+ }
+ result = append(result, r)
+ }
+ return result, nil
+}
+
+// ToPullReviewCommentList convert the CodeComments of an review to it's api format
+func ToPullReviewComment(ctx context.Context, review *issues_model.Review, comment *issues_model.Comment, doer *user_model.User) (*api.PullReviewComment, error) {
+ apiComment := &api.PullReviewComment{
+ ID: comment.ID,
+ Body: comment.Content,
+ Poster: ToUser(ctx, comment.Poster, doer),
+ Resolver: ToUser(ctx, comment.ResolveDoer, doer),
+ ReviewID: review.ID,
+ Created: comment.CreatedUnix.AsTime(),
+ Updated: comment.UpdatedUnix.AsTime(),
+ Path: comment.TreePath,
+ CommitID: comment.CommitSHA,
+ OrigCommitID: comment.OldRef,
+ DiffHunk: patch2diff(comment.Patch),
+ HTMLURL: comment.HTMLURL(ctx),
+ HTMLPullURL: review.Issue.HTMLURL(),
+ }
+
+ if comment.Line < 0 {
+ apiComment.OldLineNum = comment.UnsignedLine()
+ } else {
+ apiComment.LineNum = comment.UnsignedLine()
+ }
+
+ return apiComment, nil
+}
+
+// ToPullReviewCommentList convert the CodeComments of an review to it's api format
+func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, doer *user_model.User) ([]*api.PullReviewComment, error) {
+ if err := review.LoadAttributes(ctx); err != nil {
+ if !user_model.IsErrUserNotExist(err) {
+ return nil, err
+ }
+ review.Reviewer = user_model.NewGhostUser()
+ }
+
+ apiComments := make([]*api.PullReviewComment, 0, len(review.CodeComments))
+
+ for _, lines := range review.CodeComments {
+ for _, comments := range lines {
+ for _, comment := range comments {
+ apiComment, err := ToPullReviewComment(ctx, review, comment, doer)
+ if err != nil {
+ return nil, err
+ }
+ apiComments = append(apiComments, apiComment)
+ }
+ }
+ }
+ return apiComments, nil
+}
+
+func patch2diff(patch string) string {
+ split := strings.Split(patch, "\n@@")
+ if len(split) == 2 {
+ return "@@" + split[1]
+ }
+ return ""
+}
diff --git a/services/convert/pull_test.go b/services/convert/pull_test.go
new file mode 100644
index 0000000..1339ed5
--- /dev/null
+++ b/services/convert/pull_test.go
@@ -0,0 +1,78 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "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/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestPullRequest_APIFormat(t *testing.T) {
+ // with HeadRepo
+ require.NoError(t, unittest.PrepareTestDatabase())
+ headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
+ require.NoError(t, pr.LoadAttributes(db.DefaultContext))
+ require.NoError(t, pr.LoadIssue(db.DefaultContext))
+ apiPullRequest := ToAPIPullRequest(git.DefaultContext, pr, nil)
+ assert.NotNil(t, apiPullRequest)
+ assert.EqualValues(t, &structs.PRBranchInfo{
+ Name: "branch1",
+ Ref: "refs/pull/2/head",
+ Sha: "4a357436d925b5c974181ff12a994538ddc5a269",
+ RepoID: 1,
+ Repository: ToRepo(db.DefaultContext, headRepo, access_model.Permission{AccessMode: perm.AccessModeRead}),
+ }, apiPullRequest.Head)
+
+ // withOut HeadRepo
+ pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
+ require.NoError(t, pr.LoadIssue(db.DefaultContext))
+ require.NoError(t, pr.LoadAttributes(db.DefaultContext))
+ // simulate fork deletion
+ pr.HeadRepo = nil
+ pr.HeadRepoID = 100000
+ apiPullRequest = ToAPIPullRequest(git.DefaultContext, pr, nil)
+ assert.NotNil(t, apiPullRequest)
+ assert.Nil(t, apiPullRequest.Head.Repository)
+ assert.EqualValues(t, -1, apiPullRequest.Head.RepoID)
+}
+
+func TestPullReviewList(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+
+ t.Run("Pending review", func(t *testing.T) {
+ reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 6, ReviewerID: reviewer.ID})
+ rl := []*issues_model.Review{review}
+
+ t.Run("Anonymous", func(t *testing.T) {
+ prList, err := ToPullReviewList(db.DefaultContext, rl, nil)
+ require.NoError(t, err)
+ assert.Empty(t, prList)
+ })
+ t.Run("Reviewer", func(t *testing.T) {
+ prList, err := ToPullReviewList(db.DefaultContext, rl, reviewer)
+ require.NoError(t, err)
+ assert.Len(t, prList, 1)
+ })
+ t.Run("Admin", func(t *testing.T) {
+ adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}, unittest.Cond("id != ?", reviewer.ID))
+ prList, err := ToPullReviewList(db.DefaultContext, rl, adminUser)
+ require.NoError(t, err)
+ assert.Len(t, prList, 1)
+ })
+ })
+}
diff --git a/services/convert/quota.go b/services/convert/quota.go
new file mode 100644
index 0000000..791cd8e
--- /dev/null
+++ b/services/convert/quota.go
@@ -0,0 +1,185 @@
+// Copyright 2024 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+ "strconv"
+
+ action_model "code.gitea.io/gitea/models/actions"
+ issue_model "code.gitea.io/gitea/models/issues"
+ package_model "code.gitea.io/gitea/models/packages"
+ quota_model "code.gitea.io/gitea/models/quota"
+ repo_model "code.gitea.io/gitea/models/repo"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+func ToQuotaRuleInfo(rule quota_model.Rule, withName bool) api.QuotaRuleInfo {
+ info := api.QuotaRuleInfo{
+ Limit: rule.Limit,
+ Subjects: make([]string, len(rule.Subjects)),
+ }
+ for i := range len(rule.Subjects) {
+ info.Subjects[i] = rule.Subjects[i].String()
+ }
+
+ if withName {
+ info.Name = rule.Name
+ }
+
+ return info
+}
+
+func toQuotaInfoUsed(used *quota_model.Used) api.QuotaUsed {
+ info := api.QuotaUsed{
+ Size: api.QuotaUsedSize{
+ Repos: api.QuotaUsedSizeRepos{
+ Public: used.Size.Repos.Public,
+ Private: used.Size.Repos.Private,
+ },
+ Git: api.QuotaUsedSizeGit{
+ LFS: used.Size.Git.LFS,
+ },
+ Assets: api.QuotaUsedSizeAssets{
+ Attachments: api.QuotaUsedSizeAssetsAttachments{
+ Issues: used.Size.Assets.Attachments.Issues,
+ Releases: used.Size.Assets.Attachments.Releases,
+ },
+ Artifacts: used.Size.Assets.Artifacts,
+ Packages: api.QuotaUsedSizeAssetsPackages{
+ All: used.Size.Assets.Packages.All,
+ },
+ },
+ },
+ }
+ return info
+}
+
+func ToQuotaInfo(used *quota_model.Used, groups quota_model.GroupList, withNames bool) api.QuotaInfo {
+ info := api.QuotaInfo{
+ Used: toQuotaInfoUsed(used),
+ Groups: ToQuotaGroupList(groups, withNames),
+ }
+
+ return info
+}
+
+func ToQuotaGroup(group quota_model.Group, withNames bool) api.QuotaGroup {
+ info := api.QuotaGroup{
+ Rules: make([]api.QuotaRuleInfo, len(group.Rules)),
+ }
+ if withNames {
+ info.Name = group.Name
+ }
+ for i := range len(group.Rules) {
+ info.Rules[i] = ToQuotaRuleInfo(group.Rules[i], withNames)
+ }
+
+ return info
+}
+
+func ToQuotaGroupList(groups quota_model.GroupList, withNames bool) api.QuotaGroupList {
+ list := make(api.QuotaGroupList, len(groups))
+
+ for i := range len(groups) {
+ list[i] = ToQuotaGroup(*groups[i], withNames)
+ }
+
+ return list
+}
+
+func ToQuotaUsedAttachmentList(ctx context.Context, attachments []*repo_model.Attachment) (*api.QuotaUsedAttachmentList, error) {
+ getAttachmentContainer := func(a *repo_model.Attachment) (string, string, error) {
+ if a.ReleaseID != 0 {
+ release, err := repo_model.GetReleaseByID(ctx, a.ReleaseID)
+ if err != nil {
+ return "", "", err
+ }
+ if err = release.LoadAttributes(ctx); err != nil {
+ return "", "", err
+ }
+ return release.APIURL(), release.HTMLURL(), nil
+ }
+ if a.CommentID != 0 {
+ comment, err := issue_model.GetCommentByID(ctx, a.CommentID)
+ if err != nil {
+ return "", "", err
+ }
+ return comment.APIURL(ctx), comment.HTMLURL(ctx), nil
+ }
+ if a.IssueID != 0 {
+ issue, err := issue_model.GetIssueByID(ctx, a.IssueID)
+ if err != nil {
+ return "", "", err
+ }
+ if err = issue.LoadRepo(ctx); err != nil {
+ return "", "", err
+ }
+ return issue.APIURL(ctx), issue.HTMLURL(), nil
+ }
+ return "", "", nil
+ }
+
+ result := make(api.QuotaUsedAttachmentList, len(attachments))
+ for i, a := range attachments {
+ capiURL, chtmlURL, err := getAttachmentContainer(a)
+ if err != nil {
+ return nil, err
+ }
+
+ apiURL := capiURL + "/assets/" + strconv.FormatInt(a.ID, 10)
+ result[i] = &api.QuotaUsedAttachment{
+ Name: a.Name,
+ Size: a.Size,
+ APIURL: apiURL,
+ }
+ result[i].ContainedIn.APIURL = capiURL
+ result[i].ContainedIn.HTMLURL = chtmlURL
+ }
+
+ return &result, nil
+}
+
+func ToQuotaUsedPackageList(ctx context.Context, packages []*package_model.PackageVersion) (*api.QuotaUsedPackageList, error) {
+ result := make(api.QuotaUsedPackageList, len(packages))
+ for i, pv := range packages {
+ d, err := package_model.GetPackageDescriptor(ctx, pv)
+ if err != nil {
+ return nil, err
+ }
+
+ var size int64
+ for _, file := range d.Files {
+ size += file.Blob.Size
+ }
+
+ result[i] = &api.QuotaUsedPackage{
+ Name: d.Package.Name,
+ Type: d.Package.Type.Name(),
+ Version: d.Version.Version,
+ Size: size,
+ HTMLURL: d.VersionHTMLURL(),
+ }
+ }
+
+ return &result, nil
+}
+
+func ToQuotaUsedArtifactList(ctx context.Context, artifacts []*action_model.ActionArtifact) (*api.QuotaUsedArtifactList, error) {
+ result := make(api.QuotaUsedArtifactList, len(artifacts))
+ for i, a := range artifacts {
+ run, err := action_model.GetRunByID(ctx, a.RunID)
+ if err != nil {
+ return nil, err
+ }
+
+ result[i] = &api.QuotaUsedArtifact{
+ Name: a.ArtifactName,
+ Size: a.FileCompressedSize,
+ HTMLURL: run.HTMLURL(),
+ }
+ }
+
+ return &result, nil
+}
diff --git a/services/convert/release.go b/services/convert/release.go
new file mode 100644
index 0000000..8c0f61b
--- /dev/null
+++ b/services/convert/release.go
@@ -0,0 +1,35 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToAPIRelease convert a repo_model.Release to api.Release
+func ToAPIRelease(ctx context.Context, repo *repo_model.Repository, r *repo_model.Release) *api.Release {
+ return &api.Release{
+ ID: r.ID,
+ TagName: r.TagName,
+ Target: r.Target,
+ Title: r.Title,
+ Note: r.Note,
+ URL: r.APIURL(),
+ HTMLURL: r.HTMLURL(),
+ TarURL: r.TarURL(),
+ ZipURL: r.ZipURL(),
+ HideArchiveLinks: r.HideArchiveLinks,
+ UploadURL: r.APIUploadURL(),
+ IsDraft: r.IsDraft,
+ IsPrerelease: r.IsPrerelease,
+ CreatedAt: r.CreatedUnix.AsTime(),
+ PublishedAt: r.CreatedUnix.AsTime(),
+ Publisher: ToUser(ctx, r.Publisher, nil),
+ Attachments: ToAPIAttachments(repo, r.Attachments),
+ ArchiveDownloadCount: r.ArchiveDownloadCount,
+ }
+}
diff --git a/services/convert/release_test.go b/services/convert/release_test.go
new file mode 100644
index 0000000..2e40bb9
--- /dev/null
+++ b/services/convert/release_test.go
@@ -0,0 +1,29 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRelease_ToRelease(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ release1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 1})
+ release1.LoadAttributes(db.DefaultContext)
+
+ apiRelease := ToAPIRelease(db.DefaultContext, repo1, release1)
+ assert.NotNil(t, apiRelease)
+ assert.EqualValues(t, 1, apiRelease.ID)
+ assert.EqualValues(t, "https://try.gitea.io/api/v1/repos/user2/repo1/releases/1", apiRelease.URL)
+ assert.EqualValues(t, "https://try.gitea.io/api/v1/repos/user2/repo1/releases/1/assets", apiRelease.UploadURL)
+}
diff --git a/services/convert/repository.go b/services/convert/repository.go
new file mode 100644
index 0000000..2fb6f6d
--- /dev/null
+++ b/services/convert/repository.go
@@ -0,0 +1,254 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+ "time"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ "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"
+)
+
+// 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.Units == nil && permissionInRepo.UnitsMode == nil {
+ // If Units and UnitsMode are both nil, 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, or UnitAccessMode will always return perm.AccessModeNone.
+ _ = repo.LoadUnits(ctx) // the error is not important, so ignore it
+ permissionInRepo.Units = repo.Units
+ }
+
+ cloneLink := repo.CloneLink()
+ 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
+ globallyEditableWiki := false
+ var externalWiki *api.ExternalWiki
+ if wikiUnit, err := repo.GetUnit(ctx, unit_model.TypeWiki); err == nil {
+ hasWiki = true
+ if wikiUnit.DefaultPermissions == repo_model.UnitAccessModeWrite {
+ globallyEditableWiki = 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
+ defaultDeleteBranchAfterMerge := false
+ defaultMergeStyle := repo_model.MergeStyleMerge
+ defaultAllowMaintainerEdit := false
+ 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
+ defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
+ defaultMergeStyle = config.GetDefaultMergeStyle()
+ defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
+ }
+ hasProjects := false
+ if _, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
+ hasProjects = true
+ }
+
+ hasReleases := false
+ if _, err := repo.GetUnit(ctx, unit_model.TypeReleases); err == nil {
+ hasReleases = true
+ }
+
+ hasPackages := false
+ if _, err := repo.GetUnit(ctx, unit_model.TypePackages); err == nil {
+ hasPackages = true
+ }
+
+ hasActions := false
+ if _, err := repo.GetUnit(ctx, unit_model.TypeActions); err == nil {
+ hasActions = true
+ }
+
+ 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,
+ })
+
+ 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 := models.GetPendingRepositoryTransfer(ctx, repo)
+ if err != nil && !models.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
+ }
+
+ 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(),
+ 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,
+ 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,
+ HasIssues: hasIssues,
+ ExternalTracker: externalTracker,
+ InternalTracker: internalTracker,
+ HasWiki: hasWiki,
+ WikiBranch: repo.WikiBranch,
+ GloballyEditableWiki: globallyEditableWiki,
+ HasProjects: hasProjects,
+ HasReleases: hasReleases,
+ HasPackages: hasPackages,
+ HasActions: hasActions,
+ ExternalWiki: externalWiki,
+ HasPullRequests: hasPullRequests,
+ IgnoreWhitespaceConflicts: ignoreWhitespaceConflicts,
+ AllowMerge: allowMerge,
+ AllowRebase: allowRebase,
+ AllowRebaseMerge: allowRebaseMerge,
+ AllowSquash: allowSquash,
+ AllowFastForwardOnly: allowFastForwardOnly,
+ AllowRebaseUpdate: allowRebaseUpdate,
+ DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
+ DefaultMergeStyle: string(defaultMergeStyle),
+ DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit,
+ AvatarURL: repo.AvatarLink(ctx),
+ Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
+ MirrorInterval: mirrorInterval,
+ MirrorUpdated: mirrorUpdated,
+ RepoTransfer: transfer,
+ Topics: repo.Topics,
+ ObjectFormatName: repo.ObjectFormatName,
+ }
+}
+
+// ToRepoTransfer convert a models.RepoTransfer to a structs.RepeTransfer
+func ToRepoTransfer(ctx context.Context, t *models.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,
+ }
+}
diff --git a/services/convert/secret.go b/services/convert/secret.go
new file mode 100644
index 0000000..dd7b9f0
--- /dev/null
+++ b/services/convert/secret.go
@@ -0,0 +1,18 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ secret_model "code.gitea.io/gitea/models/secret"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToSecret converts Secret to API format
+func ToSecret(secret *secret_model.Secret) *api.Secret {
+ result := &api.Secret{
+ Name: secret.Name,
+ }
+
+ return result
+}
diff --git a/services/convert/status.go b/services/convert/status.go
new file mode 100644
index 0000000..6cef63c
--- /dev/null
+++ b/services/convert/status.go
@@ -0,0 +1,65 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+
+ git_model "code.gitea.io/gitea/models/git"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToCommitStatus converts git_model.CommitStatus to api.CommitStatus
+func ToCommitStatus(ctx context.Context, status *git_model.CommitStatus) *api.CommitStatus {
+ apiStatus := &api.CommitStatus{
+ Created: status.CreatedUnix.AsTime(),
+ Updated: status.CreatedUnix.AsTime(),
+ State: status.State,
+ TargetURL: status.TargetURL,
+ Description: status.Description,
+ ID: status.Index,
+ URL: status.APIURL(ctx),
+ Context: status.Context,
+ }
+
+ if status.CreatorID != 0 {
+ creator, _ := user_model.GetUserByID(ctx, status.CreatorID)
+ apiStatus.Creator = ToUser(ctx, creator, nil)
+ }
+
+ return apiStatus
+}
+
+// ToCombinedStatus converts List of CommitStatus to a CombinedStatus
+func ToCombinedStatus(ctx context.Context, statuses []*git_model.CommitStatus, repo *api.Repository) *api.CombinedStatus {
+ if len(statuses) == 0 {
+ return nil
+ }
+
+ retStatus := &api.CombinedStatus{
+ SHA: statuses[0].SHA,
+ TotalCount: len(statuses),
+ Repository: repo,
+ URL: "",
+ }
+
+ retStatus.Statuses = make([]*api.CommitStatus, 0, len(statuses))
+ for _, status := range statuses {
+ retStatus.Statuses = append(retStatus.Statuses, ToCommitStatus(ctx, status))
+ if retStatus.State == "" || status.State.NoBetterThan(retStatus.State) {
+ retStatus.State = status.State
+ }
+ }
+ // According to https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#get-the-combined-status-for-a-specific-reference
+ // > Additionally, a combined state is returned. The state is one of:
+ // > failure if any of the contexts report as error or failure
+ // > pending if there are no statuses or a context is pending
+ // > success if the latest status for all contexts is success
+ if retStatus.State.IsError() {
+ retStatus.State = api.CommitStatusFailure
+ }
+
+ return retStatus
+}
diff --git a/services/convert/user.go b/services/convert/user.go
new file mode 100644
index 0000000..94a400d
--- /dev/null
+++ b/services/convert/user.go
@@ -0,0 +1,113 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+
+ "code.gitea.io/gitea/models/perm"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToUser convert user_model.User to api.User
+// if doer is set, private information is added if the doer has the permission to see it
+func ToUser(ctx context.Context, user, doer *user_model.User) *api.User {
+ if user == nil {
+ return nil
+ }
+ authed := false
+ signed := false
+ if doer != nil {
+ signed = true
+ authed = doer.ID == user.ID || doer.IsAdmin
+ }
+ return toUser(ctx, user, signed, authed)
+}
+
+// ToUsers convert list of user_model.User to list of api.User
+func ToUsers(ctx context.Context, doer *user_model.User, users []*user_model.User) []*api.User {
+ result := make([]*api.User, len(users))
+ for i := range users {
+ result[i] = ToUser(ctx, users[i], doer)
+ }
+ return result
+}
+
+// ToUserWithAccessMode convert user_model.User to api.User
+// AccessMode is not none show add some more information
+func ToUserWithAccessMode(ctx context.Context, user *user_model.User, accessMode perm.AccessMode) *api.User {
+ if user == nil {
+ return nil
+ }
+ return toUser(ctx, user, accessMode != perm.AccessModeNone, false)
+}
+
+// toUser convert user_model.User to api.User
+// signed shall only be set if requester is logged in. authed shall only be set if user is site admin or user himself
+func toUser(ctx context.Context, user *user_model.User, signed, authed bool) *api.User {
+ result := &api.User{
+ ID: user.ID,
+ UserName: user.Name,
+ FullName: user.FullName,
+ Email: user.GetPlaceholderEmail(),
+ AvatarURL: user.AvatarLink(ctx),
+ HTMLURL: user.HTMLURL(),
+ Created: user.CreatedUnix.AsTime(),
+ Restricted: user.IsRestricted,
+ Location: user.Location,
+ Pronouns: user.Pronouns,
+ Website: user.Website,
+ Description: user.Description,
+ // counter's
+ Followers: user.NumFollowers,
+ Following: user.NumFollowing,
+ StarredRepos: user.NumStars,
+ }
+
+ result.Visibility = user.Visibility.String()
+
+ // hide primary email if API caller is anonymous or user keep email private
+ if signed && (!user.KeepEmailPrivate || authed) {
+ result.Email = user.Email
+ }
+
+ // only site admin will get these information and possibly user himself
+ if authed {
+ result.IsAdmin = user.IsAdmin
+ result.LoginName = user.LoginName
+ result.SourceID = user.LoginSource
+ result.LastLogin = user.LastLoginUnix.AsTime()
+ result.Language = user.Language
+ result.IsActive = user.IsActive
+ result.ProhibitLogin = user.ProhibitLogin
+ }
+ return result
+}
+
+// User2UserSettings return UserSettings based on a user
+func User2UserSettings(user *user_model.User) api.UserSettings {
+ return api.UserSettings{
+ FullName: user.FullName,
+ Website: user.Website,
+ Location: user.Location,
+ Pronouns: user.Pronouns,
+ Language: user.Language,
+ Description: user.Description,
+ Theme: user.Theme,
+ HideEmail: user.KeepEmailPrivate,
+ HideActivity: user.KeepActivityPrivate,
+ DiffViewStyle: user.DiffViewStyle,
+ EnableRepoUnitHints: user.EnableRepoUnitHints,
+ }
+}
+
+// ToUserAndPermission return User and its collaboration permission for a repository
+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.String(),
+ RoleName: accessMode.String(),
+ }
+}
diff --git a/services/convert/user_test.go b/services/convert/user_test.go
new file mode 100644
index 0000000..0f0b520
--- /dev/null
+++ b/services/convert/user_test.go
@@ -0,0 +1,41 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestUser_ToUser(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1, IsAdmin: true})
+
+ apiUser := toUser(db.DefaultContext, user1, true, true)
+ assert.True(t, apiUser.IsAdmin)
+ assert.Contains(t, apiUser.AvatarURL, "://")
+
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2, IsAdmin: false})
+
+ apiUser = toUser(db.DefaultContext, user2, true, true)
+ assert.False(t, apiUser.IsAdmin)
+
+ apiUser = toUser(db.DefaultContext, user1, false, false)
+ assert.False(t, apiUser.IsAdmin)
+ assert.EqualValues(t, api.VisibleTypePublic.String(), apiUser.Visibility)
+
+ user31 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31, IsAdmin: false, Visibility: api.VisibleTypePrivate})
+
+ apiUser = toUser(db.DefaultContext, user31, true, true)
+ assert.False(t, apiUser.IsAdmin)
+ assert.EqualValues(t, api.VisibleTypePrivate.String(), apiUser.Visibility)
+}
diff --git a/services/convert/utils.go b/services/convert/utils.go
new file mode 100644
index 0000000..fe35fd2
--- /dev/null
+++ b/services/convert/utils.go
@@ -0,0 +1,44 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Copyright 2016 The Gogs Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "strings"
+
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/structs"
+)
+
+// ToCorrectPageSize makes sure page size is in allowed range.
+func ToCorrectPageSize(size int) int {
+ if size <= 0 {
+ size = setting.API.DefaultPagingNum
+ } else if size > setting.API.MaxResponseItems {
+ size = setting.API.MaxResponseItems
+ }
+ return size
+}
+
+// ToGitServiceType return GitServiceType based on string
+func ToGitServiceType(value string) structs.GitServiceType {
+ switch strings.ToLower(value) {
+ case "github":
+ return structs.GithubService
+ case "gitea":
+ return structs.GiteaService
+ case "gitlab":
+ return structs.GitlabService
+ case "gogs":
+ return structs.GogsService
+ case "onedev":
+ return structs.OneDevService
+ case "gitbucket":
+ return structs.GitBucketService
+ case "forgejo":
+ return structs.ForgejoService
+ default:
+ return structs.PlainGitService
+ }
+}
diff --git a/services/convert/utils_test.go b/services/convert/utils_test.go
new file mode 100644
index 0000000..b464d8b
--- /dev/null
+++ b/services/convert/utils_test.go
@@ -0,0 +1,39 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestToCorrectPageSize(t *testing.T) {
+ assert.EqualValues(t, 30, ToCorrectPageSize(0))
+ assert.EqualValues(t, 30, ToCorrectPageSize(-10))
+ assert.EqualValues(t, 20, ToCorrectPageSize(20))
+ assert.EqualValues(t, 50, ToCorrectPageSize(100))
+}
+
+func TestToGitServiceType(t *testing.T) {
+ tc := []struct {
+ typ string
+ enum int
+ }{{
+ typ: "github", enum: 2,
+ }, {
+ typ: "gitea", enum: 3,
+ }, {
+ typ: "gitlab", enum: 4,
+ }, {
+ typ: "gogs", enum: 5,
+ }, {
+ typ: "forgejo", enum: 9,
+ }, {
+ typ: "trash", enum: 1,
+ }}
+ for _, test := range tc {
+ assert.EqualValues(t, test.enum, ToGitServiceType(test.typ))
+ }
+}
diff --git a/services/convert/wiki.go b/services/convert/wiki.go
new file mode 100644
index 0000000..767bfdb
--- /dev/null
+++ b/services/convert/wiki.go
@@ -0,0 +1,45 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "time"
+
+ "code.gitea.io/gitea/modules/git"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToWikiCommit convert a git commit into a WikiCommit
+func ToWikiCommit(commit *git.Commit) *api.WikiCommit {
+ return &api.WikiCommit{
+ ID: commit.ID.String(),
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: commit.Author.Name,
+ Email: commit.Author.Email,
+ },
+ Date: commit.Author.When.UTC().Format(time.RFC3339),
+ },
+ Committer: &api.CommitUser{
+ Identity: api.Identity{
+ Name: commit.Committer.Name,
+ Email: commit.Committer.Email,
+ },
+ Date: commit.Committer.When.UTC().Format(time.RFC3339),
+ },
+ Message: commit.CommitMessage,
+ }
+}
+
+// ToWikiCommitList convert a list of git commits into a WikiCommitList
+func ToWikiCommitList(commits []*git.Commit, total int64) *api.WikiCommitList {
+ result := make([]*api.WikiCommit, len(commits))
+ for i := range commits {
+ result[i] = ToWikiCommit(commits[i])
+ }
+ return &api.WikiCommitList{
+ WikiCommits: result,
+ Count: total,
+ }
+}