Files
git-contribution-graph/CLAUDE.md
T
haylan a8d5f205db docs: fix typos and reformat tables in CLAUDE.md
Fix "Prever" typo, add Windows WSL test runner command, and align
Markdown table columns for readability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 14:14:38 +02:00

8.8 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 vendor/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

Prefer to use the tests in docker compose exec graph

# Run full suite
vendor/bin/phpunit

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

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

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

On Windows, run via WSL (If Docker Desctop is not):

wsl -e bash -c "cd /mnt/g/_DEV/repos/git-contribution-graph && docker compose exec graph vendor/bin/phpunit --testdox 2>&1"

Auto-run hook

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

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

Gitea Workflows

Workflow files live in .gitea/workflows/. This project uses Gitea Actions (GitHub Actions-compatible syntax).

When editing workflow files:

  • Use gitea.* context variables, not github.* — e.g. ${{ gitea.server_url }}, ${{ gitea.actor }}, ${{ gitea.repository }}
  • GITHUB_ENV and GITHUB_OUTPUT are still the correct env file names (Gitea Actions re-uses these)
  • Secrets are set under Repository → Settings → Secrets → Actions
  • The registry hostname must be derived from gitea.server_url (strip the protocol prefix)
  • Triggers use standard on: syntax; tags: '*.*.*' matches semver pushes without a v prefix

Current workflows:

File Trigger Purpose
docker-publish.yml Push tag *.*.* Build & push multi-arch image to Gitea container registry

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 vendor/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.