Metadata-Version: 2.4
Name: pygitweb
Version: 0.1.0
Summary: Gitweb reimplementation with FastAPI and Pygit2
License-Expression: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pygittools>=0.1.0
Requires-Dist: fastapi[standard-no-fastapi-cloud-cli]>=0.104.0
Requires-Dist: uvicorn[standard]>=0.24.0
Requires-Dist: python-multipart>=0.0.6
Requires-Dist: jinja2>=3.1.0
Requires-Dist: orjson>=0.19.0
Requires-Dist: pygit2>=1.12.0
Requires-Dist: pydantic-settings>=2.13.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: python-ripgrep==0.0.9
Dynamic: license-file

# 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/settings.json` (or `PYGITWEB_SETTINGS_CONFIG`), `PYGITWEB_*`
environment variables, and optionally a `.env` file in the working directory via
`pydantic-settings`. See `pygitweb/config.py` and `pygitweb/settings.schema.json` 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` — `0` / `false` / `off` disables auth; `1` / `true` / `on` enables it. If **unset or empty**, auth defaults to **on**.
- `PYGITWEB_AUTH_CONFIG` — path to the auth JSON file (default: `~/.pygitweb/auth.json`). Generated to a reasonable default if not present.
- `PYGITWEB_SETTINGS_CONFIG` — path to the settings JSON file (default: `~/.pygitweb/settings.json`). Created from the environment on first run if missing.
- Browser login: `GET /login` (optional `?next=/path`), `POST /login` with form fields `username`, `password`, `next`; sets an HttpOnly cookie with an opaque in-process session id (also accepted by protected routes via `Authorization: Bearer`). Sessions are wiped when the process exits. `GET /logout?next=/` revokes the session and clears the cookie.

## 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_raw | `h`, `f` | `GET /project/{project}?a=blame_raw&h=...&f=...` | File blame as JSON: array of `{commit_id, commit_msg, commit_author, time, line_range}`. Contiguous lines from the same commit are merged. |
| blame | `h`, `f` | `GET /project/{project}?a=blame&h=...&f=...` | File blame HTML view (syntax-highlighted source, line numbers, commit links per line range; hover shows author and date). |
| 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 `200 OK` with an empty body on timeout/shutdown, 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 (or the server is shutting down), the response is `200 OK` with an 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 enabled, `add` and `remove`
require a valid access token (`Authorization: Bearer …` from `POST /token`, or the cookie from `POST /login`); `check` is always allowed.
