~gitpocket/gitpocket

1
c5a2e8a3f88d09ac380e1d245d568fb9ae34f511
mainfeed-discovery
gitpocket/README.md -rw-r--r--13.9 KiB ViewLogBlameView rawPermalink c5a2e8a3f8 docs(readme): describe organisations, admin and backups 21 days ago

View source

gitpocket

Small and lightweight git server for self hosting. Repositories are served over SSH and HTTPS, trees and logs and diffs and READMEs are rendered on the server rather than in the browser, and pull requests are handled without a working tree ever being checked out. The only JavaScript on the reading side is KaTeX, which is served only when a page contains mathematics.

Features

Design

Every read of git data is a call out to the plumbing with a thirty second timeout, which in practice means ls-tree -l for trees and log with a NUL separated format for history and diff-tree -p --root -M for commit pages and cat-file for blobs. Nothing is parsed out of packfiles in process, so a large repository costs exactly what git says it costs. A buffered call that tries to return more than 16 MiB gets killed. Only twice the core count may run together, or four on a single core box, and raw files and archives stream two at a time. On git.vm.fail the server idles at about 35 MB resident, so most of a 512 MB VPS is left for git.

A little is remembered between requests, though deleting all of it loses nothing. Language bars and the history behind the activity graph are stored against the mtime of the refs, so after a push one stat notices and before one nothing is read at all. That history holds sixteen bytes a commit (a hash prefix, the day and which author address it was), and the list of people that search walks is refreshed at most twice a minute.

Each client address gets a token bucket of five requests a second with a burst of 120. Search, blame, downloads, pickaxe and login draw from a second, much slower bucket because they are the pages that start real work in git. Pages are sent with a content security policy that only runs scripts served by the site, forbids framing and keeps the referrer inside the origin. Raw files go out as plain text unless they are images, audio or video, and those get a sandbox of their own, so a committed HTML or SVG file cannot run anything in a visitor's session. Go's cross origin protection rejects form posts from other sites before any handler sees them.

When a pull request is opened or updated the source branch gets fetched into refs/pull/N/head of the target repository, and from that point on the diff and the commit list and the question of whether it merges cleanly all come from merge-base and diff and merge-tree --write-tree inside that single repo. The merge itself is commit-tree with both parents followed by update-ref carrying the old value it expects to find, so if someone pushes to the branch while you are clicking the button the merge simply fails rather than silently overwriting them. Conflicts are reported back to you and left for you to sort out locally.

SQLite comes from the modernc port, which is pure Go, so a CGO_ENABLED=0 build produces one static binary that will run anywhere there happens to be a system git next to it.

Install

CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o gitpocket .
useradd -r -m -d /srv/gitpocket -s /bin/sh git
install -m 755 gitpocket /usr/local/bin/gitpocket

Copy the systemd units and nginx site from the deploy directory and change the host name in them. The service unit sets GOMEMLIMIT to 192 MiB and gives systemd a hard 512 MB ceiling covering the server and every git it starts, and the backup timer fires once a night.

install -m 644 deploy/gitpocket.service deploy/gitpocket-backup.service deploy/gitpocket-backup.timer /etc/systemd/system/
systemctl enable --now gitpocket gitpocket-backup.timer

Put a reverse proxy with TLS in front of it. The proxy needs to forward Host and X-Forwarded-Proto and should set X-Forwarded-For to the client address. The server only believes that header when the connection comes from loopback, and then takes the last address in it, so a client cannot choose its own rate limit bucket. Turn request buffering off and raise the body limit as well, otherwise larger pushes over HTTPS will be cut short. The nginx site in the deploy directory does all of this and adds a coarse request limit of its own.

Match User git
    AuthenticationMethods publickey
    AuthorizedKeysFile none
    AuthorizedKeysCommand /usr/local/bin/gitpocket -root /srv/gitpocket keys %u %h %t %k
    AuthorizedKeysCommandUser root
    PermitTTY no
    DisableForwarding yes

The keys command runs as root and opens the database read only and answers sshd with a forced command of gitpocket shell <user>. That in turn runs as the login user, checks what SSH_ORIGINAL_COMMAND is actually asking for against the database, and then execs straight into git upload-pack or git receive-pack so nothing is left running afterwards.

sudo -u git gitpocket -root /srv/gitpocket admin you
sudo -u git gitpocket -root /srv/gitpocket registration invite

Registration starts open. gitpocket registration prints or sets the mode (open, invite or closed), gitpocket invite prints a single use code, and gitpocket user <name> creates an account from the command line with the password read from stdin. -ssh-user changes the account name that appears in clone URLs if you did not call yours git. The header shows -name, which defaults to gitpocket, while -host stays the hostname used in clone URLs and feeds.

Mail is optional and nothing needs it to sign up. Without it password resets go through gitpocket passwd. It is configured in smtp.json under the root directory and read once at startup. Plain SMTP takes host, port, user, password, from and starttls, but many VPS providers block every outbound submission port, so api and key post each message as JSON to a Resend compatible HTTPS endpoint instead.

{"api": "https://api.resend.com/emails", "key": "re_...", "from": "gitpocket <noreply@git.example.org>"}
sudo -u git gitpocket -root /srv/gitpocket mail you@example.org

gitpocket feature ~you/a ~you/b chooses the projects on the logged out front page in that order, -~you/a drops one again, and with none chosen the page falls back to the most recently pushed. gitpocket backup <dir> writes a dated copy of the database with vacuum into, keeps the last fourteen, and writes a git bundle --all for every repository whose refs changed since its last bundle, so a nightly run touches only what moved. Restoring a repository is a git clone --mirror of its bundle into the repos directory.

go test ./...

The tests drive the real handler through httptest. They sign up under each registration mode, try a cross site post, poke at someone else's private repository and an organisation you are not in, follow a moved repository, merge in a throwaway bare repo, and feed the Markdown renderer <script>, onerror and javascript: payloads to check none survive.

Use

git clone https://git.example.org/~you/repo
git clone git@git.example.org:~you/repo

The .git suffix works but is not required. Pushing over SSH needs a key added under settings, while pushing over HTTPS takes a token or your password as the basic auth secret.

To send changes to something you do not own, fork it from the button beside the title and push a branch to your fork, then open a pull request from the pull requests tab and choose that branch as the source. The owner merges it from the sidebar when they are happy with it.

Limits

Raw files stream straight out of cat-file at any size, but the rendered view stops at 4 MiB and syntax highlighting at 512 KiB, beyond which a file is shown plain or offered as raw only. The quota limit counts the incoming pack, so a push that unpacks into loose objects can land slightly over it. Activity and search look at the 500 most recently created public repositories. There is no wiki and no CI, and there are no plans for either.

Licence

MIT. KaTeX is MIT as well and Source Sans 3 is under the OFL, with both notices kept beside the files.