~gitpocket/gitpocket

8
161949a0fab0794bbb7e894db46ad9ede8bc5ce2
mainfeed-discovery
gitpocket/README.md -rw-r--r--11.2 KiB ViewLogBlameView rawPermalink 161949a0fa feat(repos): cap accounts and organisations at six repositories 5 hours ago
  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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# gitpocket

gitpocket is a self hosted git server in one static Go binary that serves repositories over SSH and HTTPS with a web interface rendered on the server. It idles at about 25 MB resident so it fits comfortably on a 512 MB VPS next to the git processes it starts.

## Quick start

```
git clone https://gitpocket.io/~gitpocket/gitpocket && cd gitpocket
useradd -r -m -d /srv/gitpocket -s /bin/sh git
printf 'GITPOCKET_HOST=git.example.org\nGITPOCKET_UID=%s\nGITPOCKET_GID=%s\n' $(id -u git) $(id -g git) > deploy/.env
docker compose -f deploy/compose.yaml up -d
```

The web interface is then on `127.0.0.1:3200` and wants a reverse proxy with TLS in front of it. Sign up and then make yourself an administrator with `docker exec gitpocket gitpocket admin you`. [INSTALL.md](INSTALL.md) covers the Docker and binary installs, nginx, SSH, mail, backups and upgrades in order.

## Features

- Public, unlisted and private repositories under `~user/name`, with tree, log, blame, compare, history search and tar.gz or zip downloads of any ref
- Branches, a branch switcher on every file view and a notice when a branch with new commits was pushed that links straight to a new pull request
- Markdown in pull requests, issues and comments with a preview tab and images you paste, drop or attach
- Pull requests from a branch or a fork with inline review, range diffs between force pushes and merge, squash or rebase without a working tree
- Organisations, collaborators, issues, labels, stars, forks, branch protection and per owner disk quotas enforced during the push, with at most six repositories per account or organisation unless an administrator creates them
- Profiles with a pinned repository list, an activity graph, followers and a README from your `~you/you` repository
- Imports from GitHub, GitLab, Gitea or Forgejo over https, with the option to credit commits made under your old identity to your account
- Pull mirrors that fetch a source on a schedule and stay read only and push mirrors that copy every branch and tag to another host after each change
- Pull requests opened by pushing to `refs/for/<branch>` with no fork needed
- Static sites served from `<user>.<host>` or a domain of your own, built from a `pages` repository or the `pages` branch of any repository, private ones included
- Gists of one or more flat files with public or secret visibility, revisions shown as diffs, stars, forks, comments, iframe embeds and a command line uploader
- A JSON API that mirrors the URL of every page under `/api`, with tokens scoped to everything, reads only or gists only
- Reports to admins and per account blocking, with hourly limits on new issues, pull requests and repositories
- SSH and PGP commit verification, TOTP, access tokens and Atom feeds for commits, issues and pull requests
- Site search over repositories, people and public gists with language facets
- Light and dark themes, light by default, with an option to follow the device
- A notification bell for activity on pull requests and issues you take part in
- Open, invite only or closed registration and an `/admin` page for accounts, invites, the front page, storage and gists

There is no wiki and no CI and neither is planned.

## Design

Every read of git data calls the plumbing with a thirty second timeout so a large repository costs exactly what git says it costs and nothing is parsed out of packfiles in process. Ref tables, trees, summaries and languages are cached against the mtime of the refs, which means one `stat` after a push invalidates them and deleting the cache loses nothing.

Pull requests fetch the source into `refs/pull/N/head` of the target. Merges are `merge-tree` and `commit-tree` followed by an `update-ref` that names the old value so a push racing the merge button makes the merge fail instead of being overwritten. SQLite comes from the pure Go modernc port which is why a `CGO_ENABLED=0` build needs nothing beside a system git.

## Usage

```
git clone https://git.example.org/~you/repo
git clone [email protected]:~you/repo
```

SSH pushes need a key added under settings and HTTPS pushes take an access token or your password. To contribute to a repository you do not own you push your commits to `refs/for/` followed by the target branch, which opens a pull request on that repository without any need for a fork.

```
git push origin HEAD:refs/for/main -o title="Fix the parser"
git push origin HEAD:refs/for/main/parser
```

Pushing again with the same topic updates that pull request and records a new revision, and the topic defaults to the target branch when the ref names none. Without `-o title` the first commit subject becomes the title and its body becomes the description, while `-o description=` overrides the body and `-o draft=true` opens the pull request as a draft. Anyone who is not the owner or a collaborator can only push to `refs/for/` and those guest pushes are capped at 20 MB each and 30 an hour.

### Pages

A repository named `pages` is served at `https://you.git.example.org/` from its `pages` branch when one exists and from the default branch otherwise, and any other repository with a `pages` branch appears beneath it at `/name/`. Private repositories publish only their `pages` branch so the site is public while the source on every other branch stays private, which matches how GitHub treats Pages built from a private repository. Paths without an extension also try `.html` and directories serve their `index.html`, while a missing file serves the site's own `404.html` with a 404 status when the site provides one. Symlinks and submodules are never followed and files over 32 MB are refused, accounts younger than a day have no site at all and admins can turn any site off from the admin page.

