git-trace

romele / git-trace public

62 commits · 1 branch · 1 tag · 1 contributor (top: Romele) · first commit 3d ago · last push 17m ago

doc/web.md

# Web interface

Server-rendered, zero JavaScript, fully CSP-compliant
(`default-src 'none'`).  Light/dark follow the OS via
`prefers-color-scheme` — pure CSS, no toggle.

- `/` — for anonymous visitors a marketing landing: hero (logo,
  name, version, pitch, CTA) carrying a live "mini-trace" — the
  newest commits of the most recently pushed public repo rendered
  with the same timeline markup as the log view — then instance
  metrics (repos / users / storage), recent activity on public
  repos and featured repos;
  for logged-in users a compact dashboard.  Underneath, the
  searchable repo list with public/private badge, cached size,
  description, ordered by recent activity (relative times).
  Instance name is set by `GIT_TRACE_NAME`.
- `/new` — create a repository from the browser (name, description,
  private flag); shares `repo_create()` with the `mkrepo` CLI so
  hooks, description and DB registration are identical.
- `/<owner>/<name>` — redirects to the `tree` view.  The repo header
  shows the `owner / name` identity, a public/private badge, clone
  URLs as readonly inputs (HTTP always; SSH too when
  `GIT_TRACE_SSH_URL` is set — click → select-all → copy, no JS)
  and a quota line.  Below the file list, a root `README*` is the
  page's main content: `.md`/`.markdown` files go through a small
  in-tree renderer (`src/markdown.c`, ~300 lines — headings, fenced
  code, lists, quotes, rules, `code`/`bold`/`em`, links and bare
  autolinks restricted to inert schemes; everything else is
  escaped), other names render as preformatted text.  An unborn
  HEAD renders an "empty repository" quickstart panel with
  copy-paste `remote add`/`push -u` lines.  Every repo page carries
  a summary line under the `owner / name` header — commits,
  branches, tags and last-push time — sourced from
  `for-each-ref` + `rev-list --count`.
- **Web commits** — writers get `+ new file` / `⇪ upload` in the
  tree view and `edit` on the blob view.  Each action produces a
  real commit through bare-repo plumbing (`hash-object` → private
  `GIT_INDEX_FILE` → `write-tree` → `commit-tree` → the same
  `branch-check`/`quota-check` gates as `git push` → `update-ref`
  CAS).  Uploads are binary-safe multipart, capped at 4 MiB;
  the editor caps at 1 MiB.  `GT_VERSION` and commit
  author/committer come from the forge account.
- `/<owner>/<name>/log` — commits rendered as a vertical "trace"
  timeline (connected nodes, relative times, exact UTC in the
  tooltip) rather than a flat table — the project's namesake view.
- `/<owner>/<name>/changes` — unified list of merge requests and
  patch sets (type badge, state, author, updated), ordered by last
  update; `/mr/N` and `/patches/N` remain the canonical detail
  routes.
- `settings` and `activity` tabs; `settings` is only rendered for
  rank ≥ admin and also carries repository deletion: typing the repo
  name confirms, the on-disk tree is removed (`nftw`, no symlink
  following) and the DB row cascades to ACLs, issues, MRs, patch sets
  and branch rules.  `repo_delete` lands in the audit log.
- `/account` — user self-service (password, email, API tokens, SSH
  keys; see `security.md`).
- `/search?q=` — fixed-string code search across the tree at the
  current ref, powered by `git grep -n -F` on the bare repo (no
  worktree, no index, no regex DoS); results link straight into the
  `blob` view.  `i=1` makes it case-insensitive; 200-result cap.
- `/feed` — Atom feed of the last 40 commits
  (`application/atom+xml`), timestamps in RFC 3339, entries deep-link
  `/commit/<sha>`.  Private repos keep the same read gate, so a feed
  reader only works with a session cookie.
- Syntax highlighting — `view_blob` passes text blobs ≤ 256 KiB
  through a ~200-line in-tree tokenizer (`src/highlight.c`):
  comments, strings, keywords, numbers and C preprocessor lines get
  `<span class=hl-*>` styling on a quiet earth-tone palette.
  Profiles: C/C++-family and script/config (`#` comments).  Unknown
  extensions and oversized files render as plain escaped text.

## Collaboration

Under `/<owner>/<name>`:

- `issues` — tracker with create/comment/close-reopen (author or
  write permission), `issues/new` form for logged-in users
- `mr` — merge requests between same-repo branches.  Merging happens
  server-side, purely in the bare repo: `merge-tree --write-tree`
  produces the tree (or reports conflicts back to the page),
  `commit-tree` creates a two-parent merge commit, `update-ref`
  applies it with compare-and-swap on the old tip so concurrent
  pushes cannot be silently overwritten.  Merging requires write
  permission.
- `patches` — GNU patch sets, see `patch-workflow.md`
- `settings` — description, visibility, ACLs, branch protection,
  webhooks, mirrors, CI toggle, danger zone

## Static export

`git-trace export` renders repositories to a self-contained static
site, `stagit`-style — plain HTML with inline CSS, browsable straight
from `file://` or behind any dumb web server:

```sh
git-trace export                          # all public repos -> ./export/
git-trace export -o /var/www/site -n 50   # custom dir, cap 50 commits
git-trace export alice/demo _/site        # explicit selection
git-trace export --private                # include private repos too
```

Layout:

```
export/index.html                    repo index
export/<owner>/<repo>/index.html     file list + README + nav
export/<owner>/<repo>/log.html       recent commits
export/<owner>/<repo>/refs.html      branches and tags
export/<owner>/<repo>/commit/<sha>.html   metadata + diff
export/<owner>/<repo>/file/<path>.html    highlighted source
```

Discovery mirrors the web index: the filesystem is the source of
truth and `repo_perm` filters to what an anonymous reader could see.
`--private` lifts that filter — exporting *is* publishing, so private
code requires the explicit flag.  All links are relative, file pages
mirror the repo tree (encoded components, `.`/`..` neutralised),
binary blobs are skipped and diffs are capped at 512 KiB.

## SSH access

Single OS account model (gitolite-style): every user authenticates
via a public key bound to a forge identity.  Keys are managed in the
DB and rendered as forced commands:

```sh
git-trace sshkey add alice --name laptop < ~/.ssh/id_ed25519.pub
git-trace sshkey list
git-trace sshkey rm 3
git-trace keyfile --write        # regenerates <dbdir>/authorized_keys
# or self-service: /account -> ssh keys (same table, regenerates live)
```

Each generated line pins identity and locks down the session:

```
command="GIT_TRACE_REPOS=/srv/repos GIT_TRACE_DB=/srv/var/git-trace.db \
 /usr/bin/git-trace shell 7",no-pty,no-port-forwarding,... ssh-ed25519 AAAA...
```

`git-trace shell` parses `SSH_ORIGINAL_COMMAND`, whitelists
`upload-pack`/`receive-pack`, resolves `owner/name[.git]`, enforces
the same `repo_perm` ACL as HTTP, then execs the git binary directly —
the login shell never touches user input.  Bare `ssh git@host` prints
a gitolite-style access report instead of a shell.