Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · forge/index/index.go

Line attribution on main · 553 line(s)

653e0cfc
Keenan Nemetz
18d ago
1 // Package index holds the derived read model. Everything in it can be thrown
2 // away and rebuilt from the repos on disk, which is why it is safe for it to be
3 // the layer that knows about SQL.
4 package index
5
6 import (
ec5c519c
Keenan Nemetz
17d ago
7 "crypto/sha256"
653e0cfc
Keenan Nemetz
18d ago
8 "database/sql"
ec5c519c
Keenan Nemetz
17d ago
9 "encoding/hex"
653e0cfc
Keenan Nemetz
18d ago
10 "errors"
11 "fmt"
12 "time"
13
95425af7
Keenan Nemetz
10d ago
14 _ "github.com/mattn/go-sqlite3"
653e0cfc
Keenan Nemetz
18d ago
15
d37d301f
Keenan Nemetz
21h ago
16 "rodent.software/vole/forge/repo"
653e0cfc
Keenan Nemetz
18d ago
17 )
18
19 type Entry struct {
20 ID string
21 Name string
22 Description string
23 DefaultBranch string
24 Created time.Time
25 ACLSHA string
26 ACLPrincipals int
27 ACLDefault string
8dc56e4e
Keenan Nemetz
10d ago
28 // The default branch's summary, maintained at push time. A repository with no
29 // commits has an empty HeadSHA.
30 HeadSHA string
31 HeadSubject string
32 HeadAuthor string
33 HeadWhen int64
34 Language string
35 LangClass string
653e0cfc
Keenan Nemetz
18d ago
36 }
37
38 type Store struct{ db *sql.DB }
39
40 func Open(path string) (*Store, error) {
95425af7
Keenan Nemetz
10d ago
41 dsn := path + "?_busy_timeout=5000&_journal_mode=WAL"
42 db, err := sql.Open("sqlite3", dsn)
653e0cfc
Keenan Nemetz
18d ago
43 if err != nil {
44 return nil, err
45 }
46 db.SetMaxOpenConns(4)
47 s := &Store{db: db}
48 if _, err := db.Exec(`
49 CREATE TABLE IF NOT EXISTS repos (
50 id TEXT PRIMARY KEY,
51 name TEXT NOT NULL UNIQUE,
52 description TEXT NOT NULL DEFAULT '',
53 default_branch TEXT NOT NULL DEFAULT 'main',
54 created INTEGER NOT NULL,
55 acl_sha TEXT NOT NULL DEFAULT '',
56 acl_principals INTEGER NOT NULL DEFAULT 0,
8dc56e4e
Keenan Nemetz
10d ago
57 acl_default TEXT NOT NULL DEFAULT 'none',
58 head_sha TEXT NOT NULL DEFAULT '',
59 head_subject TEXT NOT NULL DEFAULT '',
60 head_author TEXT NOT NULL DEFAULT '',
61 head_when INTEGER NOT NULL DEFAULT 0,
62 language TEXT NOT NULL DEFAULT '',
63 lang_class TEXT NOT NULL DEFAULT ''
37c01e5a
Keenan Nemetz
18d ago
64 );`); err != nil {
65 db.Close()
66 return nil, err
67 }
68 // Added after the first release of the index; the table is disposable, so a
69 // failed ALTER (already present) is not an error.
70 _, _ = db.Exec(`ALTER TABLE repos ADD COLUMN issues_sha TEXT NOT NULL DEFAULT ''`)
bca9ec24
Keenan Nemetz
10d ago
71 // events_count is the number of feed events a repository has, so an
72 // unfiltered pager gets its total without counting the table.
73 _, _ = db.Exec(`ALTER TABLE repos ADD COLUMN events_count INTEGER NOT NULL DEFAULT 0`)
8dc56e4e
Keenan Nemetz
10d ago
74 // The head summary is the default branch's tip and the language it is mostly
75 // made of, refreshed when a push lands rather than on every page read.
76 for _, col := range []string{
77 "head_sha TEXT NOT NULL DEFAULT ''",
78 "head_subject TEXT NOT NULL DEFAULT ''",
79 "head_author TEXT NOT NULL DEFAULT ''",
80 "head_when INTEGER NOT NULL DEFAULT 0",
81 "language TEXT NOT NULL DEFAULT ''",
82 "lang_class TEXT NOT NULL DEFAULT ''",
83 } {
84 _, _ = db.Exec(`ALTER TABLE repos ADD COLUMN ` + col)
85 }
2a8f7cef
Keenan Nemetz
10d ago
86 // The overview's git-derived summaries, recorded at push time. They are
87 // keyed by repo id and replaced together with the head they describe.
88 if _, err := db.Exec(`
89 CREATE TABLE IF NOT EXISTS repo_languages (
90 repo_id TEXT NOT NULL,
91 name TEXT NOT NULL,
92 bytes INTEGER NOT NULL,
93 PRIMARY KEY (repo_id, name)
94 );
95 CREATE TABLE IF NOT EXISTS repo_commits (
96 repo_id TEXT NOT NULL,
97 pos INTEGER NOT NULL,
98 sha TEXT NOT NULL,
99 subject TEXT NOT NULL,
100 author TEXT NOT NULL,
101 when_at INTEGER NOT NULL,
102 PRIMARY KEY (repo_id, pos)
103 );
104 CREATE TABLE IF NOT EXISTS repo_tags (
105 repo_id TEXT NOT NULL,
106 name TEXT NOT NULL,
107 oid TEXT NOT NULL,
108 commit_sha TEXT NOT NULL,
109 annotated INTEGER NOT NULL,
110 tagger TEXT NOT NULL,
111 when_at INTEGER NOT NULL,
112 subject TEXT NOT NULL,
113 PRIMARY KEY (repo_id, name)
114 );`); err != nil {
115 db.Close()
116 return nil, err
117 }
37c01e5a
Keenan Nemetz
18d ago
118 if _, err := db.Exec(`
119 CREATE TABLE IF NOT EXISTS issues (
120 repo_id TEXT NOT NULL,
121 id TEXT NOT NULL,
122 title TEXT NOT NULL,
123 state TEXT NOT NULL,
124 author TEXT NOT NULL,
125 created INTEGER NOT NULL,
126 labels TEXT NOT NULL DEFAULT '',
127 comments INTEGER NOT NULL DEFAULT 0,
128 PRIMARY KEY (repo_id, id)
653e0cfc
Keenan Nemetz
18d ago
129 );`); err != nil {
130 db.Close()
131 return nil, err
132 }
c1e3e3ca
Keenan Nemetz
18d ago
133 // Body text is indexed so search does not have to touch git per query.
134 _, _ = db.Exec(`ALTER TABLE issues ADD COLUMN body TEXT NOT NULL DEFAULT ''`)
67dd8854
Keenan Nemetz
17d ago
135 // Assignees are denormalised onto the row the way labels are: the list page
136 // shows them without a join, and the filter table below is what queries use.
137 _, _ = db.Exec(`ALTER TABLE issues ADD COLUMN assignees TEXT NOT NULL DEFAULT ''`)
db4081ec
Keenan Nemetz
5d ago
138 // updated is when the metadata branch last touched the issue: an edit, a
139 // comment, or a state change. The branch keeps no such field — the commit
140 // carries the time — so the index derives it from `git log` and stores it,
141 // which is what lets the list sort by activity without walking history per
142 // page. The index is derived: a stale value is repaired by a rebuild.
143 _, _ = db.Exec(`ALTER TABLE issues ADD COLUMN updated INTEGER NOT NULL DEFAULT 0`)
c1e3e3ca
Keenan Nemetz
18d ago
144 if _, err := db.Exec(`
145 CREATE TABLE IF NOT EXISTS issue_labels (
146 repo_id TEXT NOT NULL,
147 issue_id TEXT NOT NULL,
148 label TEXT NOT NULL,
149 PRIMARY KEY (repo_id, issue_id, label)
150 );
67dd8854
Keenan Nemetz
17d ago
151 CREATE INDEX IF NOT EXISTS issue_labels_lookup ON issue_labels(repo_id, label);
152 CREATE TABLE IF NOT EXISTS issue_assignees (
153 repo_id TEXT NOT NULL,
154 issue_id TEXT NOT NULL,
155 assignee TEXT NOT NULL,
156 PRIMARY KEY (repo_id, issue_id, assignee)
157 );
158 CREATE INDEX IF NOT EXISTS issue_assignees_lookup ON issue_assignees(repo_id, assignee);`); err != nil {
159 db.Close()
160 return nil, err
161 }
e05f1c48
Keenan Nemetz
13d ago
162 // entity_numbers is the `#42` alias. It is derived from history
163 // (metadata.LoadNumbers), never written back to the metadata branch, and the
164 // primary key on the number is what makes a duplicate impossible rather than
165 // merely unlikely.
166 if _, err := db.Exec(`
167 CREATE TABLE IF NOT EXISTS entity_numbers (
168 repo_id TEXT NOT NULL,
169 number INTEGER NOT NULL,
170 kind TEXT NOT NULL,
171 id TEXT NOT NULL,
172 PRIMARY KEY (repo_id, number)
173 );
7f547ca3
Keenan Nemetz
10d ago
174 CREATE INDEX IF NOT EXISTS entity_numbers_id ON entity_numbers(repo_id, id);
175
176 CREATE TABLE IF NOT EXISTS requests (
177 repo_id TEXT NOT NULL,
178 id TEXT NOT NULL,
179 title TEXT NOT NULL,
180 state TEXT NOT NULL,
181 author TEXT NOT NULL,
182 created INTEGER NOT NULL,
183 base TEXT NOT NULL DEFAULT '',
184 head TEXT NOT NULL DEFAULT '',
185 head_ref TEXT NOT NULL DEFAULT '',
186 merged_base TEXT NOT NULL DEFAULT '',
187 merged_head TEXT NOT NULL DEFAULT '',
188 merged_by TEXT NOT NULL DEFAULT '',
189 merged_at INTEGER NOT NULL DEFAULT 0,
190 body TEXT NOT NULL DEFAULT '',
191 comments INTEGER NOT NULL DEFAULT 0,
192 PRIMARY KEY (repo_id, id)
193 );
122cbe4f
Keenan Nemetz
7d ago
194 -- A push asks whether any open request follows the branch it moved, so that a
195 -- branch nobody is proposing from costs nothing.
196 CREATE INDEX IF NOT EXISTS requests_head_ref ON requests(repo_id, head_ref);
7f547ca3
Keenan Nemetz
10d ago
197 CREATE TABLE IF NOT EXISTS request_closes (
198 repo_id TEXT NOT NULL,
199 request_id TEXT NOT NULL,
200 issue_id TEXT NOT NULL,
9d8dd1bc
Keenan Nemetz
10d ago
201 -- position keeps the order the request's file lists them in, so a rebuilt
202 -- row answers with the same list the branch does.
203 position INTEGER NOT NULL DEFAULT 0,
7f547ca3
Keenan Nemetz
10d ago
204 PRIMARY KEY (repo_id, request_id, issue_id)
205 );
206 CREATE INDEX IF NOT EXISTS request_closes_issue ON request_closes(repo_id, issue_id);
207 CREATE TABLE IF NOT EXISTS request_approvals (
208 repo_id TEXT NOT NULL,
209 request_id TEXT NOT NULL,
210 principal TEXT NOT NULL,
211 head TEXT NOT NULL,
212 base TEXT NOT NULL,
213 created INTEGER NOT NULL DEFAULT 0,
214 PRIMARY KEY (repo_id, request_id, principal)
215 );
216
217 -- comments holds the discussion on both kinds of entity. The entity row carries
218 -- the count a list shows; these rows are what search reads and what a future
219 -- thread renderer can page through.
220 CREATE TABLE IF NOT EXISTS comments (
221 repo_id TEXT NOT NULL,
222 kind TEXT NOT NULL,
223 parent TEXT NOT NULL,
224 id TEXT NOT NULL,
225 author TEXT NOT NULL,
226 created INTEGER NOT NULL,
227 body TEXT NOT NULL DEFAULT '',
228 reply_to TEXT NOT NULL DEFAULT '',
229 deleted INTEGER NOT NULL DEFAULT 0,
230 path TEXT NOT NULL DEFAULT '',
231 side TEXT NOT NULL DEFAULT '',
232 line INTEGER NOT NULL DEFAULT 0,
233 blob TEXT NOT NULL DEFAULT '',
234 PRIMARY KEY (repo_id, kind, parent, id)
235 );
236 CREATE INDEX IF NOT EXISTS comments_parent ON comments(repo_id, kind, parent);
237
238 -- artifacts are the release pointers: releases/<tag>/<name> on the metadata
239 -- branch. The size is the pointer's, which is the artifact's real size, so a
240 -- release page lists its files without reading the branch.
241 CREATE TABLE IF NOT EXISTS artifacts (
242 repo_id TEXT NOT NULL,
243 tag TEXT NOT NULL,
244 name TEXT NOT NULL,
245 oid TEXT NOT NULL,
246 size INTEGER NOT NULL DEFAULT 0,
247 PRIMARY KEY (repo_id, tag, name)
4d938fe8
Keenan Nemetz
10d ago
248 );
249
250 -- state_changes is the closed/reopened/merged history the activity feed shows.
251 -- It is derived from the metadata commit subjects, and keyed by the commit so a
252 -- refresh that sees the same range twice cannot duplicate an event.
253 CREATE TABLE IF NOT EXISTS state_changes (
254 repo_id TEXT NOT NULL,
255 sha TEXT NOT NULL,
256 kind TEXT NOT NULL,
257 id TEXT NOT NULL,
258 state TEXT NOT NULL,
259 actor TEXT NOT NULL,
260 at INTEGER NOT NULL,
261 -- ordinal is the order the commits entered the branch. Commit times are
262 -- seconds, so two changes in one second need this to keep their order.
263 ordinal INTEGER NOT NULL DEFAULT 0,
264 PRIMARY KEY (repo_id, sha)
265 );
bca9ec24
Keenan Nemetz
10d ago
266 CREATE INDEX IF NOT EXISTS state_changes_recent ON state_changes(repo_id, ordinal DESC);
267
268 -- events is the activity feed as rows: one per issue, change and release, and
269 -- one per comment, approval and state change. It is the read model the feed
270 -- pages paginate; the row carries the title and assignees the feed shows, and
271 -- ids rather than URLs so a repository rename cannot leave a dead link.
272 CREATE TABLE IF NOT EXISTS events (
273 repo_id TEXT NOT NULL,
274 id TEXT NOT NULL,
275 kind TEXT NOT NULL, -- issue | change | release
276 type TEXT NOT NULL, -- open | comment | approval | state | release
277 at INTEGER NOT NULL,
278 actor TEXT NOT NULL,
279 title TEXT NOT NULL,
280 assignees TEXT NOT NULL DEFAULT '',
281 entity_id TEXT NOT NULL,
282 parent_id TEXT NOT NULL DEFAULT '',
283 comment_id TEXT NOT NULL DEFAULT '',
284 state TEXT NOT NULL DEFAULT '',
285 PRIMARY KEY (repo_id, id)
286 );
287 -- The feed's query: one repository, newest first. The id breaks a tie inside a
288 -- second, so a page boundary cannot repeat or skip an event.
289 CREATE INDEX IF NOT EXISTS events_repo ON events(repo_id, at DESC, id DESC);
290 -- The instance feed orders across repositories; this index lets SQLite scan in
291 -- time order and filter by repository rather than sorting every visible event.
292 CREATE INDEX IF NOT EXISTS events_time ON events(at DESC, id DESC);
a6b42433
Keenan Nemetz
8d ago
293 CREATE INDEX IF NOT EXISTS events_entity ON events(repo_id, entity_id);
294 -- A profile reads one account's events across the repositories the viewer can
295 -- read. Without this the page would scan every repository's history and filter,
296 -- which is the cost the (repository, time) index exists to avoid.
297 CREATE INDEX IF NOT EXISTS events_actor ON events(actor, at DESC, id DESC);`); err != nil {
6b8f84c4
Keenan Nemetz
10d ago
298 db.Close()
299 return nil, err
300 }
7cc36ba0
Keenan Nemetz
5d ago
301 // updated is when the metadata branch last touched the request, derived from
302 // the commits that changed its directory. It is the same disposable shape as
303 // an issue's: a stale value is repaired by a rebuild.
304 _, _ = db.Exec(`ALTER TABLE requests ADD COLUMN updated INTEGER NOT NULL DEFAULT 0`)
6b8f84c4
Keenan Nemetz
10d ago
305 // search is the universal search surface: one row for every issue, change
306 // request and comment, with the text FTS5 tokenizes. The first four columns are
307 // UNINDEXED because they identify the result rather than match it; title and
308 // body are what a query looks at. FTS5 is a build choice (the bundled SQLite
309 // needs -tags sqlite_fts5, see README.md), so its absence is reported as that
310 // rather than as a bare "no such module".
311 if _, err := db.Exec(`
6762a520
Keenan Nemetz
10d ago
312 CREATE VIRTUAL TABLE IF NOT EXISTS search USING fts5(
313 repo_id UNINDEXED,
314 kind UNINDEXED,
315 entity_id UNINDEXED,
316 parent_id UNINDEXED,
317 title,
318 body,
319 author UNINDEXED,
320 tokenize = 'unicode61 remove_diacritics 2'
7f547ca3
Keenan Nemetz
10d ago
321 );`); err != nil {
e05f1c48
Keenan Nemetz
13d ago
322 db.Close()
6b8f84c4
Keenan Nemetz
10d ago
323 return nil, fmt.Errorf("index: cannot create the search table: SQLite has no FTS5 module (build with -tags sqlite_fts5): %w", err)
e05f1c48
Keenan Nemetz
13d ago
324 }
67dd8854
Keenan Nemetz
17d ago
325 if err := s.migrate(); err != nil {
c1e3e3ca
Keenan Nemetz
18d ago
326 db.Close()
327 return nil, err
328 }
653e0cfc
Keenan Nemetz
18d ago
329 return s, nil
330 }
331
67dd8854
Keenan Nemetz
17d ago
332 // schemaVersion is the shape the rows in this file were derived from. It is not
7f547ca3
Keenan Nemetz
10d ago
333 // a migration chain: the index is disposable, so a bump drops the derived rows
334 // and leaves every repo with an empty issues_sha, which makes the next read
335 // rebuild that repo from git. A stale row is worse than a rebuild, and a rebuild is the
67dd8854
Keenan Nemetz
17d ago
336 // operation the index already has to survive.
7cc36ba0
Keenan Nemetz
5d ago
337 const schemaVersion = 14
67dd8854
Keenan Nemetz
17d ago
338
339 func (s *Store) migrate() error {
340 var v int
341 if err := s.db.QueryRow(`PRAGMA user_version`).Scan(&v); err != nil {
342 return err
343 }
344 if v == schemaVersion {
345 return nil
346 }
347 if _, err := s.db.Exec(`
348 DELETE FROM issues;
349 DELETE FROM issue_labels;
350 DELETE FROM issue_assignees;
e05f1c48
Keenan Nemetz
13d ago
351 DELETE FROM entity_numbers;
7f547ca3
Keenan Nemetz
10d ago
352 DELETE FROM requests;
353 DELETE FROM request_closes;
354 DELETE FROM request_approvals;
355 DELETE FROM comments;
356 DELETE FROM artifacts;
4d938fe8
Keenan Nemetz
10d ago
357 DELETE FROM state_changes;
bca9ec24
Keenan Nemetz
10d ago
358 DELETE FROM events;
2a8f7cef
Keenan Nemetz
10d ago
359 DELETE FROM repo_languages;
360 DELETE FROM repo_commits;
a32a859a
Keenan Nemetz
5d ago
361 DROP TABLE IF EXISTS repo_contributors;
2a8f7cef
Keenan Nemetz
10d ago
362 DELETE FROM repo_tags;
6762a520
Keenan Nemetz
10d ago
363 DELETE FROM search;
8dc56e4e
Keenan Nemetz
10d ago
364 UPDATE repos SET issues_sha = '', events_count = 0,
365 head_sha = '', head_subject = '', head_author = '', head_when = 0,
366 language = '', lang_class = '';`); err != nil {
67dd8854
Keenan Nemetz
17d ago
367 return err
368 }
369 _, err := s.db.Exec(fmt.Sprintf(`PRAGMA user_version = %d`, schemaVersion))
370 return err
371 }
372
37c01e5a
Keenan Nemetz
18d ago
373 // IssueRow is the indexed view of an issue: enough to list and link, not enough
374 // to render. The detail page reads the issue from git so bodies are never stale.
375 type IssueRow struct {
67dd8854
Keenan Nemetz
17d ago
376 ID string
377 Title string
378 State string
379 Author string
380 Created time.Time
381 Labels []string
382 Assignees []string
383 Comments int
db4081ec
Keenan Nemetz
5d ago
384 // Updated is when the metadata branch last touched the issue: the newest
385 // commit over its directory, which covers an edit, a comment, a label change,
386 // and a close or reopen. It is at least Created, so a freshly filed issue
387 // reads as just updated rather than as never touched.
388 Updated time.Time
e05f1c48
Keenan Nemetz
13d ago
389 // Number is the `#42` alias from entity_numbers, or 0 when the branch has not
390 // been indexed yet. It is a display alias; ID remains the identity.
391 Number int
37c01e5a
Keenan Nemetz
18d ago
392 }
393
653e0cfc
Keenan Nemetz
18d ago
394 func (s *Store) Close() error { return s.db.Close() }
395
396 // Rebuild discards the index and reconstructs it from the repo store. This is
397 // the operation that proves the index is derived and not authoritative.
398 func (s *Store) Rebuild(st *repo.Store) (int, error) {
399 repos, err := st.List()
400 if err != nil {
401 return 0, err
402 }
403 tx, err := s.db.Begin()
404 if err != nil {
405 return 0, err
406 }
407 defer tx.Rollback()
408 if _, err := tx.Exec(`DELETE FROM repos`); err != nil {
409 return 0, err
410 }
7f547ca3
Keenan Nemetz
10d ago
411 // A rebuild is a replacement, not a merge: rows for a repository that no
412 // longer exists must not survive it.
413 for _, table := range []string{
414 "issues", "issue_labels", "issue_assignees", "requests", "request_closes",
bca9ec24
Keenan Nemetz
10d ago
415 "request_approvals", "comments", "artifacts", "state_changes", "events", "search", "entity_numbers",
a32a859a
Keenan Nemetz
5d ago
416 "repo_languages", "repo_commits", "repo_tags",
7f547ca3
Keenan Nemetz
10d ago
417 } {
418 if _, err := tx.Exec(`DELETE FROM ` + table); err != nil {
419 return 0, err
420 }
421 }
653e0cfc
Keenan Nemetz
18d ago
422 for _, m := range repos {
423 e, err := entryFor(m)
424 if err != nil {
425 return 0, err
426 }
427 if err := insert(tx, e); err != nil {
428 return 0, err
429 }
430 }
431 if err := tx.Commit(); err != nil {
432 return 0, err
433 }
7f547ca3
Keenan Nemetz
10d ago
434 // Derived rows are indexed per repo after the repo rows exist, because every
435 // table is keyed by repo id.
37c01e5a
Keenan Nemetz
18d ago
436 for _, m := range repos {
7f547ca3
Keenan Nemetz
10d ago
437 if err := s.IndexRepo(m); err != nil {
37c01e5a
Keenan Nemetz
18d ago
438 return 0, err
439 }
440 }
653e0cfc
Keenan Nemetz
18d ago
441 return len(repos), nil
442 }
443
444 // Upsert refreshes one repo without a full rebuild.
445 func (s *Store) Upsert(m *repo.Meta) error {
446 e, err := entryFor(m)
447 if err != nil {
448 return err
449 }
450 return insert(s.db, e)
451 }
452
453 func (s *Store) Remove(id string) error {
2a8f7cef
Keenan Nemetz
10d ago
454 // A removed repository must leave no rows behind: every table is keyed by
455 // repo id and nothing else would ever delete them.
456 tx, err := s.db.Begin()
457 if err != nil {
458 return err
459 }
460 defer tx.Rollback()
461 for _, table := range []string{
462 "issues", "issue_labels", "issue_assignees", "requests", "request_closes",
463 "request_approvals", "comments", "artifacts", "state_changes", "events", "search",
a32a859a
Keenan Nemetz
5d ago
464 "entity_numbers", "repo_languages", "repo_commits", "repo_tags",
2a8f7cef
Keenan Nemetz
10d ago
465 } {
466 if _, err := tx.Exec(`DELETE FROM `+table+` WHERE repo_id=?`, id); err != nil {
467 return err
468 }
469 }
470 if _, err := tx.Exec(`DELETE FROM repos WHERE id=?`, id); err != nil {
471 return err
472 }
473 return tx.Commit()
653e0cfc
Keenan Nemetz
18d ago
474 }
475
476 func (s *Store) List() ([]Entry, error) {
8dc56e4e
Keenan Nemetz
10d ago
477 rows, err := s.db.Query(`SELECT id, name, description, default_branch, created, acl_sha, acl_principals, acl_default,
478 head_sha, head_subject, head_author, head_when, language, lang_class
479 FROM repos ORDER BY name`)
653e0cfc
Keenan Nemetz
18d ago
480 if err != nil {
481 return nil, err
482 }
483 defer rows.Close()
484 var out []Entry
485 for rows.Next() {
486 var e Entry
487 var created int64
8dc56e4e
Keenan Nemetz
10d ago
488 if err := rows.Scan(&e.ID, &e.Name, &e.Description, &e.DefaultBranch, &created, &e.ACLSHA, &e.ACLPrincipals, &e.ACLDefault,
489 &e.HeadSHA, &e.HeadSubject, &e.HeadAuthor, &e.HeadWhen, &e.Language, &e.LangClass); err != nil {
653e0cfc
Keenan Nemetz
18d ago
490 return nil, err
491 }
492 e.Created = time.Unix(created, 0).UTC()
493 out = append(out, e)
494 }
495 return out, rows.Err()
496 }
497
498 func (s *Store) Get(name string) (*Entry, error) {
499 var e Entry
500 var created int64
8dc56e4e
Keenan Nemetz
10d ago
501 err := s.db.QueryRow(`SELECT id, name, description, default_branch, created, acl_sha, acl_principals, acl_default,
502 head_sha, head_subject, head_author, head_when, language, lang_class
503 FROM repos WHERE name=?`, name).
504 Scan(&e.ID, &e.Name, &e.Description, &e.DefaultBranch, &created, &e.ACLSHA, &e.ACLPrincipals, &e.ACLDefault,
505 &e.HeadSHA, &e.HeadSubject, &e.HeadAuthor, &e.HeadWhen, &e.Language, &e.LangClass)
653e0cfc
Keenan Nemetz
18d ago
506 if errors.Is(err, sql.ErrNoRows) {
507 return nil, osErrNotExist
508 }
509 if err != nil {
510 return nil, err
511 }
512 e.Created = time.Unix(created, 0).UTC()
513 return &e, nil
514 }
515
516 var osErrNotExist = fmt.Errorf("index: repo not found")
517
518 func entryFor(m *repo.Meta) (Entry, error) {
519 e := Entry{
520 ID: m.ID,
521 Name: m.Name,
522 Description: m.Description,
523 DefaultBranch: m.DefaultBranch,
524 Created: m.Created,
525 }
526 a, err := m.ACL()
527 if err != nil {
528 return e, err
529 }
530 e.ACLDefault = a.Default
531 e.ACLPrincipals = len(a.Entries)
ec5c519c
Keenan Nemetz
17d ago
532 // A digest rather than a commit id: the rules live in the instance database now, so
533 // the thing worth recording is which version of them this row was built from.
534 if raw, err := a.Bytes(); err == nil {
535 sum := sha256.Sum256(raw)
536 e.ACLSHA = hex.EncodeToString(sum[:])
653e0cfc
Keenan Nemetz
18d ago
537 }
538 return e, nil
539 }
540
541 type execer interface {
542 Exec(query string, args ...any) (sql.Result, error)
543 }
544
545 func insert(x execer, e Entry) error {
546 _, err := x.Exec(`INSERT INTO repos(id, name, description, default_branch, created, acl_sha, acl_principals, acl_default)
547 VALUES(?,?,?,?,?,?,?,?)
548 ON CONFLICT(id) DO UPDATE SET
549 name=excluded.name, description=excluded.description, default_branch=excluded.default_branch,
550 acl_sha=excluded.acl_sha, acl_principals=excluded.acl_principals, acl_default=excluded.acl_default`,
551 e.ID, e.Name, e.Description, e.DefaultBranch, e.Created.Unix(), e.ACLSHA, e.ACLPrincipals, e.ACLDefault)
552 return err
553 }