~gitpocket/gitpocket

docs(basin): add mit licence

commit
4b74ef808b842cc08084616176bd12d1a1d764b4 browse tree
parent
f6ae2b8457269cd3b8d443e7a50fd07646e1d8cb
author
james <j@cpl0.zip>
signature
Verified ~james signed with a key registered by ~james
date
2026-11-12 19:33 UTC

5 files changed, +84 −43

LICENSE added

@@ -0,0 +1,21 @@
1+MIT License
2+
3+Copyright (c) 2026 james
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
11+
12+The above copyright notice and this permission notice shall be included in all
13+copies or substantial portions of the Software.
14+
15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+SOFTWARE.

README.md modified

@@ -1,27 +1,33 @@
1-basin 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.
1+# basin
22
3-## Build
3+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.
44
5-```
6-CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o basin .
7-```
5+## Features
86
9-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`.
7+- 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
8+- Tree and blob and log and commit and refs views, with chroma doing the highlighting and goldmark rendering the Markdown before bluemonday cleans it. Anything in `$x$` or `$$x$$` gets typeset in the browser
9+- 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
10+- 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
11+- Forks and pull requests from either a branch or a fork. Comments are Markdown, merges take an editable message, and a request can be closed or reopened or brought up to date from its source at any point
12+- SSH signature verification. Sign with `gpg.format ssh`, register the same key in settings, and your commits start showing `Verified ~you`
13+- SSH keys and access tokens live in settings. HTTPS auth is plain basic auth with a token or your password
14+- 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
1015
11-## Usage
16+## Design
1217
13-```
14-basin -root /srv/basin serve -listen 127.0.0.1:3200 -host git.example.org
15-basin -root /srv/basin invite
16-```
18+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.
1719
18-The data directory holds `basin.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.
20+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.
1921
20-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.
22+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.
2123
22-## Deployment
24+## Install
2325
24-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:
26+```
27+CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o basin .
28+useradd -r -m -d /srv/basin -s /bin/sh git
29+install -m 755 basin /usr/local/bin/basin
30+```
2531
2632 ```
2733 [Service]
@@ -32,11 +38,7 @@ ProtectSystem=strict
3238 ReadWritePaths=/srv/basin
3339 ```
3440
35-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.
36-
37-## SSH
38-
39-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:
41+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.
4042
4143 ```
4244 Match User git
@@ -48,24 +50,29 @@ Match User git
4850 DisableForwarding yes
4951 ```
5052
51-The lookup command finds the presented key by SHA256 fingerprint and answers with a forced command of `basin 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.
53+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.
5254
53-## HTTPS
55+```
56+sudo -u git basin -root /srv/basin invite
57+```
5458
55-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.
59+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`.
5660
57-## Signatures
61+## Use
5862
59-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.
63+```
64+git clone https://git.example.org/~you/repo
65+git clone git@git.example.org:~you/repo
66+```
6067
61-## Profiles
68+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.
6269
63-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.
70+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.
6471
65-## Pull requests
72+## Limits
6673
67-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.
74+There is no search inside files and no inline review comments, no squash or rebase merges, no collaborators beyond the single owner of a repo, and no disk quota of any kind. Blobs over 1 MiB are not rendered and anything over 64 MiB is not served raw either, while highlighting gives up past 256 KiB. Signature checking is SSH only for now, so commits signed with GPG show up as unverified.
6875
69-## Limitations
76+## Licence
7077
71-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.
78+MIT. KaTeX is MIT as well and Source Sans 3 is under the OFL, with both notices kept beside the files.

static/style.css modified

