1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
# basin
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
- Repos sit under `~user/name` and can be public or unlisted or private. Each one takes up to three tags, and there is a `/projects` page that searches across all of them sorted by whoever pushed most recently
- Tree and blob and log and commit and refs views, plus a case insensitive search across file contents at any ref backed by `git grep`. Chroma does the highlighting and goldmark rendering the Markdown before bluemonday cleans it. Anything in `$x$` or `$$x$$` gets typeset in the browser
- The last commit shown against every tree entry comes out of one `git log --name-only` walk per directory instead of a separate process per file
- Profiles have a display name and a Markdown bio along with location and workplace and website, plus an avatar that you crop in the browser and the server then resamples down to a 256px PNG
- Forks and pull requests from either a branch or a fork. Discussion comments and inline review comments on any line of the diff are Markdown, review threads can be resolved and go to an outdated section when the line leaves the diff, and a request can be closed or reopened or brought up to date from its source at any point
- Three merge strategies, a merge commit with an editable message, a squash authored by the request author, or a rebase that replays each commit onto the target. All three are built from `merge-tree` and `commit-tree` without a working tree and fail cleanly on conflict
- Collaborators per repository who can push, read it when private and merge pull requests, while settings and deletion stay with the owner
- A per user disk quota set with `basin quota 2G` and enforced on push over both transports
- Signature verification for both SSH and PGP. Register the key in settings, sign commits with it, and they show `Verified ~you`. PGP keys are imported into a server keyring and matched by primary fingerprint, SSH keys through an allowed signers file matched by fingerprint. Keys are public at `~you.keys` and `~you.pgp`
- Issues per repository with Markdown bodies and comments, open and closed states, and an open count in the tab strip
- Stars, so a profile shows what someone found worth keeping, and a star count next to every repository title
- Blame, a compare page for any two refs, and tar.gz and zip downloads of any ref. The tar.gz is `git archive --format=tar` passed through a gzip writer with a zeroed header and a fixed level, so the same commit always produces the same bytes and `archive/<ref>.tar.gz.sha256` publishes the checksum that a package recipe can pin
- Every pushed revision of a pull request is kept under `refs/pull/<n>/v<k>`, and the request page links a `git range-diff` between any two of them, so a reviewer sees what a force push actually changed in each commit instead of rereading the whole series
- History search on the log page through git's pickaxe, either commits that change how often a string occurs (`-S`) or commits whose diff has a line matching a regex (`-G`), which answers when a line arrived or vanished without cloning
- Atom feeds of commits per branch, of issues and of pull requests per repository, and of every public commit by a user at `~you/feed`, which stand in for notifications when mail is not configured
- Rename, transfer to another user and archive from the repository settings. An archived repository refuses pushes, pull requests and comments but stays readable
- An optional email address on each account, verified by a link, used for password resets and for notifications about pull requests, reviews and issues on your repositories. Both can be turned off and nothing works without the address, so the whole thing is opt in
- TOTP two factor authentication with a QR code for the authenticator app and eight single use recovery codes, which is what a password alone cannot give you on something that accepts pushes
- SSH keys and access tokens live in settings. HTTPS auth is plain basic auth with a token or your password
- Relative links and images in a README resolve against the repository, so a `docs/diagram.png` reference renders rather than breaking
- Registration is invite only unless you say otherwise, and there is a failure counter per address covering login and register and password changes and HTTPS auth
## 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 cached anywhere and nothing is parsed out of packfiles in process, so a large repository costs exactly what git says it costs and the server itself holds no state between one request and the next.
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 basin .
useradd -r -m -d /srv/basin -s /bin/sh git
install -m 755 basin /usr/local/bin/basin
```
```
[Service]
User=git
ExecStart=/usr/local/bin/basin -root /srv/basin serve -listen 127.0.0.1:3200 -host git.example.org
Restart=always
ProtectSystem=strict
ReadWritePaths=/srv/basin
```
Put a reverse proxy with TLS in front of it. The proxy needs to forward `Host` and `X-Forwarded-Proto`, and it should set `X-Forwarded-For` to the client address rather than appending to it because the rate limiter keys on the first value it finds there. Turn request buffering off and raise the body limit as well, otherwise larger pushes over HTTPS will be cut short.
```
Match User git
AuthenticationMethods publickey
AuthorizedKeysFile none
AuthorizedKeysCommand /usr/local/bin/basin -root /srv/basin 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 `basin 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 basin -root /srv/basin invite
```
If you would rather let anyone sign up then `serve -open` skips the invite step entirely. `basin user <name>` creates an account from the command line with the password read from stdin, and `-ssh-user` changes the account name that appears in clone URLs if you did not call yours `git`.
Mail is configured in `smtp.json` under the root directory and is 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": "basin <noreply@git.example.org>"}
sudo -u git basin -root /srv/basin mail you@example.org
```
`basin 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. `basin backup file` writes a consistent copy of the database with `vacuum into`, which is safe to run from a timer while the server is live.
## 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 is checked before a push starts rather than during it, so a single push can overshoot by its own size. 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.
|