haylan 78e7a72ee9 fix(provider): normalize baseUrl before assigning readonly property
Promoted readonly properties can't be reassigned in the constructor
body, so GitLabProvider and GiteaProvider threw on every instantiation.
Normalize the plain parameter first, then assign to a separately
declared readonly property.
2026-07-11 16:46:04 +02: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-04 00:44:18 +02: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
614 KiB
2026-07-12 21:55:28 +00:00
Languages
PHP 98.6%
Dockerfile 1.4%