1package filesqlite
2
3import (
4 "bufio"
5 "context"
6 "os"
7 "path/filepath"
8 "sync"
9 "time"
10
11 "github.com/charmbracelet/soft-serve/git"
12 "github.com/charmbracelet/soft-serve/server/backend"
13 "github.com/charmbracelet/soft-serve/server/db/sqlite"
14 "github.com/jmoiron/sqlx"
15)
16
17var _ backend.Repository = (*Repo)(nil)
18
19// Repo is a Git repository with metadata stored in a SQLite database.
20type Repo struct {
21 name string
22 path string
23 db *sqlx.DB
24
25 // cache
26 // updatedAt is cached in "last-modified" file.
27 mu sync.Mutex
28 desc *string
29 projectName *string
30 isMirror *bool
31 isPrivate *bool
32 isHidden *bool
33}
34
35// Description returns the repository's description.
36//
37// It implements backend.Repository.
38func (r *Repo) Description() string {
39 r.mu.Lock()
40 defer r.mu.Unlock()
41 if r.desc != nil {
42 return *r.desc
43 }
44
45 var desc string
46 if err := sqlite.WrapTx(r.db, context.Background(), func(tx *sqlx.Tx) error {
47 return tx.Get(&desc, "SELECT description FROM repo WHERE name = ?", r.name)
48 }); err != nil {
49 return ""
50 }
51
52 r.desc = &desc
53 return desc
54}
55
56// IsMirror returns whether the repository is a mirror.
57//
58// It implements backend.Repository.
59func (r *Repo) IsMirror() bool {
60 r.mu.Lock()
61 defer r.mu.Unlock()
62 if r.isMirror != nil {
63 return *r.isMirror
64 }
65
66 var mirror bool
67 if err := sqlite.WrapTx(r.db, context.Background(), func(tx *sqlx.Tx) error {
68 return tx.Get(&mirror, "SELECT mirror FROM repo WHERE name = ?", r.name)
69 }); err != nil {
70 return false
71 }
72
73 r.isMirror = &mirror
74 return mirror
75}
76
77// IsPrivate returns whether the repository is private.
78//
79// It implements backend.Repository.
80func (r *Repo) IsPrivate() bool {
81 r.mu.Lock()
82 defer r.mu.Unlock()
83 if r.isPrivate != nil {
84 return *r.isPrivate
85 }
86
87 var private bool
88 if err := sqlite.WrapTx(r.db, context.Background(), func(tx *sqlx.Tx) error {
89 return tx.Get(&private, "SELECT private FROM repo WHERE name = ?", r.name)
90 }); err != nil {
91 return false
92 }
93
94 r.isPrivate = &private
95 return private
96}
97
98// Name returns the repository's name.
99//
100// It implements backend.Repository.
101func (r *Repo) Name() string {
102 return r.name
103}
104
105// Open opens the repository.
106//
107// It implements backend.Repository.
108func (r *Repo) Open() (*git.Repository, error) {
109 return git.Open(r.path)
110}
111
112// ProjectName returns the repository's project name.
113//
114// It implements backend.Repository.
115func (r *Repo) ProjectName() string {
116 r.mu.Lock()
117 defer r.mu.Unlock()
118 if r.projectName != nil {
119 return *r.projectName
120 }
121
122 var name string
123 if err := sqlite.WrapTx(r.db, context.Background(), func(tx *sqlx.Tx) error {
124 return tx.Get(&name, "SELECT project_name FROM repo WHERE name = ?", r.name)
125 }); err != nil {
126 return ""
127 }
128
129 r.projectName = &name
130 return name
131}
132
133// IsHidden returns whether the repository is hidden.
134//
135// It implements backend.Repository.
136func (r *Repo) IsHidden() bool {
137 r.mu.Lock()
138 defer r.mu.Unlock()
139 if r.isHidden != nil {
140 return *r.isHidden
141 }
142
143 var hidden bool
144 if err := sqlite.WrapTx(r.db, context.Background(), func(tx *sqlx.Tx) error {
145 return tx.Get(&hidden, "SELECT hidden FROM repo WHERE name = ?", r.name)
146 }); err != nil {
147 return false
148 }
149
150 r.isHidden = &hidden
151 return hidden
152}
153
154// UpdatedAt returns the repository's last update time.
155func (r *Repo) UpdatedAt() time.Time {
156 var updatedAt time.Time
157
158 // Try to read the last modified time from the info directory.
159 if t, err := readOneline(filepath.Join(r.path, "info", "last-modified")); err == nil {
160 if t, err := time.Parse(time.RFC3339, t); err == nil {
161 return t
162 }
163 }
164
165 rr, err := git.Open(r.path)
166 if err == nil {
167 t, err := rr.LatestCommitTime()
168 if err == nil {
169 updatedAt = t
170 }
171 }
172
173 if updatedAt.IsZero() {
174 if err := sqlite.WrapTx(r.db, context.Background(), func(tx *sqlx.Tx) error {
175 return tx.Get(&updatedAt, "SELECT updated_at FROM repo WHERE name = ?", r.name)
176 }); err != nil {
177 return time.Time{}
178 }
179 }
180
181 return updatedAt
182}
183
184func (r *Repo) writeLastModified(t time.Time) error {
185 fp := filepath.Join(r.path, "info", "last-modified")
186 if err := os.MkdirAll(filepath.Dir(fp), os.ModePerm); err != nil {
187 return err
188 }
189
190 return os.WriteFile(fp, []byte(t.Format(time.RFC3339)), os.ModePerm)
191}
192
193func readOneline(path string) (string, error) {
194 f, err := os.Open(path)
195 if err != nil {
196 return "", err
197 }
198
199 defer f.Close() // nolint: errcheck
200 s := bufio.NewScanner(f)
201 s.Scan()
202 return s.Text(), s.Err()
203}