1// Copyright 2020 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package git
5
6import (
7 "context"
8 "crypto/sha256"
9 "fmt"
10
11 "forgejo.org/modules/log"
12 "forgejo.org/modules/setting"
13)
14
15// Cache represents a caching interface
16type Cache interface {
17 // Put puts value into cache with key and expire time.
18 Put(key string, val any, timeout int64) error
19 // Get gets cached value by given key.
20 Get(key string) any
21}
22
23func getCacheKey(repoPath, commitID, entryPath string) string {
24 hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", repoPath, commitID, entryPath)))
25 return fmt.Sprintf("last_commit:%x", hashBytes)
26}
27
28// LastCommitCache represents a cache to store last commit
29type LastCommitCache struct {
30 repoPath string
31 ttl func() int64
32 repo *Repository
33 commitCache map[string]*Commit
34 cache Cache
35}
36
37// NewLastCommitCache creates a new last commit cache for repo
38func NewLastCommitCache(count int64, repoPath string, gitRepo *Repository, cache Cache) *LastCommitCache {
39 if cache == nil {
40 return nil
41 }
42 if count < setting.CacheService.LastCommit.CommitsCount {
43 return nil
44 }
45
46 return &LastCommitCache{
47 repoPath: repoPath,
48 repo: gitRepo,
49 ttl: setting.LastCommitCacheTTLSeconds,
50 cache: cache,
51 }
52}
53
54// Put put the last commit id with commit and entry path
55func (c *LastCommitCache) Put(ref, entryPath, commitID string) error {
56 if c == nil || c.cache == nil {
57 return nil
58 }
59 log.Debug("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID)
60 return c.cache.Put(getCacheKey(c.repoPath, ref, entryPath), commitID, c.ttl())
61}
62
63// Get gets the last commit information by commit id and entry path
64func (c *LastCommitCache) Get(ref, entryPath string) (*Commit, error) {
65 if c == nil || c.cache == nil {
66 return nil, nil
67 }
68
69 commitID, ok := c.cache.Get(getCacheKey(c.repoPath, ref, entryPath)).(string)
70 if !ok || commitID == "" {
71 return nil, nil
72 }
73
74 log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, commitID)
75 if c.commitCache != nil {
76 if commit, ok := c.commitCache[commitID]; ok {
77 log.Debug("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, commitID)
78 return commit, nil
79 }
80 }
81
82 commit, err := c.repo.GetCommit(commitID)
83 if err != nil {
84 return nil, err
85 }
86 if c.commitCache == nil {
87 c.commitCache = make(map[string]*Commit)
88 }
89 c.commitCache[commitID] = commit
90 return commit, nil
91}
92
93// GetCommitByPath gets the last commit for the entry in the provided commit
94func (c *LastCommitCache) GetCommitByPath(commitID, entryPath string) (*Commit, error) {
95 sha, err := NewIDFromString(commitID)
96 if err != nil {
97 return nil, err
98 }
99
100 lastCommit, err := c.Get(sha.String(), entryPath)
101 if err != nil || lastCommit != nil {
102 return lastCommit, err
103 }
104
105 lastCommit, err = c.repo.getCommitByPathWithID(sha, entryPath)
106 if err != nil {
107 return nil, err
108 }
109
110 if err := c.Put(commitID, entryPath, lastCommit.ID.String()); err != nil {
111 log.Error("Unable to cache %s as the last commit for %q in %s %s. Error %v", lastCommit.ID.String(), entryPath, commitID, c.repoPath, err)
112 }
113
114 return lastCommit, nil
115}
116
117// CacheCommit will cache the commit from the gitRepository
118func (c *Commit) CacheCommit(ctx context.Context) error {
119 if c.repo.LastCommitCache == nil {
120 return nil
121 }
122 return c.recursiveCache(ctx, &c.Tree, "", 1)
123}
124
125func (c *Commit) recursiveCache(ctx context.Context, tree *Tree, treePath string, level int) error {
126 if level == 0 {
127 return nil
128 }
129
130 entries, err := tree.ListEntries()
131 if err != nil {
132 return err
133 }
134
135 entryPaths := make([]string, len(entries))
136 for i, entry := range entries {
137 entryPaths[i] = entry.Name()
138 }
139
140 _, err = WalkGitLog(ctx, c.repo, c, treePath, entryPaths...)
141 if err != nil {
142 return err
143 }
144
145 for _, treeEntry := range entries {
146 // entryMap won't contain "" therefore skip this.
147 if treeEntry.IsDir() {
148 subTree, err := tree.SubTree(treeEntry.Name())
149 if err != nil {
150 return err
151 }
152 if err := c.recursiveCache(ctx, subTree, treeEntry.Name(), level-1); err != nil {
153 return err
154 }
155 }
156 }
157
158 return nil
159}