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

229 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development
```bash
# 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
<?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`
```bash
# 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):
```powershell
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:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "vendor/bin/phpunit 2>&1 | tail -20"
}
]
}
}
```
## Gitea Workflows
Workflow files live in [.gitea/workflows/](.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).
```bash
# 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:
```bash
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.