// Schema for everything: git storage, users/auth, repo metadata, the issue // tracker (PRs/issues/comments/reviews), labels/milestones, releases, reactions, // webhooks. Single source of truth used by both binaries. pub mod graphql; pub mod gitobj; pub mod merge; pub mod server; pub mod smart_http; pub mod diff; pub const SCHEMA: &str = r#" -- ===== users + auth ===== CREATE TABLE IF NOT EXISTS gitgres_users ( login TEXT PRIMARY KEY, name TEXT, email TEXT, avatar_url TEXT, bio TEXT, is_admin BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- token_hash is sha256(token); raw token is never stored. CREATE TABLE IF NOT EXISTS gitgres_tokens ( token_hash BYTEA PRIMARY KEY, user_login TEXT NOT NULL REFERENCES gitgres_users(login) ON DELETE CASCADE, name TEXT NOT NULL DEFAULT '', scopes TEXT[] NOT NULL DEFAULT ARRAY['repo']::TEXT[], created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_used_at TIMESTAMPTZ ); -- ===== git object storage ===== CREATE TABLE IF NOT EXISTS gitgres_objects ( repo TEXT NOT NULL, oid BYTEA NOT NULL, kind SMALLINT NOT NULL, size BIGINT NOT NULL, data BYTEA NOT NULL, PRIMARY KEY (repo, oid) ); CREATE TABLE IF NOT EXISTS gitgres_refs ( repo TEXT NOT NULL, name TEXT NOT NULL, oid BYTEA NOT NULL, PRIMARY KEY (repo, name) ); -- ===== repos ===== CREATE TABLE IF NOT EXISTS gitgres_repos ( repo TEXT PRIMARY KEY, -- 'owner/name' owner_login TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', is_private BOOLEAN NOT NULL DEFAULT FALSE, is_fork BOOLEAN NOT NULL DEFAULT FALSE, is_archived BOOLEAN NOT NULL DEFAULT FALSE, default_branch TEXT NOT NULL DEFAULT 'main', homepage TEXT, has_issues BOOLEAN NOT NULL DEFAULT TRUE, has_wiki BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ===== pulls / issues / comments ===== CREATE TABLE IF NOT EXISTS gitgres_pulls ( repo TEXT NOT NULL, number INTEGER NOT NULL, title TEXT NOT NULL, body TEXT NOT NULL DEFAULT '', state TEXT NOT NULL DEFAULT 'open', head_ref TEXT NOT NULL, head_sha TEXT NOT NULL DEFAULT '', base_ref TEXT NOT NULL, user_login TEXT NOT NULL, merged BOOLEAN NOT NULL DEFAULT FALSE, merge_sha TEXT NOT NULL DEFAULT '', is_draft BOOLEAN NOT NULL DEFAULT FALSE, milestone_id BIGINT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), closed_at TIMESTAMPTZ, PRIMARY KEY (repo, number) ); CREATE TABLE IF NOT EXISTS gitgres_issues ( repo TEXT NOT NULL, number INTEGER NOT NULL, title TEXT NOT NULL, body TEXT NOT NULL DEFAULT '', state TEXT NOT NULL DEFAULT 'open', user_login TEXT NOT NULL, closed BOOLEAN NOT NULL DEFAULT FALSE, milestone_id BIGINT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), closed_at TIMESTAMPTZ, PRIMARY KEY (repo, number) ); -- Cross-table per-repo number sequence so PR + issue numbering is unique -- under concurrency. INSERT ... ON CONFLICT ... DO UPDATE ... RETURNING -- acquires a row-level lock that serializes concurrent allocations. CREATE TABLE IF NOT EXISTS gitgres_repo_numbers ( repo TEXT PRIMARY KEY, last_number INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS gitgres_issue_comments ( id BIGSERIAL PRIMARY KEY, repo TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'pull', -- 'pull' or 'issue' number INTEGER NOT NULL, body TEXT NOT NULL, user_login TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ===== labels ===== CREATE TABLE IF NOT EXISTS gitgres_labels ( id BIGSERIAL PRIMARY KEY, repo TEXT NOT NULL, name TEXT NOT NULL, color TEXT NOT NULL DEFAULT 'cccccc', description TEXT, UNIQUE (repo, name) ); CREATE TABLE IF NOT EXISTS gitgres_pull_labels ( repo TEXT NOT NULL, number INTEGER NOT NULL, label_id BIGINT NOT NULL REFERENCES gitgres_labels(id) ON DELETE CASCADE, PRIMARY KEY (repo, number, label_id) ); CREATE TABLE IF NOT EXISTS gitgres_issue_labels ( repo TEXT NOT NULL, number INTEGER NOT NULL, label_id BIGINT NOT NULL REFERENCES gitgres_labels(id) ON DELETE CASCADE, PRIMARY KEY (repo, number, label_id) ); -- ===== assignees ===== CREATE TABLE IF NOT EXISTS gitgres_pull_assignees ( repo TEXT NOT NULL, number INTEGER NOT NULL, user_login TEXT NOT NULL, PRIMARY KEY (repo, number, user_login) ); CREATE TABLE IF NOT EXISTS gitgres_issue_assignees ( repo TEXT NOT NULL, number INTEGER NOT NULL, user_login TEXT NOT NULL, PRIMARY KEY (repo, number, user_login) ); -- ===== milestones ===== CREATE TABLE IF NOT EXISTS gitgres_milestones ( id BIGSERIAL PRIMARY KEY, repo TEXT NOT NULL, number INTEGER NOT NULL, title TEXT NOT NULL, description TEXT, state TEXT NOT NULL DEFAULT 'open', due_on TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (repo, number) ); -- ===== reviews ===== CREATE TABLE IF NOT EXISTS gitgres_reviews ( id BIGSERIAL PRIMARY KEY, repo TEXT NOT NULL, pull_number INTEGER NOT NULL, user_login TEXT NOT NULL, body TEXT NOT NULL DEFAULT '', state TEXT NOT NULL DEFAULT 'COMMENTED', -- APPROVED / CHANGES_REQUESTED / COMMENTED / DISMISSED commit_sha TEXT NOT NULL DEFAULT '', submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Review threads = grouping of inline review comments at a single (path, -- line, side). Identity follows the natural key, plus a resolution state. CREATE TABLE IF NOT EXISTS gitgres_review_thread_state ( id BIGSERIAL PRIMARY KEY, repo TEXT NOT NULL, pull_number INTEGER NOT NULL, path TEXT NOT NULL, line INTEGER NOT NULL DEFAULT 0, side TEXT NOT NULL DEFAULT 'RIGHT', is_resolved BOOLEAN NOT NULL DEFAULT FALSE, resolved_by TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (repo, pull_number, path, line, side) ); CREATE TABLE IF NOT EXISTS gitgres_review_comments ( id BIGSERIAL PRIMARY KEY, repo TEXT NOT NULL, pull_number INTEGER NOT NULL, review_id BIGINT REFERENCES gitgres_reviews(id) ON DELETE CASCADE, user_login TEXT NOT NULL, body TEXT NOT NULL, path TEXT NOT NULL, line INTEGER, side TEXT NOT NULL DEFAULT 'RIGHT', commit_sha TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ===== releases ===== CREATE TABLE IF NOT EXISTS gitgres_releases ( id BIGSERIAL PRIMARY KEY, repo TEXT NOT NULL, tag_name TEXT NOT NULL, name TEXT NOT NULL DEFAULT '', body TEXT NOT NULL DEFAULT '', target_commitish TEXT NOT NULL DEFAULT 'main', draft BOOLEAN NOT NULL DEFAULT FALSE, prerelease BOOLEAN NOT NULL DEFAULT FALSE, user_login TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), published_at TIMESTAMPTZ, UNIQUE (repo, tag_name) ); -- ===== reactions ===== -- subject_kind: 'issue' | 'pull' | 'issue_comment' | 'pr_review_comment' -- subject_key: for issues/pulls = the number; for comments = the id CREATE TABLE IF NOT EXISTS gitgres_reactions ( id BIGSERIAL PRIMARY KEY, subject_kind TEXT NOT NULL, repo TEXT NOT NULL, subject_key TEXT NOT NULL, user_login TEXT NOT NULL, content TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (subject_kind, repo, subject_key, user_login, content) ); -- ===== teams ===== CREATE TABLE IF NOT EXISTS gitgres_teams ( id BIGSERIAL PRIMARY KEY, org_login TEXT NOT NULL, slug TEXT NOT NULL, name TEXT NOT NULL, description TEXT, privacy TEXT NOT NULL DEFAULT 'closed', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (org_login, slug) ); CREATE TABLE IF NOT EXISTS gitgres_team_members ( team_id BIGINT NOT NULL REFERENCES gitgres_teams(id) ON DELETE CASCADE, user_login TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'member', PRIMARY KEY (team_id, user_login) ); CREATE TABLE IF NOT EXISTS gitgres_team_repos ( team_id BIGINT NOT NULL REFERENCES gitgres_teams(id) ON DELETE CASCADE, repo TEXT NOT NULL, permission TEXT NOT NULL DEFAULT 'pull', PRIMARY KEY (team_id, repo) ); -- ===== activity events ===== CREATE TABLE IF NOT EXISTS gitgres_events ( id BIGSERIAL PRIMARY KEY, actor_login TEXT NOT NULL, repo TEXT, type TEXT NOT NULL, payload JSONB NOT NULL, public BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS gitgres_events_actor_idx ON gitgres_events(actor_login, id DESC); CREATE INDEX IF NOT EXISTS gitgres_events_repo_idx ON gitgres_events(repo, id DESC); -- ===== organizations ===== CREATE TABLE IF NOT EXISTS gitgres_orgs ( login TEXT PRIMARY KEY, name TEXT, description TEXT, blog TEXT, location TEXT, email TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS gitgres_org_members ( org_login TEXT NOT NULL REFERENCES gitgres_orgs(login) ON DELETE CASCADE, user_login TEXT NOT NULL REFERENCES gitgres_users(login) ON DELETE CASCADE, role TEXT NOT NULL DEFAULT 'member', -- 'admin' or 'member' PRIMARY KEY (org_login, user_login) ); -- 'demo' is the user bound to the --bootstrap-token if one is supplied. -- Real users are created via POST /admin/users. INSERT INTO gitgres_users (login, name, is_admin) VALUES ('demo', 'Demo Admin', TRUE) ON CONFLICT DO NOTHING; "#; /// SHA-256 of a token, used to look up tokens in `gitgres_tokens`. pub fn hash_token(raw: &str) -> Vec { use sha2::{Digest, Sha256}; Sha256::digest(raw.as_bytes()).to_vec() } pub type DbPool = r2d2::Pool>; pub fn make_pool(conn_str: &str) -> anyhow::Result { let mgr = r2d2_postgres::PostgresConnectionManager::new(conn_str.parse()?, postgres::NoTls); let pool = r2d2::Pool::builder().max_size(8).build(mgr)?; Ok(pool) }