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.
Store the overview's summaries when a push lands
2a8f7cef47240564179a45126863d1ccd8d9d362 · Keenan Nemetz · 10d ago · parent 8dc56e4e
Store the overview's summaries when a push lands The repository overview walked git on every read: `rev-parse` for the tip, `log` for the last five commits, `for-each-ref` for tags, `ls-tree` plus `shortlog` for languages and contributors, and a `cat-file` for the README. All of those describe a ref except the README, and a ref moves only when a push lands, so the push now records them. - Schema v10 adds `repo_languages`, `repo_commits`, `repo_contributors` and `repo_tags`. - `RefreshHead` stores the tip's newest commits and its language breakdown from one `ls-tree -r -l`, and returns whether the tip moved. - `RefreshContributors` stores the top authors; the hook calls it only when the tip changed, never on a metadata or tag push. - `RefreshTags` stores the tag rows beside the release events it already rewrote, and `IndexRepo` now refreshes tags and contributors too, so a rebuild restores everything a push would have written. - The overview reads those rows when the recorded head is the branch tip (`RefCommit` is a loose-ref file read) and falls back to git when it is not, so a missed hook shows the correct page rather than a stale one. If one stored section is empty on a repository with commits, that section alone is re-read from git. - The sidebar asks SQL for five open issues and changes instead of sorting the whole open queue in memory. - The rendered README is cached by repository, commit and ref under a byte budget, so the overview's last git spawn is gone after the first read of a commit. - The language fallback reads one `ls-tree -r -l` instead of `ls-tree` followed by a `cat-file --batch-check`. - `Remove` deletes every derived row for a repository, not only its row. Load harness at concurrency 4: overview goes from 473 to about 740 req/s, p50 from 8 ms to 5 ms, and the server runs no git child process for the warm requests.
forge/index/index.go +65−3
| @@ -83,6 +83,45 @@ CREATE TABLE IF NOT EXISTS repos ( | |||
| 83 | 83 | } { | |
| 84 | 84 | _, _ = db.Exec(`ALTER TABLE repos ADD COLUMN ` + col) | |
| 85 | 85 | } | |
| 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_contributors ( | ||
| 105 | repo_id TEXT NOT NULL, | ||
| 106 | pos INTEGER NOT NULL, | ||
| 107 | name TEXT NOT NULL, | ||
| 108 | commits INTEGER NOT NULL, | ||
| 109 | PRIMARY KEY (repo_id, pos) | ||
| 110 | ); | ||
| 111 | CREATE TABLE IF NOT EXISTS repo_tags ( | ||
| 112 | repo_id TEXT NOT NULL, | ||
| 113 | name TEXT NOT NULL, | ||
| 114 | oid TEXT NOT NULL, | ||
| 115 | commit_sha TEXT NOT NULL, | ||
| 116 | annotated INTEGER NOT NULL, | ||
| 117 | tagger TEXT NOT NULL, | ||
| 118 | when_at INTEGER NOT NULL, | ||
| 119 | subject TEXT NOT NULL, | ||
| 120 | PRIMARY KEY (repo_id, name) | ||
| 121 | );`); err != nil { | ||
| 122 | db.Close() | ||
| 123 | return nil, err | ||
| 124 | } | ||
| 86 | 125 | if _, err := db.Exec(` | |
| 87 | 126 | CREATE TABLE IF NOT EXISTS issues ( | |
| 88 | 127 | repo_id TEXT NOT NULL, | |
| @@ -289,7 +328,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS search USING fts5( | |||
| 289 | 328 | // and leaves every repo with an empty issues_sha, which makes the next read | |
| 290 | 329 | // rebuild that repo from git. A stale row is worse than a rebuild, and a rebuild is the | |
| 291 | 330 | // operation the index already has to survive. | |
| 292 | const schemaVersion = 9 | ||
| 331 | const schemaVersion = 10 | ||
| 293 | 332 | ||
| 294 | 333 | func (s *Store) migrate() error { | |
| 295 | 334 | var v int | |
| @@ -311,6 +350,10 @@ DELETE FROM comments; | |||
| 311 | 350 | DELETE FROM artifacts; | |
| 312 | 351 | DELETE FROM state_changes; | |
| 313 | 352 | DELETE FROM events; | |
| 353 | DELETE FROM repo_languages; | ||
| 354 | DELETE FROM repo_commits; | ||
| 355 | DELETE FROM repo_contributors; | ||
| 356 | DELETE FROM repo_tags; | ||
| 314 | 357 | DELETE FROM search; | |
| 315 | 358 | UPDATE repos SET issues_sha = '', events_count = 0, | |
| 316 | 359 | head_sha = '', head_subject = '', head_author = '', head_when = 0, | |
| @@ -359,6 +402,7 @@ func (s *Store) Rebuild(st *repo.Store) (int, error) { | |||
| 359 | 402 | for _, table := range []string{ | |
| 360 | 403 | "issues", "issue_labels", "issue_assignees", "requests", "request_closes", | |
| 361 | 404 | "request_approvals", "comments", "artifacts", "state_changes", "events", "search", "entity_numbers", | |
| 405 | "repo_languages", "repo_commits", "repo_contributors", "repo_tags", | ||
| 362 | 406 | } { | |
| 363 | 407 | if _, err := tx.Exec(`DELETE FROM ` + table); err != nil { | |
| 364 | 408 | return 0, err | |
| @@ -396,8 +440,26 @@ func (s *Store) Upsert(m *repo.Meta) error { | |||
| 396 | 440 | } | |
| 397 | 441 | ||
| 398 | 442 | func (s *Store) Remove(id string) error { | |
| 399 | _, err := s.db.Exec(`DELETE FROM repos WHERE id=?`, id) | ||
| 400 | return err | ||
| 443 | // A removed repository must leave no rows behind: every table is keyed by | ||
| 444 | // repo id and nothing else would ever delete them. | ||
| 445 | tx, err := s.db.Begin() | ||
| 446 | if err != nil { | ||
| 447 | return err | ||
| 448 | } | ||
| 449 | defer tx.Rollback() | ||
| 450 | for _, table := range []string{ | ||
| 451 | "issues", "issue_labels", "issue_assignees", "requests", "request_closes", | ||
| 452 | "request_approvals", "comments", "artifacts", "state_changes", "events", "search", | ||
| 453 | "entity_numbers", "repo_languages", "repo_commits", "repo_contributors", "repo_tags", | ||
| 454 | } { | ||
| 455 | if _, err := tx.Exec(`DELETE FROM `+table+` WHERE repo_id=?`, id); err != nil { | ||
| 456 | return err | ||
| 457 | } | ||
| 458 | } | ||
| 459 | if _, err := tx.Exec(`DELETE FROM repos WHERE id=?`, id); err != nil { | ||
| 460 | return err | ||
| 461 | } | ||
| 462 | return tx.Commit() | ||
| 401 | 463 | } | |
| 402 | 464 | ||
| 403 | 465 | func (s *Store) List() ([]Entry, error) { | |