*/ 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(); } }