socle

README.md

socle๐Ÿ”—

A read-only web host for local git repositories.

Point it at some repos via a small config file, and it serves them: browse trees and files at any ref, with markdown rendered to HTML, and git clone works anonymously over smart HTTP. No javascript required, no push, no auth, no database, no dynamic anything beyond rendering.

socle shows committed state at refs, never the working tree. (That's the line between socle and its sibling vitrine: vitrine shows the filesystem as it is right now; socle shows what git has committed.)

Config๐Ÿ”—

socle takes exactly one config file, via the required -config flag (there is no default location and no search). The format is YAML:

sitename: parnassus
baseurl: https://code.example
repos:
  parnass:
    path: "%"
  work/infra/deploy:
    path: ../deploy
    description: "deploy scripts"
  • sitename (optional) is the display identity of this socle: it appears in the header, page titles, the entrance masthead, and the footer, and it seeds the accent color. When unset, the identity derives from the baseurl host's first label when one is configured, else from the machine hostname -- but a published socle usually wants to say its own name, not its machine's.

  • baseurl (optional) is the canonical outside address of this socle: an absolute http(s) URL naming only a host. When set, it becomes the displayed clone URL (otherwise that's derived from each request, which a rewriting proxy can fool) and enables serving Go vanity imports (see Go modules below).

  • repos maps URL names to local git repositories. A name may contain slashes, so the URL space can have depth. Name segments are limited to [A-Za-z0-9._-], with no leading dot or dash, and no name may be a path-prefix of another (that keeps URL dispatch unambiguous).

  • path (required) is the repository to serve: a worktree checkout or a bare repo, both work. The placeholder "%" means "same as the repo name". Relative paths resolve against the config file's directory, which makes a config portable alongside the repos it describes.

  • description (optional) shows on the index and the repo's home page.

A bad config (parse error, unknown key, invalid name, nothing configured at all) is a startup error: socle refuses to run rather than serve the wrong thing. A configured directory that's absent or not a git repository is only a warning and a skip, so one config can describe repos that only some hosts happen to have.

Config changes apply live: socle watches the config file (inotify for immediate pickup, plus a 5-second stat poll as backstop, so a change lands within ~5s worst case, usually instantly). Edit freely; a typo mid-edit logs a rejection and leaves the running server untouched, as does a reload in which not one configured repo can be opened. Requests in flight during a reload finish against the repo set they started with.

Browsing๐Ÿ”—

Every repo gets the same small set of pages:

/                            index of all repos
/<name>/                     repo home: rendered README, clone URL, branches, tags
/<name>/tree/<ref>/<path>    directory listings and file views
/<name>/raw/<ref>/<path>     the file's raw bytes
/<name>/log/<ref>            commit log, newest first
/<name>/commit/<ref>         one commit: metadata, message, and its diff

<ref> is a branch, tag, HEAD, or any commit sha. File views render markdown, show text in a code block, and inline images; anything binary or oversized degrades to a link to the raw bytes. URLs that address a commit by its full sha are served with immutable cache headers: they can never mean anything else.

Cloning๐Ÿ”—

The repo's home URL is its clone URL:

git clone http://host/name

Fetch is anonymous, over git's smart HTTP transport (protocol v2 included); a trailing .git on the URL also works, since git clients and habits both produce it. There is no push: socle never runs anything but read-only git subcommands, and a push attempt is refused explicitly.

Go modules๐Ÿ”—

With baseurl configured, repos are go get-able under their socle URLs as vanity import paths:

go get code.example/name

socle answers the toolchain's ?go-get=1 probes with the go-import meta tag (pointing at its own clone URL, so the fetch is the ordinary smart-HTTP transport above) and a go-source tag, so pkg.go.dev links source views back to socle's tree pages. The repo's go.mod must of course declare the matching module path, code.example/name.

Without baseurl this is all inert: an import prefix is a public claim that ends up in module caches and trackers, so socle only makes it on purpose. Note the go toolchain requires https on the probe (barring GOINSECURE), so in practice this means socle behind a TLS-terminating proxy.

Rendering and theming๐Ÿ”—

Pages are rendered by vitrine's display package (a sibling project, wired in via a replace directive in go.mod), so socle inherits its look and its markdown dialect: GFM with callout boxes and server-rendered dot diagrams, heading anchors, and light/dark following the browser's prefers-color-scheme.

Each socle wears an accent color derived from its sitename (the -theme-seed flag recolors without renaming, mostly useful for tests), so several socle instances in several tabs stay recognizable at a glance.

Known limits๐Ÿ”—

  • Branch and tag names containing / aren't addressable in URLs (they still work by sha), and the accepted ref charset is narrower than git's own rules.
  • The log page shows the newest 100 commits; there is no pagination past that.
  • Without baseurl configured, the clone URL on a repo's home page reflects the scheme and host of the direct request; behind a rewriting or TLS-terminating proxy it would be wrong.

Deployment๐Ÿ”—

Run socle as your own user, under a systemd user unit. No root anywhere.

  1. Build and install the binary somewhere on your user's path:

    go build -o ~/bin/socle .
    
  2. Write your config, e.g. ~/fusion/.socle.cfg, with paths relative to that directory (see Config above).

  3. Install the unit file from this repo as a user unit:

    mkdir -p ~/.config/systemd/user
    cp socle.service ~/.config/systemd/user/
    systemctl --user daemon-reload
    systemctl --user enable --now socle
    

    Adjust ExecStart in the unit if your binary, config, or port should differ.

  4. Keep it running after logout (user units otherwise stop with your session):

    loginctl enable-linger $USER
    

Check on it with systemctl --user status socle and journalctl --user -u socle -f.

README.md ยท 6.6 KiB ยท d6a1b2e597