# 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 `` 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 — no need to clone the repo: ```yaml services: graph: image: git.arthurerlich.de/haylan/git-contribution-graph:latest ports: - "8080:8080" env_file: - .env.local volumes: - cache:/var/www/html/var/cache volumes: cache: ``` 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= # 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= ``` 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. Put Traefik or nginx in front of it for HTTPS. ### 4. Verify ```bash curl http://localhost:8080/health # {"status":"ok"} ``` --- ## 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 ![Contribution Graph](https://your-host/graph.svg) ![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 php 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 php bin/phpunit # Human-readable output php bin/phpunit --testdox # Single file php bin/phpunit tests/Unit/Service/SvgRendererTest.php # Filter by name php bin/phpunit --filter it_renders ``` --- ## Architecture ``` GET /graph.svg?theme=dark|light └─ GraphController ├─ host check (ALLOWED_HOSTS env, optional) ├─ cache lookup (filesystem, 1h TTL, key = "graph_{theme}") │ └─ on miss: │ ├─ GitHubProvider → GitHub GraphQL API (contributionCalendar) │ ├─ GitLabProvider → GitLab REST API (/users/:id/events, paginated) │ └─ GiteaProvider → Gitea REST API (/users/:user/heatmap) │ each returns array (Y-m-d => count) │ failures are caught and logged; remaining providers still render │ └─ merge by date (sum counts across providers) │ └─ 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, 1–3 → 1, 4–6 → 2, 7–9 → 3, 10+ → 4) mapped to GitHub's colour tokens. No external assets — the SVG is fully self-contained. **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. --- ## License MIT