Documentation

Last updated: 2026-11-12

Git over HTTP

Repositories are served over HTTPS as well as SSH, using git's standard smart HTTP protocol. Clone, fetch and push work with an unmodified git client.

One backend serves three things on one host: the transport described here, the web UI, and the runner protocol that workflows use. They come up together because a runner's instance URL is the host it clones from.

Enabling it

Turn the backend on and give it a host:

/backends enable git-http
/server settings set git.http.vhost git.example.com

git.http.vhost applies live — routing, the runner server URL, the web UI's instance name, and the vhost that binds HTTPS, issues a certificate and announces a .local name all move to the new host by the time the command returns. See Server settings. Git owns the fallback of its host: path backends such as CalDAV or the MCP endpoint share it, and the setting refuses a host whose fallback a site or another backend owns (Virtual hosts).

At process start the same values come from flags:

CLI flagEnv varMeaning
--backend-git-http[=BOOL]EVERLOCK_BACKEND_GIT_HTTPserve git over HTTP (off by default)
--backend-git-http-vhost <HOST>EVERLOCK_BACKEND_GIT_HTTP_VHOSThost the transport answers on; the git.http.vhost setting wins

The enabled flag persists in config/git-http.toml in the system store, which is what /backends enable git-http writes. The Git engine itself has no enable flag: it runs when either transport does, so turning on HTTP also brings up the repositories, the storage root, and the git.gc and git.retention jobs.

Clone URLs

git clone https://git.example.com/my-project.git
git clone https://git.example.com/my-project

Both work. Git appends /info/refs to whatever clone URL it is handed, so the suffix is optional and each route is served with and without it.

PathServes
GET /{repo}[.git]/info/refs?service=…ref advertisement for that service
POST /{repo}[.git]/git-upload-packfetch and clone
POST /{repo}[.git]/git-receive-packpush

Because of this, info/refs, git-upload-pack and git-receive-pack are reserved second path segments under a repository name.

Credentials

Authenticate with a password or an API key — the same funnel every other Everlock HTTP surface uses. An API key works as the Basic password whatever username accompanies it, which is the convention git's credential helper and forge access tokens rely on:

git clone https://x:evapi_…@git.example.com/my-project.git

The key names its own user, so the username is ignored.

Access is checked against http/git/<repo>, or */git/<repo> for a grant that covers every transport at once. A fetch takes reader; a push takes reader and a write right — writer on the repository, or on <repo>:pulls for pull requests only. See Access control and grants.

Two denial codes, and the difference matters to a git client:

  • 401 when no credentials were offered. Git reads the challenge and retries with its credential helper, so an anonymous denial must not be a flat 403.
  • 403 when credentials were offered and are insufficient.

A git-receive-pack advertisement requires what a push requires and answers 401 otherwise, so a client authenticates before uploading a pack rather than after. Wrong credentials feed the same per-IP failure window SMTP and IMAP use; a request carrying none is git's ordinary first probe and is not counted against it.

Protocol details

  • The advertisement carries the smart-HTTP prologue — the service name as a pkt-line, then a flush — with Cache-Control: no-cache, so no proxy hands a client a stale ref list.
  • The two services take different advertisements: the fetch one carries shallow boundary lines, the push one declares report-status, delete-refs and atomic.
  • A fetch may ask for a commit by its id — git fetch <remote> <commit> — when a branch or tag still reaches it. A commit no ref reaches is refused with not our ref.
  • info/refs for a service other than git-upload-pack and git-receive-pack is answered 403.
  • Both POSTs take only the type git sends, application/x-git-<service>-request, and answer anything else 415, so a page elsewhere cannot have a browser post to them. Both responses carry application/x-git-<service>-result.
  • Request bodies arrive chunked, may carry Expect: 100-continue, and are gunzipped when Content-Encoding: gzip is set. A fetch request — the commits a client wants and has — inflates to at most 64 MiB; past that the answer is 413. A push is as large as the history it brings.
  • Each request is a session of its own. The fetch side acknowledges the commits a client has in common with it (multi_ack_detailed), so an incremental fetch transfers only what is new.
  • A client offering Git-Protocol: version=2 is answered with the v0 advertisement the engine speaks, and falls back on its own.

Shallow clones work here exactly as they do over SSH: the boundary handling, deepen negotiation, per-ref capability enforcement and missing-object rejection live in the protocol engine, which is transport-free. See History retention.

Load and limits

Pack generation is the one endpoint that does real work for an unauthenticated caller, so a global semaphore bounds concurrent anonymous fetches at four. Beyond it a request is answered 503 with Retry-After.

Authenticated actors are not counted against that limit, because a fleet of workflow runners cloning once per task must not be throttled against strangers.

A fetch streams: the pack generator runs on the blocking pool and hands chunks through a bounded channel, so a slow client slows the generator rather than buffering a whole pack in memory, and a client that disappears releases the thread. A push is unpacked in memory before any of it is written, as over SSH; one that needs more memory than the machine has available is refused with not enough memory to unpack this push, and the server keeps running.

git http https transport credentials