Files
git-contribution-graph/CLAUDE.md
T

6.7 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Development

# Install dependencies (generates composer.lock if missing — commit it)
composer install

# Run dev server
APP_ENV=dev php -S localhost:8080 -t public

# Test the graph endpoint
curl "http://localhost:8080/graph.svg?theme=dark" -o graph.svg
curl "http://localhost:8080/health"

Testing conventions (apply to every test file)

  • declare(strict_types=1) at the top of every test file
  • Test classes are final and extend TestCase
  • Methods: #[Test] attribute + it_ prefix + snake_case — e.g. it_returns_empty_when_no_contributions
  • Structure: Arrange → Act → Assert, separated by blank lines; one assertion per test when possible
  • Use $this->assert*() not self::assert*()
<?php

declare(strict_types=1);

namespace App\Tests\Service;

use App\Service\SvgRenderer;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

final class SvgRendererTest extends TestCase
{
    #[Test]
    public function it_renders_an_svg_with_no_contributions(): void
    {
        $renderer = new SvgRenderer();

        $svg = $renderer->render([], 'light');

        $this->assertStringContainsString('<svg', $svg);
    }
}

TDD: Red → Green → Refactor

Always drive new features and bug fixes with this cycle. Claude writes implementation first by default — override that with explicit prompts.

Phase 1 — Red (write a failing test first)

Write a failing test for [feature description].
Do NOT write the implementation yet.
The test should fail because the class/method does not exist.

Phase 2 — Green (minimal implementation)

The tests are written and failing. Now implement the minimum code to make them pass. Nothing more.

Phase 3 — Refactor

Tests are green. Refactor the implementation for [readability / removing duplication / naming].
Run php bin/phpunit after each change to confirm tests stay green.

Common anti-patterns

Wrong prompt Why it breaks TDD Correct prompt
"Write tests for this feature" Claude implements first, then fits tests to it "Write failing tests for [feature]. Stop before any implementation."
"Add tests and implementation" Loses the design feedback of failing tests Two separate prompts: Red, then Green
"Make the tests pass" Encourages skipping to a green state "Implement the minimum to make the failing tests pass."
Combining Red + Green in one request No failing baseline Always separate the two phases

Running tests

# Run full suite
php bin/phpunit

# Run with human-readable output
php bin/phpunit --testdox

# Run a single test file
php bin/phpunit tests/Unit/Service/SvgRendererTest.php

# Run tests matching a filter
php bin/phpunit --filter it_renders

Auto-run hook

Add to .claude/settings.json to run PHPUnit automatically after every file edit:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "php bin/phpunit 2>&1 | tail -20"
      }
    ]
  }
}

Docker

Development

docker-compose.override.yml is picked up automatically and targets the dev stage (Xdebug + all deps, source mounted).

# Start dev container (override applied automatically)
docker compose up -d --build

# Shell into the container to run commands
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

# Force cache clear
docker compose exec graph rm -rf var/cache/*

# View logs
docker compose logs -f graph

Xdebug: listens on port 9003. Configure your IDE to accept connections from Docker. On Linux host.docker.internal is resolved via extra_hosts: host-gateway in the override file.

Production

Use only the base compose file to skip the dev override:

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

The final stage runs as a non-root app user and contains no Composer binary. The build pipeline is:

base → deps (composer install --no-dev)
     → build (copy source + dump-autoload)
     → final (copy vendor + source, chown app, USER app)

There is no composer.lock in the repo. If you add or change dependencies, run composer install locally and commit the resulting lock file — without it, Docker builds resolve versions fresh each time and may produce inconsistent results.

Architecture

Single-controller Symfony app with no database. The request flow:

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 query)
       │         ├─ GitLabProvider  → GitLab REST /users/:id/events (paginated, 100/page)
       │         └─ GiteaProvider   → Gitea REST /api/v1/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 contributions → level 0, 13 → 1, 46 → 2, 79 → 3, 10+ → 4) mapped to GitHub's exact 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.

Environment variables

Variable Required Notes
APP_SECRET Yes 32+ char random string
GITHUB_USER / GITHUB_TOKEN For GitHub Token scope: read:user
GITLAB_USER / GITLAB_TOKEN For GitLab Token scopes: read_user, read_api
GITLAB_URL No Defaults to https://gitlab.com
GITEA_USER / GITEA_TOKEN / GITEA_URL For Gitea Token scope: read:user
ALLOWED_HOSTS No Comma-separated hostnames; empty = allow all

Copy .env to .env.local for local development — .env.local is gitignored.