LibWorld
← All features

Self-hosted git forge

The mirror already speaks git, for repositories it caches from somebody else. The forge is the other half of that: repositories whose canonical home IS your machine — your own code, hosted beside the dependencies it builds against, in one URL space and behind one set of credentials.

Management and browse

Create, list, update, and delete repos over REST, and browse branches, commits, trees, blobs, and READMEs — all of it shelling out to git plumbing, with names, refs, and paths strictly validated, and every mutation audited.

Clone is anonymous; push is token-gated, over git http-backend. A push is handed to a detached worker so the accept loop never stalls behind a large receive-pack.

Push triggers the build

This is the part that makes the forge worth having rather than just convenient: git push can be the whole deploy. Creating a repo installs a post-receive hook, inert until the coordinator's URL and a shared secret are present in its environment. On a push to the project's default ref it HMAC-signs the repo, ref, and commit and posts to the coordinator, which verifies the signature and queues one CI target.

Hooks are self-healing: they are reinstalled for every repo at server startup, so a repo created over ssh — or one predating the feature — gets one on the next restart.

A client can write one file, and it deploys nothing

Editing a build recipe used to mean a push from a laptop, which is a strange requirement for a one-line change to a JSON file. A client can now commit a single file over REST — but never to the default ref, which is refused with a 409 saying why, because a push to the default ref is the thing that queues a build and, for this project, deploys the hub. An edit from a phone lands on its own branch.

"It runs no hook" is a property of the MECHANISM rather than a flag somebody remembered to set. The write is git plumbing, and receive hooks only run under receive-pack — so there is no configuration in which this path quietly triggers a deploy.

The cost of bypassing receive-pack is that the archive gate below does not fire either, so this route checks that itself. The two refusals are tested separately: a test that exercised only the push would report a protection an archived repo does not actually have.

Concurrent edits are a compare-and-swap, not a last-writer-wins. The update names the commit it expected to be replacing, so a second editor working from a stale view gets a 409 instead of silently erasing the first one.

Archived means read-only, and a hook is what says so

A repo can be archived, which keeps clone and fetch working — that is the reason it was archived rather than deleted — and refuses every push, naming the repo that succeeded it.

The refusal lives in a pre-receive hook rather than in the HTTP layer, and that choice is load-bearing twice over. It covers ssh and smart-HTTP alike, where an HTTP refusal would have covered one of the two ways anybody pushes here; and git push shows a hook's remote: output while it does not show the body of a 403, so the successor actually reaches the person being refused.

The state lives in the repo's own git config, so archiving takes effect at the next push with nothing restarted. post-receive could never have done this job: it runs after the write.

What it costs

Stated plainly, because a claim with no stated cost is the one nobody re-checks.

The smart-HTTP worker reads by Content-Length rather than chunked, so a push larger than git's default http.postBuffer (~1 MB) needs that setting raised on the client.