diff options
author | KN4CK3R <admin@oldschoolhack.me> | 2022-12-29 03:57:15 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-12-29 10:57:15 +0800 |
commit | a35749893b91db48310d91ae0a32fee3ad3bb901 (patch) | |
tree | 09521c51fe8d2c2366694141d82454aa881b853e /services/convert | |
parent | 309e86a9bf305e807ead2854fa757c4d704dcfce (diff) | |
download | gitea-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.go | 30 | ||||
-rw-r--r-- | services/convert/convert.go | 458 | ||||
-rw-r--r-- | services/convert/git_commit.go | 202 | ||||
-rw-r--r-- | services/convert/git_commit_test.go | 41 | ||||
-rw-r--r-- | services/convert/issue.go | 235 | ||||
-rw-r--r-- | services/convert/issue_comment.go | 175 | ||||
-rw-r--r-- | services/convert/issue_test.go | 57 | ||||
-rw-r--r-- | services/convert/main_test.go | 17 | ||||
-rw-r--r-- | services/convert/mirror.go | 38 | ||||
-rw-r--r-- | services/convert/notification.go | 96 | ||||
-rw-r--r-- | services/convert/package.go | 52 | ||||
-rw-r--r-- | services/convert/pull.go | 204 | ||||
-rw-r--r-- | services/convert/pull_review.go | 127 | ||||
-rw-r--r-- | services/convert/pull_test.go | 48 | ||||
-rw-r--r-- | services/convert/release.go | 30 | ||||
-rw-r--r-- | services/convert/repository.go | 202 | ||||
-rw-r--r-- | services/convert/status.go | 57 | ||||
-rw-r--r-- | services/convert/user.go | 106 | ||||
-rw-r--r-- | services/convert/user_test.go | 39 | ||||
-rw-r--r-- | services/convert/utils.go | 42 | ||||
-rw-r--r-- | services/convert/utils_test.go | 39 | ||||
-rw-r--r-- | services/convert/wiki.go | 59 |
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), + } +} |