@@ -137,9 +137,9 @@ table.tree .last{max-width:30rem;overflow:hidden;text-overflow:ellipsis;width:10
137137 table.tree .when{text-align:right}
138138 table.refs td,table.keys td{padding:.3rem 1rem .3rem 0;border-bottom:1px solid var(--line)}
139139 .code{margin:0 -1rem;line-height:1.45;min-height:70vh}
140-.code .lntd:last-child{position:relative}
141-.code .lntd:last-child::after{content:"";position:absolute;top:0;bottom:0;left:calc(80ch + 1rem);border-left:1px solid var(--bar)}
142-table.blob,.code table{width:100%}
140+.code .chroma .lntable{width:auto;min-width:0}
141+.code .chroma .lntd:last-child{border-right:1px solid var(--bar);min-width:calc(80ch + 1rem);padding-right:1rem;width:auto}
142+table.blob{width:100%}
143143 table.blob td,table.hunk td,.code td{padding:0 .5rem;white-space:pre;vertical-align:top}
144144 td.n{text-align:right;color:var(--muted);user-select:none;width:1%}
145145 td.n a{color:var(--link);text-decoration:underline}
@@ -147,8 +147,9 @@ table.blob td.n{border-right:1px solid var(--bar);padding:0 .5rem 0 .6rem}
147147 td.l{white-space:pre-wrap;overflow-wrap:anywhere;width:100%}
148148 tr:target td{background:#fff8c5}
149149 .code pre,.code .chroma{background:#fff;border:0;padding:0;margin:0}
150+.code .chroma table.lntable{width:auto}
150151 .code .lntable{margin-top:0}
151-.code .lntd:first-child{width:1%;user-select:none;border-right:1px solid var(--bar);padding-top:.4rem;vertical-align:top}
152+.code .lntd:first-child{width:auto;user-select:none;border-right:1px solid var(--bar);padding-top:.4rem;vertical-align:top}
152153 .code .lntd:last-child{padding-top:.4rem}
153154 .code .lntd:first-child{min-height:70vh}
154155 .code .lnlinks{text-decoration:underline}
@@ -226,3 +227,9 @@ select{font:inherit;padding:.35rem .5rem;border:1px solid #ced4da;background:#ff
226227 .panel a.btn.block{margin-top:.4rem}
227228 .verified{display:inline-block;font-size:.72rem;border:1px solid #1a7f37;color:#1a7f37;border-radius:3px;padding:0 .3rem;vertical-align:middle;margin-left:.3rem}
228229 .verified.unknown{border-color:var(--muted);color:var(--muted)}
230+.auth{max-width:920px;margin:0 auto}
231+.auth h3 small{font-size:.9rem;font-weight:400;margin-left:.5rem}
232+.auth p{margin:.6rem 0 1rem}
233+form.stack.centred{margin:0 auto 1.2rem;max-width:34rem}
234+form.stack.centred input{width:100%}
235+.privacy{background:#fff3cd;border:1px solid #ffeeba;color:#5c4b00;padding:.6rem .8rem}

templates/login.html modified

@@ -1,8 +1,10 @@
11 {{define "content"}}
2-<div class="panel narrow"><h3>Log in</h3>
3-<form method="post" action="/login" class="stack">
2+<div class="auth">
3+<h3 class="rule">Log in to {{.Host}} <small>or <a href="/register">register</a></small></h3>
4+<form method="post" action="/login" class="stack centred">
45 <label>Username <input name="name" required autofocus autocomplete="username"></label>
56 <label>Password <input name="password" type="password" required autocomplete="current-password"></label>
67 <div class="actions"><button class="btn primary">Log in ▸</button></div>
7-</form></div>
8+</form>
9+</div>
810 {{end}}

templates/register.html modified

@@ -1,9 +1,13 @@
11 {{define "content"}}
2-<div class="panel narrow"><h3>Register</h3>
3-<form method="post" action="/register" class="stack">
4-<label>Username <input name="name" required autofocus autocomplete="username" pattern="[a-z0-9][a-z0-9_.-]{0,38}"><small>Lowercase letters, digits, dot, dash or underscore. Your page will be ~username.</small></label>
2+<div class="auth">
3+<h3 class="rule">Register for {{.Host}} <small>or <a href="/login">log in</a></small></h3>
4+<p>Registration gives you a page at <code>~username</code>, repositories under it, SSH and HTTPS push, and pull requests on anything public. No email is needed and you are logged in straight away.</p>
5+<form method="post" action="/register" class="stack centred">
6+<label>Username <input name="name" required autofocus autocomplete="username" pattern="[a-z0-9][a-z0-9_.-]{0,38}"><small>Lowercase letters, digits, dot, dash or underscore.</small></label>
57 <label>Password <input name="password" type="password" required minlength="8" autocomplete="new-password"></label>
68 {{if not .Data.Open}}<label>Invite code <input name="invite" required value="{{.Data.Invite}}"></label>{{end}}
7-<div class="actions"><button class="btn primary">Create account ▸</button></div>
8-</form></div>
9+<div class="actions"><button class="btn primary">Register ▸</button></div>
10+</form>
11+<p class="privacy"><strong>Privacy notice</strong>: {{.Host}} stores your username, a hash of your password, the keys and profile details you choose to add, and the address of each failed login attempt for a few minutes. Nothing is collected for marketing or analytics and nothing is shared with third parties.</p>
12+</div>
913 {{end}}