Concepts
This page lists what amber-git enforces today. Where a protection is missing or partial, it says “Not yet”.
What needs approval
amber-git sorts every ref update in a push by whether it can lose history:
| Update | Loses history? | What happens |
|---|---|---|
| Create a branch or tag | No | Applied at once |
| Fast-forward a branch | No | Applied at once |
| Force-push (non-fast-forward update) | Yes | Queued for the owner’s approval |
| Delete a branch or tag | Yes | Queued for the owner’s approval |
| Move an existing tag, forward or not | Either way | Refused, for everyone |
A tag doesn’t move once it exists, because people and tools treat a release tag’s name as fixed. To repoint one, delete it (queued for the owner’s approval) and push it again.
In a push of several refs, each is decided on its own: the free ones apply and the gated ones are refused with their handles. Every repository is gated this way; there is no setting that turns it off.
Queued force-pushes and deletes
When you push a gated update:
- amber-git refuses the visible ref move. git prints
! [remote rejected] <ref> -> <ref> (queued for approval by the repo owner (ref <handle>)). - It files a request with Granite for the repository’s owner, whoever pushed. The request names the repository, the ref, the old and new commits, who asked (a person, or an agent and its client), and your handle.
- The owner approves or denies it in Granite.
- On approval, amber-git applies the update. Granite notifies it as soon as the owner decides; amber-git also checks Granite for decisions every 5 minutes, so a lost notification only delays the update.
Rules that hold throughout:
- Approved for one exact move. A force-push applies only if the ref still points where it did when you pushed. If anyone has moved it since, the approval applies nothing; push again.
- A delete names the commit it removes. Your push must send the ref’s current commit as its old value, as stock git does; a delete that names no commit is refused. The owner’s approval deletes the ref at once, and only if it still points at that commit; if it has moved since, nothing is deleted. You can push the branch again later.
- Denied or withdrawn means never. Once the owner denies a request or you withdraw it, nothing can apply it.
- The handle is not the Granite request. The pusher only ever sees the handle. Use it to withdraw the update before the owner decides.
- You can check on it. Ask by the commit you pushed whether the update is still waiting; an empty answer means it never will apply, or already has.
- A deadline. Granite gives the owner 7 days to decide. After that the request can’t be approved; withdraw it and push again if you still need it.
- The owner approves their own pushes too. A force-push by the owner is queued like anyone else’s.
Default branch protected by CI
A repository can be set up so that its default branch moves only to commits
whose required CI checks passed. It applies to repositories registered with
Stackwell’s CI whose default branch holds .amber/ci.toml. In such a
repository, a push that creates or moves the default branch is refused unless
CI reports that every required check of the new commit passed:
| Refusal | What to do |
|---|---|
amber-ci gate: no CI run for <sha>; push it to a branch first, then push main once amber-ci/* is green | Push the commit to another branch, wait for CI, then push it to the default branch |
amber-ci gate: required checks for <sha> are still running ... | Push again once they pass |
amber-ci gate: required checks for <sha> did not pass ... | Fix the commit |
If amber-git can’t get an answer from CI, it refuses the push and asks you to retry. A force-push to the default branch must pass this check before it is queued for approval.
Storage
Every object you push is stored append-only: amber-git never deletes it. Branches and tags are pointers, updated only by compare-and-set, so two writers can’t silently overwrite each other’s update.
Objects from a push that was refused, in whole or in part, are kept too. They can’t be fetched or read: amber-git serves only commits reachable from a branch or tag.
Renamed and retired repositories
Stackwell can rename or retire a repository, with the owner’s approval. A
renamed repository answers 410 with its new name. A retired one answers
410 and stops accepting pushes and serving fetches. Neither erases its
contents, and a used name is never given to another repository.
GitHub mirror
A repository can be set up to mirror to GitHub. amber-git then pushes every
accepted ref update to the mirror, so the mirror follows amber-git, never the
other way round. Mirroring is best effort: a mirror push that fails in transit
is tried four more times, after 30 seconds, 2, 5 and 10 minutes (to ride out
a GitHub outage of several minutes), and then dropped; one
GitHub refuses is dropped at once. POST /<repo>.git/remirror re-sends every
ref, and every delete; see
API examples.
Reading files without cloning
GET /api/repos/<repo>/tree/<sha> lists a commit’s files and
GET /api/repos/<repo>/blob/<sha>/<path> returns one file. Both are
authorized like a fetch and serve only commits reachable from a branch or
tag. See API examples.
Not yet
- A failed apply is not retried. If applying an approved update fails on the server, the update is dropped and the approval is used up. Check the ref after approval, and push again if it didn’t move.
- Large histories. One clone, fetch or push may read only a bounded amount of commit and tree data: about 64 MB for a clone or fetch, about 16 MB for a push. Past it, the operation is refused with a message. Fetch fewer branches or a shallow clone, or push a long history in parts.
- Deleting contents. Nothing removes a pushed object or a repository’s contents; see Data and privacy.
- No public repositories. Every read needs a token and a grant.