A static single-page app that browses any git repository served as plain static files (git's "dumb" HTTP protocol) — entirely in the browser. No server-side git, no preprocessing, no API: point it at a URL where a repo's files are reachable and it fetches refs, trees, blobs, and packfiles on demand.
The UI loosely follows GitHub's "Code" tab:
- Tree browsing with breadcrumbs, branch/tag selector, and README rendering below directory listings
- File view with shiki syntax highlighting, line numbers, and shareable
#L10-L20line-range anchors; images render inline, binaries offer download - GitHub-flavored markdown (tables, task lists, raw HTML sanitized) with relative links and images resolved from inside the repository
- Commit history with pagination and per-file history (the "History" button on any file or directory)
- Commit view with per-file diffs against the first parent
- Find file (
t) with fuzzy matching - Code button with the clone URL (the served files double as a
git cloneremote) and a ZIP download of the tree, built in the browser - Branches and tags pages, dark mode, recent-repos landing page
isomorphic-git can't fetch over dumb HTTP (it only speaks the smart
protocol), but all of its read APIs go through a pluggable fs. browsegit
implements a read-only fs (src/git/httpFs.ts) whose readFile is an HTTP
fetch against the repo URL:
- loose objects,
HEAD,packed-refsmap 1:1 to URLs readdir('objects/pack')is synthesized fromobjects/info/packsreaddir('refs/…')is synthesized frominfo/refs- 404s become
ENOENT, which isomorphic-git already handles as "fall back" (loose object → packfile, loose ref → packed-refs)
Everything else — commit walking, tree diffs, path-filtered logs — is stock isomorphic-git running against that fs, with responses cached per session (git objects are content-addressed, so object caches never go stale).
The repo must be reachable from the page — same origin, or served with CORS
headers (Access-Control-Allow-Origin: * is enough; only GET is used) —
and set up for dumb-HTTP access:
# one-off
git -C /path/to/repo.git update-server-info
# or automatically on every push (bare repos)
mv hooks/post-update.sample hooks/post-updateThat writes info/refs and objects/info/packs, which is how browsegit
discovers refs and packfiles (directory listings don't exist over dumb HTTP).
Any static file server works: nginx, GitHub Pages, S3, python -m http.server,
npx http-server --cors, …
Then open the app with the repo URL in the query string:
https://browsegit.namazso.eu/?repo=https://example.com/myrepo.git
https://browsegit.namazso.eu/?repo=/repos/myrepo.git (same origin)
npm install
npm run fixtures # generate sample repos under public/fixtures/
npm run dev # then open /?repo=/fixtures/test.gitnpm test— unit tests plus an end-to-end suite that runs the whole git layer against real repositories created with the git CLI (fetch is stubbed to serve their files from disk, byte-for-byte what a static server returns)npm run typecheck— app and test configsnpm run build/npm run preview— production build (hash routing and a relative base, so it deploys to any static host or subdirectory)
.github/workflows/deploy.yml builds the site (tests included) and publishes
it to GitHub Pages on every push to the default branch. Enable it once under
repo Settings → Pages by setting the source to "GitHub Actions" (the
workflow also attempts to enable this itself). The deployed site ships the
sample repos, so it works immediately at
https://<user>.github.io/<repo>/?repo=fixtures/test.git — note the relative
repo= value: on a project page a leading / would point outside the site.
Stack: React 19 + Vite + TypeScript, Tailwind CSS 4, TanStack Query, react-router (hash routing), isomorphic-git, shiki, react-markdown + remark-gfm + rehype-sanitize, jsdiff + react-diff-view, Primer octicons.
Inherent to the dumb protocol or to keeping the client honest:
git update-server-infois required on the served repo (see above); without it there is no way to enumerate refs or find packfiles.- Whole packfiles are downloaded the first time a packed object is read
(isomorphic-git parses packs in memory). Fine for small-to-medium repos;
a future optimization could use HTTP Range reads via the pack
.idx. - No last-commit-per-file column, blame, code search, or language statistics — each would require downloading large fractions of history.
- Mode-only changes (e.g. chmod) don't show a content diff; renames are displayed as delete + add; submodules appear as inert entries.
- Per-file history doesn't apply git's history simplification, so around merges it can list a few commits that didn't actually touch the file (isomorphic-git's path filter tracks the file linearly across the walk).