Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer collective, backed by the Pireon OAuth identity server. The public site is a separate, server-rendered application consuming /api/public/v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
105 lines
3.2 KiB
PHP
105 lines
3.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Git;
|
|
|
|
use App\Enums\GitProvider;
|
|
use App\Models\Repository;
|
|
use App\Services\Git\Contracts\RepositoryProvider;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Resolves the right driver for a repository and applies what it returns.
|
|
*
|
|
* This is the only class that both knows about drivers and touches models,
|
|
* which keeps the drivers free of persistence concerns and the models free of
|
|
* HTTP concerns.
|
|
*/
|
|
class GitManager
|
|
{
|
|
/** @var array<string, RepositoryProvider> */
|
|
protected array $drivers = [];
|
|
|
|
public function driver(GitProvider|string $provider): RepositoryProvider
|
|
{
|
|
$key = $provider instanceof GitProvider ? $provider->value : $provider;
|
|
|
|
if (isset($this->drivers[$key])) {
|
|
return $this->drivers[$key];
|
|
}
|
|
|
|
$config = config("git.providers.{$key}");
|
|
|
|
if (! is_array($config) || ! isset($config['driver'])) {
|
|
throw GitException::unsupportedProvider($key);
|
|
}
|
|
|
|
/** @var class-string $class */
|
|
$class = $config['driver'];
|
|
|
|
return $this->drivers[$key] = $class::fromConfig($config);
|
|
}
|
|
|
|
/**
|
|
* Refresh one repository from its provider.
|
|
*
|
|
* Both outcomes are recorded: a success clears any previous error, and a
|
|
* failure increments a counter so the panel can flag repositories that
|
|
* have been broken for a while instead of quietly showing stale numbers.
|
|
*/
|
|
public function sync(Repository $repository): Repository
|
|
{
|
|
try {
|
|
$snapshot = $this->driver($repository->provider)->fetch($repository->full_name);
|
|
} catch (GitException $e) {
|
|
$repository->forceFill([
|
|
'sync_error' => $e->getMessage(),
|
|
'sync_failures' => $repository->sync_failures + 1,
|
|
'last_synced_at' => $repository->last_synced_at,
|
|
])->save();
|
|
|
|
throw $e;
|
|
}
|
|
|
|
DB::transaction(function () use ($repository, $snapshot): void {
|
|
$repository->forceFill([
|
|
...$snapshot->toAttributes(),
|
|
'full_name' => $snapshot->fullName,
|
|
'sync_error' => null,
|
|
'sync_failures' => 0,
|
|
'last_synced_at' => now(),
|
|
])->save();
|
|
|
|
$this->syncIssues($repository, $snapshot);
|
|
});
|
|
|
|
return $repository->refresh();
|
|
}
|
|
|
|
/**
|
|
* Replace the mirrored issues with what the provider currently reports.
|
|
*
|
|
* Issues that were closed or had their label removed upstream must
|
|
* disappear here too — an "open" issue on the site that is actually closed
|
|
* wastes a would-be contributor's time.
|
|
*/
|
|
protected function syncIssues(Repository $repository, RepositorySnapshot $snapshot): void
|
|
{
|
|
$seen = [];
|
|
|
|
foreach ($snapshot->issues as $issue) {
|
|
$repository->issues()->updateOrCreate(
|
|
['number' => $issue->number],
|
|
$issue->toAttributes(),
|
|
);
|
|
|
|
$seen[] = $issue->number;
|
|
}
|
|
|
|
$repository->issues()
|
|
->when($seen !== [], fn ($q) => $q->whereNotIn('number', $seen))
|
|
->delete();
|
|
}
|
|
}
|