]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/topics.go
sgblog-topics helper command
[sgblog.git] / cmd / sgblog / topics.go
1 /*
2 SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
3 Copyright (C) 2020-2021 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         "go.stargrave.org/sgblog"
31 )
32
33 type TopicsCache map[string][]plumbing.Hash
34
35 type TopicsCacheState struct {
36         Top   plumbing.Hash
37         Cache TopicsCache
38 }
39
40 func (tc TopicsCache) Topics() []string {
41         topics := make([]string, 0, len(tc))
42         for t, _ := range tc {
43                 topics = append(topics, t)
44         }
45         sort.Strings(topics)
46         return topics
47 }
48
49 func getTopicsCache(cfg *Cfg, repoLog object.CommitIter) (TopicsCache, error) {
50         cache := TopicsCache(make(map[string][]plumbing.Hash))
51         if topicsTree == nil {
52                 return cache, nil
53         }
54         top := topicsRef.Hash()
55
56         if cfg.TopicsCachePath != "" {
57                 fd, err := os.Open(cfg.TopicsCachePath)
58                 if err != nil {
59                         goto NoCache
60                 }
61                 dec := gob.NewDecoder(fd)
62                 var cacheState TopicsCacheState
63                 err = dec.Decode(&cacheState)
64                 fd.Close()
65                 if err != nil {
66                         goto NoCache
67                 }
68                 if cacheState.Top == top {
69                         return cacheState.Cache, nil
70                 }
71         }
72
73 NoCache:
74         for {
75                 commit, err := repoLog.Next()
76                 if err != nil {
77                         break
78                 }
79                 for _, topic := range sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)) {
80                         cache[topic] = append(cache[topic], commit.Hash)
81                 }
82         }
83
84         if cfg.TopicsCachePath != "" {
85                 // Assume that probability of suffix collision is negligible
86                 suffix := strconv.FormatInt(time.Now().UnixNano()+int64(os.Getpid()), 16)
87                 tmpPath := cfg.TopicsCachePath + suffix
88                 fd, err := os.OpenFile(
89                         tmpPath,
90                         os.O_RDWR|os.O_CREATE|os.O_EXCL,
91                         os.FileMode(0666),
92                 )
93                 if err != nil {
94                         return cache, err
95                 }
96                 enc := gob.NewEncoder(fd)
97                 err = enc.Encode(&TopicsCacheState{top, cache})
98                 if err != nil {
99                         os.Remove(tmpPath)
100                         fd.Close()
101                         return cache, err
102                 }
103                 if err = fd.Sync(); err != nil {
104                         os.Remove(tmpPath)
105                         fd.Close()
106                         return cache, err
107                 }
108                 if err = fd.Close(); err != nil {
109                         os.Remove(tmpPath)
110                         return cache, err
111                 }
112                 if err = os.Rename(tmpPath, cfg.TopicsCachePath); err != nil {
113                         os.Remove(tmpPath)
114                         return cache, err
115                 }
116         }
117
118         return cache, nil
119 }
120
121 type HashesIter struct {
122         hashes []plumbing.Hash
123 }
124
125 func (s *HashesIter) Next() (*object.Commit, error) {
126         if len(s.hashes) == 0 {
127                 return nil, io.EOF
128         }
129         var h plumbing.Hash
130         h, s.hashes = s.hashes[0], s.hashes[1:]
131         return repo.CommitObject(h)
132 }