kitgit

open #1 temp

feat/better-search → main

migrations/010_repo_mirrors.sql +0 −14
diff --git a/migrations/010_repo_mirrors.sql b/migrations/010_repo_mirrors.sql
deleted file mode 100644
index 6d5bb5c..0000000
--- a/migrations/010_repo_mirrors.sql
+++ /dev/null
@@ -1,15 +0,0 @@
--- Pull mirrors: fetch from a remote into the local bare repository.
-CREATE TABLE IF NOT EXISTS repo_mirrors (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    repo_id UUID NOT NULL UNIQUE REFERENCES repositories(id) ON DELETE CASCADE,
-    remote_url TEXT NOT NULL,
-    enabled BOOLEAN NOT NULL DEFAULT TRUE,
-    last_synced_at TIMESTAMPTZ,
-    last_error TEXT,
-    created_by UUID REFERENCES users(id) ON DELETE SET NULL,
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-    CONSTRAINT repo_mirrors_remote_url_nonempty CHECK (char_length(trim(remote_url)) > 0)
-);
-CREATE INDEX IF NOT EXISTS repo_mirrors_enabled_idx
-    ON repo_mirrors(enabled) WHERE enabled = TRUE;
migrations/011_user_audit_log.sql +0 −17
diff --git a/migrations/011_user_audit_log.sql b/migrations/011_user_audit_log.sql
deleted file mode 100644
index 13a5f1b..0000000
--- a/migrations/011_user_audit_log.sql
+++ /dev/null
@@ -1,18 +0,0 @@
--- Per-user security / account audit trail (separate from activity_events social feed).
-CREATE TABLE IF NOT EXISTS audit_log (
-    id BIGSERIAL PRIMARY KEY,
-    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-    actor_id UUID REFERENCES users(id) ON DELETE SET NULL,
-    action TEXT NOT NULL,
-    ip TEXT,
-    user_agent TEXT,
-    metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
-);
-
-CREATE INDEX IF NOT EXISTS audit_log_user_id_created_at_idx
-    ON audit_log(user_id, created_at DESC);
-CREATE INDEX IF NOT EXISTS audit_log_created_at_idx
-    ON audit_log(created_at DESC);
-CREATE INDEX IF NOT EXISTS audit_log_action_idx
-    ON audit_log(action);
migrations/012_user_theme.sql +0 −9
diff --git a/migrations/012_user_theme.sql b/migrations/012_user_theme.sql
deleted file mode 100644
index 7b57126..0000000
--- a/migrations/012_user_theme.sql
+++ /dev/null
@@ -1,10 +0,0 @@
--- Per-user appearance preference: light | dark | system
-
-ALTER TABLE users
-    ADD COLUMN IF NOT EXISTS theme TEXT NOT NULL DEFAULT 'system';
-
-ALTER TABLE users
-    DROP CONSTRAINT IF EXISTS users_theme_check;
-
-ALTER TABLE users
-    ADD CONSTRAINT users_theme_check CHECK (theme IN ('light', 'dark', 'system'));
migrations/013_deploy_keys.sql +0 −13
diff --git a/migrations/013_deploy_keys.sql b/migrations/013_deploy_keys.sql
deleted file mode 100644
index dd351b3..0000000
--- a/migrations/013_deploy_keys.sql
+++ /dev/null
@@ -1,14 +0,0 @@
--- Repo-scoped SSH deploy keys (read-only or read-write).
-CREATE TABLE IF NOT EXISTS deploy_keys (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    repo_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
-    name TEXT NOT NULL DEFAULT 'deploy',
-    public_key TEXT NOT NULL,
-    fingerprint TEXT NOT NULL UNIQUE,
-    read_only BOOLEAN NOT NULL DEFAULT TRUE,
-    created_by UUID REFERENCES users(id) ON DELETE SET NULL,
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
-);
-
-CREATE INDEX IF NOT EXISTS deploy_keys_repo_id_idx ON deploy_keys(repo_id);
-CREATE INDEX IF NOT EXISTS deploy_keys_fingerprint_idx ON deploy_keys(fingerprint);
\ No newline at end of file
migrations/014_webhooks.sql +0 −26
diff --git a/migrations/014_webhooks.sql b/migrations/014_webhooks.sql
deleted file mode 100644
index 880aed9..0000000
--- a/migrations/014_webhooks.sql
+++ /dev/null
@@ -1,27 +0,0 @@
--- Repository webhooks + delivery log
-
-CREATE TABLE webhooks (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    repo_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
-    url TEXT NOT NULL,
-    secret TEXT NOT NULL DEFAULT '',
-    events TEXT[] NOT NULL DEFAULT '{}',
-    active BOOLEAN NOT NULL DEFAULT TRUE,
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-    CONSTRAINT webhooks_url_http CHECK (url ~* '^https?://')
-);
-CREATE INDEX webhooks_repo_id_idx ON webhooks(repo_id);
-
-CREATE TABLE webhook_deliveries (
-    id BIGSERIAL PRIMARY KEY,
-    webhook_id UUID NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
-    event TEXT NOT NULL,
-    action TEXT NOT NULL DEFAULT '',
-    success BOOLEAN NOT NULL DEFAULT FALSE,
-    status_code INT,
-    error TEXT,
-    duration_ms INT,
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
-);
-CREATE INDEX webhook_deliveries_webhook_id_idx ON webhook_deliveries(webhook_id);
-CREATE INDEX webhook_deliveries_created_at_idx ON webhook_deliveries(created_at DESC);
\ No newline at end of file
migrations/016_notifications.sql +0 −19
diff --git a/migrations/016_notifications.sql b/migrations/016_notifications.sql
deleted file mode 100644
index 0439544..0000000
--- a/migrations/016_notifications.sql
+++ /dev/null
@@ -1,20 +0,0 @@
--- In-app notifications for watched repos and thread participants
-
-CREATE TABLE IF NOT EXISTS notifications (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-    kind TEXT NOT NULL,
-    title TEXT NOT NULL,
-    body TEXT NOT NULL DEFAULT '',
-    href TEXT NOT NULL DEFAULT '',
-    repo_id UUID REFERENCES repositories(id) ON DELETE SET NULL,
-    read_at TIMESTAMPTZ,
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
-);
-
-CREATE INDEX IF NOT EXISTS notifications_user_id_created_at_idx
-    ON notifications (user_id, created_at DESC);
-
-CREATE INDEX IF NOT EXISTS notifications_user_id_unread_idx
-    ON notifications (user_id)
-    WHERE read_at IS NULL;
migrations/017_labels_milestones.sql +0 −53
diff --git a/migrations/017_labels_milestones.sql b/migrations/017_labels_milestones.sql
deleted file mode 100644
index f4ce6e1..0000000
--- a/migrations/017_labels_milestones.sql
+++ /dev/null
@@ -1,55 +0,0 @@
--- Labels and milestones for issues and pull requests.
-
--- Replace the legacy text-only issue_labels table with proper label entities.
-DROP TABLE IF EXISTS issue_labels;
-
-CREATE TABLE labels (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    repo_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
-    name TEXT NOT NULL,
-    color TEXT NOT NULL DEFAULT '0969da',
-    description TEXT NOT NULL DEFAULT '',
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-    UNIQUE (repo_id, name),
-    CONSTRAINT labels_name_len CHECK (char_length(name) BETWEEN 1 AND 50),
-    CONSTRAINT labels_color_format CHECK (color ~ '^[0-9a-fA-F]{6}$')
-);
-CREATE INDEX labels_repo_id_idx ON labels(repo_id);
-
-CREATE TABLE milestones (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    repo_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
-    title TEXT NOT NULL,
-    description TEXT NOT NULL DEFAULT '',
-    due_on DATE,
-    state TEXT NOT NULL DEFAULT 'open' CHECK (state IN ('open', 'closed')),
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-    closed_at TIMESTAMPTZ,
-    UNIQUE (repo_id, title),
-    CONSTRAINT milestones_title_len CHECK (char_length(title) BETWEEN 1 AND 120)
-);
-CREATE INDEX milestones_repo_id_idx ON milestones(repo_id);
-CREATE INDEX milestones_repo_state_idx ON milestones(repo_id, state);
-
-ALTER TABLE issues
-    ADD COLUMN IF NOT EXISTS milestone_id UUID REFERENCES milestones(id) ON DELETE SET NULL;
-CREATE INDEX IF NOT EXISTS issues_milestone_id_idx ON issues(milestone_id);
-
-ALTER TABLE pull_requests
-    ADD COLUMN IF NOT EXISTS milestone_id UUID REFERENCES milestones(id) ON DELETE SET NULL;
-CREATE INDEX IF NOT EXISTS pull_requests_milestone_id_idx ON pull_requests(milestone_id);
-
-CREATE TABLE issue_labels (
-    issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE,

Large diffs are not rendered by default. Showing the first 50 of 61 lines. Show full diff

migrations/018_pull_reviews.sql +0 −9
diff --git a/migrations/018_pull_reviews.sql b/migrations/018_pull_reviews.sql
deleted file mode 100644
index bbc690f..0000000
--- a/migrations/018_pull_reviews.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-CREATE TABLE IF NOT EXISTS pull_reviews (
-  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-  pull_id UUID NOT NULL REFERENCES pull_requests(id) ON DELETE CASCADE,
-  reviewer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-  state TEXT NOT NULL CHECK (state IN ('approved','changes_requested','commented')),
-  body TEXT NOT NULL DEFAULT '',
-  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
-);
-CREATE INDEX IF NOT EXISTS pull_reviews_pull_id_idx ON pull_reviews(pull_id);
src/db/deploy_keys.rs +0 −98
diff --git a/src/db/deploy_keys.rs b/src/db/deploy_keys.rs
deleted file mode 100644
index d538af3..0000000
--- a/src/db/deploy_keys.rs
+++ /dev/null
@@ -1,98 +0,0 @@
-//! Repo-scoped SSH deploy keys (read-only or read-write).
-
-use anyhow::Result;
-use chrono::{DateTime, Utc};
-use serde::Serialize;
-use sqlx::{FromRow, PgPool};
-use uuid::Uuid;
-
-#[derive(Debug, Clone, FromRow, Serialize)]
-pub struct DeployKey {
-    pub id: Uuid,
-    pub repo_id: Uuid,
-    pub name: String,
-    pub public_key: String,
-    pub fingerprint: String,
-    pub read_only: bool,
-    pub created_by: Option<Uuid>,
-    pub created_at: DateTime<Utc>,
-}
-
-impl DeployKey {
-    pub fn permission_label(&self) -> &'static str {
-        if self.read_only {
-            "Read-only"
-        } else {
-            "Read-write"
-        }
-    }
-}
-
-pub async fn add_deploy_key(
-    pool: &PgPool,
-    repo_id: Uuid,
-    name: &str,
-    public_key: &str,
-    fingerprint: &str,
-    read_only: bool,
-    created_by: Option<Uuid>,
-) -> Result<DeployKey> {
-    let clash: Option<(Uuid,)> = sqlx::query_as(
-        "SELECT id FROM ssh_keys WHERE rtrim(fingerprint, '=') = rtrim($1, '=') LIMIT 1",
-    )
-    .bind(fingerprint)
-    .fetch_optional(pool)

Large diffs are not rendered by default. Showing the first 50 of 105 lines. Show full diff

src/db/mod.rs +0 −3
diff --git a/src/db/mod.rs b/src/db/mod.rs
index 2b7d249..8d84a74 100644
--- a/src/db/mod.rs
+++ b/src/db/mod.rs
@@ -1,9 +1,6 @@
-pub mod deploy_keys;
 pub mod models;
 pub mod queries;
 
-pub use deploy_keys::DeployKey;
-
 use anyhow::{Context, Result};
 use sqlx::postgres::PgPoolOptions;
 use sqlx::PgPool;
src/db/models.rs +0 −131
diff --git a/src/db/models.rs b/src/db/models.rs
index 42d2653..874e00c 100644
--- a/src/db/models.rs
+++ b/src/db/models.rs
@@ -17,21 +17,10 @@ pub struct User {
     pub is_suspended: bool,
     pub show_email: bool,
     pub vigilant_mode: bool,
-    /// `light`, `dark`, or `system` (follows prefers-color-scheme).
-    pub theme: String,
     pub created_at: DateTime<Utc>,
     pub updated_at: DateTime<Utc>,
 }
 
-impl User {
-    pub fn theme_pref(&self) -> &str {
-        match self.theme.as_str() {
-            "light" | "dark" => self.theme.as_str(),
-            _ => "system",
-        }
-    }
-}
-
 #[derive(Debug, Clone, FromRow)]
 pub struct UserMfa {
     pub user_id: Uuid,
@@ -112,19 +101,6 @@ pub struct BranchRule {
     pub created_at: DateTime<Utc>,
 }
 
-#[derive(Debug, Clone, FromRow, Serialize)]
-pub struct RepoMirror {
-    pub id: Uuid,
-    pub repo_id: Uuid,
-    pub remote_url: String,
-    pub enabled: bool,
-    pub last_synced_at: Option<DateTime<Utc>>,
-    pub last_error: Option<String>,
-    pub created_by: Option<Uuid>,
-    pub created_at: DateTime<Utc>,
-    pub updated_at: DateTime<Utc>,
-}
-
 #[derive(Debug, Clone, FromRow)]
 pub struct CommentReaction {
     pub comment_id: Uuid,
@@ -182,19 +158,6 @@ pub struct ActivityEvent {
     pub created_at: DateTime<Utc>,
 }
 

Large diffs are not rendered by default. Showing the first 50 of 195 lines. Show full diff

src/db/queries.rs +33 −1027
diff --git a/src/db/queries.rs b/src/db/queries.rs
index 8df7cab..340cf0a 100644
--- a/src/db/queries.rs
+++ b/src/db/queries.rs
@@ -1,7 +1,7 @@
 use super::models::*;
 use anyhow::Result;
 use chrono::{DateTime, NaiveDate, Utc};
-use sqlx::{FromRow, PgPool};
+use sqlx::PgPool;
 use uuid::Uuid;
 
 pub async fn upsert_user_from_oidc(
@@ -1326,29 +1326,6 @@ pub async fn user_owns_email(pool: &PgPool, user_id: Uuid, email: &str) -> Resul
     Ok(row.is_some())
 }
 
-/// Resolve a kitgit username from a commit author email (primary or secondary).
-pub async fn username_by_email(pool: &PgPool, email: &str) -> Result<Option<String>> {
-    let email = email.trim().to_ascii_lowercase();
-    if email.is_empty() {
-        return Ok(None);
-    }
-    let row: Option<(String,)> = sqlx::query_as(
-        r#"
-        SELECT u.username FROM users u
-        WHERE lower(u.email) = $1
-        UNION
-        SELECT u.username FROM users u
-        JOIN user_emails e ON e.user_id = u.id
-        WHERE lower(e.email) = $1
-        LIMIT 1
-        "#,
-    )
-    .bind(&email)
-    .fetch_optional(pool)
-    .await?;
-    Ok(row.map(|(u,)| u))
-}
-
 pub async fn list_all_gpg_keys(pool: &PgPool) -> Result<Vec<GpgKey>> {
     Ok(sqlx::query_as::<_, GpgKey>("SELECT * FROM gpg_keys ORDER BY created_at DESC")
         .fetch_all(pool)
@@ -1522,50 +1499,22 @@ pub async fn create_issue(
 }
 
 pub async fn list_issues(pool: &PgPool, repo_id: Uuid, state: Option<&str>) -> Result<Vec<Issue>> {
-    list_issues_filtered(pool, repo_id, state, None, None).await
-}
-

Large diffs are not rendered by default. Showing the first 50 of 1124 lines. Show full diff

src/git/blame.rs +0 −134
diff --git a/src/git/blame.rs b/src/git/blame.rs
deleted file mode 100644
index 1b55286..0000000
--- a/src/git/blame.rs
+++ /dev/null
@@ -1,134 +0,0 @@
-//! Blame and path history helpers (git2).
-
-use anyhow::{anyhow, Result};
-use git2::{BlameOptions, Commit, Oid, Repository as G2Repo, Sort};
-use std::collections::HashMap;
-use std::path::Path;
-
-use super::repo::{read_blob, resolve_ref, CommitInfo};
-
-/// One displayed line in a blame view.
-#[derive(Debug, Clone)]
-pub struct BlameLine {
-    pub line_no: usize,
-    pub content: String,
-    pub commit_id: String,
-    pub short_id: String,
-    pub author: String,
-    pub time: i64,
-    pub summary: String,
-    /// First line of a contiguous blame hunk (same final commit).
-    pub hunk_start: bool,
-}
-
-/// Commits that introduced a change to `path` (blob OID differs from parent), newest first.
-pub fn list_commits_for_path(
-    repo: &G2Repo,
-    reference: &str,
-    path: &str,
-    limit: usize,
-) -> Result<Vec<CommitInfo>> {
-    let oid = resolve_ref(repo, reference)?;
-    let mut revwalk = repo.revwalk()?;
-    revwalk.push(oid)?;
-    revwalk.set_sorting(Sort::TIME)?;
-    let mut out = Vec::new();
-    for id in revwalk {
-        let id = id?;
-        let c = repo.find_commit(id)?;
-        if !path_changed_in_commit(&c, path)? {
-            continue;
-        }
-        let author = c.author();
-        out.push(CommitInfo {
-            id: id.to_string(),

Large diffs are not rendered by default. Showing the first 50 of 140 lines. Show full diff

src/git/http.rs +0 −12
diff --git a/src/git/http.rs b/src/git/http.rs
index a816ab9..05a5e73 100644
--- a/src/git/http.rs
+++ b/src/git/http.rs
@@ -166,18 +166,6 @@ async fn post_push_hooks(
         .bind(repo.id)
         .execute(&state.pool)
         .await;
-    crate::webhooks::spawn_dispatch(
-        state.pool.clone(),
-        crate::webhooks::EVENT_PUSH,
-        "push".into(),
-        repo.clone(),
-        owner.to_string(),
-        Some(user.clone()),
-        serde_json::json!({
-            "ref": format!("refs/heads/{}", repo.default_branch),
-            "default_branch": repo.default_branch,
-        }),
-    );
     Ok(())
 }
 
src/git/mod.rs +0 −2
diff --git a/src/git/mod.rs b/src/git/mod.rs
index 9dc4ce8..b402e6d 100644
--- a/src/git/mod.rs
+++ b/src/git/mod.rs
@@ -1,4 +1,3 @@
-pub mod blame;
 pub mod http;
 pub mod languages;
 pub mod lfs;
@@ -6,6 +5,5 @@ pub mod repo;
 pub mod ssh;
 pub mod verify;
 
-pub use blame::{blame_file, list_commits_for_path};
 pub use repo::*;
 pub use verify::{extract_commit_signature, verify_commit_signature};
src/git/repo.rs +0 −84
diff --git a/src/git/repo.rs b/src/git/repo.rs
index 16cc994..06e53dd 100644
--- a/src/git/repo.rs
+++ b/src/git/repo.rs
@@ -79,83 +79,6 @@ pub fn rename_branch(repo: &G2Repo, old: &str, new: &str) -> Result<()> {
     Ok(())
 }
 
-/// Whether `url` is an allowed remote for pull-mirroring (no local/file paths).
-pub fn is_safe_mirror_url(url: &str) -> bool {
-    let url = url.trim();
-    if url.is_empty() || url.contains('\0') || url.contains('\n') || url.contains('\r') {
-        return false;
-    }
-    let lower = url.to_ascii_lowercase();
-    if lower.starts_with("file:")
-        || lower.starts_with('/')
-        || lower.starts_with("\\\\")
-        || (url.len() >= 2 && url.as_bytes()[1] == b':')
-    {
-        return false;
-    }
-    lower.starts_with("https://")
-        || lower.starts_with("http://")
-        || lower.starts_with("git://")
-        || lower.starts_with("ssh://")
-        || (url.contains('@') && url.contains(':') && !lower.contains("://"))
-}
-
-/// Fetch refs from `remote_url` into the existing bare repo (pull mirror).
-pub fn mirror_fetch(repos_root: &Path, owner: &str, name: &str, remote_url: &str) -> Result<()> {
-    if !is_safe_mirror_url(remote_url) {
-        return Err(anyhow!("unsupported or unsafe mirror URL"));
-    }
-    let path = bare_path(repos_root, owner, name);
-    if !path.exists() {
-        return Err(anyhow!("local bare repository missing"));
-    }
-    let path_str = path
-        .to_str()
-        .ok_or_else(|| anyhow!("repository path is not valid UTF-8"))?;
-    let url = remote_url.trim();
-
-    // Dedicated remote so we do not clobber a user's `origin`.
-    let _ = std::process::Command::new("git")
-        .args(["-C", path_str, "remote", "remove", "kitgit-mirror"])
-        .output();
-    let add = std::process::Command::new("git")
-        .args(["-C", path_str, "remote", "add", "kitgit-mirror", url])
-        .output()

Large diffs are not rendered by default. Showing the first 50 of 102 lines. Show full diff

src/git/ssh.rs +37 −104
diff --git a/src/git/ssh.rs b/src/git/ssh.rs
index 89f0474..95d2ba2 100644
--- a/src/git/ssh.rs
+++ b/src/git/ssh.rs
@@ -101,46 +101,28 @@ impl Server for SshServer {
             state: self.state.clone(),
             user_id: None,
             username: None,
-            deploy_key: None,
             stdin: None,
             receive_meta: None,
         }
     }
 }
 
-/// Authenticated via a repo-scoped deploy key (not a user key).
-struct DeployKeyAuth {
-    repo_id: Uuid,
-    read_only: bool,
-    created_by: Option<Uuid>,
-    name: String,
-}
-
 struct SshHandler {
     state: AppState,
     user_id: Option<Uuid>,
     username: Option<String>,
-    deploy_key: Option<DeployKeyAuth>,
     stdin: Option<Arc<Mutex<tokio::process::ChildStdin>>>,
     receive_meta: Option<(String, String)>,
 }
 
 impl SshHandler {
-    fn is_authenticated(&self) -> bool {
-        self.user_id.is_some() || self.deploy_key.is_some()
-    }
-
     /// Print `static/text.txt` (with `{user}` substituted) and close — no shell access.
     async fn greet_and_quit(
         &self,
         channel: ChannelId,
         session: &mut Session,
     ) -> Result<(), anyhow::Error> {
-        let username = self
-            .username
-            .as_deref()
-            .or_else(|| self.deploy_key.as_ref().map(|d| d.name.as_str()))
-            .unwrap_or("git");
+        let username = self.username.as_deref().unwrap_or("git");
         let path = self.state.config.static_dir.join("text.txt");

Large diffs are not rendered by default. Showing the first 50 of 217 lines. Show full diff

src/main.rs +0 −1
diff --git a/src/main.rs b/src/main.rs
index 9be6dec..2a913a5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,7 +8,6 @@ mod mfa;
 mod og;
 mod state;
 mod web;
-mod webhooks;
 
 use crate::auth::AuthState;
 use crate::config::Config;
src/web/mod.rs +7 −189
diff --git a/src/web/mod.rs b/src/web/mod.rs
index 2c3401a..6c6969c 100644
--- a/src/web/mod.rs
+++ b/src/web/mod.rs
@@ -4,74 +4,16 @@ pub mod templates;
 
 use crate::git;
 use crate::state::AppState;
-use axum::body::{Body, to_bytes};
 use axum::extract::{DefaultBodyLimit, Request};
-use axum::http::{header, HeaderValue, StatusCode};
+use axum::http::{header, StatusCode};
 use axum::middleware::{from_fn, Next};
 use axum::response::{IntoResponse, Response};
 use axum::routing::{get, post};
 use axum::Router;
-use std::time::{Duration, Instant};
 use tower_http::limit::RequestBodyLimitLayer;
 use tower_http::services::ServeDir;
 
-const RENDER_MS_MARKER: &[u8] = b"<!--kg-render-ms-->";
-
-fn format_render_duration(elapsed: Duration) -> String {
-    let ms = elapsed.as_secs_f64() * 1000.0;
-    if ms < 10.0 {
-        format!("{ms:.1}ms")
-    } else {
-        format!("{:.0}ms", ms.round())
-    }
-}
-
-fn inject_render_marker(html: &[u8], label: &str) -> Option<Vec<u8>> {
-    let pos = html
-        .windows(RENDER_MS_MARKER.len())
-        .position(|window| window == RENDER_MS_MARKER)?;
-    let mut out = Vec::with_capacity(html.len() - RENDER_MS_MARKER.len() + label.len());
-    out.extend_from_slice(&html[..pos]);
-    out.extend_from_slice(label.as_bytes());
-    out.extend_from_slice(&html[pos + RENDER_MS_MARKER.len()..]);
-    Some(out)
-}
-
-/// Measure HTML page handler time and fill the layout footer timing marker.
-async fn inject_render_timing(req: Request, next: Next) -> Response {
-    let start = Instant::now();
-    let res = next.run(req).await;
-    let elapsed = start.elapsed();
-
-    let is_html = res
-        .headers()

Large diffs are not rendered by default. Showing the first 50 of 315 lines. Show full diff

src/web/routes.rs +115 −1313
diff --git a/src/web/routes.rs b/src/web/routes.rs
index b99d0d7..2db68d1 100644
--- a/src/web/routes.rs
+++ b/src/web/routes.rs
@@ -20,7 +20,7 @@ use serde::Deserialize;
 use std::path::PathBuf;
 use uuid::Uuid;
 
-// ── helpers ──────────────────────────────────────────────────────────────────
+// ── helpers ──────────────────────────────────────────────────────────────────
 
 pub type AppResult<T> = Result<T, AppError>;
 
@@ -113,95 +113,6 @@ fn redirect_with_cookies(to: &str, cookies: Vec<HeaderValue>) -> Response {
     res
 }
 
-/// Best-effort client IP / User-Agent from reverse-proxy headers.
-pub fn request_client_meta(headers: &HeaderMap) -> (Option<String>, Option<String>) {
-    let ua = headers
-        .get(axum::http::header::USER_AGENT)
-        .and_then(|v| v.to_str().ok())
-        .map(|s| s.chars().take(512).collect::<String>());
-    let ip = headers
-        .get("x-forwarded-for")
-        .and_then(|v| v.to_str().ok())
-        .and_then(|s| s.split(',').next())
-        .map(|s| s.trim().to_string())
-        .filter(|s| !s.is_empty())
-        .or_else(|| {
-            headers
-                .get("x-real-ip")
-                .and_then(|v| v.to_str().ok())
-                .map(|s| s.trim().to_string())
-                .filter(|s| !s.is_empty())
-        });
-    (ip, ua)
-}
-
-/// Write an audit_log row; failures are logged and never fail the request.
-pub async fn record_audit(
-    state: &AppState,
-    headers: &HeaderMap,
-    user_id: Uuid,
-    actor_id: Option<Uuid>,
-    action: &str,
-    metadata: serde_json::Value,
-) {
-    let (ip, ua) = request_client_meta(headers);
-    if let Err(e) = queries::record_audit_log(

Large diffs are not rendered by default. Showing the first 50 of 2112 lines. Show full diff

src/web/routes_extra.rs +13 −599
diff --git a/src/web/routes_extra.rs b/src/web/routes_extra.rs
index b5767bf..712ab5f 100644
--- a/src/web/routes_extra.rs
+++ b/src/web/routes_extra.rs
@@ -1,15 +1,14 @@
 //! Social, account, branch, raw, and admin helpers.
 
 use super::routes::{
-    audit_entries_view, avatar_url_for, clone_urls, load_repo_context, record_audit,
-    redirect_see_other, require_login, AppError, AppResult,
+    avatar_url_for, load_repo_context, redirect_see_other, require_login, AppError, AppResult,
 };
 use crate::auth::{self, clear_session_cookie, hash_token, token_from_headers};
 use crate::db::queries;
 use crate::git;
 use crate::state::AppState;
 use crate::web::templates::*;
-use axum::extract::{Form, Path, Query, State};
+use axum::extract::{Form, Path, State};
 use axum::http::header::{CONTENT_DISPOSITION, CONTENT_TYPE, SET_COOKIE};
 use axum::http::{HeaderMap, HeaderValue, StatusCode};
 use axum::response::{IntoResponse, Response};
@@ -234,14 +233,13 @@ pub async fn branch_rule_delete(
 
 #[derive(Deserialize)]
 pub struct RenameBranchForm {
-    pub branch: String,
     pub new_name: String,
 }
 
 pub async fn branch_rename(
     State(state): State<AppState>,
     headers: HeaderMap,
-    Path((owner, repo)): Path<(String, String)>,
+    Path((owner, repo, branch)): Path<(String, String, String)>,
     Form(form): Form<RenameBranchForm>,
 ) -> AppResult<Response> {
     let (repository, _, viewer, access) =
@@ -250,38 +248,22 @@ pub async fn branch_rename(
     if !access.can_write() {
         return Err(AppError::forbidden());
     }
-    let branch = form.branch.trim();
-    if branch.is_empty() {
-        return Err(AppError::bad("branch required"));
-    }
     if branch == repository.default_branch {
         return Err(AppError::bad("cannot rename default branch here"));
     }
     let new_name = form.new_name.trim();

Large diffs are not rendered by default. Showing the first 50 of 818 lines. Show full diff

src/web/templates.rs +6 −237
diff --git a/src/web/templates.rs b/src/web/templates.rs
index f62af84..15b504a 100644
--- a/src/web/templates.rs
+++ b/src/web/templates.rs
@@ -1,9 +1,8 @@
 use crate::og::SocialMeta;
 use crate::db::models::{
-    Access, BranchRule, CommitDay, GpgKey, Issue, Label, Milestone, Notification, PullRequest,
-    Release, RepoMirror, Repository, SshKey, User, UserEmail,
+    Access, BranchRule, CommitDay, GpgKey, Issue, PullRequest, Release, Repository, SshKey, User,
+    UserEmail,
 };
-use crate::db::DeployKey;
 use askama::Template;
 use askama_web::WebTemplate;
 use chrono::{DateTime, Utc};
@@ -41,34 +40,6 @@ pub struct CommentView {
     pub reactions: Vec<ReactionView>,
 }
 
-pub struct PullReviewView {
-    pub id: Uuid,
-    pub reviewer: User,
-    pub avatar_url: String,
-    pub state: String,
-    pub body: String,
-    pub body_html: String,
-    pub created_at: DateTime<Utc>,
-}
-
-impl PullReviewView {
-    pub fn state_label(&self) -> &'static str {
-        match self.state.as_str() {
-            "approved" => "approved",
-            "changes_requested" => "changes requested",
-            _ => "commented",
-        }
-    }
-
-    pub fn state_class(&self) -> &'static str {
-        match self.state.as_str() {
-            "approved" => "kg-badge--approved",
-            "changes_requested" => "kg-badge--changes",
-            _ => "kg-badge--commented",
-        }
-    }
-}
-
 pub struct DiffFileView {
     pub path: String,

Large diffs are not rendered by default. Showing the first 50 of 410 lines. Show full diff

src/webhooks.rs +0 −189
diff --git a/src/webhooks.rs b/src/webhooks.rs
deleted file mode 100644
index f2af0e6..0000000
--- a/src/webhooks.rs
+++ /dev/null
@@ -1,189 +0,0 @@
-//! Repository webhook delivery (JSON POST + optional HMAC-SHA256).
-
-use crate::db::models::{Repository, User, Webhook};
-use crate::db::queries;
-use anyhow::Result;
-use hmac::{Hmac, Mac};
-use serde_json::{json, Value};
-use sha2::Sha256;
-use sqlx::PgPool;
-use std::time::Instant;
-use uuid::Uuid;
-
-type HmacSha256 = Hmac<Sha256>;
-
-pub const EVENT_PUSH: &str = "push";
-pub const EVENT_ISSUES: &str = "issues";
-pub const EVENT_PULL_REQUEST: &str = "pull_request";
-pub const EVENT_RELEASE: &str = "release";
-
-pub const ALL_EVENTS: &[&str] = &[EVENT_PUSH, EVENT_ISSUES, EVENT_PULL_REQUEST, EVENT_RELEASE];
-
-pub fn normalize_events(raw: &[String]) -> Vec<String> {
-    let mut out = Vec::new();
-    for e in raw {
-        let e = e.trim();
-        if ALL_EVENTS.contains(&e) && !out.iter().any(|x| x == e) {
-            out.push(e.to_string());
-        }
-    }
-    out
-}
-
-fn sign_body(secret: &str, body: &[u8]) -> Option<String> {
-    if secret.is_empty() {
-        return None;
-    }
-    let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).ok()?;
-    mac.update(body);
-    Some(format!(
-        "sha256={}",
-        hex::encode(mac.finalize().into_bytes())
-    ))
-}
-

Large diffs are not rendered by default. Showing the first 50 of 196 lines. Show full diff

static/brand.css +49 −525
diff --git a/static/brand.css b/static/brand.css
index 8f082ff..db813d2 100644
--- a/static/brand.css
+++ b/static/brand.css
@@ -1,8 +1,7 @@
 /* kitgit — brand theme
    ink on paper. no chrome. */
 
-:root,
-[data-theme="light"] {
+:root {
   --kg-bg: #ffffff;
   --kg-bg-raised: #f7f7f7;
   --kg-fg: #0a0a0a;
@@ -35,7 +34,6 @@
   color-scheme: light;
 }
 
-[data-theme="dark"],
 html.kg-dark {
   --kg-bg: #0a0a0a;
   --kg-bg-raised: #141414;
@@ -45,7 +43,6 @@ html.kg-dark {
   --kg-line: #262626;
   --kg-line-strong: #f5f5f5;
   --kg-link: #f5f5f5;
-  --kg-focus: #f5f5f5;
   --kg-danger: #ff4d5a;
   --kg-danger-bg: #2a1214;
   --kg-invert-bg: #f5f5f5;
@@ -54,26 +51,6 @@ html.kg-dark {
   color-scheme: dark;
 }
 
-@media (prefers-color-scheme: dark) {
-  [data-theme="system"] {
-    --kg-bg: #0a0a0a;
-    --kg-bg-raised: #141414;
-    --kg-fg: #f5f5f5;
-    --kg-muted: #8a8a8a;
-    --kg-faint: #5c5c5c;
-    --kg-line: #262626;
-    --kg-line-strong: #f5f5f5;
-    --kg-link: #f5f5f5;
-    --kg-focus: #f5f5f5;
-    --kg-danger: #ff4d5a;
-    --kg-danger-bg: #2a1214;
-    --kg-invert-bg: #f5f5f5;
-    --kg-invert-fg: #0a0a0a;
-    --kg-invert-muted: #525252;

Large diffs are not rendered by default. Showing the first 50 of 759 lines. Show full diff

static/icons/bell.svg +0 −1
diff --git a/static/icons/bell.svg b/static/icons/bell.svg
deleted file mode 100644
index 142388b..0000000
--- a/static/icons/bell.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/></svg>
templates/account_settings.html +7 −62
diff --git a/templates/account_settings.html b/templates/account_settings.html
index 2b8cbee..248b87f 100644
--- a/templates/account_settings.html
+++ b/templates/account_settings.html
@@ -20,28 +20,6 @@
   {% endif %}
 
   <div class="kg-settings-section" style="margin-top:1rem;border-top:0;padding-top:0">
-    <span class="kg-kicker">appearance</span>
-    <p class="kg-muted">Ink on paper by day, charcoal by night — or follow your system.</p>
-    <form class="kg-form" method="post" action="/settings/account/theme">
-      <label class="row">
-        <input type="radio" name="theme" value="light"{% if user.theme == "light" %} checked{% endif %} />
-        light
-      </label>
-      <label class="row">
-        <input type="radio" name="theme" value="dark"{% if user.theme == "dark" %} checked{% endif %} />
-        dark
-      </label>
-      <label class="row">
-        <input type="radio" name="theme" value="system"{% if user.theme != "light" && user.theme != "dark" %} checked{% endif %} />
-        system
-      </label>
-      <div class="kg-actions">
-        <button class="kg-btn" type="submit">save theme</button>
-      </div>
-    </form>
-  </div>
-
-  <div class="kg-settings-section">
     <span class="kg-kicker">username</span>
     <form class="kg-form" method="post" action="/settings/account/username">
       <label>
@@ -97,16 +75,11 @@
           {% if e.is_primary %}<span class="kg-badge">primary</span>{% endif %}
           {% if e.verified %}<span class="kg-badge">verified</span>{% endif %}
         </span>
-        <span class="kg-actions">
-          {% if !e.is_primary %}
-          <form method="post" action="/settings/account/emails/{{ e.id }}/primary">
-            <button class="kg-btn kg-btn--ghost" type="submit">make primary</button>
-          </form>
-          <form method="post" action="/settings/account/emails/{{ e.id }}/delete">
-            <button class="kg-btn kg-btn--danger-ghost" type="submit">remove</button>
-          </form>
-          {% endif %}
-        </span>
+        {% if !e.is_primary %}
+        <form method="post" action="/settings/account/emails/{{ e.id }}/delete">
+          <button class="kg-btn kg-btn--danger-ghost" type="submit">remove</button>

Large diffs are not rendered by default. Showing the first 50 of 101 lines. Show full diff

templates/admin.html +0 −1
diff --git a/templates/admin.html b/templates/admin.html
index 2723110..f16f5c8 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -104,7 +104,6 @@
         </span>
       </div>
       <div class="kg-admin-actions">
-        <a class="kg-btn kg-btn--ghost" href="/admin/users/{{ u.username }}/audit">audit</a>
         <form method="post" action="/admin/users/suspend" style="display:inline">
           <input type="hidden" name="user_id" value="{{ u.id }}" />
           {% if u.is_suspended %}
templates/admin_user_audit.html +0 −37
diff --git a/templates/admin_user_audit.html b/templates/admin_user_audit.html
deleted file mode 100644
index 5dc5a6b..0000000
--- a/templates/admin_user_audit.html
+++ /dev/null
@@ -1,37 +0,0 @@
-{% extends "layout.html" %}
-{% block title %}audit — {{ user.username }} — kitgit{% endblock %}
-{% block content %}
-<section class="kg-section">
-  <span class="kg-kicker">admin</span>
-  <h1 class="kg-title">Audit log · {{ user.username }}</h1>
-  <p class="kg-muted">
-    <a href="/admin">← back to admin</a>
-    · <a href="/{{ user.username }}">profile</a>
-  </p>
-
-  {% if audit_entries.is_empty() %}
-    <p class="kg-muted" style="margin-top:1.5rem">No audit events for this user.</p>
-  {% else %}
-    <ul class="kg-list" style="margin-top:1.5rem">
-      {% for e in audit_entries %}
-      <li>
-        <span>
-          <strong>{{ e.action_label }}</strong>
-          <code class="kg-meta">{{ e.action }}</code>
-          <span class="kg-meta"> · {{ e.created_at }}</span>
-          {% if !e.ip.is_empty() %}
-            <span class="kg-meta"> · {{ e.ip }}</span>
-          {% endif %}
-          {% if !e.user_agent.is_empty() %}
-            <span class="kg-meta"> · {{ e.user_agent }}</span>
-          {% endif %}
-          {% if !e.metadata.is_empty() %}
-            <span class="kg-meta"> · <code>{{ e.metadata }}</code></span>
-          {% endif %}
-        </span>
-      </li>
-      {% endfor %}
-    </ul>
-  {% endif %}
-</section>
-{% endblock %}
templates/explore.html +3 −3
diff --git a/templates/explore.html b/templates/explore.html
index 257b735..52b996b 100644
--- a/templates/explore.html
+++ b/templates/explore.html
@@ -62,7 +62,7 @@
                 <p class="kg-meta" style="margin:0.25rem 0 0">{{ item.repo.description }}</p>
               {% endif %}
             </div>
-            <span class="kg-meta">{{ item.repo.updated_at.format("%Y-%m-%d") }}</span>
+            <span class="kg-meta">{{ item.repo.updated_at }}</span>
           </li>
           {% endfor %}
         </ul>
@@ -113,7 +113,7 @@
                 {{ item.owner }}/{{ item.repo_name }}#{{ item.number }} {{ item.title }}
               </a>
             </div>
-            <span class="kg-meta">{{ item.updated_at.format("%Y-%m-%d") }}</span>
+            <span class="kg-meta">{{ item.updated_at }}</span>
           </li>
           {% endfor %}
         </ul>
@@ -138,7 +138,7 @@
                 {{ item.owner }}/{{ item.repo_name }}#{{ item.number }} {{ item.title }}
               </a>
             </div>
-            <span class="kg-meta">{{ item.updated_at.format("%Y-%m-%d") }}</span>
+            <span class="kg-meta">{{ item.updated_at }}</span>
           </li>
           {% endfor %}
         </ul>
templates/issue_new.html +0 −22
diff --git a/templates/issue_new.html b/templates/issue_new.html
index 5cabf33..0319760 100644
--- a/templates/issue_new.html
+++ b/templates/issue_new.html
@@ -20,28 +20,6 @@
       body
       <textarea name="body" rows="8"></textarea>
     </label>
-    {% if !labels.is_empty() %}
-    <fieldset class="kg-fieldset">
-      <legend>labels</legend>
-      {% for l in labels %}
-        <label class="row">
-          <input type="checkbox" name="label_id" value="{{ l.id }}" />
-          <span class="kg-label-chip" style="background:{{ l.bg_color() }};color:{{ l.text_color() }}">{{ l.name }}</span>
-        </label>
-      {% endfor %}
-    </fieldset>
-    {% endif %}
-    {% if !milestones.is_empty() %}
-    <label>
-      milestone
-      <select name="milestone_id">
-        <option value="">none</option>
-        {% for m in milestones %}
-          <option value="{{ m.id }}">{{ m.title }}</option>
-        {% endfor %}
-      </select>
-    </label>
-    {% endif %}
     <div class="kg-actions">
       <button class="kg-btn" type="submit">create</button>
       <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/issues">cancel</a>
templates/issue_view.html +36 −91
diff --git a/templates/issue_view.html b/templates/issue_view.html
index 42e80ee..2158d27 100644
--- a/templates/issue_view.html
+++ b/templates/issue_view.html
@@ -12,101 +12,46 @@
   {% let archive_ref = repo.default_branch %}
   {% include "partials/repo_tabs.html" %}
 
-  <div class="kg-issue-layout">
-    <div class="kg-issue-main">
-      <article class="kg-comment">
-        <header>
-          <span>
-            <img class="kg-avatar kg-avatar--sm" src="{{ author_avatar }}" alt="" />
-            <a href="/{{ author.username }}">{{ author.username }}</a>
-          </span>
-          <span>{{ issue.created_at }}</span>
-        </header>
-        <div class="kg-readme">{{ body_html|safe }}</div>
-      </article>
+  <article class="kg-comment">
+    <header>
+      <span>
+        <img class="kg-avatar kg-avatar--sm" src="{{ author_avatar }}" alt="" />
+        <a href="/{{ author.username }}">{{ author.username }}</a>
+      </span>
+      <span>{{ issue.created_at }}</span>
+    </header>
+    <div class="kg-readme">{{ body_html|safe }}</div>
+  </article>
 
-      {% for c in comments %}
-      <article class="kg-comment">
-        <header>
-          <span>
-            <img class="kg-avatar kg-avatar--sm" src="{{ c.avatar_url }}" alt="" />
-            <a href="/{{ c.author.username }}">{{ c.author.username }}</a>
-          </span>
-          <span>{{ c.created_at }}</span>
-        </header>
-        <div class="kg-readme">{{ c.body_html|safe }}</div>
-        {% include "partials/reactions.html" %}
-      </article>
-      {% endfor %}
+  {% for c in comments %}
+  <article class="kg-comment">
+    <header>
+      <span>
+        <img class="kg-avatar kg-avatar--sm" src="{{ c.avatar_url }}" alt="" />
+        <a href="/{{ c.author.username }}">{{ c.author.username }}</a>

Large diffs are not rendered by default. Showing the first 50 of 142 lines. Show full diff

templates/issues_list.html +4 −12
diff --git a/templates/issues_list.html b/templates/issues_list.html
index 0cfb259..6c1629d 100644
--- a/templates/issues_list.html
+++ b/templates/issues_list.html
@@ -18,27 +18,19 @@
     {% if viewer.is_some() %}
       <a class="kg-btn" href="/{{ owner.username }}/{{ repo.name }}/issues/new">new issue</a>
     {% endif %}
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/labels">labels</a>
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/milestones">milestones</a>
   </div>
 
   {% if issues.is_empty() %}
     <p class="kg-muted">No {{ state_filter }} issues.</p>
   {% else %}
     <ul class="kg-list">
-      {% for item in issues %}
+      {% for issue in issues %}
       <li>
         <span>
-          <span class="kg-badge {% if item.issue.state == "open" %}kg-badge--open{% else %}kg-badge--closed{% endif %}">{{ item.issue.state }}</span>
-          <a href="/{{ owner.username }}/{{ repo.name }}/issues/{{ item.issue.number }}">#{{ item.issue.number }} {{ item.issue.title }}</a>
-          {% for l in item.labels %}
-            <span class="kg-label-chip" style="background:{{ l.bg_color() }};color:{{ l.text_color() }}">{{ l.name }}</span>
-          {% endfor %}
-          {% if let Some(m) = item.milestone %}
-            <span class="kg-meta">milestone: {{ m.title }}</span>
-          {% endif %}
+          <span class="kg-badge {% if issue.state == "open" %}kg-badge--open{% else %}kg-badge--closed{% endif %}">{{ issue.state }}</span>
+          <a href="/{{ owner.username }}/{{ repo.name }}/issues/{{ issue.number }}">#{{ issue.number }} {{ issue.title }}</a>
         </span>
-        <span class="kg-meta">{{ item.issue.updated_at.format("%Y-%m-%d") }}</span>
+        <span class="kg-meta">{{ issue.updated_at }}</span>
       </li>
       {% endfor %}
     </ul>
templates/keys_settings.html +1 −16
diff --git a/templates/keys_settings.html b/templates/keys_settings.html
index 64d02b4..ecd9ef5 100644
--- a/templates/keys_settings.html
+++ b/templates/keys_settings.html
@@ -17,7 +17,7 @@
   {% endif %}
 
   <span class="kg-kicker" style="margin-top:1.5rem">SSH keys</span>
-  <p class="kg-muted">Authentication keys let you push/pull over SSH. Signing keys verify signed commits on kitgit (Verified badge).</p>
+  <p class="kg-muted">Authentication keys let you push/pull over SSH. Signing keys verify signed commits.</p>
   {% if !keys.is_empty() %}
     <ul class="kg-list">
       {% for key in keys %}
@@ -70,14 +70,6 @@
     </div>
   </form>
 
-  <div class="kg-signing-hint">
-    <strong>Sign commits with SSH</strong>
-    <p class="kg-muted" style="margin:0.35rem 0 0">Add the public key above with usage <em>Signing</em> or <em>Authentication &amp; Signing</em>, then configure git locally. Author email must match a verified email on your account.</p>
-    <pre>git config --global gpg.format ssh
-git config --global user.signingkey ~/.ssh/id_ed25519.pub
-git config --global commit.gpgsign true</pre>
-  </div>
-
   <span class="kg-kicker" style="margin-top:2.5rem">GPG keys</span>
   <p class="kg-muted">Used for commit verification. Vigilant mode flags unsigned commits on your profile.</p>
   {% if !gpg_keys.is_empty() %}
@@ -112,12 +104,5 @@ git config --global commit.gpgsign true</pre>
       <button class="kg-btn" type="submit">add</button>
     </div>
   </form>
-
-  <div class="kg-signing-hint">
-    <strong>Sign commits with GPG</strong>
-    <p class="kg-muted" style="margin:0.35rem 0 0">Paste your public key above, then point git at the key id. Author email must match a verified email on your account.</p>
-    <pre>git config --global user.signingkey YOUR_KEY_ID
-git config --global commit.gpgsign true</pre>
-  </div>
 </section>
 {% endblock %}
templates/labels_list.html +0 −15
diff --git a/templates/labels_list.html b/templates/labels_list.html
deleted file mode 100644
index d31a1bc..0000000
--- a/templates/labels_list.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{% extends "layout.html" %}
-{% block title %}labels · {{ owner.username }}/{{ repo.name }} — kitgit{% endblock %}
-{% block content %}
-<section class="kg-section">
-  <h1 class="kg-title"><a href="/{{ owner.username }}/{{ repo.name }}">{{ owner.username }}/{{ repo.name }}</a> · labels</h1>
-  {% let tab = "settings" %}
-  {% let archive_ref = repo.default_branch %}
-  {% include "partials/repo_tabs.html" %}
-  {% if let Some(err) = error %}<div class="kg-flash">{{ err }}</div>{% endif %}
-  <div class="kg-actions"><a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/milestones">milestones</a><a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/settings">settings</a></div>
-  {% if labels.is_empty() %}<p class="kg-muted">No labels yet.</p>{% else %}
-  <ul class="kg-list">{% for l in labels %}<li><span><span class="kg-label-chip" style="background:{{ l.bg_color() }};color:{{ l.text_color() }}">{{ l.name }}</span> <span class="kg-meta">{{ l.description }}</span></span>{% if access.can_admin() %}<form method="post" action="/{{ owner.username }}/{{ repo.name }}/labels/{{ l.id }}/delete"><button class="kg-btn kg-btn--danger-ghost" type="submit">delete</button></form>{% endif %}</li>{% if access.can_admin() %}<li><form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/labels/{{ l.id }}/update"><label>name <input name="name" value="{{ l.name }}" required /></label><label>color <input name="color" value="{{ l.color }}" required /></label><label>description <input name="description" value="{{ l.description }}" /></label><button class="kg-btn kg-btn--ghost" type="submit">update</button></form></li>{% endif %}{% endfor %}</ul>{% endif %}
-  {% if access.can_admin() %}<form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/labels"><span class="kg-kicker">new label</span><label>name <input name="name" required /></label><label>color <input name="color" value="0969da" required /></label><label>description <input name="description" /></label><button class="kg-btn" type="submit">create label</button></form>{% endif %}
-</section>
-{% endblock %}
templates/layout.html +2 −23
diff --git a/templates/layout.html b/templates/layout.html
index 16b2e34..f589a61 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="en" data-theme="{% if let Some(u) = viewer %}{{ u.theme_pref() }}{% else %}system{% endif %}">
+<html lang="en">
 <head>
   <meta charset="utf-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -23,11 +23,6 @@
         <a href="/explore"><span class="kg-icon" style="--icon:url('/static/icons/code.svg')"></span> explore</a>
         {% if let Some(u) = viewer %}
           <a href="/new"><span class="kg-icon" style="--icon:url('/static/icons/plus.svg')"></span> new</a>
-          <a id="kg-nav-notifications" href="/notifications">
-            <span class="kg-icon" style="--icon:url('/static/icons/bell.svg')"></span>
-            notifications
-            <span id="kg-notif-count" class="kg-notif-count" hidden></span>
-          </a>
           {% if u.is_site_admin %}<a href="/admin"><span class="kg-icon" style="--icon:url('/static/icons/settings.svg')"></span> admin</a>{% endif %}
           <a href="/settings/profile"><span class="kg-icon" style="--icon:url('/static/icons/user.svg')"></span> settings</a>
           <a class="kg-userchip" href="/{{ u.username }}">
@@ -44,10 +39,7 @@
     <main>
       {% block content %}{% endblock %}
     </main>
-    <footer class="kg-footer">
-      <span>kitgit</span>
-      <span class="kg-footer__timing">rendered in <!--kg-render-ms--></span>
-    </footer>
+    <footer class="kg-footer">kitgit</footer>
   </div>
   <script>
   (async () => {
@@ -62,19 +54,6 @@
       }
     } catch (_) {}
   })();
-  (async () => {
-    const badge = document.getElementById('kg-notif-count');
-    if (!badge) return;
-    try {
-      const r = await fetch('/notifications/unread.json', { credentials: 'same-origin' });
-      if (!r.ok) return;
-      const j = await r.json();
-      const n = Number(j && j.count);
-      if (!n) return;
-      badge.textContent = n > 99 ? '99+' : String(n);
-      badge.hidden = false;

Large diffs are not rendered by default. Showing the first 50 of 55 lines. Show full diff

templates/milestones_list.html +0 −19
diff --git a/templates/milestones_list.html b/templates/milestones_list.html
deleted file mode 100644
index 7947f22..0000000
--- a/templates/milestones_list.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{% extends "layout.html" %}
-{% block title %}milestones · {{ owner.username }}/{{ repo.name }} — kitgit{% endblock %}
-{% block content %}
-<section class="kg-section">
-  <h1 class="kg-title"><a href="/{{ owner.username }}/{{ repo.name }}">{{ owner.username }}/{{ repo.name }}</a> · milestones</h1>
-  {% let tab = "settings" %}
-  {% let archive_ref = repo.default_branch %}
-  {% include "partials/repo_tabs.html" %}
-  {% if let Some(err) = error %}<div class="kg-flash">{{ err }}</div>{% endif %}
-  <div class="kg-actions">
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/milestones?state=open"{% if state_filter == "open" %} aria-current="page"{% endif %}>open</a>
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/milestones?state=closed"{% if state_filter == "closed" %} aria-current="page"{% endif %}>closed</a>
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/labels">labels</a>
-  </div>
-  {% if milestones.is_empty() %}<p class="kg-muted">No {{ state_filter }} milestones.</p>{% else %}
-  <ul class="kg-list">{% for m in milestones %}<li><span><span class="kg-badge {% if m.state == "open" %}kg-badge--open{% else %}kg-badge--closed{% endif %}">{{ m.state }}</span> <strong>{{ m.title }}</strong>{% if let Some(due) = m.due_on %} <span class="kg-meta">due {{ due }}</span>{% endif %}</span>{% if access.can_admin() %}<span class="kg-actions">{% if m.state == "open" %}<form method="post" action="/{{ owner.username }}/{{ repo.name }}/milestones/{{ m.id }}/close"><button class="kg-btn kg-btn--ghost" type="submit">close</button></form>{% else %}<form method="post" action="/{{ owner.username }}/{{ repo.name }}/milestones/{{ m.id }}/reopen"><button class="kg-btn kg-btn--ghost" type="submit">reopen</button></form>{% endif %}<form method="post" action="/{{ owner.username }}/{{ repo.name }}/milestones/{{ m.id }}/delete"><button class="kg-btn kg-btn--danger-ghost" type="submit">delete</button></form></span>{% endif %}</li>{% endfor %}</ul>{% endif %}
-  {% if access.can_admin() %}<form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/milestones"><span class="kg-kicker">new milestone</span><label>title <input name="title" required /></label><label>due on <input type="date" name="due_on" /></label><label>description <textarea name="description" rows="3"></textarea></label><button class="kg-btn" type="submit">create milestone</button></form>{% endif %}
-</section>
-{% endblock %}
templates/notifications.html +0 −47
diff --git a/templates/notifications.html b/templates/notifications.html
deleted file mode 100644
index 59c90c1..0000000
--- a/templates/notifications.html
+++ /dev/null
@@ -1,47 +0,0 @@
-{% extends "layout.html" %}
-
-{% block title %}notifications — kitgit{% endblock %}
-
-{% block content %}
-<section class="kg-section">
-  <div class="kg-actions" style="justify-content: space-between; align-items: baseline;">
-    <h1 class="kg-title">notifications</h1>
-    {% if unread_count > 0 %}
-      <form method="post" action="/notifications/read-all">
-        <button class="kg-btn kg-btn--ghost" type="submit">mark all read</button>
-      </form>
-    {% endif %}
-  </div>
-
-  {% if notifications.is_empty() %}
-    <p class="kg-muted">No notifications yet.</p>
-  {% else %}
-    <ul class="kg-list kg-notif-list">
-      {% for n in notifications %}
-      <li class="{% if n.read_at.is_none() %}kg-notif--unread{% endif %}">
-        <span class="kg-notif-main">
-          {% if n.href.is_empty() %}
-            <strong>{{ n.title }}</strong>
-          {% else %}
-            <a href="{{ n.href }}"><strong>{{ n.title }}</strong></a>
-          {% endif %}
-          {% if !n.body.is_empty() %}
-            <span class="kg-muted">{{ n.body }}</span>
-          {% endif %}
-          <span class="kg-meta">{{ n.created_at }} · {{ n.kind }}</span>
-        </span>
-        <span class="kg-notif-actions">
-          {% if n.read_at.is_none() %}
-            <form method="post" action="/notifications/{{ n.id }}/read">
-              <button class="kg-btn kg-btn--ghost" type="submit">mark read</button>
-            </form>
-          {% else %}
-            <span class="kg-faint">read</span>
-          {% endif %}
-        </span>
-      </li>
-      {% endfor %}
-    </ul>

Large diffs are not rendered by default. Showing the first 50 of 53 lines. Show full diff

templates/partials/branch_bar.html +30 −31
diff --git a/templates/partials/branch_bar.html b/templates/partials/branch_bar.html
index 91ce761..5069541 100644
--- a/templates/partials/branch_bar.html
+++ b/templates/partials/branch_bar.html
@@ -1,10 +1,33 @@
-{# Expects: owner, repo, branch, latest_commit: Option<CommitView>, access, upload_path, commit_count #}
-<div class="kg-toolbar">
-  <a class="kg-btn kg-btn--ghost kg-toolbar__branch" href="/{{ owner.username }}/{{ repo.name }}/branches">
-    <span class="kg-icon" style="--icon:url('/static/icons/git-branch.svg')"></span>
-    {{ branch }}
-  </a>
-  <div class="kg-toolbar__actions">
+{# Expects: owner, repo, branch, latest_commit: Option<CommitView>, access (optional for upload), upload_path (optional) #}
+<div class="kg-branchbar">
+  <div class="kg-branchbar__left">
+    <a class="kg-branchbar__branch" href="/{{ owner.username }}/{{ repo.name }}/branches">
+      <span class="kg-icon" style="--icon:url('/static/icons/git-branch.svg')"></span>
+      {{ branch }}
+    </a>
+    {% if let Some(c) = latest_commit %}
+    <div class="kg-branchbar__commit">
+      <a class="kg-branchbar__sha" href="/{{ owner.username }}/{{ repo.name }}/commits" title="View commits">
+        <span class="kg-icon" style="--icon:url('/static/icons/git-commit.svg')"></span>
+        {{ c.short_id }}
+      </a>
+      <a class="kg-branchbar__msg" href="/{{ owner.username }}/{{ repo.name }}/commit/{{ c.id }}" title="View commit">{{ c.message }}</a>
+      {% if c.verified %}
+      <details class="kg-verify">
+        <summary class="kg-badge kg-badge--verified">Verified</summary>
+        <div class="kg-verify__panel" role="dialog" aria-label="Commit signature">
+          <p class="kg-verify__fp">
+            <strong>{{ c.verify_fingerprint_label }}:</strong>
+            <code>{{ c.verify_fingerprint }}</code>
+          </p>
+          <p class="kg-meta">Verified on {{ c.verified_at }}</p>
+        </div>
+      </details>
+      {% endif %}
+    </div>
+    {% endif %}
+  </div>
+  <div class="kg-branchbar__actions">
     {% if upload_path != "" %}
     <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/archive.zip?ref={{ branch }}&path={{ upload_path }}" title="Download this folder as ZIP">
       <span class="kg-icon" style="--icon:url('/static/icons/download.svg')"></span>
@@ -45,27 +68,3 @@
     {% endif %}
   </div>
 </div>
-

Large diffs are not rendered by default. Showing the first 50 of 73 lines. Show full diff

templates/partials/repo_tabs.html +0 −2
diff --git a/templates/partials/repo_tabs.html b/templates/partials/repo_tabs.html
index d56147e..5ca44db 100644
--- a/templates/partials/repo_tabs.html
+++ b/templates/partials/repo_tabs.html
@@ -31,7 +31,6 @@
     </a>
     {% endif %}
   </div>
-  {% if tab == "code" %}
   <details class="kg-dropdown">
     <summary class="kg-btn kg-btn--ghost">
       <span class="kg-icon" style="--icon:url('/static/icons/download.svg')"></span>
@@ -59,5 +58,4 @@
       </div>
     </div>
   </details>
-  {% endif %}
 </nav>
templates/partials/verified_badge.html +4 −16
diff --git a/templates/partials/verified_badge.html b/templates/partials/verified_badge.html
index 6697dca..d448c5c 100644
--- a/templates/partials/verified_badge.html
+++ b/templates/partials/verified_badge.html
@@ -1,24 +1,12 @@
-{# Expects CommitView as `c`. Optional: `show_unverified` (bool) — show Unverified for unsigned too. #}
-{% if c.verified %}
+{% if let Some(v) = verification %}
 <details class="kg-verify">
   <summary class="kg-badge kg-badge--verified">Verified</summary>
   <div class="kg-verify__panel" role="dialog" aria-label="Commit signature">
-    <p class="kg-verify__row"><strong>Type:</strong> {{ c.verify_kind_label() }}</p>
     <p class="kg-verify__fp">
-      <strong>{{ c.verify_fingerprint_label }}:</strong>
-      <code>{{ c.verify_fingerprint }}</code>
+      <strong>{{ v.fingerprint_label() }}:</strong>
+      <code>{{ v.fingerprint }}</code>
     </p>
-    <p class="kg-meta">Verified on {{ c.verified_at }}</p>
+    <p class="kg-meta">Verified on {{ v.verified_at }}</p>
   </div>
 </details>
-{% else if c.signed %}
-<details class="kg-verify">
-  <summary class="kg-badge kg-badge--unverified">Unverified</summary>
-  <div class="kg-verify__panel" role="dialog" aria-label="Commit signature">
-    <p class="kg-verify__row">Signature present but could not be verified.</p>
-    <p class="kg-meta">Add a matching signing SSH or GPG key under <a href="/settings/keys">Settings → SSH / GPG</a>. The commit author email must match a verified email on that account.</p>
-  </div>
-</details>
-{% else if show_unverified %}
-<span class="kg-badge kg-badge--unverified" title="Commit is not signed">Unverified</span>
 {% endif %}
templates/pull_new.html +33 −8
diff --git a/templates/pull_new.html b/templates/pull_new.html
index b8957a8..21d97f5 100644
--- a/templates/pull_new.html
+++ b/templates/pull_new.html
@@ -1,20 +1,45 @@
 {% extends "layout.html" %}
+
 {% block title %}new pull · {{ owner.username }}/{{ repo.name }} — kitgit{% endblock %}
+
 {% block content %}
 <section class="kg-section">
   <h1 class="kg-title">New pull request</h1>
   {% let tab = "pulls" %}
   {% let archive_ref = repo.default_branch %}
   {% include "partials/repo_tabs.html" %}
-  {% if let Some(err) = error %}<div class="kg-flash">{{ err }}</div>{% endif %}
+  {% if let Some(err) = error %}
+    <div class="kg-flash">{{ err }}</div>
+  {% endif %}
   <form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/pulls">
-    <label>title <input type="text" name="title" required autofocus /></label>
-    <label>source branch<select name="source_branch" required>{% for b in branches %}<option value="{{ b }}">{{ b }}</option>{% endfor %}</select></label>
-    <label>target branch<select name="target_branch" required>{% for b in branches %}<option value="{{ b }}"{% if b.as_str() == repo.default_branch.as_str() %} selected{% endif %}>{{ b }}</option>{% endfor %}</select></label>
-    <label>body <textarea name="body" rows="8"></textarea></label>
-    {% if !labels.is_empty() %}<fieldset class="kg-fieldset"><legend>labels</legend>{% for l in labels %}<label class="row"><input type="checkbox" name="label_id" value="{{ l.id }}" /> <span class="kg-label-chip" style="background:{{ l.bg_color() }};color:{{ l.text_color() }}">{{ l.name }}</span></label>{% endfor %}</fieldset>{% endif %}
-    {% if !milestones.is_empty() %}<label>milestone<select name="milestone_id"><option value="">none</option>{% for m in milestones %}<option value="{{ m.id }}">{{ m.title }}</option>{% endfor %}</select></label>{% endif %}
-    <div class="kg-actions"><button class="kg-btn" type="submit">create</button><a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/pulls">cancel</a></div>
+    <label>
+      title
+      <input type="text" name="title" required autofocus />
+    </label>
+    <label>
+      source branch
+      <select name="source_branch" required>
+        {% for b in branches %}
+          <option value="{{ b }}">{{ b }}</option>
+        {% endfor %}
+      </select>
+    </label>
+    <label>
+      target branch
+      <select name="target_branch" required>
+        {% for b in branches %}
+          <option value="{{ b }}"{% if b.as_str() == repo.default_branch.as_str() %} selected{% endif %}>{{ b }}</option>
+        {% endfor %}
+      </select>
+    </label>
+    <label>
+      body
+      <textarea name="body" rows="8"></textarea>

Large diffs are not rendered by default. Showing the first 50 of 58 lines. Show full diff

templates/pull_view.html +14 −63
diff --git a/templates/pull_view.html b/templates/pull_view.html
index 843c4e1..92b3f55 100644
--- a/templates/pull_view.html
+++ b/templates/pull_view.html
@@ -9,19 +9,6 @@
     #{{ pull.number }} {{ pull.title }}
   </h1>
   <p class="kg-meta">{{ pull.source_branch }} → {{ pull.target_branch }}</p>
-  <aside class="kg-issue-sidebar" style="margin:1rem 0">
-    <div class="kg-sidebar-block">
-      <h2 class="kg-kicker">labels</h2>
-      {% if labels.is_empty() %}<p class="kg-muted">None yet.</p>{% else %}<div class="kg-label-row">{% for l in labels %}<span class="kg-label-chip" style="background:{{ l.bg_color() }};color:{{ l.text_color() }}">{{ l.name }}</span>{% endfor %}</div>{% endif %}
-      {% if can_triage %}<form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/pulls/{{ pull.number }}/labels">{% for opt in label_options %}<label class="row"><input type="checkbox" name="label_id" value="{{ opt.label.id }}"{% if opt.selected %} checked{% endif %} /> <span class="kg-label-chip" style="background:{{ opt.label.bg_color() }};color:{{ opt.label.text_color() }}">{{ opt.label.name }}</span></label>{% endfor %}{% if !label_options.is_empty() %}<button class="kg-btn kg-btn--ghost" type="submit">save labels</button>{% endif %}</form>{% endif %}
-    </div>
-    <div class="kg-sidebar-block">
-      <h2 class="kg-kicker">milestone</h2>
-      {% if let Some(m) = milestone %}<p>{{ m.title }}</p>{% else %}<p class="kg-muted">No milestone</p>{% endif %}
-      {% if can_triage %}<form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/pulls/{{ pull.number }}/milestone"><label><select name="milestone_id"><option value="">none</option>{% for opt in milestone_options %}<option value="{{ opt.milestone.id }}"{% if opt.selected %} selected{% endif %}>{{ opt.milestone.title }}</option>{% endfor %}</select></label><button class="kg-btn kg-btn--ghost" type="submit">save milestone</button></form>{% endif %}
-    </div>
-  </aside>
-
   {% let pr_tab = tab %}
   {% let tab = "pulls" %}
   {% let archive_ref = repo.default_branch %}
@@ -38,7 +25,7 @@
       commits
       <span class="kg-pr-tabs__count">{{ commits.len() }}</span>
     </a>
-    <a href="/{{ owner.username }}/{{ repo.name }}/pulls/{{ pull.number }}?tab=files{% if show_full %}&amp;full=1{% endif %}"{% if pr_tab == "files" %} aria-current="page"{% endif %}>
+    <a href="/{{ owner.username }}/{{ repo.name }}/pulls/{{ pull.number }}?tab=files"{% if pr_tab == "files" %} aria-current="page"{% endif %}>
       <span class="kg-icon" style="--icon:url('/static/icons/file.svg')"></span>
       files
       <span class="kg-pr-tabs__count">{{ diff_files.len() }}</span>
@@ -71,51 +58,6 @@
   </article>
   {% endfor %}
 
-  {% for r in reviews %}
-  <article class="kg-comment kg-review">
-    <header>
-      <span>
-        <img class="kg-avatar kg-avatar--sm" src="{{ r.avatar_url }}" alt="" />
-        <a href="/{{ r.reviewer.username }}">{{ r.reviewer.username }}</a>
-        <span class="kg-badge {{ r.state_class() }}">{{ r.state_label() }}</span>
-      </span>
-      <span>{{ r.created_at }}</span>
-    </header>
-    {% if !r.body.is_empty() %}
-    <div class="kg-readme">{{ r.body_html|safe }}</div>
-    {% endif %}

Large diffs are not rendered by default. Showing the first 50 of 119 lines. Show full diff

templates/pulls_list.html +27 −6
diff --git a/templates/pulls_list.html b/templates/pulls_list.html
index 50da817..69425f5 100644
--- a/templates/pulls_list.html
+++ b/templates/pulls_list.html
@@ -1,20 +1,41 @@
 {% extends "layout.html" %}
+
 {% block title %}pulls · {{ owner.username }}/{{ repo.name }} — kitgit{% endblock %}
+
 {% block content %}
 <section class="kg-section">
-  <h1 class="kg-title"><a href="/{{ owner.username }}/{{ repo.name }}">{{ owner.username }}/{{ repo.name }}</a> · pulls</h1>
+  <h1 class="kg-title">
+    <a href="/{{ owner.username }}/{{ repo.name }}">{{ owner.username }}/{{ repo.name }}</a>
+    · pulls
+  </h1>
   {% let tab = "pulls" %}
   {% let archive_ref = repo.default_branch %}
   {% include "partials/repo_tabs.html" %}
+
   <div class="kg-actions">
     <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/pulls?state=open"{% if state_filter == "open" %} aria-current="page"{% endif %}>open</a>
     <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/pulls?state=closed"{% if state_filter == "closed" %} aria-current="page"{% endif %}>closed</a>
     <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/pulls?state=merged"{% if state_filter == "merged" %} aria-current="page"{% endif %}>merged</a>
-    {% if viewer.is_some() %}<a class="kg-btn" href="/{{ owner.username }}/{{ repo.name }}/pulls/new">new pull</a>{% endif %}
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/labels">labels</a>
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/milestones">milestones</a>
+    {% if viewer.is_some() %}
+      <a class="kg-btn" href="/{{ owner.username }}/{{ repo.name }}/pulls/new">new pull</a>
+    {% endif %}
   </div>
-  {% if pulls.is_empty() %}<p class="kg-muted">No {{ state_filter }} pulls.</p>{% else %}
-  <ul class="kg-list">{% for item in pulls %}<li><span><span class="kg-badge {% if item.pull.state == "open" %}kg-badge--open{% else %}kg-badge--closed{% endif %}">{{ item.pull.state }}</span> <a href="/{{ owner.username }}/{{ repo.name }}/pulls/{{ item.pull.number }}">#{{ item.pull.number }} {{ item.pull.title }}</a> <span class="kg-meta">{{ item.pull.source_branch }} → {{ item.pull.target_branch }}</span>{% for l in item.labels %} <span class="kg-label-chip" style="background:{{ l.bg_color() }};color:{{ l.text_color() }}">{{ l.name }}</span>{% endfor %}{% if let Some(m) = item.milestone %} <span class="kg-meta">milestone: {{ m.title }}</span>{% endif %}</span><span class="kg-meta">{{ item.pull.updated_at.format("%Y-%m-%d") }}</span></li>{% endfor %}</ul>{% endif %}
+
+  {% if pulls.is_empty() %}
+    <p class="kg-muted">No {{ state_filter }} pulls.</p>
+  {% else %}
+    <ul class="kg-list">
+      {% for pull in pulls %}
+      <li>
+        <span>
+          <span class="kg-badge {% if pull.state == "open" %}kg-badge--open{% else %}kg-badge--closed{% endif %}">{{ pull.state }}</span>
+          <a href="/{{ owner.username }}/{{ repo.name }}/pulls/{{ pull.number }}">#{{ pull.number }} {{ pull.title }}</a>
+          <span class="kg-meta">{{ pull.source_branch }} → {{ pull.target_branch }}</span>
+        </span>
+        <span class="kg-meta">{{ pull.updated_at }}</span>
+      </li>
+      {% endfor %}
+    </ul>
+  {% endif %}

Large diffs are not rendered by default. Showing the first 50 of 52 lines. Show full diff

templates/repo_blame.html +0 −66
diff --git a/templates/repo_blame.html b/templates/repo_blame.html
deleted file mode 100644
index e3f291d..0000000
--- a/templates/repo_blame.html
+++ /dev/null
@@ -1,66 +0,0 @@
-{% extends "layout.html" %}
-
-{% block title %}blame · {{ path }} · {{ owner.username }}/{{ repo.name }} — kitgit{% endblock %}
-
-{% block content %}
-<section class="kg-section">
-  <h1 class="kg-title">
-    <a href="/{{ owner.username }}/{{ repo.name }}">{{ owner.username }}/{{ repo.name }}</a>
-    · blame
-  </h1>
-  {% let tab = "code" %}
-  {% let archive_ref = branch %}
-  {% include "partials/repo_tabs.html" %}
-
-  <p class="kg-meta kg-breadcrumbs">
-    <a href="/{{ owner.username }}/{{ repo.name }}/tree/{{ branch }}">{{ branch }}</a>
-    {% for (name, full) in breadcrumbs %}
-      /
-      {% if loop.last %}
-        {{ name }}
-      {% else %}
-        <a href="/{{ owner.username }}/{{ repo.name }}/tree/{{ branch }}/{{ full }}">{{ name }}</a>
-      {% endif %}
-    {% endfor %}
-  </p>
-
-  <div class="kg-blobbar">
-    <span class="kg-meta">{{ path }}</span>
-    <div class="kg-blobbar__actions">
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/blob/{{ branch }}/{{ path }}">View file</a>
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/history/{{ branch }}/{{ path }}">History</a>
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/raw/{{ branch }}/{{ path }}">
-        <span class="kg-icon" style="--icon:url('/static/icons/download.svg')"></span>
-        Download raw
-      </a>
-    </div>
-  </div>
-
-  {% if binary %}
-    <p class="kg-muted">Binary file — blame is not available.</p>
-  {% else if lines.is_empty() %}
-    <p class="kg-muted">Empty file.</p>
-  {% else %}
-    <div class="kg-blame" role="table" aria-label="Blame for {{ path }}">

Large diffs are not rendered by default. Showing the first 50 of 72 lines. Show full diff

templates/repo_blob.html +0 −2
diff --git a/templates/repo_blob.html b/templates/repo_blob.html
index f00dbf7..5b0ff48 100644
--- a/templates/repo_blob.html
+++ b/templates/repo_blob.html
@@ -33,8 +33,6 @@
   <div class="kg-blobbar">
     <span class="kg-meta">{{ path }}</span>
     <div class="kg-blobbar__actions">
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/blame/{{ branch }}/{{ path }}">Blame</a>
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/history/{{ branch }}/{{ path }}">History</a>
       <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/raw/{{ branch }}/{{ path }}">
         <span class="kg-icon" style="--icon:url('/static/icons/download.svg')"></span>
         Download raw
templates/repo_branches.html +4 −8
diff --git a/templates/repo_branches.html b/templates/repo_branches.html
index 03f22f1..5691acd 100644
--- a/templates/repo_branches.html
+++ b/templates/repo_branches.html
@@ -61,15 +61,13 @@
           <td>
             {% if access.can_write() && !b.is_default %}
             <div class="kg-branch-actions">
-              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/branches/rename" style="display:inline-flex;gap:0.35rem">
-                <input type="hidden" name="branch" value="{{ b.name }}" />
+              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/branches/{{ b.name }}/rename" style="display:inline-flex;gap:0.35rem">
                 <input type="text" name="new_name" placeholder="rename" required style="width:7rem;font:inherit;border:1px solid var(--kg-line);padding:0.25em 0.4em" />
                 <button class="kg-btn kg-btn--ghost" type="submit" title="Rename">
                   <span class="kg-icon" style="--icon:url('/static/icons/pencil.svg')"></span>
                 </button>
               </form>
-              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/branches/delete" style="display:inline" onsubmit="return confirm('Delete branch {{ b.name }}?')">
-                <input type="hidden" name="branch" value="{{ b.name }}" />
+              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/branches/{{ b.name }}/delete" style="display:inline" onsubmit="return confirm('Delete branch {{ b.name }}?')">
                 <button class="kg-btn kg-btn--danger-ghost" type="submit" title="Delete">
                   <span class="kg-icon" style="--icon:url('/static/icons/trash.svg')"></span>
                 </button>
@@ -141,15 +139,13 @@
           <td>
             {% if access.can_write() %}
             <div class="kg-branch-actions">
-              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/tags/rename" style="display:inline-flex;gap:0.35rem">
-                <input type="hidden" name="tag" value="{{ t.name }}" />
+              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/tags/{{ t.name }}/rename" style="display:inline-flex;gap:0.35rem">
                 <input type="text" name="new_name" placeholder="rename" required style="width:7rem;font:inherit;border:1px solid var(--kg-line);padding:0.25em 0.4em" />
                 <button class="kg-btn kg-btn--ghost" type="submit" title="Rename">
                   <span class="kg-icon" style="--icon:url('/static/icons/pencil.svg')"></span>
                 </button>
               </form>
-              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/tags/delete" style="display:inline" onsubmit="return confirm('Delete tag {{ t.name }}?')">
-                <input type="hidden" name="tag" value="{{ t.name }}" />
+              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/tags/{{ t.name }}/delete" style="display:inline" onsubmit="return confirm('Delete tag {{ t.name }}?')">
                 <button class="kg-btn kg-btn--danger-ghost" type="submit" title="Delete">
                   <span class="kg-icon" style="--icon:url('/static/icons/trash.svg')"></span>
                 </button>
templates/repo_commit.html +14 −12
diff --git a/templates/repo_commit.html b/templates/repo_commit.html
index 3a965a9..0b1244e 100644
--- a/templates/repo_commit.html
+++ b/templates/repo_commit.html
@@ -15,20 +15,22 @@
   <p><code>{{ commit.id }}</code></p>
   <p class="kg-title">
     {{ message_html|safe }}
-    {% let c = commit %}
-    {% let show_unverified = true %}
-    {% include "partials/verified_badge.html" %}
+    {% if commit.verified %}
+    <details class="kg-verify">
+      <summary class="kg-badge kg-badge--verified">Verified</summary>
+      <div class="kg-verify__panel" role="dialog" aria-label="Commit signature">
+        <p class="kg-verify__fp">
+          <strong>{{ commit.verify_fingerprint_label }}:</strong>
+          <code>{{ commit.verify_fingerprint }}</code>
+        </p>
+        <p class="kg-meta">Verified on {{ commit.verified_at }}</p>
+      </div>
+    </details>
+    {% endif %}
   </p>
-  <p class="kg-meta">{{ commit.author }} &lt;{{ commit.email }}&gt; · {{ commit.time_display() }}</p>
+  <p class="kg-meta">{{ commit.author }} &lt;{{ commit.email }}&gt; · {{ commit.time }}</p>
 
-  <p><a href="/{{ owner.username }}/{{ repo.name }}/diff/{{ commit.id }}{% if show_full %}?full=1{% endif %}">view full diff</a></p>
+  <p><a href="/{{ owner.username }}/{{ repo.name }}/diff/{{ commit.id }}">view full diff</a></p>
   {{ diff_html|safe }}
-  {% if truncated && !show_full %}
-  <p class="kg-diff__truncated">
-    Large diffs are not rendered by default.
-    Showing the first 50 of {{ total_lines }} lines.
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/commit/{{ commit.id }}?full=1">Show full diff</a>
-  </p>
-  {% endif %}
 </section>
 {% endblock %}
templates/repo_commits.html +13 −3
diff --git a/templates/repo_commits.html b/templates/repo_commits.html
index 517248d..a763fd2 100644
--- a/templates/repo_commits.html
+++ b/templates/repo_commits.html
@@ -31,11 +31,21 @@
             <code>{{ c.short_id }}</code>
           </a>
           <a href="/{{ owner.username }}/{{ repo.name }}/commit/{{ c.id }}">{{ c.message }}</a>
-          {% let show_unverified = false %}
-          {% include "partials/verified_badge.html" %}
+          {% if c.verified %}
+          <details class="kg-verify">
+            <summary class="kg-badge kg-badge--verified">Verified</summary>
+            <div class="kg-verify__panel" role="dialog" aria-label="Commit signature">
+              <p class="kg-verify__fp">
+                <strong>{{ c.verify_fingerprint_label }}:</strong>
+                <code>{{ c.verify_fingerprint }}</code>
+              </p>
+              <p class="kg-meta">Verified on {{ c.verified_at }}</p>
+            </div>
+          </details>
+          {% endif %}
           <span class="kg-muted"> — {{ c.author }}</span>
         </span>
-        <span class="kg-meta">{{ c.time_display() }}</span>
+        <span class="kg-meta">{{ c.time }}</span>
       </li>
       {% endfor %}
     </ul>
templates/repo_diff.html +1 −11
diff --git a/templates/repo_diff.html b/templates/repo_diff.html
index 122515a..129dab6 100644
--- a/templates/repo_diff.html
+++ b/templates/repo_diff.html
@@ -20,19 +20,9 @@
     · {{ commit.author }}
   </p>
   <p class="kg-title">
-    <a href="/{{ owner.username }}/{{ repo.name }}/commit/{{ commit.id }}{% if show_full %}?full=1{% endif %}">{{ commit.message }}</a>
-    {% let c = commit %}
-    {% let show_unverified = true %}
-    {% include "partials/verified_badge.html" %}
+    <a href="/{{ owner.username }}/{{ repo.name }}/commit/{{ commit.id }}">{{ commit.message }}</a>
   </p>
 
   {{ diff_html|safe }}
-  {% if truncated && !show_full %}
-  <p class="kg-diff__truncated">
-    Large diffs are not rendered by default.
-    Showing the first 50 of {{ total_lines }} lines.
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/diff/{{ commit.id }}?full=1">Show full diff</a>
-  </p>
-  {% endif %}
 </section>
 {% endblock %}
templates/repo_history.html +0 −67
diff --git a/templates/repo_history.html b/templates/repo_history.html
deleted file mode 100644
index d8e95b6..0000000
--- a/templates/repo_history.html
+++ /dev/null
@@ -1,67 +0,0 @@
-{% extends "layout.html" %}
-
-{% block title %}history · {{ path }} · {{ owner.username }}/{{ repo.name }} — kitgit{% endblock %}
-
-{% block content %}
-<section class="kg-section">
-  <h1 class="kg-title">
-    <a href="/{{ owner.username }}/{{ repo.name }}">{{ owner.username }}/{{ repo.name }}</a>
-    · history
-  </h1>
-  {% let tab = "code" %}
-  {% let archive_ref = branch %}
-  {% include "partials/repo_tabs.html" %}
-
-  <p class="kg-meta kg-breadcrumbs">
-    <a href="/{{ owner.username }}/{{ repo.name }}/tree/{{ branch }}">{{ branch }}</a>
-    {% for (name, full) in breadcrumbs %}
-      /
-      {% if loop.last %}
-        {{ name }}
-      {% else %}
-        <a href="/{{ owner.username }}/{{ repo.name }}/tree/{{ branch }}/{{ full }}">{{ name }}</a>
-      {% endif %}
-    {% endfor %}
-  </p>
-
-  <div class="kg-blobbar">
-    <span class="kg-meta">{{ path }}</span>
-    <div class="kg-blobbar__actions">
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/blob/{{ branch }}/{{ path }}">View file</a>
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/blame/{{ branch }}/{{ path }}">Blame</a>
-    </div>
-  </div>
-
-  {% if commits.is_empty() %}
-    <p class="kg-muted">No commits touch this path.</p>
-  {% else %}
-    <ul class="kg-list">
-      {% for c in commits %}
-      <li>
-        <span>
-          <a href="/{{ owner.username }}/{{ repo.name }}/commit/{{ c.id }}" title="View commit">
-            <span class="kg-icon" style="--icon:url('/static/icons/git-commit.svg')"></span>
-            <code>{{ c.short_id }}</code>

Large diffs are not rendered by default. Showing the first 50 of 73 lines. Show full diff

templates/repo_home.html +2 −4
diff --git a/templates/repo_home.html b/templates/repo_home.html
index f2e3424..7ee008a 100644
--- a/templates/repo_home.html
+++ b/templates/repo_home.html
@@ -104,8 +104,7 @@
         {% if e.is_dir %}
           <span class="kg-icon" style="--icon:url('/static/icons/folder.svg')"></span>
           <a href="/{{ owner.username }}/{{ repo.name }}/tree/{{ current_branch }}/{{ e.path }}">{{ e.name }}</a>
-          <span class="kg-tree__commit kg-meta" title="{{ e.commit_message }}">{{ e.commit_message }}</span>
-          <span class="kg-tree__time kg-meta">{{ e.commit_time }}</span>
+          <span class="kg-meta">{{ e.mode }}</span>
           <a class="kg-tree__dl kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/archive.zip?ref={{ current_branch }}&path={{ e.path }}" title="Download {{ e.name }} as ZIP">
             <span class="kg-icon" style="--icon:url('/static/icons/download.svg')"></span>
             <span class="kg-sr-only">Download</span>
@@ -113,8 +112,7 @@
         {% else %}
           <span class="kg-icon" style="--icon:url('/static/icons/file.svg')"></span>
           <a href="/{{ owner.username }}/{{ repo.name }}/blob/{{ current_branch }}/{{ e.path }}">{{ e.name }}</a>
-          <span class="kg-tree__commit kg-meta" title="{{ e.commit_message }}">{{ e.commit_message }}</span>
-          <span class="kg-tree__time kg-meta">{{ e.commit_time }}</span>
+          <span class="kg-meta">{{ e.mode }}</span>
         {% endif %}
       </li>
       {% endfor %}
templates/repo_settings.html +2 −176
diff --git a/templates/repo_settings.html b/templates/repo_settings.html
index e6ae11e..effe71d 100644
--- a/templates/repo_settings.html
+++ b/templates/repo_settings.html
@@ -146,73 +146,6 @@
   </div>
 
   <div class="kg-settings-section">
-    <span class="kg-kicker">mirror</span>
-    <p class="kg-muted">Pull from another remote into this repository (fetch heads and tags).</p>
-    {% if let Some(m) = mirror %}
-      <p class="kg-meta" style="margin:0.5rem 0">
-        {% if m.enabled %}
-          <span class="kg-badge">enabled</span>
-        {% else %}
-          <span class="kg-badge">disabled</span>
-        {% endif %}
-        {% if let Some(synced) = m.last_synced_at %}
-          · last synced {{ synced }}
-        {% else %}
-          · never synced
-        {% endif %}
-      </p>
-      {% if let Some(err) = m.last_error %}
-        <div class="kg-flash">{{ err }}</div>
-      {% endif %}
-      <form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/settings/mirror" style="margin-top:1rem">
-        <label>
-          remote URL
-          <input type="text" name="remote_url" required value="{{ m.remote_url }}" placeholder="https://github.com/org/repo.git" />
-        </label>
-        <label class="row">
-          <input type="checkbox" name="enabled"{% if m.enabled %} checked{% endif %} />
-          enabled
-        </label>
-        <div class="kg-actions">
-          <button class="kg-btn" type="submit">save mirror</button>
-        </div>
-      </form>
-      <div class="kg-actions" style="margin-top:0.75rem;gap:0.5rem;display:flex;flex-wrap:wrap">
-        <form method="post" action="/{{ owner.username }}/{{ repo.name }}/settings/mirror/sync">
-          <button class="kg-btn kg-btn--ghost" type="submit">sync now</button>
-        </form>
-        <form method="post" action="/{{ owner.username }}/{{ repo.name }}/settings/mirror/delete">
-          <button class="kg-btn kg-btn--danger-ghost" type="submit">remove mirror</button>
-        </form>
-      </div>
-    {% else %}
-      <p class="kg-muted">No mirror configured.</p>
-      <form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/settings/mirror" style="margin-top:1rem">

Large diffs are not rendered by default. Showing the first 50 of 203 lines. Show full diff

templates/repo_tree.html +2 −4
diff --git a/templates/repo_tree.html b/templates/repo_tree.html
index 3124b29..9a27884 100644
--- a/templates/repo_tree.html
+++ b/templates/repo_tree.html
@@ -27,8 +27,7 @@
       {% if e.is_dir %}
         <span class="kg-icon" style="--icon:url('/static/icons/folder.svg')"></span>
         <a href="/{{ owner.username }}/{{ repo.name }}/tree/{{ branch }}/{{ e.path }}">{{ e.name }}</a>
-        <span class="kg-tree__commit kg-meta" title="{{ e.commit_message }}">{{ e.commit_message }}</span>
-        <span class="kg-tree__time kg-meta">{{ e.commit_time }}</span>
+        <span class="kg-meta">{{ e.mode }}</span>
         <a class="kg-tree__dl kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/archive.zip?ref={{ branch }}&path={{ e.path }}" title="Download {{ e.name }} as ZIP">
           <span class="kg-icon" style="--icon:url('/static/icons/download.svg')"></span>
           <span class="kg-sr-only">Download</span>
@@ -36,8 +35,7 @@
       {% else %}
         <span class="kg-icon" style="--icon:url('/static/icons/file.svg')"></span>
         <a href="/{{ owner.username }}/{{ repo.name }}/blob/{{ branch }}/{{ e.path }}">{{ e.name }}</a>
-        <span class="kg-tree__commit kg-meta" title="{{ e.commit_message }}">{{ e.commit_message }}</span>
-        <span class="kg-tree__time kg-meta">{{ e.commit_time }}</span>
+        <span class="kg-meta">{{ e.mode }}</span>
       {% endif %}
     </li>
     {% endfor %}
templates/signup.html +14 −8
diff --git a/templates/signup.html b/templates/signup.html
index 9ed43de..5eceaf6 100644
--- a/templates/signup.html
+++ b/templates/signup.html
@@ -5,13 +5,7 @@
 {% block content %}
 <section class="kg-section kg-auth">
   {% if !signups_enabled %}
-    <div class="kg-flash kg-flash--banner">
-      {% if invite.is_empty() %}
-        {{ signup_disabled_message }}
-      {% else %}
-        Invite-only signup — your invite code unlocks registration.
-      {% endif %}
-    </div>
+    <div class="kg-flash kg-flash--banner">{{ signup_disabled_message }}</div>
   {% endif %}
   <p class="kg-landing__brand">
     <span class="kg-mark__glyph" aria-hidden="true">狐</span>
@@ -21,11 +15,12 @@
   {% if let Some(err) = error %}
     <div class="kg-flash">{{ err }}</div>
   {% endif %}
+  {% if signups_enabled || !invite.is_empty() %}
   <form class="kg-form" method="post" action="/auth/signup" autocomplete="on">
     {% if !signups_enabled %}
     <label>
       invite code
-      <input type="text" name="invite" required value="{{ invite }}" autocomplete="off" spellcheck="false" placeholder="Enter invite code" />
+      <input type="text" name="invite" required value="{{ invite }}" autocomplete="off" spellcheck="false" />
     </label>
     {% endif %}
     <label>
@@ -48,6 +43,17 @@
       <button class="kg-btn" type="submit">create account</button>
     </div>
   </form>
+  {% else %}
+  <form class="kg-form" method="get" action="/auth/signup" style="margin-top:1rem">
+    <label>
+      invite code
+      <input type="text" name="invite" required autocomplete="off" spellcheck="false" placeholder="Enter an invite code" />
+    </label>
+    <div class="kg-actions">
+      <button class="kg-btn" type="submit">continue</button>
+    </div>
+  </form>
+  {% endif %}
   <p class="kg-meta" style="margin-top:1.25rem">
     Already have an account? <a href="/auth/login">Log in</a>

Large diffs are not rendered by default. Showing the first 50 of 51 lines. Show full diff