~gitpocket/gitpocket

1
e2df530c6f518a87782bc488543936bef5e9fc4b
mainfeed-discovery
gitpocket/README.md -rw-r--r--6.0 KiB ViewLogBlameView rawPermalink 49e889a370 feat(basin): configurable ssh user and deploy docs 1 months ago

View source

forge is a small self hosted git server in Go modelled on the browsing side of sourcehut. It serves repositories over ssh and smart https, renders trees, blobs, logs, diffs, refs and READMEs as server side HTML, and keeps users, profiles, keys, tokens and repository metadata in a single SQLite file. Source is highlighted with chroma, Markdown is rendered with goldmark and sanitised with bluemonday, and LaTeX in Markdown is typeset in the browser by a bundled KaTeX, which is the only JavaScript a reader ever loads.

Build

CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o forge .

The SQLite driver is the pure Go modernc port, so the binary is static and copies to the server as one file next to a system git.

Usage

forge -root /srv/forge serve -listen 127.0.0.1:3200 -host git.example.org
forge -root /srv/forge invite

The data directory holds forge.db and a repos/<user>/<name>.git tree of bare repositories. Registration needs a single use invite from the second command unless serve is given -open. Clone URLs take the sourcehut shape https://host/~user/repo and git@host:~user/repo, with the .git suffix optional.

Every read of git data shells out to the plumbing with a 30 second timeout rather than parsing packfiles in process. Tree listings use ls-tree -l, history uses log with a NUL separated format, and commit pages parse the unified output of diff-tree -p --root -M into hunks for rendering. This keeps the server stateless and lets large repositories inherit git's own performance, at the cost of one process spawn per page section.

Deployment

Nothing in the binary refers to a particular host. The public name comes from -host, the data directory from -root, the ssh account shown in clone urls from -ssh-user and the listen address from -listen, so a deployment is one static binary, one directory and a reverse proxy. A minimal unit for systemd runs it as the same unix user that sshd will hand git connections to:

[Service]
User=git
ExecStart=/usr/local/bin/forge -root /srv/forge serve -listen 127.0.0.1:3200 -host git.example.org
Restart=always
ProtectSystem=strict
ReadWritePaths=/srv/forge

Put nginx or caddy in front with TLS, forwarding Host, X-Forwarded-For and X-Forwarded-Proto, and raise the body size limit so large pushes over https get through. The address in X-Forwarded-For is what the login rate limiter keys on, so the proxy must set it rather than append to it.

SSH

Pushes over ssh reuse the host sshd. Add a block such as the following, where the key lookup runs as root and the forced command runs as the login user:

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

The lookup command finds the presented key by SHA256 fingerprint and answers with a forced command of forge shell <user>. That forced command reads SSH_ORIGINAL_COMMAND, checks ownership and visibility against the database, and execs git upload-pack or git receive-pack directly, so nothing stays resident between connections. The lookup opens the database read only and runs as root, while the forced command runs as the git user who owns the repositories.

HTTPS

Anonymous clients may fetch public repositories. Private repositories and all pushes require basic auth with either the account password or an access token created in settings. Token hashes are stored as SHA256, passwords as bcrypt. The web forms rely on the standard library cross origin protection, so a POST without a same origin Sec-Fetch-Site or Origin header from a browser is refused while non browser git clients pass.

Signatures

Commits signed with ssh keys (git config gpg.format ssh) are verified against an allowed-signers file that the server regenerates from every registered key whenever a key is added or removed. The principal pattern is a wildcard, so the committer email does not have to match anything, and the fingerprint git reports is mapped back to the account that registered the key, which is what the Verified ~user badge on log, commit and pull request pages means. A valid signature from a key nobody registered shows as unverified.

Profiles

Each user has a bio, location, workplace, website and an avatar. Avatar uploads are decoded on the server, cropped to the square the settings page chose with a small canvas script, resampled to 256 pixels and stored as PNG, so the served image never contains the original file's metadata or dimensions. Without JavaScript the crop falls back to the centred square.

Pull requests

Any account can fork a public repository, which runs a local bare clone, and open a pull request from a branch of the target repository or of their own fork. Creating the request fetches the source branch into refs/pull/N/head of the target, so the diff, commit list and mergeability are computed inside one repository with merge-base, diff and merge-tree --write-tree. Merging builds the merge commit with commit-tree and advances the branch with a compare and swap update-ref, so no working tree is ever checked out and a concurrent push makes the merge fail cleanly rather than clobber. Discussion is a flat list of Markdown comments. Repositories also carry a visibility of public, unlisted or private and up to three tags, which the /projects page searches and sorts by last push time taken from ref mtimes.

Limitations

There is no search, no collaborators beyond the owner and no disk quota, so a pushed repository is bounded only by the nginx body limit and free space. Login, registration and https basic auth share a per address failure counter of ten misses per five minutes, which nginx must feed through X-Forwarded-For. Blobs over 1 MiB are not rendered and over 64 MiB are not served raw. Pull requests have no inline review comments, no required reviews and no rebase or squash strategies, only a merge commit.