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