]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/topics.go
9308a8a4909923891056547a8e5b7140b3478879
[sgblog.git] / cmd / sgblog / topics.go
1 // SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
2 // Copyright (C) 2020-2024 Sergey Matveev <stargrave@stargrave.org>
3 //
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU Affero General Public License as
6 // published by the Free Software Foundation, version 3 of the License.
7 //
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU Affero General Public License
14 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16 package main
17
18 import (
19         "encoding/gob"
20         "io"
21         "os"
22         "sort"
23         "strconv"
24         "time"
25
26         "github.com/go-git/go-git/v5/plumbing"
27         "github.com/go-git/go-git/v5/plumbing/object"
28         "go.stargrave.org/sgblog"
29 )
30
31 type TopicsCache map[string][]plumbing.Hash
32
33 type TopicsCacheState struct {
34         Top   plumbing.Hash
35         Cache TopicsCache
36 }
37
38 func (tc TopicsCache) Topics() []string {
39         topics := make([]string, 0, len(tc))
40         for t := range tc {
41                 topics = append(topics, t)
42         }
43         sort.Strings(topics)
44         return topics
45 }
46
47 func getTopicsCache(cfg *Cfg, repoLog object.CommitIter) (TopicsCache, error) {
48         cache := TopicsCache(make(map[string][]plumbing.Hash))
49         if topicsTree == nil {
50                 return cache, nil
51         }
52         top := topicsRef.Hash()
53
54         if cfg.TopicsCachePath != "" {
55                 fd, err := os.Open(cfg.TopicsCachePath)
56                 if err != nil {
57                         goto NoCache
58                 }
59                 dec := gob.NewDecoder(fd)
60                 var cacheState TopicsCacheState
61                 err = dec.Decode(&cacheState)
62                 fd.Close()
63                 if err != nil {
64                         goto NoCache
65                 }
66                 if cacheState.Top == top {
67                         return cacheState.Cache, nil
68                 }
69         }
70
71 NoCache:
72         for {
73                 commit, err := repoLog.Next()
74                 if err != nil {
75                         break
76                 }
77                 for _, topic := range sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)) {
78                         cache[topic] = append(cache[topic], commit.Hash)
79                 }
80         }
81
82         if cfg.TopicsCachePath != "" {
83                 // Assume that probability of suffix collision is negligible
84                 suffix := strconv.FormatInt(time.Now().UnixNano()+int64(os.Getpid()), 16)
85                 tmpPath := cfg.TopicsCachePath + suffix
86                 fd, err := os.OpenFile(
87                         tmpPath,
88                         os.O_RDWR|os.O_CREATE|os.O_EXCL,
89                         os.FileMode(0666),
90                 )
91                 if err != nil {
92                         return cache, err
93                 }
94                 enc := gob.NewEncoder(fd)
95                 err = enc.Encode(&TopicsCacheState{top, cache})
96                 if err != nil {
97                         os.Remove(tmpPath)
98                         fd.Close()
99                         return cache, err
100                 }
101                 if err = fd.Sync(); err != nil {
102                         os.Remove(tmpPath)
103                         fd.Close()
104                         return cache, err
105                 }
106                 if err = fd.Close(); err != nil {
107                         os.Remove(tmpPath)
108                         return cache, err
109                 }
110                 if err = os.Rename(tmpPath, cfg.TopicsCachePath); err != nil {
111                         os.Remove(tmpPath)
112                         return cache, err
113                 }
114         }
115
116         return cache, nil
117 }
118
119 type HashesIter struct {
120         hashes []plumbing.Hash
121 }
122
123 func (s *HashesIter) Next() (*object.Commit, error) {
124         if len(s.hashes) == 0 {
125                 return nil, io.EOF
126         }
127         var h plumbing.Hash
128         h, s.hashes = s.hashes[0], s.hashes[1:]
129         return repo.CommitObject(h)
130 }