# lilgit

A read-only HTTP(S) frontend for git, 
built on [Lilush](https://lilush.link) runtime.

Features:

- `git clone`/`fetch` over smart-HTTP
-  browsing UI:
   - repo index
   - directory trees at any ref
   - markdown/djot rendered as HTML
   - raw file serving. 
- optional HTTP Basic auth for selected repositories

Everything is served straight from the git object database,
i.e. from bare repositories on disk.

Pushes to served repositories appear immediately — the git core's
mtime caches invalidate themselves. 

No support for `git push` over HTTP, push over ssh to the bare repo instead.

## URL scheme

| Path | Behavior |
|---|---|
| `/` | repo index |
| `/{repo}` | redirect to the default-branch tree |
| `/{repo}/tree/{ref}/{path}/` | directory listing, README rendered below |
| `/{repo}/blob/{ref}/{path}` | markdown/djot as HTML; source as text; images/binaries raw |
| `/{repo}/raw/{ref}/{path}` | raw bytes, extension-guessed MIME |
| `/{repo}/refs` | branches and tags |
| `/{repo}/log/{ref}` | commit log (first-parent, paged via `?after=<sha>`) |
| `/{repo}/commit/{sha}` | commit view: metadata, message, changed files |

Every route of a repository listed in `auth.private` answers 401 until
credentials are presented; see "Private repositories" below.

`{repo}` is either a plain name (`lilush`) or a category-qualified one
(`tools/lilgit`) for repositories one directory level deeper in
`repos_dir`. `{ref}` is a branch (slashes allowed), tag, full `refs/...`
path, `HEAD`, or a full 40-hex sha; sha-pinned URLs are served as
immutable. Clone URLs (`/{repo}` or `/{repo}.git`) coexist with browse
URLs.
Tree and blob pages carry breadcrumbs and a ref switcher; setting
`show_sizes` in the config adds blob sizes to listings (costs a full
blob inflate per entry).

## Configuration

Read from `LILGIT_CONFIG_FILE` (default `/etc/lilgit/config.json`); a
missing file means pure defaults. CLI flags (`--ip`, `--port`, `--dir`,
`--title`) override everything.

```json
{
    "ip": "127.0.0.1",
    "port": 8091,
    "site_title": "my code",
    "repos_dir": "/srv/git",
    "repos": [
        {
            "name": "lilush",
            "path": "/srv/git/lilush.git",
            "description": "Linux Shell & Lua Framework",
            "default_branch": "master"
        }
    ],
    "ssl": { "default": { "cert": "...", "key": "..." } }
}
```

Repositories are discovered in `repos_dir` at startup (dirs with `HEAD`
or `.git/HEAD`). A top-level directory that is not itself a repository
is treated as a category and scanned one level deep: `/srv/git/repo.git`
serves as `repo`, `/srv/git/tools/lilgit.git` serves as `tools/lilgit`,
so repositories with the same basename can live in different categories.
A repository directory is never scanned for nested repositories, and
discovery never goes deeper than one category level. Explicit `repos`
entries (plain or category-qualified names) win on name collision. The
`ssl` table enables built-in TLS (SNI-capable); omit it when running
behind a reverse proxy.

`markdown_cache` sets the size of the rendered-markdown cache, keyed by
blob sha: the number of documents held per cache generation (at most
twice that many are live). The default is 32; `0` disables caching.

### Private repositories

Repositories named in `auth.private` are served only to visitors holding
HTTP Basic credentials from `auth.users` — browsing pages, raw blobs and
the smart-HTTP clone endpoints alike — and are left out of the repo index
until the visitor authenticates. Any valid user unlocks any private repo;
there is no per-repo access list. An empty `private` list (the default)
means auth is off and every discovered repository is public.

```json
{
    "auth": {
        "realm": "lilgit",
        "users": { "alice": "hunter2", "bob": "s3cret" },
        "private": ["work/secret", "scratch"]
    }
}
```

Names in `private` are registry names, exactly as they appear in the
index and in URLs (`repo` or `category/repo`). A name matching no
repository is logged as a warning at startup, not an error.

Caveats worth knowing before relying on this:

- Passwords are stored in the clear, so the config file belongs to root
  at mode 0600.
- Basic credentials travel in the clear as well: serve over TLS (the
  `ssl` table) or behind a TLS-terminating proxy.
- A private repository answers 401, not 404, so its existence remains
  inferable from its URL. Only its contents, description and index entry
  are hidden.
- There is no rate limiting or lockout on failed attempts.
- Responses for private repositories are marked `private` rather than
  `public` in `Cache-Control`, so shared caches will not store them.

## Tests

Run from the pack root:

```
lilush tests/run.lua
LILGIT_E2E=1 lilush tests/run.lua
```

The e2e spec forks a real server and drives it with the git and curl
binaries.

## Roadmap

Performance sweep:

- Header-only object reads (`read_object_header`) in the lilush git
  core, so `show_sizes` listings stop inflating every blob.
- Pack building on `git clone`, profiled against the lilush repo
  (~9.5k objects, 21 MB generated pack, ~3.5 s clone wall time, of
  which ~1 s is `collect_objects` and ~2 s is `build_pack`):
  - Every object is read and fully inflated twice — once during
    collection, once during serialization (2N `read_object` calls,
    ~116 MB inflated for the 21 MB pack). Memoizing contents between
    the two passes, or header-only reads for blobs during collection
    (collection parses only commits, trees and tags), halves the read
    cost.
  - `zlib.compress` accounts for roughly a third of the build (58 MB
    in, 21 MB out); the trailing sha1 over the pack is negligible.
  - Undeltified output makes the pack about 3.4x larger than git's
    own for the same repo (20.7 MB vs 6.1 MB), which inflates both
    compression and transfer cost. Delta reuse from existing packs is
    the larger, deferred fix.

Deferred features:

- SIGHUP rescan of `repos_dir` (today: restart to pick up new repos).
- Source blob view with line numbers and optional client-side
  syntax highlighting.
- Content diffs in the commit view (needs a line-diff implementation;
  today it lists changed files only, against the first parent).
- Full-history log across all parents (today: first-parent walk).
- Search across repos.
- Tarball snapshots (needs a tar writer).
