Initial commit: Pireon Admin — headless admin panel and public API
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>
This commit is contained in:
104
app/Services/Git/GitManager.php
Normal file
104
app/Services/Git/GitManager.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user