diff options
Diffstat (limited to 'services/convert')
26 files changed, 2968 insertions, 0 deletions
diff --git a/services/convert/activity.go b/services/convert/activity.go new file mode 100644 index 0000000..01fef73 --- /dev/null +++ b/services/convert/activity.go @@ -0,0 +1,52 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + + activities_model "code.gitea.io/gitea/models/activities" + perm_model "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + api "code.gitea.io/gitea/modules/structs" +) + +func ToActivity(ctx context.Context, ac *activities_model.Action, doer *user_model.User) *api.Activity { + p, err := access_model.GetUserRepoPermission(ctx, ac.Repo, doer) + if err != nil { + log.Error("GetUserRepoPermission[%d]: %v", ac.RepoID, err) + p.AccessMode = perm_model.AccessModeNone + } + + result := &api.Activity{ + ID: ac.ID, + UserID: ac.UserID, + OpType: ac.OpType.String(), + ActUserID: ac.ActUserID, + ActUser: ToUser(ctx, ac.ActUser, doer), + RepoID: ac.RepoID, + Repo: ToRepo(ctx, ac.Repo, p), + RefName: ac.RefName, + IsPrivate: ac.IsPrivate, + Content: ac.Content, + Created: ac.CreatedUnix.AsTime(), + } + + if ac.Comment != nil { + result.CommentID = ac.CommentID + result.Comment = ToAPIComment(ctx, ac.Repo, ac.Comment) + } + + return result +} + +func ToActivities(ctx context.Context, al activities_model.ActionList, doer *user_model.User) []*api.Activity { + result := make([]*api.Activity, 0, len(al)) + for _, ac := range al { + result = append(result, ToActivity(ctx, ac, doer)) + } + return result +} diff --git a/services/convert/attachment.go b/services/convert/attachment.go new file mode 100644 index 0000000..d632c94 --- /dev/null +++ b/services/convert/attachment.go @@ -0,0 +1,63 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + repo_model "code.gitea.io/gitea/models/repo" + api "code.gitea.io/gitea/modules/structs" +) + +func WebAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachment) string { + if attach.ExternalURL != "" { + return attach.ExternalURL + } + + return attach.DownloadURL() +} + +func APIAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachment) string { + return attach.DownloadURL() +} + +// ToAttachment converts models.Attachment to api.Attachment for API usage +func ToAttachment(repo *repo_model.Repository, a *repo_model.Attachment) *api.Attachment { + return toAttachment(repo, a, WebAssetDownloadURL) +} + +// ToAPIAttachment converts models.Attachment to api.Attachment for API usage +func ToAPIAttachment(repo *repo_model.Repository, a *repo_model.Attachment) *api.Attachment { + return toAttachment(repo, a, APIAssetDownloadURL) +} + +// toAttachment converts models.Attachment to api.Attachment for API usage +func toAttachment(repo *repo_model.Repository, a *repo_model.Attachment, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) *api.Attachment { + var typeName string + if a.ExternalURL != "" { + typeName = "external" + } else { + typeName = "attachment" + } + return &api.Attachment{ + ID: a.ID, + Name: a.Name, + Created: a.CreatedUnix.AsTime(), + DownloadCount: a.DownloadCount, + Size: a.Size, + UUID: a.UUID, + DownloadURL: getDownloadURL(repo, a), // for web request json and api request json, return different download urls + Type: typeName, + } +} + +func ToAPIAttachments(repo *repo_model.Repository, attachments []*repo_model.Attachment) []*api.Attachment { + return toAttachments(repo, attachments, APIAssetDownloadURL) +} + +func toAttachments(repo *repo_model.Repository, attachments []*repo_model.Attachment, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) []*api.Attachment { + converted := make([]*api.Attachment, 0, len(attachments)) + for _, attachment := range attachments { + converted = append(converted, toAttachment(repo, attachment, getDownloadURL)) + } + return converted +} diff --git a/services/convert/convert.go b/services/convert/convert.go new file mode 100644 index 0000000..7a09449 --- /dev/null +++ b/services/convert/convert.go @@ -0,0 +1,510 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + actions_model "code.gitea.io/gitea/models/actions" + asymkey_model "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/models/auth" + git_model "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/gitdiff" +) + +// ToEmail convert models.EmailAddress to api.Email +func ToEmail(email *user_model.EmailAddress) *api.Email { + return &api.Email{ + Email: email.Email, + Verified: email.IsActivated, + Primary: email.IsPrimary, + } +} + +// ToEmail convert models.EmailAddress to api.Email +func ToEmailSearch(email *user_model.SearchEmailResult) *api.Email { + return &api.Email{ + Email: email.Email, + Verified: email.IsActivated, + Primary: email.IsPrimary, + UserID: email.UID, + UserName: email.Name, + } +} + +// ToBranch convert a git.Commit and git.Branch to an api.Branch +func ToBranch(ctx context.Context, repo *repo_model.Repository, branchName string, c *git.Commit, bp *git_model.ProtectedBranch, user *user_model.User, isRepoAdmin bool) (*api.Branch, error) { + if bp == nil { + var hasPerm bool + var canPush bool + var err error + if user != nil { + hasPerm, err = access_model.HasAccessUnit(ctx, user, repo, unit.TypeCode, perm.AccessModeWrite) + if err != nil { + return nil, err + } + + perms, err := access_model.GetUserRepoPermission(ctx, repo, user) + if err != nil { + return nil, err + } + canPush = issues_model.CanMaintainerWriteToBranch(ctx, perms, branchName, user) + } + + return &api.Branch{ + Name: branchName, + Commit: ToPayloadCommit(ctx, repo, c), + Protected: false, + RequiredApprovals: 0, + EnableStatusCheck: false, + StatusCheckContexts: []string{}, + UserCanPush: canPush, + UserCanMerge: hasPerm, + }, nil + } + + branch := &api.Branch{ + Name: branchName, + Commit: ToPayloadCommit(ctx, repo, c), + Protected: true, + RequiredApprovals: bp.RequiredApprovals, + EnableStatusCheck: bp.EnableStatusCheck, + StatusCheckContexts: bp.StatusCheckContexts, + } + + if isRepoAdmin { + branch.EffectiveBranchProtectionName = bp.RuleName + } + + if user != nil { + permission, err := access_model.GetUserRepoPermission(ctx, repo, user) + if err != nil { + return nil, err + } + bp.Repo = repo + branch.UserCanPush = bp.CanUserPush(ctx, user) + branch.UserCanMerge = git_model.IsUserMergeWhitelisted(ctx, bp, user.ID, permission) + } + + return branch, nil +} + +// getWhitelistEntities returns the names of the entities that are in the whitelist +func getWhitelistEntities[T *user_model.User | *organization.Team](entities []T, whitelistIDs []int64) []string { + whitelistUserIDsSet := container.SetOf(whitelistIDs...) + whitelistNames := make([]string, 0) + for _, entity := range entities { + switch v := any(entity).(type) { + case *user_model.User: + if whitelistUserIDsSet.Contains(v.ID) { + whitelistNames = append(whitelistNames, v.Name) + } + case *organization.Team: + if whitelistUserIDsSet.Contains(v.ID) { + whitelistNames = append(whitelistNames, v.Name) + } + } + } + + return whitelistNames +} + +// ToBranchProtection convert a ProtectedBranch to api.BranchProtection +func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo *repo_model.Repository) *api.BranchProtection { + readers, err := access_model.GetRepoReaders(ctx, repo) + if err != nil { + log.Error("GetRepoReaders: %v", err) + } + + pushWhitelistUsernames := getWhitelistEntities(readers, bp.WhitelistUserIDs) + mergeWhitelistUsernames := getWhitelistEntities(readers, bp.MergeWhitelistUserIDs) + approvalsWhitelistUsernames := getWhitelistEntities(readers, bp.ApprovalsWhitelistUserIDs) + + teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead) + if err != nil { + log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err) + } + + pushWhitelistTeams := getWhitelistEntities(teamReaders, bp.WhitelistTeamIDs) + mergeWhitelistTeams := getWhitelistEntities(teamReaders, bp.MergeWhitelistTeamIDs) + approvalsWhitelistTeams := getWhitelistEntities(teamReaders, bp.ApprovalsWhitelistTeamIDs) + + branchName := "" + if !git_model.IsRuleNameSpecial(bp.RuleName) { + branchName = bp.RuleName + } + + return &api.BranchProtection{ + BranchName: branchName, + RuleName: bp.RuleName, + EnablePush: bp.CanPush, + EnablePushWhitelist: bp.EnableWhitelist, + PushWhitelistUsernames: pushWhitelistUsernames, + PushWhitelistTeams: pushWhitelistTeams, + PushWhitelistDeployKeys: bp.WhitelistDeployKeys, + EnableMergeWhitelist: bp.EnableMergeWhitelist, + MergeWhitelistUsernames: mergeWhitelistUsernames, + MergeWhitelistTeams: mergeWhitelistTeams, + EnableStatusCheck: bp.EnableStatusCheck, + StatusCheckContexts: bp.StatusCheckContexts, + RequiredApprovals: bp.RequiredApprovals, + EnableApprovalsWhitelist: bp.EnableApprovalsWhitelist, + ApprovalsWhitelistUsernames: approvalsWhitelistUsernames, + ApprovalsWhitelistTeams: approvalsWhitelistTeams, + BlockOnRejectedReviews: bp.BlockOnRejectedReviews, + BlockOnOfficialReviewRequests: bp.BlockOnOfficialReviewRequests, + BlockOnOutdatedBranch: bp.BlockOnOutdatedBranch, + DismissStaleApprovals: bp.DismissStaleApprovals, + IgnoreStaleApprovals: bp.IgnoreStaleApprovals, + RequireSignedCommits: bp.RequireSignedCommits, + ProtectedFilePatterns: bp.ProtectedFilePatterns, + UnprotectedFilePatterns: bp.UnprotectedFilePatterns, + ApplyToAdmins: bp.ApplyToAdmins, + Created: bp.CreatedUnix.AsTime(), + Updated: bp.UpdatedUnix.AsTime(), + } +} + +// ToTag convert a git.Tag to an api.Tag +func ToTag(repo *repo_model.Repository, t *git.Tag) *api.Tag { + return &api.Tag{ + Name: t.Name, + Message: strings.TrimSpace(t.Message), + ID: t.ID.String(), + Commit: ToCommitMeta(repo, t), + ZipballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".zip"), + TarballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".tar.gz"), + ArchiveDownloadCount: t.ArchiveDownloadCount, + } +} + +// ToActionTask convert a actions_model.ActionTask to an api.ActionTask +func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.ActionTask, error) { + if err := t.LoadAttributes(ctx); err != nil { + return nil, err + } + + url := strings.TrimSuffix(setting.AppURL, "/") + t.GetRunLink() + + return &api.ActionTask{ + ID: t.ID, + Name: t.Job.Name, + HeadBranch: t.Job.Run.PrettyRef(), + HeadSHA: t.Job.CommitSHA, + RunNumber: t.Job.Run.Index, + Event: t.Job.Run.TriggerEvent, + DisplayTitle: t.Job.Run.Title, + Status: t.Status.String(), + WorkflowID: t.Job.Run.WorkflowID, + URL: url, + CreatedAt: t.Created.AsLocalTime(), + UpdatedAt: t.Updated.AsLocalTime(), + RunStartedAt: t.Started.AsLocalTime(), + }, nil +} + +// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification +func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerification { + verif := asymkey_model.ParseCommitWithSignature(ctx, c) + commitVerification := &api.PayloadCommitVerification{ + Verified: verif.Verified, + Reason: verif.Reason, + } + if c.Signature != nil { + commitVerification.Signature = c.Signature.Signature + commitVerification.Payload = c.Signature.Payload + } + if verif.SigningUser != nil { + commitVerification.Signer = &api.PayloadUser{ + Name: verif.SigningUser.Name, + Email: verif.SigningUser.Email, + } + } + return commitVerification +} + +// ToPublicKey convert asymkey_model.PublicKey to api.PublicKey +func ToPublicKey(apiLink string, key *asymkey_model.PublicKey) *api.PublicKey { + return &api.PublicKey{ + ID: key.ID, + Key: key.Content, + URL: fmt.Sprintf("%s%d", apiLink, key.ID), + Title: key.Name, + Fingerprint: key.Fingerprint, + Created: key.CreatedUnix.AsTime(), + } +} + +// ToGPGKey converts models.GPGKey to api.GPGKey +func ToGPGKey(key *asymkey_model.GPGKey) *api.GPGKey { + subkeys := make([]*api.GPGKey, len(key.SubsKey)) + for id, k := range key.SubsKey { + subkeys[id] = &api.GPGKey{ + ID: k.ID, + PrimaryKeyID: k.PrimaryKeyID, + KeyID: k.KeyID, + PublicKey: k.Content, + Created: k.CreatedUnix.AsTime(), + Expires: k.ExpiredUnix.AsTime(), + CanSign: k.CanSign, + CanEncryptComms: k.CanEncryptComms, + CanEncryptStorage: k.CanEncryptStorage, + CanCertify: k.CanSign, + Verified: k.Verified, + } + } + emails := make([]*api.GPGKeyEmail, len(key.Emails)) + for i, e := range key.Emails { + emails[i] = ToGPGKeyEmail(e) + } + return &api.GPGKey{ + ID: key.ID, + PrimaryKeyID: key.PrimaryKeyID, + KeyID: key.KeyID, + PublicKey: key.Content, + Created: key.CreatedUnix.AsTime(), + Expires: key.ExpiredUnix.AsTime(), + Emails: emails, + SubsKey: subkeys, + CanSign: key.CanSign, + CanEncryptComms: key.CanEncryptComms, + CanEncryptStorage: key.CanEncryptStorage, + CanCertify: key.CanSign, + Verified: key.Verified, + } +} + +// ToGPGKeyEmail convert models.EmailAddress to api.GPGKeyEmail +func ToGPGKeyEmail(email *user_model.EmailAddress) *api.GPGKeyEmail { + return &api.GPGKeyEmail{ + Email: email.Email, + Verified: email.IsActivated, + } +} + +// ToGitHook convert git.Hook to api.GitHook +func ToGitHook(h *git.Hook) *api.GitHook { + return &api.GitHook{ + Name: h.Name(), + IsActive: h.IsActive, + Content: h.Content, + } +} + +// ToDeployKey convert asymkey_model.DeployKey to api.DeployKey +func ToDeployKey(apiLink string, key *asymkey_model.DeployKey) *api.DeployKey { + return &api.DeployKey{ + ID: key.ID, + KeyID: key.KeyID, + Key: key.Content, + Fingerprint: key.Fingerprint, + URL: fmt.Sprintf("%s%d", apiLink, key.ID), + Title: key.Name, + Created: key.CreatedUnix.AsTime(), + ReadOnly: key.Mode == perm.AccessModeRead, // All deploy keys are read-only. + } +} + +// ToOrganization convert user_model.User to api.Organization +func ToOrganization(ctx context.Context, org *organization.Organization) *api.Organization { + return &api.Organization{ + ID: org.ID, + AvatarURL: org.AsUser().AvatarLink(ctx), + Name: org.Name, + UserName: org.Name, + FullName: org.FullName, + Email: org.Email, + Description: org.Description, + Website: org.Website, + Location: org.Location, + Visibility: org.Visibility.String(), + RepoAdminChangeTeamAccess: org.RepoAdminChangeTeamAccess, + } +} + +// ToTeam convert models.Team to api.Team +func ToTeam(ctx context.Context, team *organization.Team, loadOrg ...bool) (*api.Team, error) { + teams, err := ToTeams(ctx, []*organization.Team{team}, len(loadOrg) != 0 && loadOrg[0]) + if err != nil || len(teams) == 0 { + return nil, err + } + return teams[0], nil +} + +// ToTeams convert models.Team list to api.Team list +func ToTeams(ctx context.Context, teams []*organization.Team, loadOrgs bool) ([]*api.Team, error) { + cache := make(map[int64]*api.Organization) + apiTeams := make([]*api.Team, 0, len(teams)) + for _, t := range teams { + if err := t.LoadUnits(ctx); err != nil { + return nil, err + } + + apiTeam := &api.Team{ + ID: t.ID, + Name: t.Name, + Description: t.Description, + IncludesAllRepositories: t.IncludesAllRepositories, + CanCreateOrgRepo: t.CanCreateOrgRepo, + Permission: t.AccessMode.String(), + Units: t.GetUnitNames(), + UnitsMap: t.GetUnitsMap(), + } + + if loadOrgs { + apiOrg, ok := cache[t.OrgID] + if !ok { + org, err := organization.GetOrgByID(ctx, t.OrgID) + if err != nil { + return nil, err + } + apiOrg = ToOrganization(ctx, org) + cache[t.OrgID] = apiOrg + } + apiTeam.Organization = apiOrg + } + + apiTeams = append(apiTeams, apiTeam) + } + return apiTeams, nil +} + +// ToAnnotatedTag convert git.Tag to api.AnnotatedTag +func ToAnnotatedTag(ctx context.Context, repo *repo_model.Repository, t *git.Tag, c *git.Commit) *api.AnnotatedTag { + return &api.AnnotatedTag{ + Tag: t.Name, + SHA: t.ID.String(), + Object: ToAnnotatedTagObject(repo, c), + Message: t.Message, + URL: util.URLJoin(repo.APIURL(), "git/tags", t.ID.String()), + Tagger: ToCommitUser(t.Tagger), + Verification: ToVerification(ctx, c), + ArchiveDownloadCount: t.ArchiveDownloadCount, + } +} + +// ToAnnotatedTagObject convert a git.Commit to an api.AnnotatedTagObject +func ToAnnotatedTagObject(repo *repo_model.Repository, commit *git.Commit) *api.AnnotatedTagObject { + return &api.AnnotatedTagObject{ + SHA: commit.ID.String(), + Type: string(git.ObjectCommit), + URL: util.URLJoin(repo.APIURL(), "git/commits", commit.ID.String()), + } +} + +// ToTagProtection convert a git.ProtectedTag to an api.TagProtection +func ToTagProtection(ctx context.Context, pt *git_model.ProtectedTag, repo *repo_model.Repository) *api.TagProtection { + readers, err := access_model.GetRepoReaders(ctx, repo) + if err != nil { + log.Error("GetRepoReaders: %v", err) + } + + whitelistUsernames := getWhitelistEntities(readers, pt.AllowlistUserIDs) + + teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead) + if err != nil { + log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err) + } + + whitelistTeams := getWhitelistEntities(teamReaders, pt.AllowlistTeamIDs) + + return &api.TagProtection{ + ID: pt.ID, + NamePattern: pt.NamePattern, + WhitelistUsernames: whitelistUsernames, + WhitelistTeams: whitelistTeams, + Created: pt.CreatedUnix.AsTime(), + Updated: pt.UpdatedUnix.AsTime(), + } +} + +// ToTopicResponse convert from models.Topic to api.TopicResponse +func ToTopicResponse(topic *repo_model.Topic) *api.TopicResponse { + return &api.TopicResponse{ + ID: topic.ID, + Name: topic.Name, + RepoCount: topic.RepoCount, + Created: topic.CreatedUnix.AsTime(), + Updated: topic.UpdatedUnix.AsTime(), + } +} + +// ToOAuth2Application convert from auth.OAuth2Application to api.OAuth2Application +func ToOAuth2Application(app *auth.OAuth2Application) *api.OAuth2Application { + return &api.OAuth2Application{ + ID: app.ID, + Name: app.Name, + ClientID: app.ClientID, + ClientSecret: app.ClientSecret, + ConfidentialClient: app.ConfidentialClient, + RedirectURIs: app.RedirectURIs, + Created: app.CreatedUnix.AsTime(), + } +} + +// ToLFSLock convert a LFSLock to api.LFSLock +func ToLFSLock(ctx context.Context, l *git_model.LFSLock) *api.LFSLock { + u, err := user_model.GetUserByID(ctx, l.OwnerID) + if err != nil { + return nil + } + return &api.LFSLock{ + ID: strconv.FormatInt(l.ID, 10), + Path: l.Path, + LockedAt: l.Created.Round(time.Second), + Owner: &api.LFSLockOwner{ + Name: u.Name, + }, + } +} + +// ToChangedFile convert a gitdiff.DiffFile to api.ChangedFile +func ToChangedFile(f *gitdiff.DiffFile, repo *repo_model.Repository, commit string) *api.ChangedFile { + status := "changed" + previousFilename := "" + if f.IsDeleted { + status = "deleted" + } else if f.IsCreated { + status = "added" + } else if f.IsRenamed && f.Type == gitdiff.DiffFileCopy { + status = "copied" + } else if f.IsRenamed && f.Type == gitdiff.DiffFileRename { + status = "renamed" + previousFilename = f.OldName + } else if f.Addition == 0 && f.Deletion == 0 { + status = "unchanged" + } + + file := &api.ChangedFile{ + Filename: f.GetDiffFileName(), + Status: status, + Additions: f.Addition, + Deletions: f.Deletion, + Changes: f.Addition + f.Deletion, + PreviousFilename: previousFilename, + HTMLURL: fmt.Sprint(repo.HTMLURL(), "/src/commit/", commit, "/", util.PathEscapeSegments(f.GetDiffFileName())), + ContentsURL: fmt.Sprint(repo.APIURL(), "/contents/", util.PathEscapeSegments(f.GetDiffFileName()), "?ref=", commit), + RawURL: fmt.Sprint(repo.HTMLURL(), "/raw/commit/", commit, "/", util.PathEscapeSegments(f.GetDiffFileName())), + } + + return file +} diff --git a/services/convert/git_commit.go b/services/convert/git_commit.go new file mode 100644 index 0000000..e0efcdd --- /dev/null +++ b/services/convert/git_commit.go @@ -0,0 +1,228 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + "net/url" + "time" + + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + ctx "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/gitdiff" +) + +// ToCommitUser convert a git.Signature to an api.CommitUser +func ToCommitUser(sig *git.Signature) *api.CommitUser { + return &api.CommitUser{ + Identity: api.Identity{ + Name: sig.Name, + Email: sig.Email, + }, + Date: sig.When.UTC().Format(time.RFC3339), + } +} + +// ToCommitMeta convert a git.Tag to an api.CommitMeta +func ToCommitMeta(repo *repo_model.Repository, tag *git.Tag) *api.CommitMeta { + return &api.CommitMeta{ + SHA: tag.Object.String(), + URL: util.URLJoin(repo.APIURL(), "git/commits", tag.ID.String()), + Created: tag.Tagger.When, + } +} + +// ToPayloadCommit convert a git.Commit to api.PayloadCommit +func ToPayloadCommit(ctx context.Context, repo *repo_model.Repository, c *git.Commit) *api.PayloadCommit { + authorUsername := "" + if author, err := user_model.GetUserByEmail(ctx, c.Author.Email); err == nil { + authorUsername = author.Name + } else if !user_model.IsErrUserNotExist(err) { + log.Error("GetUserByEmail: %v", err) + } + + committerUsername := "" + if committer, err := user_model.GetUserByEmail(ctx, c.Committer.Email); err == nil { + committerUsername = committer.Name + } else if !user_model.IsErrUserNotExist(err) { + log.Error("GetUserByEmail: %v", err) + } + + return &api.PayloadCommit{ + ID: c.ID.String(), + Message: c.Message(), + URL: util.URLJoin(repo.HTMLURL(), "commit", c.ID.String()), + Author: &api.PayloadUser{ + Name: c.Author.Name, + Email: c.Author.Email, + UserName: authorUsername, + }, + Committer: &api.PayloadUser{ + Name: c.Committer.Name, + Email: c.Committer.Email, + UserName: committerUsername, + }, + Timestamp: c.Author.When, + Verification: ToVerification(ctx, c), + } +} + +type ToCommitOptions struct { + Stat bool + Verification bool + Files bool +} + +func ParseCommitOptions(ctx *ctx.APIContext) ToCommitOptions { + return ToCommitOptions{ + Stat: ctx.FormString("stat") == "" || ctx.FormBool("stat"), + Files: ctx.FormString("files") == "" || ctx.FormBool("files"), + Verification: ctx.FormString("verification") == "" || ctx.FormBool("verification"), + } +} + +// ToCommit convert a git.Commit to api.Commit +func ToCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, userCache map[string]*user_model.User, opts ToCommitOptions) (*api.Commit, error) { + var apiAuthor, apiCommitter *api.User + + // Retrieve author and committer information + + var cacheAuthor *user_model.User + var ok bool + if userCache == nil { + cacheAuthor = (*user_model.User)(nil) + ok = false + } else { + cacheAuthor, ok = userCache[commit.Author.Email] + } + + if ok { + apiAuthor = ToUser(ctx, cacheAuthor, nil) + } else { + author, err := user_model.GetUserByEmail(ctx, commit.Author.Email) + if err != nil && !user_model.IsErrUserNotExist(err) { + return nil, err + } else if err == nil { + apiAuthor = ToUser(ctx, author, nil) + if userCache != nil { + userCache[commit.Author.Email] = author + } + } + } + + var cacheCommitter *user_model.User + if userCache == nil { + cacheCommitter = (*user_model.User)(nil) + ok = false + } else { + cacheCommitter, ok = userCache[commit.Committer.Email] + } + + if ok { + apiCommitter = ToUser(ctx, cacheCommitter, nil) + } else { + committer, err := user_model.GetUserByEmail(ctx, commit.Committer.Email) + if err != nil && !user_model.IsErrUserNotExist(err) { + return nil, err + } else if err == nil { + apiCommitter = ToUser(ctx, committer, nil) + if userCache != nil { + userCache[commit.Committer.Email] = committer + } + } + } + + // Retrieve parent(s) of the commit + apiParents := make([]*api.CommitMeta, commit.ParentCount()) + for i := 0; i < commit.ParentCount(); i++ { + sha, _ := commit.ParentID(i) + apiParents[i] = &api.CommitMeta{ + URL: repo.APIURL() + "/git/commits/" + url.PathEscape(sha.String()), + SHA: sha.String(), + } + } + + res := &api.Commit{ + CommitMeta: &api.CommitMeta{ + URL: repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String()), + SHA: commit.ID.String(), + Created: commit.Committer.When, + }, + HTMLURL: repo.HTMLURL() + "/commit/" + url.PathEscape(commit.ID.String()), + RepoCommit: &api.RepoCommit{ + URL: repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String()), + Author: &api.CommitUser{ + Identity: api.Identity{ + Name: commit.Author.Name, + Email: commit.Author.Email, + }, + Date: commit.Author.When.Format(time.RFC3339), + }, + Committer: &api.CommitUser{ + Identity: api.Identity{ + Name: commit.Committer.Name, + Email: commit.Committer.Email, + }, + Date: commit.Committer.When.Format(time.RFC3339), + }, + Message: commit.Message(), + Tree: &api.CommitMeta{ + URL: repo.APIURL() + "/git/trees/" + url.PathEscape(commit.ID.String()), + SHA: commit.ID.String(), + Created: commit.Committer.When, + }, + }, + Author: apiAuthor, + Committer: apiCommitter, + Parents: apiParents, + } + + // Retrieve verification for commit + if opts.Verification { + res.RepoCommit.Verification = ToVerification(ctx, commit) + } + + // Retrieve files affected by the commit + if opts.Files { + fileStatus, err := git.GetCommitFileStatus(gitRepo.Ctx, repo.RepoPath(), commit.ID.String()) + if err != nil { + return nil, err + } + + affectedFileList := make([]*api.CommitAffectedFiles, 0, len(fileStatus.Added)+len(fileStatus.Removed)+len(fileStatus.Modified)) + for filestatus, files := range map[string][]string{"added": fileStatus.Added, "removed": fileStatus.Removed, "modified": fileStatus.Modified} { + for _, filename := range files { + affectedFileList = append(affectedFileList, &api.CommitAffectedFiles{ + Filename: filename, + Status: filestatus, + }) + } + } + + res.Files = affectedFileList + } + + // Get diff stats for commit + if opts.Stat { + diff, err := gitdiff.GetDiff(ctx, gitRepo, &gitdiff.DiffOptions{ + AfterCommitID: commit.ID.String(), + }) + if err != nil { + return nil, err + } + + res.Stats = &api.CommitStats{ + Total: diff.TotalAddition + diff.TotalDeletion, + Additions: diff.TotalAddition, + Deletions: diff.TotalDeletion, + } + } + + return res, nil +} diff --git a/services/convert/git_commit_test.go b/services/convert/git_commit_test.go new file mode 100644 index 0000000..68d1b05 --- /dev/null +++ b/services/convert/git_commit_test.go @@ -0,0 +1,42 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "testing" + "time" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/git" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestToCommitMeta(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + sha1 := git.Sha1ObjectFormat + signature := &git.Signature{Name: "Test Signature", Email: "test@email.com", When: time.Unix(0, 0)} + tag := &git.Tag{ + Name: "Test Tag", + ID: sha1.EmptyObjectID(), + Object: sha1.EmptyObjectID(), + Type: "Test Type", + Tagger: signature, + Message: "Test Message", + } + + commitMeta := ToCommitMeta(headRepo, tag) + + assert.NotNil(t, commitMeta) + assert.EqualValues(t, &api.CommitMeta{ + SHA: sha1.EmptyObjectID().String(), + URL: util.URLJoin(headRepo.APIURL(), "git/commits", sha1.EmptyObjectID().String()), + Created: time.Unix(0, 0), + }, commitMeta) +} diff --git a/services/convert/issue.go b/services/convert/issue.go new file mode 100644 index 0000000..f514dc4 --- /dev/null +++ b/services/convert/issue.go @@ -0,0 +1,288 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + "fmt" + "net/url" + "strings" + + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/label" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" +) + +func ToIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue { + return toIssue(ctx, doer, issue, WebAssetDownloadURL) +} + +// ToAPIIssue converts an Issue to API format +// it assumes some fields assigned with values: +// Required - Poster, Labels, +// Optional - Milestone, Assignee, PullRequest +func ToAPIIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue { + return toIssue(ctx, doer, issue, APIAssetDownloadURL) +} + +func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) *api.Issue { + if err := issue.LoadPoster(ctx); err != nil { + return &api.Issue{} + } + if err := issue.LoadRepo(ctx); err != nil { + return &api.Issue{} + } + if err := issue.LoadAttachments(ctx); err != nil { + return &api.Issue{} + } + + apiIssue := &api.Issue{ + ID: issue.ID, + Index: issue.Index, + Poster: ToUser(ctx, issue.Poster, doer), + Title: issue.Title, + Body: issue.Content, + Attachments: toAttachments(issue.Repo, issue.Attachments, getDownloadURL), + Ref: issue.Ref, + State: issue.State(), + IsLocked: issue.IsLocked, + Comments: issue.NumComments, + Created: issue.CreatedUnix.AsTime(), + Updated: issue.UpdatedUnix.AsTime(), + PinOrder: issue.PinOrder, + } + + if issue.Repo != nil { + if err := issue.Repo.LoadOwner(ctx); err != nil { + return &api.Issue{} + } + apiIssue.URL = issue.APIURL(ctx) + apiIssue.HTMLURL = issue.HTMLURL() + if err := issue.LoadLabels(ctx); err != nil { + return &api.Issue{} + } + apiIssue.Labels = ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner) + apiIssue.Repo = &api.RepositoryMeta{ + ID: issue.Repo.ID, + Name: issue.Repo.Name, + Owner: issue.Repo.OwnerName, + FullName: issue.Repo.FullName(), + } + } + + if issue.ClosedUnix != 0 { + apiIssue.Closed = issue.ClosedUnix.AsTimePtr() + } + + if err := issue.LoadMilestone(ctx); err != nil { + return &api.Issue{} + } + if issue.Milestone != nil { + apiIssue.Milestone = ToAPIMilestone(issue.Milestone) + } + + if err := issue.LoadAssignees(ctx); err != nil { + return &api.Issue{} + } + if len(issue.Assignees) > 0 { + for _, assignee := range issue.Assignees { + apiIssue.Assignees = append(apiIssue.Assignees, ToUser(ctx, assignee, nil)) + } + apiIssue.Assignee = ToUser(ctx, issue.Assignees[0], nil) // For compatibility, we're keeping the first assignee as `apiIssue.Assignee` + } + if issue.IsPull { + if err := issue.LoadPullRequest(ctx); err != nil { + return &api.Issue{} + } + if issue.PullRequest != nil { + apiIssue.PullRequest = &api.PullRequestMeta{ + HasMerged: issue.PullRequest.HasMerged, + IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx), + } + if issue.PullRequest.HasMerged { + apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr() + } + // Add pr's html url + apiIssue.PullRequest.HTMLURL = issue.HTMLURL() + } + } + if issue.DeadlineUnix != 0 { + apiIssue.Deadline = issue.DeadlineUnix.AsTimePtr() + } + + return apiIssue +} + +// ToIssueList converts an IssueList to API format +func ToIssueList(ctx context.Context, doer *user_model.User, il issues_model.IssueList) []*api.Issue { + result := make([]*api.Issue, len(il)) + for i := range il { + result[i] = ToIssue(ctx, doer, il[i]) + } + return result +} + +// ToAPIIssueList converts an IssueList to API format +func ToAPIIssueList(ctx context.Context, doer *user_model.User, il issues_model.IssueList) []*api.Issue { + result := make([]*api.Issue, len(il)) + for i := range il { + result[i] = ToAPIIssue(ctx, doer, il[i]) + } + return result +} + +// ToTrackedTime converts TrackedTime to API format +func ToTrackedTime(ctx context.Context, doer *user_model.User, t *issues_model.TrackedTime) (apiT *api.TrackedTime) { + apiT = &api.TrackedTime{ + ID: t.ID, + IssueID: t.IssueID, + UserID: t.UserID, + Time: t.Time, + Created: t.Created, + } + if t.Issue != nil { + apiT.Issue = ToAPIIssue(ctx, doer, t.Issue) + } + if t.User != nil { + apiT.UserName = t.User.Name + } + return apiT +} + +// ToStopWatches convert Stopwatch list to api.StopWatches +func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.StopWatches, error) { + result := api.StopWatches(make([]api.StopWatch, 0, len(sws))) + + issueCache := make(map[int64]*issues_model.Issue) + repoCache := make(map[int64]*repo_model.Repository) + var ( + issue *issues_model.Issue + repo *repo_model.Repository + ok bool + err error + ) + + for _, sw := range sws { + issue, ok = issueCache[sw.IssueID] + if !ok { + issue, err = issues_model.GetIssueByID(ctx, sw.IssueID) + if err != nil { + return nil, err + } + } + repo, ok = repoCache[issue.RepoID] + if !ok { + repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID) + if err != nil { + return nil, err + } + } + + result = append(result, api.StopWatch{ + Created: sw.CreatedUnix.AsTime(), + Seconds: sw.Seconds(), + Duration: sw.Duration(), + IssueIndex: issue.Index, + IssueTitle: issue.Title, + RepoOwnerName: repo.OwnerName, + RepoName: repo.Name, + }) + } + return result, nil +} + +// ToTrackedTimeList converts TrackedTimeList to API format +func ToTrackedTimeList(ctx context.Context, doer *user_model.User, tl issues_model.TrackedTimeList) api.TrackedTimeList { + result := make([]*api.TrackedTime, 0, len(tl)) + for _, t := range tl { + result = append(result, ToTrackedTime(ctx, doer, t)) + } + return result +} + +// ToLabel converts Label to API format +func ToLabel(label *issues_model.Label, repo *repo_model.Repository, org *user_model.User) *api.Label { + result := &api.Label{ + ID: label.ID, + Name: label.Name, + Exclusive: label.Exclusive, + Color: strings.TrimLeft(label.Color, "#"), + Description: label.Description, + IsArchived: label.IsArchived(), + } + + labelBelongsToRepo := label.BelongsToRepo() + + // calculate URL + if labelBelongsToRepo && repo != nil { + result.URL = fmt.Sprintf("%s/labels/%d", repo.APIURL(), label.ID) + } else { // BelongsToOrg + if org != nil { + result.URL = fmt.Sprintf("%sapi/v1/orgs/%s/labels/%d", setting.AppURL, url.PathEscape(org.Name), label.ID) + } else { + log.Error("ToLabel did not get org to calculate url for label with id '%d'", label.ID) + } + } + + if labelBelongsToRepo && repo == nil { + log.Error("ToLabel did not get repo to calculate url for label with id '%d'", label.ID) + } + + return result +} + +// ToLabelList converts list of Label to API format +func ToLabelList(labels []*issues_model.Label, repo *repo_model.Repository, org *user_model.User) []*api.Label { + result := make([]*api.Label, len(labels)) + for i := range labels { + result[i] = ToLabel(labels[i], repo, org) + } + return result +} + +// ToAPIMilestone converts Milestone into API Format +func ToAPIMilestone(m *issues_model.Milestone) *api.Milestone { + apiMilestone := &api.Milestone{ + ID: m.ID, + State: m.State(), + Title: m.Name, + Description: m.Content, + OpenIssues: m.NumOpenIssues, + ClosedIssues: m.NumClosedIssues, + Created: m.CreatedUnix.AsTime(), + Updated: m.UpdatedUnix.AsTimePtr(), + } + if m.IsClosed { + apiMilestone.Closed = m.ClosedDateUnix.AsTimePtr() + } + if m.DeadlineUnix.Year() < 9999 { + apiMilestone.Deadline = m.DeadlineUnix.AsTimePtr() + } + return apiMilestone +} + +// ToLabelTemplate converts Label to API format +func ToLabelTemplate(label *label.Label) *api.LabelTemplate { + result := &api.LabelTemplate{ + Name: label.Name, + Exclusive: label.Exclusive, + Color: strings.TrimLeft(label.Color, "#"), + Description: label.Description, + } + + return result +} + +// ToLabelTemplateList converts list of Label to API format +func ToLabelTemplateList(labels []*label.Label) []*api.LabelTemplate { + result := make([]*api.LabelTemplate, len(labels)) + for i := range labels { + result[i] = ToLabelTemplate(labels[i]) + } + return result +} diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go new file mode 100644 index 0000000..9ec9ac7 --- /dev/null +++ b/services/convert/issue_comment.go @@ -0,0 +1,187 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/log" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" +) + +// ToAPIComment converts a issues_model.Comment to the api.Comment format for API usage +func ToAPIComment(ctx context.Context, repo *repo_model.Repository, c *issues_model.Comment) *api.Comment { + return &api.Comment{ + ID: c.ID, + Poster: ToUser(ctx, c.Poster, nil), + HTMLURL: c.HTMLURL(ctx), + IssueURL: c.IssueURL(ctx), + PRURL: c.PRURL(ctx), + Body: c.Content, + Attachments: ToAPIAttachments(repo, c.Attachments), + Created: c.CreatedUnix.AsTime(), + Updated: c.UpdatedUnix.AsTime(), + } +} + +// ToTimelineComment converts a issues_model.Comment to the api.TimelineComment format +func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issues_model.Comment, doer *user_model.User) *api.TimelineComment { + err := c.LoadMilestone(ctx) + if err != nil { + log.Error("LoadMilestone: %v", err) + return nil + } + + err = c.LoadAssigneeUserAndTeam(ctx) + if err != nil { + log.Error("LoadAssigneeUserAndTeam: %v", err) + return nil + } + + err = c.LoadResolveDoer(ctx) + if err != nil { + log.Error("LoadResolveDoer: %v", err) + return nil + } + + err = c.LoadDepIssueDetails(ctx) + if err != nil { + log.Error("LoadDepIssueDetails: %v", err) + return nil + } + + err = c.LoadTime(ctx) + if err != nil { + log.Error("LoadTime: %v", err) + return nil + } + + err = c.LoadLabel(ctx) + if err != nil { + log.Error("LoadLabel: %v", err) + return nil + } + + if c.Content != "" { + if (c.Type == issues_model.CommentTypeAddTimeManual || + c.Type == issues_model.CommentTypeStopTracking || + c.Type == issues_model.CommentTypeDeleteTimeManual) && + c.Content[0] == '|' { + // TimeTracking Comments from v1.21 on store the seconds instead of an formatted string + // so we check for the "|" delimiter and convert new to legacy format on demand + c.Content = util.SecToTime(c.Content[1:]) + } + } + + comment := &api.TimelineComment{ + ID: c.ID, + Type: c.Type.String(), + Poster: ToUser(ctx, c.Poster, nil), + HTMLURL: c.HTMLURL(ctx), + IssueURL: c.IssueURL(ctx), + PRURL: c.PRURL(ctx), + Body: c.Content, + Created: c.CreatedUnix.AsTime(), + Updated: c.UpdatedUnix.AsTime(), + + OldProjectID: c.OldProjectID, + ProjectID: c.ProjectID, + + OldTitle: c.OldTitle, + NewTitle: c.NewTitle, + + OldRef: c.OldRef, + NewRef: c.NewRef, + + RefAction: c.RefAction.String(), + RefCommitSHA: c.CommitSHA, + + ReviewID: c.ReviewID, + + RemovedAssignee: c.RemovedAssignee, + } + + if c.OldMilestone != nil { + comment.OldMilestone = ToAPIMilestone(c.OldMilestone) + } + if c.Milestone != nil { + comment.Milestone = ToAPIMilestone(c.Milestone) + } + + if c.Time != nil { + err = c.Time.LoadAttributes(ctx) + if err != nil { + log.Error("Time.LoadAttributes: %v", err) + return nil + } + + comment.TrackedTime = ToTrackedTime(ctx, doer, c.Time) + } + + if c.RefIssueID != 0 { + issue, err := issues_model.GetIssueByID(ctx, c.RefIssueID) + if err != nil { + log.Error("GetIssueByID(%d): %v", c.RefIssueID, err) + return nil + } + comment.RefIssue = ToAPIIssue(ctx, doer, issue) + } + + if c.RefCommentID != 0 { + com, err := issues_model.GetCommentByID(ctx, c.RefCommentID) + if err != nil { + log.Error("GetCommentByID(%d): %v", c.RefCommentID, err) + return nil + } + err = com.LoadPoster(ctx) + if err != nil { + log.Error("LoadPoster: %v", err) + return nil + } + comment.RefComment = ToAPIComment(ctx, repo, com) + } + + if c.Label != nil { + var org *user_model.User + var repo *repo_model.Repository + if c.Label.BelongsToOrg() { + var err error + org, err = user_model.GetUserByID(ctx, c.Label.OrgID) + if err != nil { + log.Error("GetUserByID(%d): %v", c.Label.OrgID, err) + return nil + } + } + if c.Label.BelongsToRepo() { + var err error + repo, err = repo_model.GetRepositoryByID(ctx, c.Label.RepoID) + if err != nil { + log.Error("GetRepositoryByID(%d): %v", c.Label.RepoID, err) + return nil + } + } + comment.Label = ToLabel(c.Label, repo, org) + } + + if c.Assignee != nil { + comment.Assignee = ToUser(ctx, c.Assignee, nil) + } + if c.AssigneeTeam != nil { + comment.AssigneeTeam, _ = ToTeam(ctx, c.AssigneeTeam) + } + + if c.ResolveDoer != nil { + comment.ResolveDoer = ToUser(ctx, c.ResolveDoer, nil) + } + + if c.DependentIssue != nil { + comment.DependentIssue = ToAPIIssue(ctx, doer, c.DependentIssue) + } + + return comment +} diff --git a/services/convert/issue_test.go b/services/convert/issue_test.go new file mode 100644 index 0000000..0aeb3e5 --- /dev/null +++ b/services/convert/issue_test.go @@ -0,0 +1,58 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "fmt" + "testing" + "time" + + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLabel_ToLabel(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: label.RepoID}) + assert.Equal(t, &api.Label{ + ID: label.ID, + Name: label.Name, + Color: "abcdef", + URL: fmt.Sprintf("%sapi/v1/repos/user2/repo1/labels/%d", setting.AppURL, label.ID), + }, ToLabel(label, repo, nil)) +} + +func TestMilestone_APIFormat(t *testing.T) { + milestone := &issues_model.Milestone{ + ID: 3, + RepoID: 4, + Name: "milestoneName", + Content: "milestoneContent", + IsClosed: false, + NumOpenIssues: 5, + NumClosedIssues: 6, + CreatedUnix: timeutil.TimeStamp(time.Date(1999, time.January, 1, 0, 0, 0, 0, time.UTC).Unix()), + UpdatedUnix: timeutil.TimeStamp(time.Date(1999, time.March, 1, 0, 0, 0, 0, time.UTC).Unix()), + DeadlineUnix: timeutil.TimeStamp(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC).Unix()), + } + assert.Equal(t, api.Milestone{ + ID: milestone.ID, + State: api.StateOpen, + Title: milestone.Name, + Description: milestone.Content, + OpenIssues: milestone.NumOpenIssues, + ClosedIssues: milestone.NumClosedIssues, + Created: milestone.CreatedUnix.AsTime(), + Updated: milestone.UpdatedUnix.AsTimePtr(), + Deadline: milestone.DeadlineUnix.AsTimePtr(), + }, *ToAPIMilestone(milestone)) +} diff --git a/services/convert/main_test.go b/services/convert/main_test.go new file mode 100644 index 0000000..363cc4a --- /dev/null +++ b/services/convert/main_test.go @@ -0,0 +1,16 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" + + _ "code.gitea.io/gitea/models/actions" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} diff --git a/services/convert/mirror.go b/services/convert/mirror.go new file mode 100644 index 0000000..85e0d1c --- /dev/null +++ b/services/convert/mirror.go @@ -0,0 +1,27 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + + repo_model "code.gitea.io/gitea/models/repo" + api "code.gitea.io/gitea/modules/structs" +) + +// ToPushMirror convert from repo_model.PushMirror and remoteAddress to api.TopicResponse +func ToPushMirror(ctx context.Context, pm *repo_model.PushMirror) (*api.PushMirror, error) { + repo := pm.GetRepository(ctx) + return &api.PushMirror{ + RepoName: repo.Name, + RemoteName: pm.RemoteName, + RemoteAddress: pm.RemoteAddress, + CreatedUnix: pm.CreatedUnix.AsTime(), + LastUpdateUnix: pm.LastUpdateUnix.AsTimePtr(), + LastError: pm.LastError, + Interval: pm.Interval.String(), + SyncOnCommit: pm.SyncOnCommit, + PublicKey: pm.GetPublicKey(), + }, nil +} diff --git a/services/convert/notification.go b/services/convert/notification.go new file mode 100644 index 0000000..41063cf --- /dev/null +++ b/services/convert/notification.go @@ -0,0 +1,98 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + "net/url" + + activities_model "code.gitea.io/gitea/models/activities" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + api "code.gitea.io/gitea/modules/structs" +) + +// ToNotificationThread convert a Notification to api.NotificationThread +func ToNotificationThread(ctx context.Context, n *activities_model.Notification) *api.NotificationThread { + result := &api.NotificationThread{ + ID: n.ID, + Unread: !(n.Status == activities_model.NotificationStatusRead || n.Status == activities_model.NotificationStatusPinned), + Pinned: n.Status == activities_model.NotificationStatusPinned, + UpdatedAt: n.UpdatedUnix.AsTime(), + URL: n.APIURL(), + } + + // since user only get notifications when he has access to use minimal access mode + if n.Repository != nil { + result.Repository = ToRepo(ctx, n.Repository, access_model.Permission{AccessMode: perm.AccessModeRead}) + + // This permission is not correct and we should not be reporting it + for repository := result.Repository; repository != nil; repository = repository.Parent { + repository.Permissions = nil + } + } + + // handle Subject + switch n.Source { + case activities_model.NotificationSourceIssue: + result.Subject = &api.NotificationSubject{Type: api.NotifySubjectIssue} + if n.Issue != nil { + result.Subject.Title = n.Issue.Title + result.Subject.URL = n.Issue.APIURL(ctx) + result.Subject.HTMLURL = n.Issue.HTMLURL() + result.Subject.State = n.Issue.State() + comment, err := n.Issue.GetLastComment(ctx) + if err == nil && comment != nil { + result.Subject.LatestCommentURL = comment.APIURL(ctx) + result.Subject.LatestCommentHTMLURL = comment.HTMLURL(ctx) + } + } + case activities_model.NotificationSourcePullRequest: + result.Subject = &api.NotificationSubject{Type: api.NotifySubjectPull} + if n.Issue != nil { + result.Subject.Title = n.Issue.Title + result.Subject.URL = n.Issue.APIURL(ctx) + result.Subject.HTMLURL = n.Issue.HTMLURL() + result.Subject.State = n.Issue.State() + comment, err := n.Issue.GetLastComment(ctx) + if err == nil && comment != nil { + result.Subject.LatestCommentURL = comment.APIURL(ctx) + result.Subject.LatestCommentHTMLURL = comment.HTMLURL(ctx) + } + + if err := n.Issue.LoadPullRequest(ctx); err == nil && + n.Issue.PullRequest != nil && + n.Issue.PullRequest.HasMerged { + result.Subject.State = "merged" + } + } + case activities_model.NotificationSourceCommit: + url := n.Repository.HTMLURL() + "/commit/" + url.PathEscape(n.CommitID) + result.Subject = &api.NotificationSubject{ + Type: api.NotifySubjectCommit, + Title: n.CommitID, + URL: url, + HTMLURL: url, + } + case activities_model.NotificationSourceRepository: + result.Subject = &api.NotificationSubject{ + Type: api.NotifySubjectRepository, + Title: n.Repository.FullName(), + // FIXME: this is a relative URL, rather useless and inconsistent, but keeping for backwards compat + URL: n.Repository.Link(), + HTMLURL: n.Repository.HTMLURL(), + } + } + + return result +} + +// ToNotifications convert list of Notification to api.NotificationThread list +func ToNotifications(ctx context.Context, nl activities_model.NotificationList) []*api.NotificationThread { + result := make([]*api.NotificationThread, 0, len(nl)) + for _, n := range nl { + result = append(result, ToNotificationThread(ctx, n)) + } + return result +} diff --git a/services/convert/package.go b/services/convert/package.go new file mode 100644 index 0000000..b5fca21 --- /dev/null +++ b/services/convert/package.go @@ -0,0 +1,53 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + + "code.gitea.io/gitea/models/packages" + access_model "code.gitea.io/gitea/models/perm/access" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" +) + +// ToPackage convert a packages.PackageDescriptor to api.Package +func ToPackage(ctx context.Context, pd *packages.PackageDescriptor, doer *user_model.User) (*api.Package, error) { + var repo *api.Repository + if pd.Repository != nil { + permission, err := access_model.GetUserRepoPermission(ctx, pd.Repository, doer) + if err != nil { + return nil, err + } + + if permission.HasAccess() { + repo = ToRepo(ctx, pd.Repository, permission) + } + } + + return &api.Package{ + ID: pd.Version.ID, + Owner: ToUser(ctx, pd.Owner, doer), + Repository: repo, + Creator: ToUser(ctx, pd.Creator, doer), + Type: string(pd.Package.Type), + Name: pd.Package.Name, + Version: pd.Version.Version, + CreatedAt: pd.Version.CreatedUnix.AsTime(), + HTMLURL: pd.VersionHTMLURL(), + }, nil +} + +// ToPackageFile converts packages.PackageFileDescriptor to api.PackageFile +func ToPackageFile(pfd *packages.PackageFileDescriptor) *api.PackageFile { + return &api.PackageFile{ + ID: pfd.File.ID, + Size: pfd.Blob.Size, + Name: pfd.File.Name, + HashMD5: pfd.Blob.HashMD5, + HashSHA1: pfd.Blob.HashSHA1, + HashSHA256: pfd.Blob.HashSHA256, + HashSHA512: pfd.Blob.HashSHA512, + } +} diff --git a/services/convert/pull.go b/services/convert/pull.go new file mode 100644 index 0000000..4ec24a8 --- /dev/null +++ b/services/convert/pull.go @@ -0,0 +1,261 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + "fmt" + + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/log" + api "code.gitea.io/gitea/modules/structs" +) + +// ToAPIPullRequest assumes following fields have been assigned with valid values: +// Required - Issue +// Optional - Merger +func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) *api.PullRequest { + var ( + baseBranch *git.Branch + headBranch *git.Branch + baseCommit *git.Commit + err error + ) + + if err = pr.Issue.LoadRepo(ctx); err != nil { + log.Error("pr.Issue.LoadRepo[%d]: %v", pr.ID, err) + return nil + } + + apiIssue := ToAPIIssue(ctx, doer, pr.Issue) + if err := pr.LoadBaseRepo(ctx); err != nil { + log.Error("GetRepositoryById[%d]: %v", pr.ID, err) + return nil + } + + if err := pr.LoadHeadRepo(ctx); err != nil { + log.Error("GetRepositoryById[%d]: %v", pr.ID, err) + return nil + } + + var doerID int64 + if doer != nil { + doerID = doer.ID + } + + const repoDoerPermCacheKey = "repo_doer_perm_cache" + p, err := cache.GetWithContextCache(ctx, repoDoerPermCacheKey, fmt.Sprintf("%d_%d", pr.BaseRepoID, doerID), + func() (access_model.Permission, error) { + return access_model.GetUserRepoPermission(ctx, pr.BaseRepo, doer) + }) + if err != nil { + log.Error("GetUserRepoPermission[%d]: %v", pr.BaseRepoID, err) + p.AccessMode = perm.AccessModeNone + } + + apiPullRequest := &api.PullRequest{ + ID: pr.ID, + URL: pr.Issue.HTMLURL(), + Index: pr.Index, + Poster: apiIssue.Poster, + Title: apiIssue.Title, + Body: apiIssue.Body, + Labels: apiIssue.Labels, + Milestone: apiIssue.Milestone, + Assignee: apiIssue.Assignee, + Assignees: apiIssue.Assignees, + State: apiIssue.State, + Draft: pr.IsWorkInProgress(ctx), + IsLocked: apiIssue.IsLocked, + Comments: apiIssue.Comments, + ReviewComments: pr.GetReviewCommentsCount(ctx), + HTMLURL: pr.Issue.HTMLURL(), + DiffURL: pr.Issue.DiffURL(), + PatchURL: pr.Issue.PatchURL(), + HasMerged: pr.HasMerged, + MergeBase: pr.MergeBase, + Mergeable: pr.Mergeable(ctx), + Deadline: apiIssue.Deadline, + Created: pr.Issue.CreatedUnix.AsTimePtr(), + Updated: pr.Issue.UpdatedUnix.AsTimePtr(), + PinOrder: apiIssue.PinOrder, + + AllowMaintainerEdit: pr.AllowMaintainerEdit, + + Base: &api.PRBranchInfo{ + Name: pr.BaseBranch, + Ref: pr.BaseBranch, + RepoID: pr.BaseRepoID, + Repository: ToRepo(ctx, pr.BaseRepo, p), + }, + Head: &api.PRBranchInfo{ + Name: pr.HeadBranch, + Ref: fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index), + RepoID: -1, + }, + } + + if err = pr.LoadRequestedReviewers(ctx); err != nil { + log.Error("LoadRequestedReviewers[%d]: %v", pr.ID, err) + return nil + } + if err = pr.LoadRequestedReviewersTeams(ctx); err != nil { + log.Error("LoadRequestedReviewersTeams[%d]: %v", pr.ID, err) + return nil + } + + for _, reviewer := range pr.RequestedReviewers { + apiPullRequest.RequestedReviewers = append(apiPullRequest.RequestedReviewers, ToUser(ctx, reviewer, nil)) + } + + for _, reviewerTeam := range pr.RequestedReviewersTeams { + convertedTeam, err := ToTeam(ctx, reviewerTeam, true) + if err != nil { + log.Error("LoadRequestedReviewersTeams[%d]: %v", pr.ID, err) + return nil + } + + apiPullRequest.RequestedReviewersTeams = append(apiPullRequest.RequestedReviewersTeams, convertedTeam) + } + + if pr.Issue.ClosedUnix != 0 { + apiPullRequest.Closed = pr.Issue.ClosedUnix.AsTimePtr() + } + + gitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo) + if err != nil { + log.Error("OpenRepository[%s]: %v", pr.BaseRepo.RepoPath(), err) + return nil + } + defer gitRepo.Close() + + baseBranch, err = gitRepo.GetBranch(pr.BaseBranch) + if err != nil && !git.IsErrBranchNotExist(err) { + log.Error("GetBranch[%s]: %v", pr.BaseBranch, err) + return nil + } + + if err == nil { + baseCommit, err = baseBranch.GetCommit() + if err != nil && !git.IsErrNotExist(err) { + log.Error("GetCommit[%s]: %v", baseBranch.Name, err) + return nil + } + + if err == nil { + apiPullRequest.Base.Sha = baseCommit.ID.String() + } + } + + if pr.Flow == issues_model.PullRequestFlowAGit { + gitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo) + if err != nil { + log.Error("OpenRepository[%s]: %v", pr.GetGitRefName(), err) + return nil + } + defer gitRepo.Close() + + apiPullRequest.Head.Sha, err = gitRepo.GetRefCommitID(pr.GetGitRefName()) + if err != nil { + log.Error("GetRefCommitID[%s]: %v", pr.GetGitRefName(), err) + return nil + } + apiPullRequest.Head.RepoID = pr.BaseRepoID + apiPullRequest.Head.Repository = apiPullRequest.Base.Repository + apiPullRequest.Head.Name = "" + } + + if pr.HeadRepo != nil && pr.Flow == issues_model.PullRequestFlowGithub { + p, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer) + if err != nil { + log.Error("GetUserRepoPermission[%d]: %v", pr.HeadRepoID, err) + p.AccessMode = perm.AccessModeNone + } + + apiPullRequest.Head.RepoID = pr.HeadRepo.ID + apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p) + + headGitRepo, err := gitrepo.OpenRepository(ctx, pr.HeadRepo) + if err != nil { + log.Error("OpenRepository[%s]: %v", pr.HeadRepo.RepoPath(), err) + return nil + } + defer headGitRepo.Close() + + headBranch, err = headGitRepo.GetBranch(pr.HeadBranch) + if err != nil && !git.IsErrBranchNotExist(err) { + log.Error("GetBranch[%s]: %v", pr.HeadBranch, err) + return nil + } + + // Outer scope variables to be used in diff calculation + var ( + startCommitID string + endCommitID string + ) + + if git.IsErrBranchNotExist(err) { + headCommitID, err := headGitRepo.GetRefCommitID(apiPullRequest.Head.Ref) + if err != nil && !git.IsErrNotExist(err) { + log.Error("GetCommit[%s]: %v", pr.HeadBranch, err) + return nil + } + if err == nil { + apiPullRequest.Head.Sha = headCommitID + endCommitID = headCommitID + } + } else { + commit, err := headBranch.GetCommit() + if err != nil && !git.IsErrNotExist(err) { + log.Error("GetCommit[%s]: %v", headBranch.Name, err) + return nil + } + if err == nil { + apiPullRequest.Head.Ref = pr.HeadBranch + apiPullRequest.Head.Sha = commit.ID.String() + endCommitID = commit.ID.String() + } + } + + // Calculate diff + startCommitID = pr.MergeBase + + apiPullRequest.ChangedFiles, apiPullRequest.Additions, apiPullRequest.Deletions, err = gitRepo.GetDiffShortStat(startCommitID, endCommitID) + if err != nil { + log.Error("GetDiffShortStat: %v", err) + } + } + + if len(apiPullRequest.Head.Sha) == 0 && len(apiPullRequest.Head.Ref) != 0 { + baseGitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo) + if err != nil { + log.Error("OpenRepository[%s]: %v", pr.BaseRepo.RepoPath(), err) + return nil + } + defer baseGitRepo.Close() + refs, err := baseGitRepo.GetRefsFiltered(apiPullRequest.Head.Ref) + if err != nil { + log.Error("GetRefsFiltered[%s]: %v", apiPullRequest.Head.Ref, err) + return nil + } else if len(refs) == 0 { + log.Error("unable to resolve PR head ref") + } else { + apiPullRequest.Head.Sha = refs[0].Object.String() + } + } + + if pr.HasMerged { + apiPullRequest.Merged = pr.MergedUnix.AsTimePtr() + apiPullRequest.MergedCommitID = &pr.MergedCommitID + apiPullRequest.MergedBy = ToUser(ctx, pr.Merger, nil) + } + + return apiPullRequest +} diff --git a/services/convert/pull_review.go b/services/convert/pull_review.go new file mode 100644 index 0000000..f7990e7 --- /dev/null +++ b/services/convert/pull_review.go @@ -0,0 +1,139 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + "strings" + + issues_model "code.gitea.io/gitea/models/issues" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" +) + +// ToPullReview convert a review to api format +func ToPullReview(ctx context.Context, r *issues_model.Review, doer *user_model.User) (*api.PullReview, error) { + if err := r.LoadAttributes(ctx); err != nil { + if !user_model.IsErrUserNotExist(err) { + return nil, err + } + r.Reviewer = user_model.NewGhostUser() + } + + result := &api.PullReview{ + ID: r.ID, + Reviewer: ToUser(ctx, r.Reviewer, doer), + State: api.ReviewStateUnknown, + Body: r.Content, + CommitID: r.CommitID, + Stale: r.Stale, + Official: r.Official, + Dismissed: r.Dismissed, + CodeCommentsCount: r.GetCodeCommentsCount(ctx), + Submitted: r.CreatedUnix.AsTime(), + Updated: r.UpdatedUnix.AsTime(), + HTMLURL: r.HTMLURL(ctx), + HTMLPullURL: r.Issue.HTMLURL(), + } + + if r.ReviewerTeam != nil { + var err error + result.ReviewerTeam, err = ToTeam(ctx, r.ReviewerTeam) + if err != nil { + return nil, err + } + } + + switch r.Type { + case issues_model.ReviewTypeApprove: + result.State = api.ReviewStateApproved + case issues_model.ReviewTypeReject: + result.State = api.ReviewStateRequestChanges + case issues_model.ReviewTypeComment: + result.State = api.ReviewStateComment + case issues_model.ReviewTypePending: + result.State = api.ReviewStatePending + case issues_model.ReviewTypeRequest: + result.State = api.ReviewStateRequestReview + } + + return result, nil +} + +// ToPullReviewList convert a list of review to it's api format +func ToPullReviewList(ctx context.Context, rl []*issues_model.Review, doer *user_model.User) ([]*api.PullReview, error) { + result := make([]*api.PullReview, 0, len(rl)) + for i := range rl { + // show pending reviews only for the user who created them + if rl[i].Type == issues_model.ReviewTypePending && (doer == nil || !(doer.IsAdmin || doer.ID == rl[i].ReviewerID)) { + continue + } + r, err := ToPullReview(ctx, rl[i], doer) + if err != nil { + return nil, err + } + result = append(result, r) + } + return result, nil +} + +// ToPullReviewCommentList convert the CodeComments of an review to it's api format +func ToPullReviewComment(ctx context.Context, review *issues_model.Review, comment *issues_model.Comment, doer *user_model.User) (*api.PullReviewComment, error) { + apiComment := &api.PullReviewComment{ + ID: comment.ID, + Body: comment.Content, + Poster: ToUser(ctx, comment.Poster, doer), + Resolver: ToUser(ctx, comment.ResolveDoer, doer), + ReviewID: review.ID, + Created: comment.CreatedUnix.AsTime(), + Updated: comment.UpdatedUnix.AsTime(), + Path: comment.TreePath, + CommitID: comment.CommitSHA, + OrigCommitID: comment.OldRef, + DiffHunk: patch2diff(comment.Patch), + HTMLURL: comment.HTMLURL(ctx), + HTMLPullURL: review.Issue.HTMLURL(), + } + + if comment.Line < 0 { + apiComment.OldLineNum = comment.UnsignedLine() + } else { + apiComment.LineNum = comment.UnsignedLine() + } + + return apiComment, nil +} + +// ToPullReviewCommentList convert the CodeComments of an review to it's api format +func ToPullReviewCommentList(ctx context.Context, review *issues_model.Review, doer *user_model.User) ([]*api.PullReviewComment, error) { + if err := review.LoadAttributes(ctx); err != nil { + if !user_model.IsErrUserNotExist(err) { + return nil, err + } + review.Reviewer = user_model.NewGhostUser() + } + + apiComments := make([]*api.PullReviewComment, 0, len(review.CodeComments)) + + for _, lines := range review.CodeComments { + for _, comments := range lines { + for _, comment := range comments { + apiComment, err := ToPullReviewComment(ctx, review, comment, doer) + if err != nil { + return nil, err + } + apiComments = append(apiComments, apiComment) + } + } + } + return apiComments, nil +} + +func patch2diff(patch string) string { + split := strings.Split(patch, "\n@@") + if len(split) == 2 { + return "@@" + split[1] + } + return "" +} diff --git a/services/convert/pull_test.go b/services/convert/pull_test.go new file mode 100644 index 0000000..1339ed5 --- /dev/null +++ b/services/convert/pull_test.go @@ -0,0 +1,78 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPullRequest_APIFormat(t *testing.T) { + // with HeadRepo + require.NoError(t, unittest.PrepareTestDatabase()) + headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) + require.NoError(t, pr.LoadAttributes(db.DefaultContext)) + require.NoError(t, pr.LoadIssue(db.DefaultContext)) + apiPullRequest := ToAPIPullRequest(git.DefaultContext, pr, nil) + assert.NotNil(t, apiPullRequest) + assert.EqualValues(t, &structs.PRBranchInfo{ + Name: "branch1", + Ref: "refs/pull/2/head", + Sha: "4a357436d925b5c974181ff12a994538ddc5a269", + RepoID: 1, + Repository: ToRepo(db.DefaultContext, headRepo, access_model.Permission{AccessMode: perm.AccessModeRead}), + }, apiPullRequest.Head) + + // withOut HeadRepo + pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) + require.NoError(t, pr.LoadIssue(db.DefaultContext)) + require.NoError(t, pr.LoadAttributes(db.DefaultContext)) + // simulate fork deletion + pr.HeadRepo = nil + pr.HeadRepoID = 100000 + apiPullRequest = ToAPIPullRequest(git.DefaultContext, pr, nil) + assert.NotNil(t, apiPullRequest) + assert.Nil(t, apiPullRequest.Head.Repository) + assert.EqualValues(t, -1, apiPullRequest.Head.RepoID) +} + +func TestPullReviewList(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("Pending review", func(t *testing.T) { + reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 6, ReviewerID: reviewer.ID}) + rl := []*issues_model.Review{review} + + t.Run("Anonymous", func(t *testing.T) { + prList, err := ToPullReviewList(db.DefaultContext, rl, nil) + require.NoError(t, err) + assert.Empty(t, prList) + }) + t.Run("Reviewer", func(t *testing.T) { + prList, err := ToPullReviewList(db.DefaultContext, rl, reviewer) + require.NoError(t, err) + assert.Len(t, prList, 1) + }) + t.Run("Admin", func(t *testing.T) { + adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}, unittest.Cond("id != ?", reviewer.ID)) + prList, err := ToPullReviewList(db.DefaultContext, rl, adminUser) + require.NoError(t, err) + assert.Len(t, prList, 1) + }) + }) +} diff --git a/services/convert/quota.go b/services/convert/quota.go new file mode 100644 index 0000000..791cd8e --- /dev/null +++ b/services/convert/quota.go @@ -0,0 +1,185 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + "strconv" + + action_model "code.gitea.io/gitea/models/actions" + issue_model "code.gitea.io/gitea/models/issues" + package_model "code.gitea.io/gitea/models/packages" + quota_model "code.gitea.io/gitea/models/quota" + repo_model "code.gitea.io/gitea/models/repo" + api "code.gitea.io/gitea/modules/structs" +) + +func ToQuotaRuleInfo(rule quota_model.Rule, withName bool) api.QuotaRuleInfo { + info := api.QuotaRuleInfo{ + Limit: rule.Limit, + Subjects: make([]string, len(rule.Subjects)), + } + for i := range len(rule.Subjects) { + info.Subjects[i] = rule.Subjects[i].String() + } + + if withName { + info.Name = rule.Name + } + + return info +} + +func toQuotaInfoUsed(used *quota_model.Used) api.QuotaUsed { + info := api.QuotaUsed{ + Size: api.QuotaUsedSize{ + Repos: api.QuotaUsedSizeRepos{ + Public: used.Size.Repos.Public, + Private: used.Size.Repos.Private, + }, + Git: api.QuotaUsedSizeGit{ + LFS: used.Size.Git.LFS, + }, + Assets: api.QuotaUsedSizeAssets{ + Attachments: api.QuotaUsedSizeAssetsAttachments{ + Issues: used.Size.Assets.Attachments.Issues, + Releases: used.Size.Assets.Attachments.Releases, + }, + Artifacts: used.Size.Assets.Artifacts, + Packages: api.QuotaUsedSizeAssetsPackages{ + All: used.Size.Assets.Packages.All, + }, + }, + }, + } + return info +} + +func ToQuotaInfo(used *quota_model.Used, groups quota_model.GroupList, withNames bool) api.QuotaInfo { + info := api.QuotaInfo{ + Used: toQuotaInfoUsed(used), + Groups: ToQuotaGroupList(groups, withNames), + } + + return info +} + +func ToQuotaGroup(group quota_model.Group, withNames bool) api.QuotaGroup { + info := api.QuotaGroup{ + Rules: make([]api.QuotaRuleInfo, len(group.Rules)), + } + if withNames { + info.Name = group.Name + } + for i := range len(group.Rules) { + info.Rules[i] = ToQuotaRuleInfo(group.Rules[i], withNames) + } + + return info +} + +func ToQuotaGroupList(groups quota_model.GroupList, withNames bool) api.QuotaGroupList { + list := make(api.QuotaGroupList, len(groups)) + + for i := range len(groups) { + list[i] = ToQuotaGroup(*groups[i], withNames) + } + + return list +} + +func ToQuotaUsedAttachmentList(ctx context.Context, attachments []*repo_model.Attachment) (*api.QuotaUsedAttachmentList, error) { + getAttachmentContainer := func(a *repo_model.Attachment) (string, string, error) { + if a.ReleaseID != 0 { + release, err := repo_model.GetReleaseByID(ctx, a.ReleaseID) + if err != nil { + return "", "", err + } + if err = release.LoadAttributes(ctx); err != nil { + return "", "", err + } + return release.APIURL(), release.HTMLURL(), nil + } + if a.CommentID != 0 { + comment, err := issue_model.GetCommentByID(ctx, a.CommentID) + if err != nil { + return "", "", err + } + return comment.APIURL(ctx), comment.HTMLURL(ctx), nil + } + if a.IssueID != 0 { + issue, err := issue_model.GetIssueByID(ctx, a.IssueID) + if err != nil { + return "", "", err + } + if err = issue.LoadRepo(ctx); err != nil { + return "", "", err + } + return issue.APIURL(ctx), issue.HTMLURL(), nil + } + return "", "", nil + } + + result := make(api.QuotaUsedAttachmentList, len(attachments)) + for i, a := range attachments { + capiURL, chtmlURL, err := getAttachmentContainer(a) + if err != nil { + return nil, err + } + + apiURL := capiURL + "/assets/" + strconv.FormatInt(a.ID, 10) + result[i] = &api.QuotaUsedAttachment{ + Name: a.Name, + Size: a.Size, + APIURL: apiURL, + } + result[i].ContainedIn.APIURL = capiURL + result[i].ContainedIn.HTMLURL = chtmlURL + } + + return &result, nil +} + +func ToQuotaUsedPackageList(ctx context.Context, packages []*package_model.PackageVersion) (*api.QuotaUsedPackageList, error) { + result := make(api.QuotaUsedPackageList, len(packages)) + for i, pv := range packages { + d, err := package_model.GetPackageDescriptor(ctx, pv) + if err != nil { + return nil, err + } + + var size int64 + for _, file := range d.Files { + size += file.Blob.Size + } + + result[i] = &api.QuotaUsedPackage{ + Name: d.Package.Name, + Type: d.Package.Type.Name(), + Version: d.Version.Version, + Size: size, + HTMLURL: d.VersionHTMLURL(), + } + } + + return &result, nil +} + +func ToQuotaUsedArtifactList(ctx context.Context, artifacts []*action_model.ActionArtifact) (*api.QuotaUsedArtifactList, error) { + result := make(api.QuotaUsedArtifactList, len(artifacts)) + for i, a := range artifacts { + run, err := action_model.GetRunByID(ctx, a.RunID) + if err != nil { + return nil, err + } + + result[i] = &api.QuotaUsedArtifact{ + Name: a.ArtifactName, + Size: a.FileCompressedSize, + HTMLURL: run.HTMLURL(), + } + } + + return &result, nil +} diff --git a/services/convert/release.go b/services/convert/release.go new file mode 100644 index 0000000..8c0f61b --- /dev/null +++ b/services/convert/release.go @@ -0,0 +1,35 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + + repo_model "code.gitea.io/gitea/models/repo" + api "code.gitea.io/gitea/modules/structs" +) + +// ToAPIRelease convert a repo_model.Release to api.Release +func ToAPIRelease(ctx context.Context, repo *repo_model.Repository, r *repo_model.Release) *api.Release { + return &api.Release{ + ID: r.ID, + TagName: r.TagName, + Target: r.Target, + Title: r.Title, + Note: r.Note, + URL: r.APIURL(), + HTMLURL: r.HTMLURL(), + TarURL: r.TarURL(), + ZipURL: r.ZipURL(), + HideArchiveLinks: r.HideArchiveLinks, + UploadURL: r.APIUploadURL(), + IsDraft: r.IsDraft, + IsPrerelease: r.IsPrerelease, + CreatedAt: r.CreatedUnix.AsTime(), + PublishedAt: r.CreatedUnix.AsTime(), + Publisher: ToUser(ctx, r.Publisher, nil), + Attachments: ToAPIAttachments(repo, r.Attachments), + ArchiveDownloadCount: r.ArchiveDownloadCount, + } +} diff --git a/services/convert/release_test.go b/services/convert/release_test.go new file mode 100644 index 0000000..2e40bb9 --- /dev/null +++ b/services/convert/release_test.go @@ -0,0 +1,29 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRelease_ToRelease(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + release1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 1}) + release1.LoadAttributes(db.DefaultContext) + + apiRelease := ToAPIRelease(db.DefaultContext, repo1, release1) + assert.NotNil(t, apiRelease) + assert.EqualValues(t, 1, apiRelease.ID) + assert.EqualValues(t, "https://try.gitea.io/api/v1/repos/user2/repo1/releases/1", apiRelease.URL) + assert.EqualValues(t, "https://try.gitea.io/api/v1/repos/user2/repo1/releases/1/assets", apiRelease.UploadURL) +} diff --git a/services/convert/repository.go b/services/convert/repository.go new file mode 100644 index 0000000..2fb6f6d --- /dev/null +++ b/services/convert/repository.go @@ -0,0 +1,254 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + "time" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/log" + api "code.gitea.io/gitea/modules/structs" +) + +// ToRepo converts a Repository to api.Repository +func ToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission) *api.Repository { + return innerToRepo(ctx, repo, permissionInRepo, false) +} + +func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInRepo access_model.Permission, isParent bool) *api.Repository { + var parent *api.Repository + + if permissionInRepo.Units == nil && permissionInRepo.UnitsMode == nil { + // If Units and UnitsMode are both nil, it means that it's a hard coded permission, + // like access_model.Permission{AccessMode: perm.AccessModeAdmin}. + // So we need to load units for the repo, or UnitAccessMode will always return perm.AccessModeNone. + _ = repo.LoadUnits(ctx) // the error is not important, so ignore it + permissionInRepo.Units = repo.Units + } + + cloneLink := repo.CloneLink() + permission := &api.Permission{ + Admin: permissionInRepo.AccessMode >= perm.AccessModeAdmin, + Push: permissionInRepo.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeWrite, + Pull: permissionInRepo.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeRead, + } + if !isParent { + err := repo.GetBaseRepo(ctx) + if err != nil { + return nil + } + if repo.BaseRepo != nil { + // FIXME: The permission of the parent repo is not correct. + // It's the permission of the current repo, so it's probably different from the parent repo. + // But there isn't a good way to get the permission of the parent repo, because the doer is not passed in. + // Use the permission of the current repo to keep the behavior consistent with the old API. + // Maybe the right way is setting the permission of the parent repo to nil, empty is better than wrong. + parent = innerToRepo(ctx, repo.BaseRepo, permissionInRepo, true) + } + } + + // check enabled/disabled units + hasIssues := false + var externalTracker *api.ExternalTracker + var internalTracker *api.InternalTracker + if unit, err := repo.GetUnit(ctx, unit_model.TypeIssues); err == nil { + config := unit.IssuesConfig() + hasIssues = true + internalTracker = &api.InternalTracker{ + EnableTimeTracker: config.EnableTimetracker, + AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime, + EnableIssueDependencies: config.EnableDependencies, + } + } else if unit, err := repo.GetUnit(ctx, unit_model.TypeExternalTracker); err == nil { + config := unit.ExternalTrackerConfig() + hasIssues = true + externalTracker = &api.ExternalTracker{ + ExternalTrackerURL: config.ExternalTrackerURL, + ExternalTrackerFormat: config.ExternalTrackerFormat, + ExternalTrackerStyle: config.ExternalTrackerStyle, + ExternalTrackerRegexpPattern: config.ExternalTrackerRegexpPattern, + } + } + hasWiki := false + globallyEditableWiki := false + var externalWiki *api.ExternalWiki + if wikiUnit, err := repo.GetUnit(ctx, unit_model.TypeWiki); err == nil { + hasWiki = true + if wikiUnit.DefaultPermissions == repo_model.UnitAccessModeWrite { + globallyEditableWiki = true + } + } else if unit, err := repo.GetUnit(ctx, unit_model.TypeExternalWiki); err == nil { + hasWiki = true + config := unit.ExternalWikiConfig() + externalWiki = &api.ExternalWiki{ + ExternalWikiURL: config.ExternalWikiURL, + } + } + hasPullRequests := false + ignoreWhitespaceConflicts := false + allowMerge := false + allowRebase := false + allowRebaseMerge := false + allowSquash := false + allowFastForwardOnly := false + allowRebaseUpdate := false + defaultDeleteBranchAfterMerge := false + defaultMergeStyle := repo_model.MergeStyleMerge + defaultAllowMaintainerEdit := false + if unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests); err == nil { + config := unit.PullRequestsConfig() + hasPullRequests = true + ignoreWhitespaceConflicts = config.IgnoreWhitespaceConflicts + allowMerge = config.AllowMerge + allowRebase = config.AllowRebase + allowRebaseMerge = config.AllowRebaseMerge + allowSquash = config.AllowSquash + allowFastForwardOnly = config.AllowFastForwardOnly + allowRebaseUpdate = config.AllowRebaseUpdate + defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge + defaultMergeStyle = config.GetDefaultMergeStyle() + defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit + } + hasProjects := false + if _, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil { + hasProjects = true + } + + hasReleases := false + if _, err := repo.GetUnit(ctx, unit_model.TypeReleases); err == nil { + hasReleases = true + } + + hasPackages := false + if _, err := repo.GetUnit(ctx, unit_model.TypePackages); err == nil { + hasPackages = true + } + + hasActions := false + if _, err := repo.GetUnit(ctx, unit_model.TypeActions); err == nil { + hasActions = true + } + + if err := repo.LoadOwner(ctx); err != nil { + return nil + } + + numReleases, _ := db.Count[repo_model.Release](ctx, repo_model.FindReleasesOptions{ + IncludeDrafts: false, + IncludeTags: false, + RepoID: repo.ID, + }) + + mirrorInterval := "" + var mirrorUpdated time.Time + if repo.IsMirror { + pullMirror, err := repo_model.GetMirrorByRepoID(ctx, repo.ID) + if err == nil { + mirrorInterval = pullMirror.Interval.String() + mirrorUpdated = pullMirror.UpdatedUnix.AsTime() + } + } + + var transfer *api.RepoTransfer + if repo.Status == repo_model.RepositoryPendingTransfer { + t, err := models.GetPendingRepositoryTransfer(ctx, repo) + if err != nil && !models.IsErrNoPendingTransfer(err) { + log.Warn("GetPendingRepositoryTransfer: %v", err) + } else { + if err := t.LoadAttributes(ctx); err != nil { + log.Warn("LoadAttributes of RepoTransfer: %v", err) + } else { + transfer = ToRepoTransfer(ctx, t) + } + } + } + + var language string + if repo.PrimaryLanguage != nil { + language = repo.PrimaryLanguage.Language + } + + repoAPIURL := repo.APIURL() + + return &api.Repository{ + ID: repo.ID, + Owner: ToUserWithAccessMode(ctx, repo.Owner, permissionInRepo.AccessMode), + Name: repo.Name, + FullName: repo.FullName(), + Description: repo.Description, + Private: repo.IsPrivate, + Template: repo.IsTemplate, + Empty: repo.IsEmpty, + Archived: repo.IsArchived, + Size: int(repo.Size / 1024), + Fork: repo.IsFork, + Parent: parent, + Mirror: repo.IsMirror, + HTMLURL: repo.HTMLURL(), + URL: repoAPIURL, + SSHURL: cloneLink.SSH, + CloneURL: cloneLink.HTTPS, + OriginalURL: repo.SanitizedOriginalURL(), + Website: repo.Website, + Language: language, + LanguagesURL: repoAPIURL + "/languages", + Stars: repo.NumStars, + Forks: repo.NumForks, + Watchers: repo.NumWatches, + OpenIssues: repo.NumOpenIssues, + OpenPulls: repo.NumOpenPulls, + Releases: int(numReleases), + DefaultBranch: repo.DefaultBranch, + Created: repo.CreatedUnix.AsTime(), + Updated: repo.UpdatedUnix.AsTime(), + ArchivedAt: repo.ArchivedUnix.AsTime(), + Permissions: permission, + HasIssues: hasIssues, + ExternalTracker: externalTracker, + InternalTracker: internalTracker, + HasWiki: hasWiki, + WikiBranch: repo.WikiBranch, + GloballyEditableWiki: globallyEditableWiki, + HasProjects: hasProjects, + HasReleases: hasReleases, + HasPackages: hasPackages, + HasActions: hasActions, + ExternalWiki: externalWiki, + HasPullRequests: hasPullRequests, + IgnoreWhitespaceConflicts: ignoreWhitespaceConflicts, + AllowMerge: allowMerge, + AllowRebase: allowRebase, + AllowRebaseMerge: allowRebaseMerge, + AllowSquash: allowSquash, + AllowFastForwardOnly: allowFastForwardOnly, + AllowRebaseUpdate: allowRebaseUpdate, + DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge, + DefaultMergeStyle: string(defaultMergeStyle), + DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit, + AvatarURL: repo.AvatarLink(ctx), + Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate, + MirrorInterval: mirrorInterval, + MirrorUpdated: mirrorUpdated, + RepoTransfer: transfer, + Topics: repo.Topics, + ObjectFormatName: repo.ObjectFormatName, + } +} + +// ToRepoTransfer convert a models.RepoTransfer to a structs.RepeTransfer +func ToRepoTransfer(ctx context.Context, t *models.RepoTransfer) *api.RepoTransfer { + teams, _ := ToTeams(ctx, t.Teams, false) + + return &api.RepoTransfer{ + Doer: ToUser(ctx, t.Doer, nil), + Recipient: ToUser(ctx, t.Recipient, nil), + Teams: teams, + } +} diff --git a/services/convert/secret.go b/services/convert/secret.go new file mode 100644 index 0000000..dd7b9f0 --- /dev/null +++ b/services/convert/secret.go @@ -0,0 +1,18 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + secret_model "code.gitea.io/gitea/models/secret" + api "code.gitea.io/gitea/modules/structs" +) + +// ToSecret converts Secret to API format +func ToSecret(secret *secret_model.Secret) *api.Secret { + result := &api.Secret{ + Name: secret.Name, + } + + return result +} diff --git a/services/convert/status.go b/services/convert/status.go new file mode 100644 index 0000000..6cef63c --- /dev/null +++ b/services/convert/status.go @@ -0,0 +1,65 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + + git_model "code.gitea.io/gitea/models/git" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" +) + +// ToCommitStatus converts git_model.CommitStatus to api.CommitStatus +func ToCommitStatus(ctx context.Context, status *git_model.CommitStatus) *api.CommitStatus { + apiStatus := &api.CommitStatus{ + Created: status.CreatedUnix.AsTime(), + Updated: status.CreatedUnix.AsTime(), + State: status.State, + TargetURL: status.TargetURL, + Description: status.Description, + ID: status.Index, + URL: status.APIURL(ctx), + Context: status.Context, + } + + if status.CreatorID != 0 { + creator, _ := user_model.GetUserByID(ctx, status.CreatorID) + apiStatus.Creator = ToUser(ctx, creator, nil) + } + + return apiStatus +} + +// ToCombinedStatus converts List of CommitStatus to a CombinedStatus +func ToCombinedStatus(ctx context.Context, statuses []*git_model.CommitStatus, repo *api.Repository) *api.CombinedStatus { + if len(statuses) == 0 { + return nil + } + + retStatus := &api.CombinedStatus{ + SHA: statuses[0].SHA, + TotalCount: len(statuses), + Repository: repo, + URL: "", + } + + retStatus.Statuses = make([]*api.CommitStatus, 0, len(statuses)) + for _, status := range statuses { + retStatus.Statuses = append(retStatus.Statuses, ToCommitStatus(ctx, status)) + if retStatus.State == "" || status.State.NoBetterThan(retStatus.State) { + retStatus.State = status.State + } + } + // According to https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#get-the-combined-status-for-a-specific-reference + // > Additionally, a combined state is returned. The state is one of: + // > failure if any of the contexts report as error or failure + // > pending if there are no statuses or a context is pending + // > success if the latest status for all contexts is success + if retStatus.State.IsError() { + retStatus.State = api.CommitStatusFailure + } + + return retStatus +} diff --git a/services/convert/user.go b/services/convert/user.go new file mode 100644 index 0000000..94a400d --- /dev/null +++ b/services/convert/user.go @@ -0,0 +1,113 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + + "code.gitea.io/gitea/models/perm" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" +) + +// ToUser convert user_model.User to api.User +// if doer is set, private information is added if the doer has the permission to see it +func ToUser(ctx context.Context, user, doer *user_model.User) *api.User { + if user == nil { + return nil + } + authed := false + signed := false + if doer != nil { + signed = true + authed = doer.ID == user.ID || doer.IsAdmin + } + return toUser(ctx, user, signed, authed) +} + +// ToUsers convert list of user_model.User to list of api.User +func ToUsers(ctx context.Context, doer *user_model.User, users []*user_model.User) []*api.User { + result := make([]*api.User, len(users)) + for i := range users { + result[i] = ToUser(ctx, users[i], doer) + } + return result +} + +// ToUserWithAccessMode convert user_model.User to api.User +// AccessMode is not none show add some more information +func ToUserWithAccessMode(ctx context.Context, user *user_model.User, accessMode perm.AccessMode) *api.User { + if user == nil { + return nil + } + return toUser(ctx, user, accessMode != perm.AccessModeNone, false) +} + +// toUser convert user_model.User to api.User +// signed shall only be set if requester is logged in. authed shall only be set if user is site admin or user himself +func toUser(ctx context.Context, user *user_model.User, signed, authed bool) *api.User { + result := &api.User{ + ID: user.ID, + UserName: user.Name, + FullName: user.FullName, + Email: user.GetPlaceholderEmail(), + AvatarURL: user.AvatarLink(ctx), + HTMLURL: user.HTMLURL(), + Created: user.CreatedUnix.AsTime(), + Restricted: user.IsRestricted, + Location: user.Location, + Pronouns: user.Pronouns, + Website: user.Website, + Description: user.Description, + // counter's + Followers: user.NumFollowers, + Following: user.NumFollowing, + StarredRepos: user.NumStars, + } + + result.Visibility = user.Visibility.String() + + // hide primary email if API caller is anonymous or user keep email private + if signed && (!user.KeepEmailPrivate || authed) { + result.Email = user.Email + } + + // only site admin will get these information and possibly user himself + if authed { + result.IsAdmin = user.IsAdmin + result.LoginName = user.LoginName + result.SourceID = user.LoginSource + result.LastLogin = user.LastLoginUnix.AsTime() + result.Language = user.Language + result.IsActive = user.IsActive + result.ProhibitLogin = user.ProhibitLogin + } + return result +} + +// User2UserSettings return UserSettings based on a user +func User2UserSettings(user *user_model.User) api.UserSettings { + return api.UserSettings{ + FullName: user.FullName, + Website: user.Website, + Location: user.Location, + Pronouns: user.Pronouns, + Language: user.Language, + Description: user.Description, + Theme: user.Theme, + HideEmail: user.KeepEmailPrivate, + HideActivity: user.KeepActivityPrivate, + DiffViewStyle: user.DiffViewStyle, + EnableRepoUnitHints: user.EnableRepoUnitHints, + } +} + +// ToUserAndPermission return User and its collaboration permission for a repository +func ToUserAndPermission(ctx context.Context, user, doer *user_model.User, accessMode perm.AccessMode) api.RepoCollaboratorPermission { + return api.RepoCollaboratorPermission{ + User: ToUser(ctx, user, doer), + Permission: accessMode.String(), + RoleName: accessMode.String(), + } +} diff --git a/services/convert/user_test.go b/services/convert/user_test.go new file mode 100644 index 0000000..0f0b520 --- /dev/null +++ b/services/convert/user_test.go @@ -0,0 +1,41 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUser_ToUser(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1, IsAdmin: true}) + + apiUser := toUser(db.DefaultContext, user1, true, true) + assert.True(t, apiUser.IsAdmin) + assert.Contains(t, apiUser.AvatarURL, "://") + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2, IsAdmin: false}) + + apiUser = toUser(db.DefaultContext, user2, true, true) + assert.False(t, apiUser.IsAdmin) + + apiUser = toUser(db.DefaultContext, user1, false, false) + assert.False(t, apiUser.IsAdmin) + assert.EqualValues(t, api.VisibleTypePublic.String(), apiUser.Visibility) + + user31 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31, IsAdmin: false, Visibility: api.VisibleTypePrivate}) + + apiUser = toUser(db.DefaultContext, user31, true, true) + assert.False(t, apiUser.IsAdmin) + assert.EqualValues(t, api.VisibleTypePrivate.String(), apiUser.Visibility) +} diff --git a/services/convert/utils.go b/services/convert/utils.go new file mode 100644 index 0000000..fe35fd2 --- /dev/null +++ b/services/convert/utils.go @@ -0,0 +1,44 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Copyright 2016 The Gogs Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "strings" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" +) + +// ToCorrectPageSize makes sure page size is in allowed range. +func ToCorrectPageSize(size int) int { + if size <= 0 { + size = setting.API.DefaultPagingNum + } else if size > setting.API.MaxResponseItems { + size = setting.API.MaxResponseItems + } + return size +} + +// ToGitServiceType return GitServiceType based on string +func ToGitServiceType(value string) structs.GitServiceType { + switch strings.ToLower(value) { + case "github": + return structs.GithubService + case "gitea": + return structs.GiteaService + case "gitlab": + return structs.GitlabService + case "gogs": + return structs.GogsService + case "onedev": + return structs.OneDevService + case "gitbucket": + return structs.GitBucketService + case "forgejo": + return structs.ForgejoService + default: + return structs.PlainGitService + } +} diff --git a/services/convert/utils_test.go b/services/convert/utils_test.go new file mode 100644 index 0000000..b464d8b --- /dev/null +++ b/services/convert/utils_test.go @@ -0,0 +1,39 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToCorrectPageSize(t *testing.T) { + assert.EqualValues(t, 30, ToCorrectPageSize(0)) + assert.EqualValues(t, 30, ToCorrectPageSize(-10)) + assert.EqualValues(t, 20, ToCorrectPageSize(20)) + assert.EqualValues(t, 50, ToCorrectPageSize(100)) +} + +func TestToGitServiceType(t *testing.T) { + tc := []struct { + typ string + enum int + }{{ + typ: "github", enum: 2, + }, { + typ: "gitea", enum: 3, + }, { + typ: "gitlab", enum: 4, + }, { + typ: "gogs", enum: 5, + }, { + typ: "forgejo", enum: 9, + }, { + typ: "trash", enum: 1, + }} + for _, test := range tc { + assert.EqualValues(t, test.enum, ToGitServiceType(test.typ)) + } +} diff --git a/services/convert/wiki.go b/services/convert/wiki.go new file mode 100644 index 0000000..767bfdb --- /dev/null +++ b/services/convert/wiki.go @@ -0,0 +1,45 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "time" + + "code.gitea.io/gitea/modules/git" + api "code.gitea.io/gitea/modules/structs" +) + +// ToWikiCommit convert a git commit into a WikiCommit +func ToWikiCommit(commit *git.Commit) *api.WikiCommit { + return &api.WikiCommit{ + ID: commit.ID.String(), + Author: &api.CommitUser{ + Identity: api.Identity{ + Name: commit.Author.Name, + Email: commit.Author.Email, + }, + Date: commit.Author.When.UTC().Format(time.RFC3339), + }, + Committer: &api.CommitUser{ + Identity: api.Identity{ + Name: commit.Committer.Name, + Email: commit.Committer.Email, + }, + Date: commit.Committer.When.UTC().Format(time.RFC3339), + }, + Message: commit.CommitMessage, + } +} + +// ToWikiCommitList convert a list of git commits into a WikiCommitList +func ToWikiCommitList(commits []*git.Commit, total int64) *api.WikiCommitList { + result := make([]*api.WikiCommit, len(commits)) + for i := range commits { + result[i] = ToWikiCommit(commits[i]) + } + return &api.WikiCommitList{ + WikiCommits: result, + Count: total, + } +} |