# Milestone 2: repo browsing and the raw domain (contract between workers) Scope comes from `docs/architecture.md` §5.5, §7.4 and "M2". Global rules from `docs/m1-interfaces.md` still apply: latest dependencies added to root `[workspace.dependencies]` with `{ workspace = true }` in crates, no shell, no unwrap in non-test code, and clippy `-D warnings` must be clean. ## Rules for user-supplied revisions - **The git CLI only ever receives full hex OIDs.** User input like a branch, tag or short SHA is resolved to a full commit OID first with `browse::resolve_commit` (gix, in-process). - Accepted rev input is either a ref name that exists under `refs/heads/` or `refs/tags/`, or a 4–64-char hex OID prefix. Anything else (`..`, `@{`, `:`, `^`, `~`, a leading `-`, control characters) resolves to `None`. - Paths inside a repo are `/`-separated, with no empty segment, `.` or `..`, no NUL, and at most 4096 bytes. - **Symlinks:** blobs are read from the object database, never from the filesystem, so symlinks can't be followed. A symlink entry (mode 120000) is shown as its target text and is never resolved. ## forge-git/src/browse.rs (gix, blocking; callers use `tokio::task::spawn_blocking`) ```rust pub enum RefKind { Branch, Tag } pub struct RefInfo { pub name: String, pub full_name: String, pub target: String, pub kind: RefKind } // target = peeled commit oid pub fn list_refs(repo: &Path) -> Result>; pub fn default_branch_commit(repo: &Path, branch: &str) -> Result>; pub fn resolve_commit(repo: &Path, rev: &str) -> Result>; // full commit oid /// "feature/x/src/main.rs": longest existing branch/tag prefix wins; else a leading hex oid segment. /// Returns (ref_as_given, commit_oid, path) with path possibly "". pub fn split_ref_and_path(repo: &Path, rest: &str) -> Result>; pub enum EntryKind { Blob, Executable, Symlink, Tree, Submodule } pub struct TreeEntry { pub name: String, pub kind: EntryKind, pub oid: String, pub size: Option } pub fn read_tree(repo: &Path, commit: &str, path: &str) -> Result>>; // dirs first, then name order pub struct BlobData { pub oid: String, pub kind: EntryKind, pub size: u64, pub data: Vec, pub truncated: bool, pub is_binary: bool } pub fn read_blob(repo: &Path, commit: &str, path: &str, max_bytes: usize) -> Result>; pub struct CommitInfo { pub oid: String, pub parents: Vec, pub author_name: String, pub author_email: String, pub author_time: i64, pub committer_name: String, pub committer_email: String, pub committer_time: i64, pub summary: String, pub message: String } pub fn read_commit(repo: &Path, oid: &str) -> Result>; pub fn validate_repo_path(path: &str) -> Result<()>; // the rules above ``` `is_binary` means a NUL byte appears in the first 8000 bytes. ## forge-git/src/history.rs (git CLI, async, OIDs only) ```rust pub async fn log(env: &GitEnv, repo: &Path, commit: &str, path: Option<&str>, skip: usize, limit: usize) -> Result>; pub enum DiffStatus { Added, Deleted, Modified, Renamed, Copied, TypeChanged } pub enum LineKind { Context, Add, Del, NoNewline } pub struct DiffLine { pub kind: LineKind, pub old_no: Option, pub new_no: Option, pub text: String } pub struct Hunk { pub header: String, pub lines: Vec } pub struct FileDiff { pub old_path: Option, pub new_path: Option, pub status: DiffStatus, pub is_binary: bool, pub additions: u32, pub deletions: u32, pub hunks: Vec, pub truncated: bool } pub struct DiffLimits { pub max_files: usize, pub max_lines_per_file: usize, pub max_total_lines: usize } pub struct CommitDiff { pub files: Vec, pub truncated_files: bool } pub async fn commit_diff(env: &GitEnv, repo: &Path, oid: &str, limits: DiffLimits) -> Result; // vs first parent (root commit vs empty tree), rename detection -M, --no-color, --no-ext-diff, --no-textconv pub async fn last_commits(env: &GitEnv, repo: &Path, commit: &str, dir: &str, names: &[String], max_walk: usize) -> Result>; // one `git log` walk, not N calls pub enum ArchiveFormat { TarGz, Zip } pub fn archive_command(env: &GitEnv, repo: &Path, commit: &str, format: ArchiveFormat, prefix: &str) -> Result; ``` ## forge-core/src/raw_token.rs (signed URLs for private raw files) ```rust pub fn sign_raw(key: &[u8; 32], repo_id: &str, expires_unix: i64) -> String; // "." pub fn verify_raw(key: &[u8; 32], token: &str, repo_id: &str, now_unix: i64) -> bool; // constant-time; rejects expired ``` The key is derived for this purpose: HMAC over `"raw-v1:" + repo_id + ":" + expires`. ## forge-web/src/render/ (pure functions) ```rust // markdown.rs: comrak (GFM: tables, tasklists, strikethrough, autolink, footnotes, alerts), then ammonia pub struct LinkContext { pub tree_base: String, pub blob_base: String, pub raw_base: String } // each ends at the current dir, with no trailing slash pub fn render_markdown(src: &str, ctx: Option<&LinkContext>) -> String; // Relative links: page links go to blob_base/tree_base, images to raw_base. Absolute http(s) and mailto are kept; // any other scheme is dropped. Headings get id="user-content-". No inline styles (the CSP has no 'unsafe-inline'). // highlight.rs: syntect with ClassedHTMLGenerator (class-based, never inline style) pub fn highlight_lines(file_name: &str, text: &str) -> Option>; // one HTML fragment per line; None if unknown or >512 KiB or >20k lines pub fn highlight_css() -> String; // light + dark (prefers-color-scheme), served at /assets/highlight.css ``` ## Web routes (app origin) - `/{owner}/{repo}`: repo home. Shows the default-branch tree plus the rendered README (README.md, README, readme.markdown), or the empty-repo help. - `/{owner}/{repo}/tree/{*rest}`: a directory listing with the last commit per entry. A breadcrumb links each path segment. The branch/tag selector is a plain list of links. - `/{owner}/{repo}/blob/{*rest}`: a file view. - Text files are highlighted with line numbers and `#L12` anchors. - Markdown files are rendered, with a toggle to see the source. - Images get an `` that points at the **raw origin**. - Binary files get a "download raw" link. - Large files are truncated with a link to raw. - `/{owner}/{repo}/commits/{*rest}`: commit history for a ref and optional path, paginated with `?page=`, 50 per page. - `/{owner}/{repo}/commit/{oid}`: commit metadata plus the diff (file list with stats, unified hunks). Large or binary diffs are collapsed. - `/{owner}/{repo}/archive/{ref}.tar.gz` and `.zip`: streamed, with `Content-Disposition: attachment`. - `/{owner}/{repo}/branches` and `/{owner}/{repo}/tags`. - Every route authorizes Read through `forge_core::authz::authorize`. A missing repo or one the user can't see returns **404**. ## Raw origin (separate listener `user_content_listen`, explicit routes, NOT `.discover()`) - `GET /{owner}/{repo}/{*rest}`: `rest` is `/`, split with `split_ref_and_path`. - Public repo: no auth. Private repo: requires `?token=` that passes `verify_raw` (the app embeds signed links valid for 10 min). No cookies are read or set on this origin. - Headers: - `X-Content-Type-Options: nosniff` - `Content-Security-Policy: default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; sandbox` - `Cross-Origin-Resource-Policy: cross-origin` - `Referrer-Policy: no-referrer` - `Cache-Control: private, max-age=300` (or public for public repos) - Content-Type: - png, jpg, jpeg, gif, webp, avif and ico are served as their image types - other text is `text/plain; charset=utf-8` - everything else is `application/octet-stream` - Additionally `Content-Disposition: attachment` for svg, html, htm, xhtml, xml and any non-image binary. - Missing, hidden or bad token: 404.