A site can also answer on a domain of your own once you claim it under settings, publish the TXT record `_gitpocket-pages.<domain>` with the token shown there and point the domain at your subdomain through a CNAME, so the address of the server is never handed out. Verification checks the TXT record and a daily recheck drops any domain whose record has disappeared, while `deploy/domains.sh` on the host registers every verified domain as a Cloudflare custom hostname and writes its nginx server block.

The pages origin never sets or reads cookies and the main site only uses `__Host-` cookies, so a script running on a user site can neither read a session from the main host nor plant a cookie that the main host would later accept.

### Gists

A gist is a bare repository at `~you/.gists/<id>.git` holding a single `main` branch of flat files, so it counts towards your quota, moves with a renamed account and clones or pushes like any repository through `https://git.example.org/gist/<id>.git` or `[email protected]:gist/<id>.git`. The pre-receive hook refuses pushes from anyone but the owner, other branches, deletions of `main`, directories, symlinks, more than 300 files and files over 10 MB, while every save in the web editor becomes a commit whose diff appears on the revisions page.

Secret gists are unlisted rather than private, which means anyone holding the link can read, clone or fork them while neither the gist index nor a profile ever lists one. Creating a gist needs an account at least a day old and shares the hourly upload limit, and anyone signed in may report a gist to the admins, who can delete it from its page.

Raw files and embeds are served from `gist.git.example.org`, a separate origin that never sees a cookie, answers only `GET` and sends every file as `text/plain` or an image under a sandboxed content security policy, so a hostile file cannot run script against the main site. A pinned revision such as `/raw/<id>/<commit>/<name>` is immutable for browsers but kept for at most an hour at the Cloudflare edge, so a gist that an admin deletes stops being served within the hour, while the embed page at `/embed/<id>` may be framed by any site.

`contrib/gist` uploads files or standard input with nothing beyond `curl`, reading the token from `GITPOCKET_TOKEN` and the site from `GITPOCKET_URL`, and prints the address of the new gist. A token scoped `gist` is enough for it and cannot touch your repositories.

```
install -m 755 contrib/gist ~/.local/bin/gist
gist -p -d "closure conversion" closure.ml README.md
make 2>&1 | gist -n build.log
```

### API

Every page has a JSON twin at the same path under `/api`, so `/~you/repo/pulls/3` is read from `/api/~you/repo/pulls/3`, and `/api` itself lists every route with the scope it needs, as JSON for programs and as a table for a browser. Accounts appear as `~name` handles and repositories as `~owner/name` paths, lists answer `{items, total, next}` with `?from=` and `?limit=` and every error is `{status, error}` with the HTTP status repeated in the body.

```
curl -H "Authorization: Bearer $TOKEN" https://git.example.org/api/me
curl -H "Authorization: Bearer $TOKEN" -d '{"title":"crash on empty input"}' https://git.example.org/api/~you/repo/issues
curl -H "Authorization: Bearer $TOKEN" -X PUT -d '{"content":"hello\n","message":"greet"}' https://git.example.org/api/~you/repo/tree/main/hello.txt
curl -H "Authorization: Bearer $TOKEN" -d '{"strategy":"squash"}' https://git.example.org/api/~you/repo/pulls/3/merge
```

Tokens are sent as a bearer or as the password of HTTP basic auth and carry a scope of `all`, `read` or `gist`, where a `read` token cannot push or change anything and a `gist` token adds writes to your own gists alone. Minting a token through `POST /api/me/tokens` needs the password and an account with two-factor authentication refuses the password everywhere in the API, so a leaked token can neither create another nor outlive its revocation. Session cookies only count when the request comes from the site itself, which lets any page call the API with a token through CORS while a cross-site form can never act with your session.

The API calls the same functions as the web forms, which means archived repositories, blocks, branch protection, the daily thread budget and the hourly creation limits apply identically, and writes are further capped at one a second with a burst of 120 per account.

### Mirrors

Ticking *keep it as a read only mirror* on the import page turns the import into a pull mirror that fetches the source again every 1, 4, 8 or 24 hours and refuses pushes, web edits and merges in the meantime. Push mirrors are added under the repository settings with a URL, a username and a token, and they push with `--force --prune` whenever the refs change before retrying after ten minutes if a push fails. Every fetch and push checks the URL again and resolves it once to a public address that stays pinned for the whole transfer, with redirects refused and https as the only protocol allowed. Tokens are sealed with AES-GCM under `mirror.key` in the data directory so that a copy of the database on its own does not reveal any of them.

## Development

```
go test ./...
```

The tests drive the real handler through `httptest` and cover each registration mode, cross site posts, private repositories, moved repositories, every merge strategy and Markdown injection payloads. The `refs/for/` test builds the binary and pushes through the smart HTTP handler with a real git client, which takes several seconds, so `go test -short` skips it when you only want the quick checks.

## Licence

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