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