forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
7.8 KiB
1# Milestone 2: repo browsing and the raw domain (contract between workers)
2
3Scope 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.
4
5## Rules for user-supplied revisions
6- **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).
7- 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`.
8- Paths inside a repo are `/`-separated, with no empty segment, `.` or `..`, no NUL, and at most 4096 bytes.
9- **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.
10
11## forge-git/src/browse.rs (gix, blocking; callers use `tokio::task::spawn_blocking`)
12```rust
13pub enum RefKind { Branch, Tag }
14pub struct RefInfo { pub name: String, pub full_name: String, pub target: String, pub kind: RefKind } // target = peeled commit oid
15pub fn list_refs(repo: &Path) -> Result<Vec<RefInfo>>;
16pub fn default_branch_commit(repo: &Path, branch: &str) -> Result<Option<String>>;
17pub fn resolve_commit(repo: &Path, rev: &str) -> Result<Option<String>>; // full commit oid
18/// "feature/x/src/main.rs": longest existing branch/tag prefix wins; else a leading hex oid segment.
19/// Returns (ref_as_given, commit_oid, path) with path possibly "".
20pub fn split_ref_and_path(repo: &Path, rest: &str) -> Result<Option<(String, String, String)>>;
21pub enum EntryKind { Blob, Executable, Symlink, Tree, Submodule }
22pub struct TreeEntry { pub name: String, pub kind: EntryKind, pub oid: String, pub size: Option<u64> }
23pub fn read_tree(repo: &Path, commit: &str, path: &str) -> Result<Option<Vec<TreeEntry>>>; // dirs first, then name order
24pub struct BlobData { pub oid: String, pub kind: EntryKind, pub size: u64, pub data: Vec<u8>, pub truncated: bool, pub is_binary: bool }
25pub fn read_blob(repo: &Path, commit: &str, path: &str, max_bytes: usize) -> Result<Option<BlobData>>;
26pub struct CommitInfo { pub oid: String, pub parents: Vec<String>, pub author_name: String, pub author_email: String,
27 pub author_time: i64, pub committer_name: String, pub committer_email: String, pub committer_time: i64,
28 pub summary: String, pub message: String }
29pub fn read_commit(repo: &Path, oid: &str) -> Result<Option<CommitInfo>>;
30pub fn validate_repo_path(path: &str) -> Result<()>; // the rules above
31```
32`is_binary` means a NUL byte appears in the first 8000 bytes.
33
34## forge-git/src/history.rs (git CLI, async, OIDs only)
35```rust
36pub async fn log(env: &GitEnv, repo: &Path, commit: &str, path: Option<&str>, skip: usize, limit: usize) -> Result<Vec<browse::CommitInfo>>;
37pub enum DiffStatus { Added, Deleted, Modified, Renamed, Copied, TypeChanged }
38pub enum LineKind { Context, Add, Del, NoNewline }
39pub struct DiffLine { pub kind: LineKind, pub old_no: Option<u32>, pub new_no: Option<u32>, pub text: String }
40pub struct Hunk { pub header: String, pub lines: Vec<DiffLine> }
41pub struct FileDiff { pub old_path: Option<String>, pub new_path: Option<String>, pub status: DiffStatus,
42 pub is_binary: bool, pub additions: u32, pub deletions: u32, pub hunks: Vec<Hunk>, pub truncated: bool }
43pub struct DiffLimits { pub max_files: usize, pub max_lines_per_file: usize, pub max_total_lines: usize }
44pub struct CommitDiff { pub files: Vec<FileDiff>, pub truncated_files: bool }
45pub async fn commit_diff(env: &GitEnv, repo: &Path, oid: &str, limits: DiffLimits) -> Result<CommitDiff>;
46 // vs first parent (root commit vs empty tree), rename detection -M, --no-color, --no-ext-diff, --no-textconv
47pub async fn last_commits(env: &GitEnv, repo: &Path, commit: &str, dir: &str, names: &[String], max_walk: usize)
48 -> Result<HashMap<String, browse::CommitInfo>>; // one `git log` walk, not N calls
49pub enum ArchiveFormat { TarGz, Zip }
50pub fn archive_command(env: &GitEnv, repo: &Path, commit: &str, format: ArchiveFormat, prefix: &str) -> Result<GitCommand>;
51```
52
53## forge-core/src/raw_token.rs (signed URLs for private raw files)
54```rust
55pub fn sign_raw(key: &[u8; 32], repo_id: &str, expires_unix: i64) -> String; // "<expires>.<base64url(hmac_sha256)>"
56pub fn verify_raw(key: &[u8; 32], token: &str, repo_id: &str, now_unix: i64) -> bool; // constant-time; rejects expired
57```
58The key is derived for this purpose: HMAC over `"raw-v1:" + repo_id + ":" + expires`.
59
60## forge-web/src/render/ (pure functions)
61```rust
62// markdown.rs: comrak (GFM: tables, tasklists, strikethrough, autolink, footnotes, alerts), then ammonia
63pub struct LinkContext { pub tree_base: String, pub blob_base: String, pub raw_base: String } // each ends at the current dir, with no trailing slash
64pub fn render_markdown(src: &str, ctx: Option<&LinkContext>) -> String;
65// Relative links: page links go to blob_base/tree_base, images to raw_base. Absolute http(s) and mailto are kept;
66// any other scheme is dropped. Headings get id="user-content-<slug>". No inline styles (the CSP has no 'unsafe-inline').
67// highlight.rs: syntect with ClassedHTMLGenerator (class-based, never inline style)
68pub fn highlight_lines(file_name: &str, text: &str) -> Option<Vec<String>>; // one HTML fragment per line; None if unknown or >512 KiB or >20k lines
69pub fn highlight_css() -> String; // light + dark (prefers-color-scheme), served at /assets/highlight.css
70```
71
72## Web routes (app origin)
73- `/{owner}/{repo}`: repo home. Shows the default-branch tree plus the rendered README (README.md, README, readme.markdown), or the empty-repo help.
74- `/{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.
75- `/{owner}/{repo}/blob/{*rest}`: a file view.
76 - Text files are highlighted with line numbers and `#L12` anchors.
77 - Markdown files are rendered, with a toggle to see the source.
78 - Images get an `<img>` that points at the **raw origin**.
79 - Binary files get a "download raw" link.
80 - Large files are truncated with a link to raw.
81- `/{owner}/{repo}/commits/{*rest}`: commit history for a ref and optional path, paginated with `?page=`, 50 per page.
82- `/{owner}/{repo}/commit/{oid}`: commit metadata plus the diff (file list with stats, unified hunks). Large or binary diffs are collapsed.
83- `/{owner}/{repo}/archive/{ref}.tar.gz` and `.zip`: streamed, with `Content-Disposition: attachment`.
84- `/{owner}/{repo}/branches` and `/{owner}/{repo}/tags`.
85- Every route authorizes Read through `forge_core::authz::authorize`. A missing repo or one the user can't see returns **404**.
86
87## Raw origin (separate listener `user_content_listen`, explicit routes, NOT `.discover()`)
88- `GET /{owner}/{repo}/{*rest}`: `rest` is `<ref>/<path>`, split with `split_ref_and_path`.
89- 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.
90- Headers:
91 - `X-Content-Type-Options: nosniff`
92 - `Content-Security-Policy: default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; sandbox`
93 - `Cross-Origin-Resource-Policy: cross-origin`
94 - `Referrer-Policy: no-referrer`
95 - `Cache-Control: private, max-age=300` (or public for public repos)
96- Content-Type:
97 - png, jpg, jpeg, gif, webp, avif and ico are served as their image types
98 - other text is `text/plain; charset=utf-8`
99 - everything else is `application/octet-stream`
100- Additionally `Content-Disposition: attachment` for svg, html, htm, xhtml, xml and any non-image binary.
101- Missing, hidden or bad token: 404.