Files
git-contribution-graph/TODO.md
T

6.6 KiB
Raw Blame History

TODO: persist contribution history in SQLite + audit cleanup

Backend-only work. Follow Red → Green → Refactor per CLAUDE.md for every step that adds behavior (store, retention, provider $since handling). Order matters — later steps assume earlier ones are done.

1. Audit cleanup

  • Remove eightpoints/guzzle-bundle + idci/graphql-client-bundle. They exist only to build one near-static GitHub GraphQL query string (GitHubProvider.php:59-74); the bundle's own HTTP transport is already bypassed (comment at GitHubProvider.php:76-77). Replace $graphqlClient->buildQuery(...)->getGraphQLQuery() with a plain sprintf/heredoc query string. Delete the two packages from composer.json, config/bundles.php, and delete config/packages/eight_points_guzzle.yaml + config/packages/idci_graphql_client.yaml. Run composer update and commit the regenerated composer.lock.
  • Dedupe baseUrl normalization. GitLabProvider.php (lines 41, 53) and GiteaProvider.php (lines 42, 54) each call rtrim($this->baseUrl..., '/') twice — once in ping(), once in fetch(). Compute it once as a private readonly string $baseUrl in the constructor instead.

2. Fix stale config/services.yaml (found during exploration, blocks step 1 & 4)

  • services.yaml still binds $username/$token/$baseUrl to the pre-refactor FQCNs (App\Service\GitHubProvider etc.) and _instanceof: App\Service\ProviderInterface, left over from the Service/Provider/+Renderer/ namespace reorg. Update all of these to App\Service\Provider\.... Currently these bindings silently no-op, and scalar constructor args can't autowire without them — any edit to these constructors (steps 1, 4) needs this fixed first, or the container fails to compile.

3. ContributionStore (SQLite via native PDO)

  • New src/Service/ContributionStore.php. PDO SQLite, DB file at %kernel.project_dir%/var/data/contributions.db (configurable constructor arg), table created lazily:
    CREATE TABLE IF NOT EXISTS contributions (
        provider TEXT NOT NULL,
        date     TEXT NOT NULL,
        count    INTEGER NOT NULL,
        PRIMARY KEY (provider, date)
    );
    
  • latestDate(string $provider): ?stringSELECT MAX(date) WHERE provider = ?.
  • merge(string $provider, array $dateCounts): void — upsert via INSERT ... ON CONFLICT(provider, date) DO UPDATE SET count = excluded.count.
  • all(string $provider, ?int $sinceDays = null): arraydate => count; null returns full history (no arbitrary cutoff), a value filters to the last N days.
  • prune(): voidDELETE FROM contributions WHERE date < ? using the configured retention window; no-ops if retention is unset/0.
  • Constructor takes ?int $retentionDays bound from new env var CONTRIBUTIONS_RETENTION_DAYS (empty/default = keep forever).
  • Tests: tests/Unit/Service/ContributionStoreTest.php — store & retrieve, upsert overwrites existing date, sinceDays filtering, prune() no-ops when retention unset, prune() deletes rows older than the window when set.

4. Wire $since through providers

  • ProviderInterface::fetch()fetch(?\DateTimeImmutable $since = null): array.
  • GitHubProvider::fetch() — use $since ?? new \DateTimeImmutable('-365 days') for the GraphQL from param.
  • GitLabProvider::fetch() — same, feeds the after query param (this is what actually shrinks the pagination loop).
  • GiteaProvider::fetch() — same, feeds the $cutoff timestamp filter.
  • Update GitHubProviderTest.php (drop GraphQLApiClientRegistryInterface stub setup entirely per step 1), GitLabProviderTest.php, GiteaProviderTest.php — add a case asserting a passed $since narrows the request window.

5. Wire ContributionStore into ContributionAggregator

  • Inject ContributionStore into ContributionAggregator.
  • Per configured provider: $since = $store->latestDate($name) → if set, (new \DateTimeImmutable($latest))->modify('-3 days') (3-day overlap for late corrections), else null (first run).
  • $fresh = $provider->fetch($since), $store->merge($name, $fresh), then read back $store->all($name, sinceDays: 371) for the render window (53 weeks × 7 days) and merge into the returned array.
  • Call $store->prune() once per aggregate() call, after all providers have merged.
  • Keep the existing try/catch-and-log-per-provider behavior — a provider failure leaves its DB history stale, doesn't break the render.
  • Update ContributionAggregatorTest.php with store-interaction and prune-call cases.

6. Docker / env

  • docker-compose.yml — add a data named volume mounted at /app/var/data (same pattern as cache/logs), and pass through CONTRIBUTIONS_RETENTION_DAYS: "${CONTRIBUTIONS_RETENTION_DAYS:-}".
  • Dockerfile — add var/data to the mkdir -p in the final stage alongside var/cache/prod/pools var/log, owned by app.
  • .env — document CONTRIBUTIONS_RETENTION_DAYS (empty by default), same style as the existing ALLOWED_HOSTS comment.

7. PHPStan

  • composer require --dev phpstan/phpstan (plain PHPStan — no Symfony extension needed for this app's size).
  • Add phpstan.neon: paths: [src, tests], level: 8.
  • Add composer script "phpstan": "phpstan analyse".
  • Fix whatever level-8 flags on first run.

8. CLAUDE.md update

  • Architecture section: add the ContributionStore (SQLite) tier between providers and the renderer; note the two-tier cache (1h SVG cache → SQLite raw-data store → provider APIs).
  • Environment variables table: add CONTRIBUTIONS_RETENTION_DAYS.
  • Development section: add composer phpstan next to the existing vendor/bin/phpunit commands.
  • Remove any remaining mentions of the two deleted bundles.

Verification

  • vendor/bin/phpunit --testdox green after every step.
  • composer phpstan clean at level 8.
  • composer install succeeds with no dangling references to the removed bundles.
  • docker compose up -d --build; clear the FS cache (docker compose exec graph rm -rf var/cache/*); hit /graph.svg?theme=dark twice; check docker compose logs -f graph shows a narrower since window on the second fetch.
  • Set CONTRIBUTIONS_RETENTION_DAYS=30; confirm rows older than 30 days are gone after the next refresh (sqlite3 var/data/contributions.db "SELECT date FROM contributions ORDER BY date LIMIT 1").
  • curl localhost:8080/health still reports all configured providers healthy.