aboutsummaryrefslogtreecommitdiffstats
path: root/services/convert
diff options
context:
space:
mode:
authorKN4CK3R <admin@oldschoolhack.me>2022-12-29 03:57:15 +0100
committerGitHub <noreply@github.com>2022-12-29 10:57:15 +0800
commita35749893b91db48310d91ae0a32fee3ad3bb901 (patch)
tree09521c51fe8d2c2366694141d82454aa881b853e /services/convert
parent309e86a9bf305e807ead2854fa757c4d704dcfce (diff)
downloadgitea-a35749893b91db48310d91ae0a32fee3ad3bb901.tar.gz
gitea-a35749893b91db48310d91ae0a32fee3ad3bb901.zip
Move `convert` package to services (#22264)
Addition to #22256 The `convert` package relies heavily on different models which is [disallowed by our definition of modules](https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md#design-guideline). This helps to prevent possible import cycles. Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Diffstat (limited to 'services/convert')
-rw-r--r--services/convert/attachment.go30
-rw-r--r--services/convert/convert.go458
-rw-r--r--services/convert/git_commit.go202
-rw-r--r--services/convert/git_commit_test.go41
-rw-r--r--services/convert/issue.go235
-rw-r--r--services/convert/issue_comment.go175
-rw-r--r--services/convert/issue_test.go57
-rw-r--r--services/convert/main_test.go17
-rw-r--r--services/convert/mirror.go38
-rw-r--r--services/convert/notification.go96
-rw-r--r--services/convert/package.go52
-rw-r--r--services/convert/pull.go204
-rw-r--r--services/convert/pull_review.go127
-rw-r--r--services/convert/pull_test.go48
-rw-r--r--services/convert/release.go30
-rw-r--r--services/convert/repository.go202
-rw-r--r--services/convert/status.go57
-rw-r--r--services/convert/user.go106
-rw-r--r--services/convert/user_test.go39
-rw-r--r--services/convert/utils.go42
-rw-r--r--services/convert/utils_test.go39
-rw-r--r--services/convert/wiki.go59
22 files changed, 2354 insertions, 0 deletions
diff --git a/services/convert/attachment.go b/services/convert/attachment.go
new file mode 100644
index 0000000000..ddba181a12
--- /dev/null
+++ b/services/convert/attachment.go
@@ -0,0 +1,30 @@
+// 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"
+)
+
+// ToAttachment converts models.Attachment to api.Attachment
+func ToAttachment(a *repo_model.Attachment) *api.Attachment {
+ return &api.Attachment{
+ ID: a.ID,
+ Name: a.Name,
+ Created: a.CreatedUnix.AsTime(),
+ DownloadCount: a.DownloadCount,
+ Size: a.Size,
+ UUID: a.UUID,
+ DownloadURL: a.DownloadURL(),
+ }
+}
+
+func ToAttachments(attachments []*repo_model.Attachment) []*api.Attachment {
+ converted := make([]*api.Attachment, 0, len(attachments))
+ for _, attachment := range attachments {
+ converted = append(converted, ToAttachment(attachment))
+ }
+ return converted
+}
diff --git a/services/convert/convert.go b/services/convert/convert.go
new file mode 100644
index 0000000000..756a1f95d9
--- /dev/null
+++ b/services/convert/convert.go
@@ -0,0 +1,458 @@
+// 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"
+
+ asymkey_model "code.gitea.io/gitea/models/asymkey"
+ "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/models/webhook"
+ "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"
+ "code.gitea.io/gitea/services/gitdiff"
+ webhook_service "code.gitea.io/gitea/services/webhook"
+)
+
+// 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,
+ }
+}
+
+// ToBranch convert a git.Commit and git.Branch to an api.Branch
+func ToBranch(repo *repo_model.Repository, b *git.Branch, 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(db.DefaultContext, user, repo, unit.TypeCode, perm.AccessModeWrite)
+ if err != nil {
+ return nil, err
+ }
+
+ perms, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
+ if err != nil {
+ return nil, err
+ }
+ canPush = issues_model.CanMaintainerWriteToBranch(perms, b.Name, user)
+ }
+
+ return &api.Branch{
+ Name: b.Name,
+ Commit: ToPayloadCommit(repo, c),
+ Protected: false,
+ RequiredApprovals: 0,
+ EnableStatusCheck: false,
+ StatusCheckContexts: []string{},
+ UserCanPush: canPush,
+ UserCanMerge: hasPerm,
+ }, nil
+ }
+
+ branch := &api.Branch{
+ Name: b.Name,
+ Commit: ToPayloadCommit(repo, c),
+ Protected: true,
+ RequiredApprovals: bp.RequiredApprovals,
+ EnableStatusCheck: bp.EnableStatusCheck,
+ StatusCheckContexts: bp.StatusCheckContexts,
+ }
+
+ if isRepoAdmin {
+ branch.EffectiveBranchProtectionName = bp.BranchName
+ }
+
+ if user != nil {
+ permission, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
+ if err != nil {
+ return nil, err
+ }
+ branch.UserCanPush = bp.CanUserPush(user.ID)
+ branch.UserCanMerge = git_model.IsUserMergeWhitelisted(db.DefaultContext, bp, user.ID, permission)
+ }
+
+ return branch, nil
+}
+
+// ToBranchProtection convert a ProtectedBranch to api.BranchProtection
+func ToBranchProtection(bp *git_model.ProtectedBranch) *api.BranchProtection {
+ pushWhitelistUsernames, err := user_model.GetUserNamesByIDs(bp.WhitelistUserIDs)
+ if err != nil {
+ log.Error("GetUserNamesByIDs (WhitelistUserIDs): %v", err)
+ }
+ mergeWhitelistUsernames, err := user_model.GetUserNamesByIDs(bp.MergeWhitelistUserIDs)
+ if err != nil {
+ log.Error("GetUserNamesByIDs (MergeWhitelistUserIDs): %v", err)
+ }
+ approvalsWhitelistUsernames, err := user_model.GetUserNamesByIDs(bp.ApprovalsWhitelistUserIDs)
+ if err != nil {
+ log.Error("GetUserNamesByIDs (ApprovalsWhitelistUserIDs): %v", err)
+ }
+ pushWhitelistTeams, err := organization.GetTeamNamesByID(bp.WhitelistTeamIDs)
+ if err != nil {
+ log.Error("GetTeamNamesByID (WhitelistTeamIDs): %v", err)
+ }
+ mergeWhitelistTeams, err := organization.GetTeamNamesByID(bp.MergeWhitelistTeamIDs)
+ if err != nil {
+ log.Error("GetTeamNamesByID (MergeWhitelistTeamIDs): %v", err)
+ }
+ approvalsWhitelistTeams, err := organization.GetTeamNamesByID(bp.ApprovalsWhitelistTeamIDs)
+ if err != nil {
+ log.Error("GetTeamNamesByID (ApprovalsWhitelistTeamIDs): %v", err)
+ }
+
+ return &api.BranchProtection{
+ BranchName: bp.BranchName,
+ 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,
+ RequireSignedCommits: bp.RequireSignedCommits,
+ ProtectedFilePatterns: bp.ProtectedFilePatterns,
+ UnprotectedFilePatterns: bp.UnprotectedFilePatterns,
+ 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"),
+ }
+}
+
+// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
+func ToVerification(c *git.Commit) *api.PayloadCommitVerification {
+ verif := asymkey_model.ParseCommitWithSignature(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,
+ }
+}
+
+// ToHook convert models.Webhook to api.Hook
+func ToHook(repoLink string, w *webhook.Webhook) (*api.Hook, error) {
+ config := map[string]string{
+ "url": w.URL,
+ "content_type": w.ContentType.Name(),
+ }
+ if w.Type == webhook.SLACK {
+ s := webhook_service.GetSlackHook(w)
+ config["channel"] = s.Channel
+ config["username"] = s.Username
+ config["icon_url"] = s.IconURL
+ config["color"] = s.Color
+ }
+
+ authorizationHeader, err := w.HeaderAuthorization()
+ if err != nil {
+ return nil, err
+ }
+
+ return &api.Hook{
+ ID: w.ID,
+ Type: w.Type,
+ URL: fmt.Sprintf("%s/settings/hooks/%d", repoLink, w.ID),
+ Active: w.IsActive,
+ Config: config,
+ Events: w.EventsArray(),
+ AuthorizationHeader: authorizationHeader,
+ Updated: w.UpdatedUnix.AsTime(),
+ Created: w.CreatedUnix.AsTime(),
+ }, nil
+}
+
+// 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(org *organization.Organization) *api.Organization {
+ return &api.Organization{
+ ID: org.ID,
+ AvatarURL: org.AsUser().AvatarLink(),
+ Name: org.Name,
+ UserName: org.Name,
+ FullName: org.FullName,
+ 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(team *organization.Team, loadOrg ...bool) (*api.Team, error) {
+ teams, err := ToTeams([]*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(teams []*organization.Team, loadOrgs bool) ([]*api.Team, error) {
+ if len(teams) == 0 || teams[0] == nil {
+ return nil, nil
+ }
+
+ cache := make(map[int64]*api.Organization)
+ apiTeams := make([]*api.Team, len(teams))
+ for i := range teams {
+ if err := teams[i].GetUnits(); err != nil {
+ return nil, err
+ }
+
+ apiTeams[i] = &api.Team{
+ ID: teams[i].ID,
+ Name: teams[i].Name,
+ Description: teams[i].Description,
+ IncludesAllRepositories: teams[i].IncludesAllRepositories,
+ CanCreateOrgRepo: teams[i].CanCreateOrgRepo,
+ Permission: teams[i].AccessMode.String(),
+ Units: teams[i].GetUnitNames(),
+ UnitsMap: teams[i].GetUnitsMap(),
+ }
+
+ if loadOrgs {
+ apiOrg, ok := cache[teams[i].OrgID]
+ if !ok {
+ org, err := organization.GetOrgByID(db.DefaultContext, teams[i].OrgID)
+ if err != nil {
+ return nil, err
+ }
+ apiOrg = ToOrganization(org)
+ cache[teams[i].OrgID] = apiOrg
+ }
+ apiTeams[i].Organization = apiOrg
+ }
+ }
+ return apiTeams, nil
+}
+
+// ToAnnotatedTag convert git.Tag to api.AnnotatedTag
+func ToAnnotatedTag(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(c),
+ }
+}
+
+// ToAnnotatedTagObject convert a git.Commit to an api.AnnotatedTagObject
+func ToAnnotatedTagObject(repo *repo_model.Repository, commit *git.Commit) *api.AnnotatedTagObject {
+ return &api.AnnotatedTagObject{
+ SHA: commit.ID.String(),
+ Type: string(git.ObjectCommit),
+ URL: util.URLJoin(repo.APIURL(), "git/commits", commit.ID.String()),
+ }
+}
+
+// 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"
+ 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"
+ } 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,
+ 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())),
+ }
+
+ if status == "rename" {
+ file.PreviousFilename = f.OldName
+ }
+
+ return file
+}
diff --git a/services/convert/git_commit.go b/services/convert/git_commit.go
new file mode 100644
index 0000000000..59842e4020
--- /dev/null
+++ b/services/convert/git_commit.go
@@ -0,0 +1,202 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "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"
+ "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(repo *repo_model.Repository, c *git.Commit) *api.PayloadCommit {
+ authorUsername := ""
+ if author, err := user_model.GetUserByEmail(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(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(c),
+ }
+}
+
+// ToCommit convert a git.Commit to api.Commit
+func ToCommit(repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, userCache map[string]*user_model.User, stat bool) (*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(cacheAuthor, nil)
+ } else {
+ author, err := user_model.GetUserByEmail(commit.Author.Email)
+ if err != nil && !user_model.IsErrUserNotExist(err) {
+ return nil, err
+ } else if err == nil {
+ apiAuthor = ToUser(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(cacheCommitter, nil)
+ } else {
+ committer, err := user_model.GetUserByEmail(commit.Committer.Email)
+ if err != nil && !user_model.IsErrUserNotExist(err) {
+ return nil, err
+ } else if err == nil {
+ apiCommitter = ToUser(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,
+ },
+ Verification: ToVerification(commit),
+ },
+ Author: apiAuthor,
+ Committer: apiCommitter,
+ Parents: apiParents,
+ }
+
+ // Retrieve files affected by the commit
+ if stat {
+ 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 _, files := range [][]string{fileStatus.Added, fileStatus.Removed, fileStatus.Modified} {
+ for _, filename := range files {
+ affectedFileList = append(affectedFileList, &api.CommitAffectedFiles{
+ Filename: filename,
+ })
+ }
+ }
+
+ diff, err := gitdiff.GetDiff(gitRepo, &gitdiff.DiffOptions{
+ AfterCommitID: commit.ID.String(),
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ res.Files = affectedFileList
+ 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 0000000000..8c4ef88ebe
--- /dev/null
+++ b/services/convert/git_commit_test.go
@@ -0,0 +1,41 @@
+// 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"
+)
+
+func TestToCommitMeta(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ sha1, _ := git.NewIDFromString("0000000000000000000000000000000000000000")
+ signature := &git.Signature{Name: "Test Signature", Email: "test@email.com", When: time.Unix(0, 0)}
+ tag := &git.Tag{
+ Name: "Test Tag",
+ ID: sha1,
+ Object: sha1,
+ Type: "Test Type",
+ Tagger: signature,
+ Message: "Test Message",
+ }
+
+ commitMeta := ToCommitMeta(headRepo, tag)
+
+ assert.NotNil(t, commitMeta)
+ assert.EqualValues(t, &api.CommitMeta{
+ SHA: "0000000000000000000000000000000000000000",
+ URL: util.URLJoin(headRepo.APIURL(), "git/commits", "0000000000000000000000000000000000000000"),
+ Created: time.Unix(0, 0),
+ }, commitMeta)
+}
diff --git a/services/convert/issue.go b/services/convert/issue.go
new file mode 100644
index 0000000000..f3af03ed94
--- /dev/null
+++ b/services/convert/issue.go
@@ -0,0 +1,235 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "context"
+ "fmt"
+ "net/url"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ 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"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// 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, issue *issues_model.Issue) *api.Issue {
+ if err := issue.LoadLabels(ctx); err != nil {
+ return &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.Repo.GetOwner(ctx); err != nil {
+ return &api.Issue{}
+ }
+
+ apiIssue := &api.Issue{
+ ID: issue.ID,
+ URL: issue.APIURL(),
+ HTMLURL: issue.HTMLURL(),
+ Index: issue.Index,
+ Poster: ToUser(issue.Poster, nil),
+ Title: issue.Title,
+ Body: issue.Content,
+ Attachments: ToAttachments(issue.Attachments),
+ Ref: issue.Ref,
+ Labels: ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner),
+ State: issue.State(),
+ IsLocked: issue.IsLocked,
+ Comments: issue.NumComments,
+ Created: issue.CreatedUnix.AsTime(),
+ Updated: issue.UpdatedUnix.AsTime(),
+ }
+
+ 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(assignee, nil))
+ }
+ apiIssue.Assignee = ToUser(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{}
+ }
+ apiIssue.PullRequest = &api.PullRequestMeta{
+ HasMerged: issue.PullRequest.HasMerged,
+ }
+ if issue.PullRequest.HasMerged {
+ apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr()
+ }
+ }
+ if issue.DeadlineUnix != 0 {
+ apiIssue.Deadline = issue.DeadlineUnix.AsTimePtr()
+ }
+
+ return apiIssue
+}
+
+// ToAPIIssueList converts an IssueList to API format
+func ToAPIIssueList(ctx context.Context, il issues_model.IssueList) []*api.Issue {
+ result := make([]*api.Issue, len(il))
+ for i := range il {
+ result[i] = ToAPIIssue(ctx, il[i])
+ }
+ return result
+}
+
+// ToTrackedTime converts TrackedTime to API format
+func ToTrackedTime(ctx context.Context, 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, t.Issue)
+ }
+ if t.User != nil {
+ apiT.UserName = t.User.Name
+ }
+ return apiT
+}
+
+// ToStopWatches convert Stopwatch list to api.StopWatches
+func ToStopWatches(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(db.DefaultContext, sw.IssueID)
+ if err != nil {
+ return nil, err
+ }
+ }
+ repo, ok = repoCache[issue.RepoID]
+ if !ok {
+ repo, err = repo_model.GetRepositoryByID(db.DefaultContext, 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, tl issues_model.TrackedTimeList) api.TrackedTimeList {
+ result := make([]*api.TrackedTime, 0, len(tl))
+ for _, t := range tl {
+ result = append(result, ToTrackedTime(ctx, 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,
+ Color: strings.TrimLeft(label.Color, "#"),
+ Description: label.Description,
+ }
+
+ // calculate URL
+ if label.BelongsToRepo() && repo != nil {
+ if repo != nil {
+ result.URL = fmt.Sprintf("%s/labels/%d", repo.APIURL(), label.ID)
+ } else {
+ log.Error("ToLabel did not get repo to calculate url for label with id '%d'", 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)
+ }
+ }
+
+ 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
+}
diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go
new file mode 100644
index 0000000000..6044cbcf61
--- /dev/null
+++ b/services/convert/issue_comment.go
@@ -0,0 +1,175 @@
+// 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"
+)
+
+// ToComment converts a issues_model.Comment to the api.Comment format
+func ToComment(c *issues_model.Comment) *api.Comment {
+ return &api.Comment{
+ ID: c.ID,
+ Poster: ToUser(c.Poster, nil),
+ HTMLURL: c.HTMLURL(),
+ IssueURL: c.IssueURL(),
+ PRURL: c.PRURL(),
+ Body: c.Content,
+ Attachments: ToAttachments(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, 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()
+ if err != nil {
+ log.Error("LoadAssigneeUserAndTeam: %v", err)
+ return nil
+ }
+
+ err = c.LoadResolveDoer()
+ if err != nil {
+ log.Error("LoadResolveDoer: %v", err)
+ return nil
+ }
+
+ err = c.LoadDepIssueDetails()
+ if err != nil {
+ log.Error("LoadDepIssueDetails: %v", err)
+ return nil
+ }
+
+ err = c.LoadTime()
+ if err != nil {
+ log.Error("LoadTime: %v", err)
+ return nil
+ }
+
+ err = c.LoadLabel()
+ if err != nil {
+ log.Error("LoadLabel: %v", err)
+ return nil
+ }
+
+ comment := &api.TimelineComment{
+ ID: c.ID,
+ Type: c.Type.String(),
+ Poster: ToUser(c.Poster, nil),
+ HTMLURL: c.HTMLURL(),
+ IssueURL: c.IssueURL(),
+ PRURL: c.PRURL(),
+ 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()
+ if err != nil {
+ log.Error("Time.LoadAttributes: %v", err)
+ return nil
+ }
+
+ comment.TrackedTime = ToTrackedTime(ctx, 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, 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 = ToComment(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(c.Assignee, nil)
+ }
+ if c.AssigneeTeam != nil {
+ comment.AssigneeTeam, _ = ToTeam(c.AssigneeTeam)
+ }
+
+ if c.ResolveDoer != nil {
+ comment.ResolveDoer = ToUser(c.ResolveDoer, nil)
+ }
+
+ if c.DependentIssue != nil {
+ comment.DependentIssue = ToAPIIssue(ctx, c.DependentIssue)
+ }
+
+ return comment
+}
diff --git a/services/convert/issue_test.go b/services/convert/issue_test.go
new file mode 100644
index 0000000000..4d780f3f00
--- /dev/null
+++ b/services/convert/issue_test.go
@@ -0,0 +1,57 @@
+// 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"
+)
+
+func TestLabel_ToLabel(t *testing.T) {
+ assert.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 0000000000..4c8e57bf79
--- /dev/null
+++ b/services/convert/main_test.go
@@ -0,0 +1,17 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "path/filepath"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+)
+
+func TestMain(m *testing.M) {
+ unittest.MainTest(m, &unittest.TestOptions{
+ GiteaRootPath: filepath.Join("..", ".."),
+ })
+}
diff --git a/services/convert/mirror.go b/services/convert/mirror.go
new file mode 100644
index 0000000000..1dcfc9b64d
--- /dev/null
+++ b/services/convert/mirror.go
@@ -0,0 +1,38 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/git"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToPushMirror convert from repo_model.PushMirror and remoteAddress to api.TopicResponse
+func ToPushMirror(pm *repo_model.PushMirror) (*api.PushMirror, error) {
+ repo := pm.GetRepository()
+ remoteAddress, err := getRemoteAddress(repo, pm.RemoteName)
+ if err != nil {
+ return nil, err
+ }
+ return &api.PushMirror{
+ RepoName: repo.Name,
+ RemoteName: pm.RemoteName,
+ RemoteAddress: remoteAddress,
+ CreatedUnix: pm.CreatedUnix.FormatLong(),
+ LastUpdateUnix: pm.LastUpdateUnix.FormatLong(),
+ LastError: pm.LastError,
+ Interval: pm.Interval.String(),
+ }, nil
+}
+
+func getRemoteAddress(repo *repo_model.Repository, remoteName string) (string, error) {
+ url, err := git.GetRemoteURL(git.DefaultContext, repo.RepoPath(), remoteName)
+ if err != nil {
+ return "", err
+ }
+ // remove confidential information
+ url.User = nil
+ return url.String(), nil
+}
diff --git a/services/convert/notification.go b/services/convert/notification.go
new file mode 100644
index 0000000000..5d3b078a25
--- /dev/null
+++ b/services/convert/notification.go
@@ -0,0 +1,96 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "net/url"
+
+ activities_model "code.gitea.io/gitea/models/activities"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/perm"
+ api "code.gitea.io/gitea/modules/structs"
+)
+
+// ToNotificationThread convert a Notification to api.NotificationThread
+func ToNotificationThread(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(db.DefaultContext, n.Repository, 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()
+ result.Subject.HTMLURL = n.Issue.HTMLURL()
+ result.Subject.State = n.Issue.State()
+ comment, err := n.Issue.GetLastComment()
+ if err == nil && comment != nil {
+ result.Subject.LatestCommentURL = comment.APIURL()
+ result.Subject.LatestCommentHTMLURL = comment.HTMLURL()
+ }
+ }
+ 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()
+ result.Subject.HTMLURL = n.Issue.HTMLURL()
+ result.Subject.State = n.Issue.State()
+ comment, err := n.Issue.GetLastComment()
+ if err == nil && comment != nil {
+ result.Subject.LatestCommentURL = comment.APIURL()
+ result.Subject.LatestCommentHTMLURL = comment.HTMLURL()
+ }
+
+ pr, _ := n.Issue.GetPullRequest()
+ if pr != nil && pr.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(nl activities_model.NotificationList) []*api.NotificationThread {
+ result := make([]*api.NotificationThread, 0, len(nl))
+ for _, n := range nl {
+ result = append(result, ToNotificationThread(n))
+ }
+ return result
+}
diff --git a/services/convert/package.go b/services/convert/package.go
new file mode 100644
index 0000000000..68ae6f4e62
--- /dev/null
+++ b/services/convert/package.go
@@ -0,0 +1,52 @@
+// 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.AccessMode)
+ }
+ }
+
+ return &api.Package{
+ ID: pd.Version.ID,
+ Owner: ToUser(pd.Owner, doer),
+ Repository: repo,
+ Creator: ToUser(pd.Creator, doer),
+ Type: string(pd.Package.Type),
+ Name: pd.Package.Name,
+ Version: pd.Version.Version,
+ CreatedAt: pd.Version.CreatedUnix.AsTime(),
+ }, 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 0000000000..db0add6cde
--- /dev/null
+++ b/services/convert/pull.go
@@ -0,0 +1,204 @@
+// 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/git"
+ "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, 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
+ }
+
+ p, err := 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,
+ IsLocked: apiIssue.IsLocked,
+ Comments: apiIssue.Comments,
+ HTMLURL: pr.Issue.HTMLURL(),
+ DiffURL: pr.Issue.DiffURL(),
+ PatchURL: pr.Issue.PatchURL(),
+ HasMerged: pr.HasMerged,
+ MergeBase: pr.MergeBase,
+ Mergeable: pr.Mergeable(),
+ Deadline: apiIssue.Deadline,
+ Created: pr.Issue.CreatedUnix.AsTimePtr(),
+ Updated: pr.Issue.UpdatedUnix.AsTimePtr(),
+
+ AllowMaintainerEdit: pr.AllowMaintainerEdit,
+
+ Base: &api.PRBranchInfo{
+ Name: pr.BaseBranch,
+ Ref: pr.BaseBranch,
+ RepoID: pr.BaseRepoID,
+ Repository: ToRepo(ctx, pr.BaseRepo, p.AccessMode),
+ },
+ Head: &api.PRBranchInfo{
+ Name: pr.HeadBranch,
+ Ref: fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index),
+ RepoID: -1,
+ },
+ }
+
+ gitRepo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
+ 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 := git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
+ 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.AccessMode)
+
+ headGitRepo, err := git.OpenRepository(ctx, pr.HeadRepo.RepoPath())
+ 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
+ }
+
+ 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
+ }
+ } 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()
+ }
+ }
+ }
+
+ if len(apiPullRequest.Head.Sha) == 0 && len(apiPullRequest.Head.Ref) != 0 {
+ baseGitRepo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
+ 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(pr.Merger, nil)
+ }
+
+ return apiPullRequest
+}
diff --git a/services/convert/pull_review.go b/services/convert/pull_review.go
new file mode 100644
index 0000000000..66c5018ee2
--- /dev/null
+++ b/services/convert/pull_review.go
@@ -0,0 +1,127 @@
+// 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()
+ }
+
+ apiTeam, err := ToTeam(r.ReviewerTeam)
+ if err != nil {
+ return nil, err
+ }
+
+ result := &api.PullReview{
+ ID: r.ID,
+ Reviewer: ToUser(r.Reviewer, doer),
+ ReviewerTeam: apiTeam,
+ State: api.ReviewStateUnknown,
+ Body: r.Content,
+ CommitID: r.CommitID,
+ Stale: r.Stale,
+ Official: r.Official,
+ Dismissed: r.Dismissed,
+ CodeCommentsCount: r.GetCodeCommentsCount(),
+ Submitted: r.CreatedUnix.AsTime(),
+ Updated: r.UpdatedUnix.AsTime(),
+ HTMLURL: r.HTMLURL(),
+ HTMLPullURL: r.Issue.HTMLURL(),
+ }
+
+ 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.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 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 := &api.PullReviewComment{
+ ID: comment.ID,
+ Body: comment.Content,
+ Poster: ToUser(comment.Poster, doer),
+ Resolver: ToUser(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(),
+ HTMLPullURL: review.Issue.HTMLURL(),
+ }
+
+ if comment.Line < 0 {
+ apiComment.OldLineNum = comment.UnsignedLine()
+ } else {
+ apiComment.LineNum = comment.UnsignedLine()
+ }
+ 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 0000000000..0915d096e6
--- /dev/null
+++ b/services/convert/pull_test.go
@@ -0,0 +1,48 @@
+// 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"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPullRequest_APIFormat(t *testing.T) {
+ // with HeadRepo
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
+ assert.NoError(t, pr.LoadAttributes(db.DefaultContext))
+ assert.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, perm.AccessModeRead),
+ }, apiPullRequest.Head)
+
+ // withOut HeadRepo
+ pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
+ assert.NoError(t, pr.LoadIssue(db.DefaultContext))
+ assert.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)
+}
diff --git a/services/convert/release.go b/services/convert/release.go
new file mode 100644
index 0000000000..3afa53c03f
--- /dev/null
+++ b/services/convert/release.go
@@ -0,0 +1,30 @@
+// Copyright 2020 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"
+)
+
+// ToRelease convert a repo_model.Release to api.Release
+func ToRelease(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(),
+ IsDraft: r.IsDraft,
+ IsPrerelease: r.IsPrerelease,
+ CreatedAt: r.CreatedUnix.AsTime(),
+ PublishedAt: r.CreatedUnix.AsTime(),
+ Publisher: ToUser(r.Publisher, nil),
+ Attachments: ToAttachments(r.Attachments),
+ }
+}
diff --git a/services/convert/repository.go b/services/convert/repository.go
new file mode 100644
index 0000000000..ce53a66692
--- /dev/null
+++ b/services/convert/repository.go
@@ -0,0 +1,202 @@
+// 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/perm"
+ 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, mode perm.AccessMode) *api.Repository {
+ return innerToRepo(ctx, repo, mode, false)
+}
+
+func innerToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.AccessMode, isParent bool) *api.Repository {
+ var parent *api.Repository
+
+ cloneLink := repo.CloneLink()
+ permission := &api.Permission{
+ Admin: mode >= perm.AccessModeAdmin,
+ Push: mode >= perm.AccessModeWrite,
+ Pull: mode >= perm.AccessModeRead,
+ }
+ if !isParent {
+ err := repo.GetBaseRepo(ctx)
+ if err != nil {
+ return nil
+ }
+ if repo.BaseRepo != nil {
+ parent = innerToRepo(ctx, repo.BaseRepo, mode, true)
+ }
+ }
+
+ // check enabled/disabled units
+ hasIssues := false
+ var externalTracker *api.ExternalTracker
+ var internalTracker *api.InternalTracker
+ if unit, err := repo.GetUnit(ctx, unit_model.TypeIssues); err == nil {
+ config := unit.IssuesConfig()
+ hasIssues = true
+ internalTracker = &api.InternalTracker{
+ EnableTimeTracker: config.EnableTimetracker,
+ AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime,
+ EnableIssueDependencies: config.EnableDependencies,
+ }
+ } else if unit, err := repo.GetUnit(ctx, unit_model.TypeExternalTracker); err == nil {
+ config := unit.ExternalTrackerConfig()
+ hasIssues = true
+ externalTracker = &api.ExternalTracker{
+ ExternalTrackerURL: config.ExternalTrackerURL,
+ ExternalTrackerFormat: config.ExternalTrackerFormat,
+ ExternalTrackerStyle: config.ExternalTrackerStyle,
+ ExternalTrackerRegexpPattern: config.ExternalTrackerRegexpPattern,
+ }
+ }
+ hasWiki := false
+ var externalWiki *api.ExternalWiki
+ if _, err := repo.GetUnit(ctx, unit_model.TypeWiki); err == nil {
+ hasWiki = true
+ } else if unit, err := repo.GetUnit(ctx, unit_model.TypeExternalWiki); err == nil {
+ hasWiki = true
+ config := unit.ExternalWikiConfig()
+ externalWiki = &api.ExternalWiki{
+ ExternalWikiURL: config.ExternalWikiURL,
+ }
+ }
+ hasPullRequests := false
+ ignoreWhitespaceConflicts := false
+ allowMerge := false
+ allowRebase := false
+ allowRebaseMerge := false
+ allowSquash := false
+ allowRebaseUpdate := false
+ defaultDeleteBranchAfterMerge := false
+ defaultMergeStyle := repo_model.MergeStyleMerge
+ 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
+ allowRebaseUpdate = config.AllowRebaseUpdate
+ defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
+ defaultMergeStyle = config.GetDefaultMergeStyle()
+ }
+ hasProjects := false
+ if _, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
+ hasProjects = true
+ }
+
+ if err := repo.GetOwner(ctx); err != nil {
+ return nil
+ }
+
+ numReleases, _ := repo_model.GetReleaseCountByRepoID(ctx, repo.ID, repo_model.FindReleasesOptions{IncludeDrafts: false, IncludeTags: false})
+
+ mirrorInterval := ""
+ var mirrorUpdated time.Time
+ if repo.IsMirror {
+ var err error
+ repo.Mirror, err = repo_model.GetMirrorByRepoID(ctx, repo.ID)
+ if err == nil {
+ mirrorInterval = repo.Mirror.Interval.String()
+ mirrorUpdated = repo.Mirror.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(t)
+ }
+ }
+ }
+
+ var language string
+ if repo.PrimaryLanguage != nil {
+ language = repo.PrimaryLanguage.Language
+ }
+
+ repoAPIURL := repo.APIURL()
+
+ return &api.Repository{
+ ID: repo.ID,
+ Owner: ToUserWithAccessMode(repo.Owner, mode),
+ 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(),
+ 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(),
+ Permissions: permission,
+ HasIssues: hasIssues,
+ ExternalTracker: externalTracker,
+ InternalTracker: internalTracker,
+ HasWiki: hasWiki,
+ HasProjects: hasProjects,
+ ExternalWiki: externalWiki,
+ HasPullRequests: hasPullRequests,
+ IgnoreWhitespaceConflicts: ignoreWhitespaceConflicts,
+ AllowMerge: allowMerge,
+ AllowRebase: allowRebase,
+ AllowRebaseMerge: allowRebaseMerge,
+ AllowSquash: allowSquash,
+ AllowRebaseUpdate: allowRebaseUpdate,
+ DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
+ DefaultMergeStyle: string(defaultMergeStyle),
+ AvatarURL: repo.AvatarLink(),
+ Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
+ MirrorInterval: mirrorInterval,
+ MirrorUpdated: mirrorUpdated,
+ RepoTransfer: transfer,
+ }
+}
+
+// ToRepoTransfer convert a models.RepoTransfer to a structs.RepeTransfer
+func ToRepoTransfer(t *models.RepoTransfer) *api.RepoTransfer {
+ teams, _ := ToTeams(t.Teams, false)
+
+ return &api.RepoTransfer{
+ Doer: ToUser(t.Doer, nil),
+ Recipient: ToUser(t.Recipient, nil),
+ Teams: teams,
+ }
+}
diff --git a/services/convert/status.go b/services/convert/status.go
new file mode 100644
index 0000000000..5fcf04074f
--- /dev/null
+++ b/services/convert/status.go
@@ -0,0 +1,57 @@
+// 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(),
+ Context: status.Context,
+ }
+
+ if status.CreatorID != 0 {
+ creator, _ := user_model.GetUserByID(ctx, status.CreatorID)
+ apiStatus.Creator = ToUser(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 status.State.NoBetterThan(retStatus.State) {
+ retStatus.State = status.State
+ }
+ }
+
+ return retStatus
+}
diff --git a/services/convert/user.go b/services/convert/user.go
new file mode 100644
index 0000000000..6b90539fd9
--- /dev/null
+++ b/services/convert/user.go
@@ -0,0 +1,106 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "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(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(user, signed, authed)
+}
+
+// ToUsers convert list of user_model.User to list of api.User
+func ToUsers(doer *user_model.User, users []*user_model.User) []*api.User {
+ result := make([]*api.User, len(users))
+ for i := range users {
+ result[i] = ToUser(users[i], doer)
+ }
+ return result
+}
+
+// ToUserWithAccessMode convert user_model.User to api.User
+// AccessMode is not none show add some more information
+func ToUserWithAccessMode(user *user_model.User, accessMode perm.AccessMode) *api.User {
+ if user == nil {
+ return nil
+ }
+ return toUser(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(user *user_model.User, signed, authed bool) *api.User {
+ result := &api.User{
+ ID: user.ID,
+ UserName: user.Name,
+ FullName: user.FullName,
+ Email: user.GetEmail(),
+ AvatarURL: user.AvatarLink(),
+ Created: user.CreatedUnix.AsTime(),
+ Restricted: user.IsRestricted,
+ Location: user.Location,
+ 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.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,
+ Language: user.Language,
+ Description: user.Description,
+ Theme: user.Theme,
+ HideEmail: user.KeepEmailPrivate,
+ HideActivity: user.KeepActivityPrivate,
+ DiffViewStyle: user.DiffViewStyle,
+ }
+}
+
+// ToUserAndPermission return User and its collaboration permission for a repository
+func ToUserAndPermission(user, doer *user_model.User, accessMode perm.AccessMode) api.RepoCollaboratorPermission {
+ return api.RepoCollaboratorPermission{
+ User: ToUser(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 0000000000..c3ab4187b7
--- /dev/null
+++ b/services/convert/user_test.go
@@ -0,0 +1,39 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "testing"
+
+ "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"
+)
+
+func TestUser_ToUser(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1, IsAdmin: true})
+
+ apiUser := toUser(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(user2, true, true)
+ assert.False(t, apiUser.IsAdmin)
+
+ apiUser = toUser(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(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 0000000000..cdce60831c
--- /dev/null
+++ b/services/convert/utils.go
@@ -0,0 +1,42 @@
+// 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
+ default:
+ return structs.PlainGitService
+ }
+}
diff --git a/services/convert/utils_test.go b/services/convert/utils_test.go
new file mode 100644
index 0000000000..d1ec5980ce
--- /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"
+
+ _ "github.com/mattn/go-sqlite3"
+)
+
+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: "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 0000000000..20d76162c7
--- /dev/null
+++ b/services/convert/wiki.go
@@ -0,0 +1,59 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package convert
+
+import (
+ "time"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/git"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ wiki_service "code.gitea.io/gitea/services/wiki"
+)
+
+// 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,
+ }
+}
+
+// ToWikiPageMetaData converts meta information to a WikiPageMetaData
+func ToWikiPageMetaData(title string, lastCommit *git.Commit, repo *repo_model.Repository) *api.WikiPageMetaData {
+ suburl := wiki_service.NameToSubURL(title)
+ return &api.WikiPageMetaData{
+ Title: title,
+ HTMLURL: util.URLJoin(repo.HTMLURL(), "wiki", suburl),
+ SubURL: suburl,
+ LastCommit: ToWikiCommit(lastCommit),
+ }
+}