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.
Record the default branch summary when a push lands
8dc56e4e258dd56e2c20776f80d61dbed4993f31 · Keenan Nemetz · 10d ago · parent 20442b59
Record the default branch summary when a push lands The landing card showed the default branch tip and the language it is mostly made of by running `symbolic-ref`, `rev-parse`, `log -1` and `ls-tree -r` per repository on every home page read. Those facts change only when a push moves the branch, so the push now records them. - `repos` carries head_sha, head_subject, head_author, head_when, language and lang_class, and `Index.Entry` returns them with the rest of the row. - `Store.RefreshHead` recomputes the summary and skips the tree walk when the recorded tip is still the branch tip, so a metadata or tag push costs one rev-parse. It runs from post-receive, from the rebuild, and after the default branch changes in settings. - The language mapping moved to `internal/lang`, shared by the web pages and the summary; `fileClass` and `languageFor` are now one table. - The home cards read the head, language, counts, ACL and newest event time from the index. The home page starts no git process at all; the all-git card builder stays as the fallback for a Web without an index. Load harness at concurrency 4 with three repositories: home goes from 243 to 749 req/s, p50 from 16 ms to 5 ms, and the server runs no git child process for those requests.
forge/index/index.go +41−7
| @@ -25,6 +25,14 @@ type Entry struct { | |||
| 25 | 25 | ACLSHA string | |
| 26 | 26 | ACLPrincipals int | |
| 27 | 27 | ACLDefault string | |
| 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 | ||
| 28 | 36 | } | |
| 29 | 37 | ||
| 30 | 38 | type Store struct{ db *sql.DB } | |
| @@ -46,7 +54,13 @@ CREATE TABLE IF NOT EXISTS repos ( | |||
| 46 | 54 | created INTEGER NOT NULL, | |
| 47 | 55 | acl_sha TEXT NOT NULL DEFAULT '', | |
| 48 | 56 | acl_principals INTEGER NOT NULL DEFAULT 0, | |
| 49 | acl_default TEXT NOT NULL DEFAULT 'none' | ||
| 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 '' | ||
| 50 | 64 | );`); err != nil { | |
| 51 | 65 | db.Close() | |
| 52 | 66 | return nil, err | |
| @@ -57,6 +71,18 @@ CREATE TABLE IF NOT EXISTS repos ( | |||
| 57 | 71 | // events_count is the number of feed events a repository has, so an | |
| 58 | 72 | // unfiltered pager gets its total without counting the table. | |
| 59 | 73 | _, _ = db.Exec(`ALTER TABLE repos ADD COLUMN events_count INTEGER NOT NULL DEFAULT 0`) | |
| 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 | } | ||
| 60 | 86 | if _, err := db.Exec(` | |
| 61 | 87 | CREATE TABLE IF NOT EXISTS issues ( | |
| 62 | 88 | repo_id TEXT NOT NULL, | |
| @@ -263,7 +289,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS search USING fts5( | |||
| 263 | 289 | // and leaves every repo with an empty issues_sha, which makes the next read | |
| 264 | 290 | // rebuild that repo from git. A stale row is worse than a rebuild, and a rebuild is the | |
| 265 | 291 | // operation the index already has to survive. | |
| 266 | const schemaVersion = 8 | ||
| 292 | const schemaVersion = 9 | ||
| 267 | 293 | ||
| 268 | 294 | func (s *Store) migrate() error { | |
| 269 | 295 | var v int | |
| @@ -286,7 +312,9 @@ DELETE FROM artifacts; | |||
| 286 | 312 | DELETE FROM state_changes; | |
| 287 | 313 | DELETE FROM events; | |
| 288 | 314 | DELETE FROM search; | |
| 289 | UPDATE repos SET issues_sha = '', events_count = 0;`); err != nil { | ||
| 315 | UPDATE repos SET issues_sha = '', events_count = 0, | ||
| 316 | head_sha = '', head_subject = '', head_author = '', head_when = 0, | ||
| 317 | language = '', lang_class = '';`); err != nil { | ||
| 290 | 318 | return err | |
| 291 | 319 | } | |
| 292 | 320 | _, err := s.db.Exec(fmt.Sprintf(`PRAGMA user_version = %d`, schemaVersion)) | |
| @@ -373,7 +401,9 @@ func (s *Store) Remove(id string) error { | |||
| 373 | 401 | } | |
| 374 | 402 | ||
| 375 | 403 | func (s *Store) List() ([]Entry, error) { | |
| 376 | rows, err := s.db.Query(`SELECT id, name, description, default_branch, created, acl_sha, acl_principals, acl_default FROM repos ORDER BY name`) | ||
| 404 | rows, err := s.db.Query(`SELECT id, name, description, default_branch, created, acl_sha, acl_principals, acl_default, | ||
| 405 | head_sha, head_subject, head_author, head_when, language, lang_class | ||
| 406 | FROM repos ORDER BY name`) | ||
| 377 | 407 | if err != nil { | |
| 378 | 408 | return nil, err | |
| 379 | 409 | } | |
| @@ -382,7 +412,8 @@ func (s *Store) List() ([]Entry, error) { | |||
| 382 | 412 | for rows.Next() { | |
| 383 | 413 | var e Entry | |
| 384 | 414 | var created int64 | |
| 385 | if err := rows.Scan(&e.ID, &e.Name, &e.Description, &e.DefaultBranch, &created, &e.ACLSHA, &e.ACLPrincipals, &e.ACLDefault); err != nil { | ||
| 415 | if err := rows.Scan(&e.ID, &e.Name, &e.Description, &e.DefaultBranch, &created, &e.ACLSHA, &e.ACLPrincipals, &e.ACLDefault, | ||
| 416 | &e.HeadSHA, &e.HeadSubject, &e.HeadAuthor, &e.HeadWhen, &e.Language, &e.LangClass); err != nil { | ||
| 386 | 417 | return nil, err | |
| 387 | 418 | } | |
| 388 | 419 | e.Created = time.Unix(created, 0).UTC() | |
| @@ -394,8 +425,11 @@ func (s *Store) List() ([]Entry, error) { | |||
| 394 | 425 | func (s *Store) Get(name string) (*Entry, error) { | |
| 395 | 426 | var e Entry | |
| 396 | 427 | var created int64 | |
| 397 | err := s.db.QueryRow(`SELECT id, name, description, default_branch, created, acl_sha, acl_principals, acl_default FROM repos WHERE name=?`, name). | ||
| 398 | Scan(&e.ID, &e.Name, &e.Description, &e.DefaultBranch, &created, &e.ACLSHA, &e.ACLPrincipals, &e.ACLDefault) | ||
| 428 | err := s.db.QueryRow(`SELECT id, name, description, default_branch, created, acl_sha, acl_principals, acl_default, | ||
| 429 | head_sha, head_subject, head_author, head_when, language, lang_class | ||
| 430 | FROM repos WHERE name=?`, name). | ||
| 431 | Scan(&e.ID, &e.Name, &e.Description, &e.DefaultBranch, &created, &e.ACLSHA, &e.ACLPrincipals, &e.ACLDefault, | ||
| 432 | &e.HeadSHA, &e.HeadSubject, &e.HeadAuthor, &e.HeadWhen, &e.Language, &e.LangClass) | ||
| 399 | 433 | if errors.Is(err, sql.ErrNoRows) { | |
| 400 | 434 | return nil, osErrNotExist | |
| 401 | 435 | } | |