|null */ public ?array $data = []; public ?string $fromCommit = null; public ?string $toCommit = null; public ?string $gitError = null; /** @var array a felületen kézzel kivett fájlok */ public array $skippedPaths = []; /** @var array|null az utoljára elkészített csomag adatai */ public ?array $lastPackage = null; public static function canAccess(): bool { return app(DeploymentGuard::class)->isAllowed(auth()->user()); } public function mount(): void { app(DeploymentGuard::class)->ensureAllowed(auth()->user()); /** @var array $limits */ $limits = (array) config('deployment.commit_limits', [50]); $this->form->fill([ 'target' => app(DeploymentTargets::class)->defaultKey(), 'branch' => $this->defaultBranch(), 'limit' => $limits[0] ?? 50, 'search' => null, ]); } public function form(Schema $form): Schema { /** @var array $limits */ $limits = (array) config('deployment.commit_limits', [50]); return $form ->schema([ Grid::make(4) ->schema([ Select::make('target') ->label('Célkörnyezet') ->options(app(DeploymentTargets::class)->options()) ->required() ->live() ->helperText(fn (): ?string => $this->target()['remote_path'] ?? null), Select::make('branch') ->label('Branch') ->options($this->branchOptions()) ->required() ->searchable() ->live() ->afterStateUpdated(fn () => $this->clearRange()), Select::make('limit') ->label('Commitok száma') ->options(array_combine($limits, $limits)) ->required() ->live(), TextInput::make('search') ->label('Keresés') ->placeholder('pl. árlista, migráció, szerző') ->helperText('A commit üzenetében (a törzsben is), a szerzőben és a hashben keres, a betöltött listán belül.') ->live(debounce: 400), ]), ]) ->statePath('data'); } public function selectFrom(string $hash): void { app(DeploymentGuard::class)->ensureAllowed(auth()->user()); $this->fromCommit = $this->resolveOrWarn($hash); } public function selectTo(string $hash): void { app(DeploymentGuard::class)->ensureAllowed(auth()->user()); $this->toCommit = $this->resolveOrWarn($hash); } public function swapRange(): void { app(DeploymentGuard::class)->ensureAllowed(auth()->user()); [$this->fromCommit, $this->toCommit] = [$this->toCommit, $this->fromCommit]; } public function clearRange(): void { $this->fromCommit = null; $this->toCommit = null; $this->skippedPaths = []; } /** * Egy fájl kézi ki-/visszavétele a csomagból. */ public function toggleFile(string $path): void { app(DeploymentGuard::class)->ensureAllowed(auth()->user()); $this->skippedPaths = in_array($path, $this->skippedPaths, true) ? array_values(array_diff($this->skippedPaths, [$path])) : array_merge($this->skippedPaths, [$path]); } /** * A csomag előállítása a kijelölt tartományból. */ public function createPackage(): void { app(DeploymentGuard::class)->ensureAllowed(auth()->user()); if (! $this->fromCommit || ! $this->toCommit) { Notification::make() ->title('Nincs kijelölt tartomány') ->body('Válassz egy kezdő és egy záró commitot.') ->danger() ->send(); return; } try { $this->lastPackage = app(DeploymentPackageBuilder::class)->build( (string) ($this->data['target'] ?? ''), (string) ($this->data['branch'] ?? ''), $this->fromCommit, $this->toCommit, $this->skippedPaths, auth()->user()?->email, ); } catch (RuntimeException $exception) { Notification::make() ->title('A csomag nem készült el') ->body($exception->getMessage()) ->danger() ->persistent() ->send(); return; } Notification::make() ->title('A csomag elkészült') ->body(sprintf( '%d fájl a(z) %s mappában.', $this->lastPackage['counts']['copied'], $this->lastPackage['name'], )) ->success() ->send(); } /** * @return array{key:string, label:string, domain:string, remote_path:string, expected_branch:?string}|null */ public function target(): ?array { return app(DeploymentTargets::class)->find($this->data['target'] ?? null); } /** * @return array */ #[Computed] public function commits(): array { $branch = $this->data['branch'] ?? null; if (! $branch) { return []; } try { $commits = app(GitRepository::class)->commits($branch, (int) ($this->data['limit'] ?? 50)); } catch (RuntimeException $exception) { $this->gitError = $exception->getMessage(); return []; } $search = trim((string) ($this->data['search'] ?? '')); if ($search === '') { return $commits; } $needle = mb_strtolower($search); return array_values(array_filter( $commits, fn (array $commit): bool => str_contains( mb_strtolower(implode(' ', [$commit['subject'], $commit['body'], $commit['author'], $commit['hash']])), $needle, ), )); } /** * A kijelölt tartomány elemzése, vagy null, ha még nincs két commit kiválasztva. * * @return array{ * files: array, * copied: array, * deleted: array, * counts: array{total:int, copied:int, deleted:int, excluded:int}, * warnings: array * }|null */ #[Computed] public function changeSet(): ?array { if (! $this->fromCommit || ! $this->toCommit) { return null; } $repository = app(GitRepository::class); try { $changedFiles = $repository->changedFiles($this->fromCommit, $this->toCommit); } catch (RuntimeException $exception) { $this->gitError = $exception->getMessage(); return null; } $changeSet = app(ChangeSetAnalyzer::class)->analyze( $changedFiles, $this->target(), $this->data['branch'] ?? null, ); if ($this->fromCommit === $this->toCommit) { array_unshift($changeSet['warnings'], [ 'level' => 'warning', 'title' => 'Azonos commitok', 'body' => 'A kezdő és a záró commit ugyanaz, így a tartomány üres.', ]); } elseif (! $repository->isAncestor($this->fromCommit, $this->toCommit)) { array_unshift($changeSet['warnings'], [ 'level' => 'danger', 'title' => 'Fordított vagy szétágazó tartomány', 'body' => 'A kezdő commit nem őse a zárónak, ezért a lista nem a "mi került bele azóta" kérdésre válaszol. Ellenőrizd a sorrendet (Csere gomb), vagy azt, hogy ugyanazon az ágon vagy-e.', ]); } return $changeSet; } /** * A kijelölt tartományba eső commitok - csak a lista kiemeléséhez. * * @return array */ #[Computed] public function highlightedHashes(): array { if (! $this->fromCommit || ! $this->toCommit) { return array_values(array_filter([$this->fromCommit, $this->toCommit])); } $hashes = array_column($this->commits(), 'hash'); $fromIndex = array_search($this->fromCommit, $hashes, true); $toIndex = array_search($this->toCommit, $hashes, true); if ($fromIndex === false || $toIndex === false) { return array_values(array_filter([$this->fromCommit, $this->toCommit])); } // A lista fentről lefelé a legfrissebbtől halad, tehát a záró commit van előrébb. [$start, $end] = $fromIndex <= $toIndex ? [$fromIndex, $toIndex] : [$toIndex, $fromIndex]; return array_slice($hashes, $start, $end - $start + 1); } /** * @return array */ public function branchOptions(): array { try { $branches = app(GitRepository::class)->branches(); } catch (RuntimeException $exception) { $this->gitError = $exception->getMessage(); return []; } return array_combine($branches, $branches) ?: []; } public function isRepositoryAvailable(): bool { return app(GitRepository::class)->isAvailable(); } private function defaultBranch(): ?string { $repository = app(GitRepository::class); $current = $repository->currentBranch(); if ($current) { return $current; } return $this->branchOptions() === [] ? null : array_key_first($this->branchOptions()); } private function resolveOrWarn(string $hash): ?string { $resolved = app(GitRepository::class)->resolveCommit($hash); if (! $resolved) { Notification::make() ->title('Ismeretlen commit') ->body('A kiválasztott commit nem található a repóban.') ->danger() ->send(); return null; } return $resolved; } }