Files
git-contribution-graph/README.md
haylan cf7bd8d70c docs(readme): correct outdated deploy docs and docker-compose examples
README had drifted from the codebase: wrong test path, a stale /health
sample response, a docker-compose volume mount pointing at a path the
image never writes to (with no SQLite data volume at all), a missing
CONTRIBUTIONS_RETENTION_DAYS example, and an architecture diagram
predating the SQLite store/refetch command. Also fixes
docker-compose.prod.yml, which was missing the data volume and
CONTRIBUTIONS_RETENTION_DAYS env var present in docker-compose.yml —
without it, a prod deployment on this file loses contribution history
on every container recreate.
2026-07-12 23:43:59 +02:00

297 lines
9.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# git-contribution-graph
A self-hosted Symfony service that merges contribution data from **GitHub**, **GitLab** and **Gitea** into a single GitHub-style heatmap SVG you can embed anywhere.
```
https://your-host/graph.svg?theme=dark
```
![Example graph](https://gitgraph.arthurerlich.de/graph.svg)
---
## Features
- **Three platforms** — GitHub, GitLab (cloud or self-hosted), Gitea (self-hosted)
- **Merged heatmap** — contributions from all sources are summed per day
- **GitHub colour palette** — exact dark/light theme tokens
- **Embeddable** — returns `image/svg+xml`, works in any `<img>` tag or Markdown
- **Cached** — responses cached for 1 hour, safe to embed in public READMEs
- **Graceful degradation** — if one platform fails, the others still render
---
## Deploy
### Requirements
- Docker + Docker Compose
### 1. Create a `docker-compose.yml`
Use the pre-built image
```yaml
services:
graph:
image: git.arthurerlich.de/haylan/git-contribution-graph:latest
ports:
- "8080:8080"
env_file:
- .env.local
volumes:
- cache:/app/var/cache/prod/pools
- logs:/app/var/log
- data:/app/var/data
volumes:
cache:
logs:
data:
```
The image is published to the Gitea container registry. Pull it manually with:
```bash
docker pull git.arthurerlich.de/haylan/git-contribution-graph:latest
```
Semver tags are published (`0`, `0.0`, `0.0.1`) alongside `latest` — see the [container registry](https://git.arthurerlich.de/haylan/-/packages/container/git-contribution-graph/latest) for all available tags.
### 2. Configure
Create a `.env.local` file next to your `docker-compose.yml`:
```dotenv
APP_SECRET=<generate with: openssl rand -hex 16>
# GitHub
GITHUB_USER=your-github-username
GITHUB_TOKEN=ghp_…
# GitLab (omit GITLAB_URL to use gitlab.com)
GITLAB_USER=your-gitlab-username
GITLAB_TOKEN=glpat-…
GITLAB_URL=
# Gitea
GITEA_USER=your-gitea-username
GITEA_TOKEN=…
GITEA_URL=https://git.example.com
# Optional: restrict to specific hostnames (comma-separated), leave empty to allow all
ALLOWED_HOSTS=
# Optional: days of contribution history to keep in the SQLite store, empty/0 = keep forever
CONTRIBUTIONS_RETENTION_DAYS=
```
Only configure the platforms you use — unused ones are silently skipped.
### 3. Start
```bash
docker compose up -d
```
The service listens on **port 8080** by default (served by FrankenPHP/Caddy, baked into the image — no separate PHP-FPM/nginx needed). Put Traefik or nginx in front of it only if you need HTTPS termination.
### 4. Verify
```bash
curl http://localhost:8080/health
```
```json
{
"status": "ok",
"providers": {
"github": { "status": "ok" },
"gitlab": { "status": "ok" }
}
}
```
Returns HTTP 503 with `"status": "degraded"` if any configured provider's probe fails (its entry then includes `error` and `message`).
---
## API
### `GET /graph.svg`
| Parameter | Required | Description |
|---|---|---|
| `theme` | ✗ | `dark` (default) or `light` |
All credentials are configured via environment variables — see [Deploy](#deploy).
---
## Embedding in a README
```markdown
<!-- Dark theme (default) -->
![Contribution Graph](https://your-host/graph.svg)
<!-- Light theme -->
![Contribution Graph](https://your-host/graph.svg?theme=light)
```
---
## Token setup
### GitHub
**Fine-grained token (recommended):**
1. Go to **Settings → Developer settings → Personal access tokens → Fine-grained tokens**
2. Set **Resource owner** to your account
3. Under **Permissions → Account permissions**, set **Contribution activity** → Read-only
4. Generate and copy the token
**Classic token:** create a token with the `read:user` scope.
### GitLab
1. Go to **User Settings → Access Tokens**
2. Add a token with scopes: `read_user`, `read_api`
3. Generate and copy the token
### Gitea
1. Go to **Settings → Applications → Manage Access Tokens**
2. Add a token with permission: **user** → Read
3. Generate and copy the token
---
## Development
### Local (PHP)
```bash
# Install deps
composer install
# Run dev server
APP_ENV=dev php -S localhost:8080 -t public
# Test endpoint
curl "http://localhost:8080/graph.svg" -o graph.svg
```
### Docker (recommended)
`docker-compose.override.yml` is picked up automatically and targets the `dev` stage (Xdebug enabled, source mounted).
```bash
# Start dev container
docker compose up -d --build
# Shell into the container
docker compose exec graph sh
# Run tests inside the container
docker compose exec graph vendor/bin/phpunit
# Disable Xdebug for faster test runs
XDEBUG_MODE=off docker compose up -d
```
Xdebug listens on port **9003**. On Linux, `host.docker.internal` is resolved via `host-gateway`.
To run with production config only (no override):
```bash
docker compose -f docker-compose.yml up -d --build
```
---
## Testing
```bash
# Run full suite
vendor/bin/phpunit
# Human-readable output
vendor/bin/phpunit --testdox
# Single file
vendor/bin/phpunit tests/Unit/Service/Renderer/SvgRendererTest.php
# Filter by name
vendor/bin/phpunit --filter it_renders
```
### Static analysis
```bash
composer phpstan
```
Runs PHPStan at level 8 over `src/` and `tests/` (see `phpstan.neon`). First
run's findings are catalogued in [PHP-Stan-Errors.md](PHP-Stan-Errors.md).
---
## Architecture
Two-tier cache: a 1h filesystem SVG cache in front of a SQLite raw-data store, in front of the provider APIs.
```
GET /graph.svg?theme=dark|light
└─ GraphController
├─ host check (ALLOWED_HOSTS env, optional)
├─ cache lookup (filesystem, 1h TTL, key = "graph_{theme}")
│ └─ on miss: ContributionAggregator::aggregate()
│ ├─ per configured provider: ContributionStore::latestDate($name)
│ │ → $since = latest - 3 days (trailing overlap), or null on first run
│ ├─ GitHubProvider → GitHub GraphQL API (contributionCalendar query, bounded by $since/$until)
│ ├─ GitLabProvider → GitLab REST /users/:id/events (paginated, 100/page, `after`/`before` bounded)
│ ├─ GiteaProvider → Gitea REST /api/v1/users/:user/heatmap (filtered client-side by $since/$until)
│ │ each returns array<string, int> (Y-m-d => count); providers fetch concurrently
│ │ (start/resolve split); failures are caught and logged, remaining providers still render
│ ├─ ContributionStore::merge() persists fresh data per provider (SQLite upsert)
│ ├─ ContributionStore::all(sinceDays: 371) reads back the render window (source of truth)
│ ├─ ContributionStore::prune() drops rows older than CONTRIBUTIONS_RETENTION_DAYS
│ └─ SvgRenderer::render()
└─ Response: image/svg+xml, Cache-Control: public max-age=3600
```
**Provider activation:** a provider only runs when its env vars are non-empty. GitHub and GitLab require `_USER` + `_TOKEN`; Gitea additionally requires `_URL`. GitLab resolves a numeric user ID from the username via a `/api/v4/users?username=` lookup before fetching events.
**SvgRenderer:** builds a 53-column × 7-row grid aligned so the last column always ends on the Saturday of the current week. Five intensity levels (0 → level 0, 13 → 1, 46 → 2, 79 → 3, 10+ → 4) mapped to GitHub's colour tokens. No external assets — the SVG is fully self-contained.
**SVG cache:** filesystem adapter (`var/cache/`), mounted as a Docker volume to survive container restarts. Theme is part of the cache key so dark and light are cached independently.
**ContributionStore:** PDO SQLite at `var/data/contributions.db` (also a Docker volume), keyed by `(provider, date)`. Bounds every re-fetch to a trailing window off the last stored date per provider, instead of re-pulling all 365 days on every cache miss. `CONTRIBUTIONS_RETENTION_DAYS` controls how far back rows are kept (empty/0 = forever).
**Health check:** `GET /health` probes each configured provider's credentials/reachability (without fetching contribution data) and reports `ok`/`degraded`.
---
## CLI
`graph:contributions:refetch` is a manual escape hatch that bypasses the incremental trailing-window fetch and re-pulls a full or explicit date range — useful for backfilling history or recovering from a gap.
```bash
docker compose exec graph bin/console graph:contributions:refetch --all
```
| Option | Default | Description |
| ------------ | -------------------- | --------------------------------------------------------- |
| `--provider` | all configured | Comma-separated provider names to refetch, e.g. `github,gitlab` |
| `--from` | 365 days ago | Start date (`YYYY-MM-DD`) |
| `--to` | today | End date (`YYYY-MM-DD`) |
| `--all` | off | Shorthand for `--from=2005-01-01` |
Large ranges are split into ≤365-day chunks and merged into the store as each completes.
---
## License
MIT