2026-06-03 08:27:12 +00:00
2026-05-30 16:29:31 +02:00
2026-05-29 23:04:39 +02:00
2026-05-30 14:27:58 +02:00
2026-05-30 16:51:42 +02:00
2026-06-03 08:27:12 +00:00

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


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

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:

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 for all available tags.

2. Configure

Create a .env.local file next to your docker-compose.yml:

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=

Only configure the platforms you use — unused ones are silently skipped.

3. Start

docker compose up -d

The service listens on port 8080 by default. Put Traefik or nginx in front of it for HTTPS.

4. Verify

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.


Embedding in a README

<!-- 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)

# 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-compose.override.yml is picked up automatically and targets the dev stage (Xdebug enabled, source mounted).

# 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):

docker compose -f docker-compose.yml up -d --build

Testing

# 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<string, int>  (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, 13 → 1, 46 → 2, 79 → 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

S
Description
No description provided
Readme 269 KiB
Languages
PHP 98.2%
Dockerfile 1.8%