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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# Pygitweb
Gitweb reimplementation using **Python**, **FastAPI**, and **Pygit2**, ported from `git/gitweb/gitweb.perl`.
## Setup
```bash
uv sync --package pygitweb
```
Or install everything in the workspace (all members + dev tools):
```bash
uv sync --all-packages --group dev
```
## Run
From the repository root (after `uv sync`), with the workspace `.venv` activated:
```bash
uvicorn pygitweb.main:app --reload --host 0.0.0.0 --port 8000
```
Or:
```bash
python -m pygitweb.main
```
## Config
Settings load from `PYGITWEB_*` environment variables (or a `.env` file in the working directory) via
`pydantic-settings`. See `pygitweb/config.py` for the full schema. Common ones:
- `PYGITWEB_PROJECTROOT` — absolute path to directory containing git repositories (default: `$HOME`)
- `PYGITWEB_PROJECTS_LIST` — directory to scan, or path to a project-list file (default: `PROJECTROOT`)
- `PYGITWEB_EXPORT_OK` — filename that must exist to allow export (e.g. `git-daemon-export-ok`); empty = no check
- `PYGITWEB_SITE_NAME` — site name in titles (default: `PyGitWeb`)
- `PYGITWEB_GIT` — path to the git executable (default: `git`)
- `PYGITWEB_AUTH` — fully-qualified auth provider class (e.g. `module.path.ClassName`); unset/`None` to disable
## Routes
Load the `/docs` page for a detailed view of routes (below the readme) if in debug mode.
**No-project routes**
- `GET /`: project list
- `GET /index`: plain text project index (path, owner)
- `GET /opml`: OPML feed list
- `GET /project/{name}`: Project dispatch (See actions table)
**Hook management routes**
- `GET /project/{name}/hooks`: JSON `{hooks: [...samples...], bundles: [...]}` with current status (`installed`, `not_installed`, `different`) for every pygittools sample and bundle
- `POST /project/{name}/hook?name=<sample-or-bundle>&op=<add|remove|check>`: install, remove, or check one sample or bundle. `name` may be a sample filename (e.g. `post-receive.notify`) or a bundle name (e.g. `update`, which expands to `post-commit.notify` + `post-receive.notify`). `add`/`remove` refuse to clobber a custom hook with different content (returns `409`)
**Actions**
| Action | Query parameters | URL | Description |
|--------|------------------|-----|-------------|
| summary | *(default)* or `a` | `GET /project/{project}` | Project summary (description, owner, HEAD, tree link). |
| tree | `h`, `f` | `GET /project/{project}?a=tree&h=...&f=...` | Directory listing (tree). |
| blob | `h`, `f` | `GET /project/{project}?a=blob&h=...&f=...` | File view (HTML). |
| blob_plain | `h`, `f` | `GET /project/{project}?a=blob_plain&h=...&f=...` | Raw file download. |
| log | `h`, `updates`, `pf` | `GET /project/{project}?a=log&h=...` | Commit log. Supports long-poll subscribe. |
| shortlog | `h`, `updates`, `pf` | `GET /project/{project}?a=shortlog&h=...` | Shortlog. Supports long-poll subscribe. |
| history | `h`, `f`, `updates`, `pf` | `GET /project/{project}?a=history&h=...&f=...` | History of a file or path. Supports long-poll subscribe. |
| heads | `updates`, `pf` | `GET /project/{project}?a=heads` | List branch heads. Supports long-poll subscribe. |
| tags | `updates`, `pf` | `GET /project/{project}?a=tags` | List all tags. Supports long-poll subscribe. |
| tag | `h` | `GET /project/{project}?a=tag&h=...` | Single tag view (tag ref or hash). |
| commit | — | `GET /project/{project}?a=commit` | Commit information. |
| commitdiff | — | `GET /project/{project}?a=commitdiff` | Commit diff (unified diff rendered with [diff2html](https://github.com/rtfpessoa/diff2html)). |
| patch | `h` | `GET /project/{project}?a=patch&h=...` | Single-commit patch (plain text). |
| patches | `h`, `hb` | `GET /project/{project}?a=patches&h=...&hb=...` | Multi-commit patches for range `hb..h` (plain text). |
| blobdiff | `h`, `hb`, `f`, `fp` | `GET /project/{project}?a=blobdiff&h=...&hb=...&f=...&fp=...` | Blob diff (two versions of a file) rendered with diff2html. |
| blobpatch | `h`, `hb`, `f`, `fp` | `GET /project/{project}?a=blobpatch&h=...&hb=...&f=...&fp=...` | Blob diff as plain unified diff. |
| remotes | — | `GET /project/{project}?a=remotes` | List repo remotes. |
| object | `h` | `GET /project/{project}?a=object&h=...` | Show object by type (commit, tree, tag, or blob). |
| blame | — | | TODO |
| blame_incremental | — | | TODO |
| blame_data | — | | TODO |
| rss | — | | TODO |
| atom | — | | TODO |
| search | `patterns`, `paths`, `globs`, `heading`, `sort`, `max_count`, `multiline` | `GET /project/{project}?a=search&patterns=...` | Ripgrep search (via `python-ripgrep`); returns JSON. `paths` are relative to the project tree and cannot escape it. |
| search (page) | — | `GET /project/{project}/search` | Per-project search UI page. |
| search_help | — | | TODO |
## Query parameter short names (CGI mapping)
- `p` → project
- `a` → action
- `f` → file_name
- `fp` → file_parent
- `h` → hash
- `hp` → hash_parent
- `hb` → hash_base
- `hpb` → hash_parent_base
- `pg` → page
- `o` → order
- `s` → searchtext
- `st` → searchtype
- `sf` → snapshot_format
- `opt` → extra_options
- `sr` → search_use_regexp
- `by_tag` → ctag
- `ds` → diff_style
- `pf` → project_filter
- `updates` → long-poll subscription flag (`true` parks the request up to 30s; returns `304 Not Modified` on timeout, or the action data when the queue is notified)
## Live updates (long polling)
Supported actions (`history`, `log`, `shortlog`, `heads`, `tags`) accept `updates=true` to
subscribe to the project's change queue. The request is held for up to 30 seconds:
- A `POST /_internal/notify?project=<name>` (typically from a server-side git hook) wakes
matching subscribers, who then receive the freshly-rendered action response.
- If no notification arrives in 30 seconds, the response is `304 Not Modified` (empty body).
- Combine with `pf=<prefix>` to subscribe to every project under a path prefix instead of
just the URL project (the action is still rendered for the URL project).
The notify endpoint is intended for loopback use by `pygittools` post-receive hooks (see
`pygittools/hook_samples/post-receive.notify`); production deployments should restrict it
at the reverse proxy.
## Hook management
The project summary page exposes an **Update Hook** row that installs / removes the
`update` bundle: `post-receive.notify` (feeds the change queue when refs are pushed) and
`post-commit.notify` (feeds the change queue when a working clone of this repo records
a local commit). Either is sufficient to wake long-poll subscribers; installing both
covers server-side and client-side commit paths.
The same operations are available programmatically via
`POST /project/{name}/hook?name=<sample-or-bundle>&op=<...>`. Bundle status is
`INSTALLED` only when every member is installed, `DIFFERENT` if any member's path holds
a custom hook (the bundle then refuses to install or remove anything to preserve the
custom hook), otherwise `NOT_INSTALLED`. When `PYGITWEB_AUTH` is set, `add` and `remove`
require a valid session (`X-Session-Token` header, `?session=` param, or `session`
cookie); `check` is always allowed.