forgejo/services/convert/convert.go
Lunny Xiao bd820aa9c5
Add context cache as a request level cache (#22294)
To avoid duplicated load of the same data in an HTTP request, we can set
a context cache to do that. i.e. Some pages may load a user from a
database with the same id in different areas on the same page. But the
code is hidden in two different deep logic. How should we share the
user? As a result of this PR, now if both entry functions accept
`context.Context` as the first parameter and we just need to refactor
`GetUserByID` to reuse the user from the context cache. Then it will not
be loaded twice on an HTTP request.

But of course, sometimes we would like to reload an object from the
database, that's why `RemoveContextData` is also exposed.

The core context cache is here. It defines a new context
```go
type cacheContext struct {
	ctx  context.Context
	data map[any]map[any]any
        lock sync.RWMutex
}

var cacheContextKey = struct{}{}

func WithCacheContext(ctx context.Context) context.Context {
	return context.WithValue(ctx, cacheContextKey, &cacheContext{
		ctx:  ctx,
		data: make(map[any]map[any]any),
	})
}
```

Then you can use the below 4 methods to read/write/del the data within
the same context.

```go
func GetContextData(ctx context.Context, tp, key any) any
func SetContextData(ctx context.Context, tp, key, value any)
func RemoveContextData(ctx context.Context, tp, key any)
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error)
```

Then let's take a look at how `system.GetString` implement it.

```go
func GetSetting(ctx context.Context, key string) (string, error) {
	return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
		return cache.GetString(genSettingCacheKey(key), func() (string, error) {
			res, err := GetSettingNoCache(ctx, key)
			if err != nil {
				return "", err
			}
			return res.SettingValue, nil
		})
	})
}
```

First, it will check if context data include the setting object with the
key. If not, it will query from the global cache which may be memory or
a Redis cache. If not, it will get the object from the database. In the
end, if the object gets from the global cache or database, it will be
set into the context cache.

An object stored in the context cache will only be destroyed after the
context disappeared.
2023-02-15 21:37:34 +08:00

432 lines
14 KiB
Go

// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package convert
import (
"context"
"fmt"
"strconv"
"strings"
"time"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/gitdiff"
)
// ToEmail convert models.EmailAddress to api.Email
func ToEmail(email *user_model.EmailAddress) *api.Email {
return &api.Email{
Email: email.Email,
Verified: email.IsActivated,
Primary: email.IsPrimary,
}
}
// ToBranch convert a git.Commit and git.Branch to an api.Branch
func ToBranch(ctx context.Context, repo *repo_model.Repository, b *git.Branch, c *git.Commit, bp *git_model.ProtectedBranch, user *user_model.User, isRepoAdmin bool) (*api.Branch, error) {
if bp == nil {
var hasPerm bool
var canPush bool
var err error
if user != nil {
hasPerm, err = access_model.HasAccessUnit(db.DefaultContext, user, repo, unit.TypeCode, perm.AccessModeWrite)
if err != nil {
return nil, err
}
perms, err := access_model.GetUserRepoPermission(db.DefaultContext, repo, user)
if err != nil {
return nil, err
}
canPush = issues_model.CanMaintainerWriteToBranch(perms, b.Name, user)
}
return &api.Branch{
Name: b.Name,
Commit: ToPayloadCommit(ctx, repo, c),
Protected: false,
RequiredApprovals: 0,
EnableStatusCheck: false,
StatusCheckContexts: []string{},
UserCanPush: canPush,
UserCanMerge: hasPerm,
}, nil
}
branch := &api.Branch{
Name: b.Name,
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(db.DefaultContext, repo, user)
if err != nil {
return nil, err
}
bp.Repo = repo
branch.UserCanPush = bp.CanUserPush(db.DefaultContext, user)
branch.UserCanMerge = git_model.IsUserMergeWhitelisted(db.DefaultContext, bp, user.ID, permission)
}
return branch, nil
}
// ToBranchProtection convert a ProtectedBranch to api.BranchProtection
func ToBranchProtection(bp *git_model.ProtectedBranch) *api.BranchProtection {
pushWhitelistUsernames, err := user_model.GetUserNamesByIDs(bp.WhitelistUserIDs)
if err != nil {
log.Error("GetUserNamesByIDs (WhitelistUserIDs): %v", err)
}
mergeWhitelistUsernames, err := user_model.GetUserNamesByIDs(bp.MergeWhitelistUserIDs)
if err != nil {
log.Error("GetUserNamesByIDs (MergeWhitelistUserIDs): %v", err)
}
approvalsWhitelistUsernames, err := user_model.GetUserNamesByIDs(bp.ApprovalsWhitelistUserIDs)
if err != nil {
log.Error("GetUserNamesByIDs (ApprovalsWhitelistUserIDs): %v", err)
}
pushWhitelistTeams, err := organization.GetTeamNamesByID(bp.WhitelistTeamIDs)
if err != nil {
log.Error("GetTeamNamesByID (WhitelistTeamIDs): %v", err)
}
mergeWhitelistTeams, err := organization.GetTeamNamesByID(bp.MergeWhitelistTeamIDs)
if err != nil {
log.Error("GetTeamNamesByID (MergeWhitelistTeamIDs): %v", err)
}
approvalsWhitelistTeams, err := organization.GetTeamNamesByID(bp.ApprovalsWhitelistTeamIDs)
if err != nil {
log.Error("GetTeamNamesByID (ApprovalsWhitelistTeamIDs): %v", err)
}
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,
RequireSignedCommits: bp.RequireSignedCommits,
ProtectedFilePatterns: bp.ProtectedFilePatterns,
UnprotectedFilePatterns: bp.UnprotectedFilePatterns,
Created: bp.CreatedUnix.AsTime(),
Updated: bp.UpdatedUnix.AsTime(),
}
}
// ToTag convert a git.Tag to an api.Tag
func ToTag(repo *repo_model.Repository, t *git.Tag) *api.Tag {
return &api.Tag{
Name: t.Name,
Message: strings.TrimSpace(t.Message),
ID: t.ID.String(),
Commit: ToCommitMeta(repo, t),
ZipballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".zip"),
TarballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".tar.gz"),
}
}
// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
func ToVerification(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,
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) {
if len(teams) == 0 || teams[0] == nil {
return nil, nil
}
cache := make(map[int64]*api.Organization)
apiTeams := make([]*api.Team, len(teams))
for i := range teams {
if err := teams[i].GetUnits(); err != nil {
return nil, err
}
apiTeams[i] = &api.Team{
ID: teams[i].ID,
Name: teams[i].Name,
Description: teams[i].Description,
IncludesAllRepositories: teams[i].IncludesAllRepositories,
CanCreateOrgRepo: teams[i].CanCreateOrgRepo,
Permission: teams[i].AccessMode.String(),
Units: teams[i].GetUnitNames(),
UnitsMap: teams[i].GetUnitsMap(),
}
if loadOrgs {
apiOrg, ok := cache[teams[i].OrgID]
if !ok {
org, err := organization.GetOrgByID(db.DefaultContext, teams[i].OrgID)
if err != nil {
return nil, err
}
apiOrg = ToOrganization(ctx, org)
cache[teams[i].OrgID] = apiOrg
}
apiTeams[i].Organization = apiOrg
}
}
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),
}
}
// ToAnnotatedTagObject convert a git.Commit to an api.AnnotatedTagObject
func ToAnnotatedTagObject(repo *repo_model.Repository, commit *git.Commit) *api.AnnotatedTagObject {
return &api.AnnotatedTagObject{
SHA: commit.ID.String(),
Type: string(git.ObjectCommit),
URL: util.URLJoin(repo.APIURL(), "git/commits", commit.ID.String()),
}
}
// ToTopicResponse convert from models.Topic to api.TopicResponse
func ToTopicResponse(topic *repo_model.Topic) *api.TopicResponse {
return &api.TopicResponse{
ID: topic.ID,
Name: topic.Name,
RepoCount: topic.RepoCount,
Created: topic.CreatedUnix.AsTime(),
Updated: topic.UpdatedUnix.AsTime(),
}
}
// ToOAuth2Application convert from auth.OAuth2Application to api.OAuth2Application
func ToOAuth2Application(app *auth.OAuth2Application) *api.OAuth2Application {
return &api.OAuth2Application{
ID: app.ID,
Name: app.Name,
ClientID: app.ClientID,
ClientSecret: app.ClientSecret,
ConfidentialClient: app.ConfidentialClient,
RedirectURIs: app.RedirectURIs,
Created: app.CreatedUnix.AsTime(),
}
}
// ToLFSLock convert a LFSLock to api.LFSLock
func ToLFSLock(ctx context.Context, l *git_model.LFSLock) *api.LFSLock {
u, err := user_model.GetUserByID(ctx, l.OwnerID)
if err != nil {
return nil
}
return &api.LFSLock{
ID: strconv.FormatInt(l.ID, 10),
Path: l.Path,
LockedAt: l.Created.Round(time.Second),
Owner: &api.LFSLockOwner{
Name: u.Name,
},
}
}
// ToChangedFile convert a gitdiff.DiffFile to api.ChangedFile
func ToChangedFile(f *gitdiff.DiffFile, repo *repo_model.Repository, commit string) *api.ChangedFile {
status := "changed"
if f.IsDeleted {
status = "deleted"
} else if f.IsCreated {
status = "added"
} else if f.IsRenamed && f.Type == gitdiff.DiffFileCopy {
status = "copied"
} else if f.IsRenamed && f.Type == gitdiff.DiffFileRename {
status = "renamed"
} else if f.Addition == 0 && f.Deletion == 0 {
status = "unchanged"
}
file := &api.ChangedFile{
Filename: f.GetDiffFileName(),
Status: status,
Additions: f.Addition,
Deletions: f.Deletion,
Changes: f.Addition + f.Deletion,
HTMLURL: fmt.Sprint(repo.HTMLURL(), "/src/commit/", commit, "/", util.PathEscapeSegments(f.GetDiffFileName())),
ContentsURL: fmt.Sprint(repo.APIURL(), "/contents/", util.PathEscapeSegments(f.GetDiffFileName()), "?ref=", commit),
RawURL: fmt.Sprint(repo.HTMLURL(), "/raw/commit/", commit, "/", util.PathEscapeSegments(f.GetDiffFileName())),
}
if status == "rename" {
file.PreviousFilename = f.OldName
}
return file
}