d2d.emegrendeles.hu/app/Filament/Pages/DeploymentPackage.php
E98Developer 6027511585 FIX Deployment csomagoló phase1 git ikon és a commit törzsére kiterjesztett keresés
- a topbar menüpont a most bemásolt git ikont használja; a fájl ico_gitpng néven
  érkezett, átnevezve ico_git.png-re, hogy illeszkedjen az ico_*.png konvencióhoz,
  amiből a nav az útvonalat építi
- a git log a törzset (%b) is elkéri, és a keresés a tárgysor mellett ebben is keres:
  a hash-re ritkán keres ember, az érdemi leírás viszont gyakran a törzsben van
- a --shortstat sora a törzs után érkezik, ezért a parse leválasztja róla, különben
  a "N files changed" szöveg a törzsbe és a keresésbe is beszivárogna

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:59:04 +02:00

309 lines
9.9 KiB
PHP

<?php
namespace App\Filament\Pages;
use App\Services\Deployment\ChangeSetAnalyzer;
use App\Services\Deployment\DeploymentGuard;
use App\Services\Deployment\DeploymentTargets;
use App\Services\Deployment\GitRepository;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Schema;
use Livewire\Attributes\Computed;
use RuntimeException;
/**
* Deployment csomagoló - fázis 1: célkörnyezet, commitlista és diff-előnézet.
*
* Ez az oldal (a fázis 1-ben) semmit nem ír a fájlrendszerre, csak olvassa a git
* történetet. A tényleges csomagolás a fázis 2-ben érkezik.
*
* A hozzáférést a DeploymentGuard dönti el (env kapcsoló + stage whitelist +
* developer szerep + feature flag), és a mount()-on kívül minden akció elején
* újra lefut - a felület elrejtése önmagában nem védelem.
*/
class DeploymentPackage extends Page implements HasForms
{
use InteractsWithForms;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-rocket-launch';
protected string $view = 'filament.pages.deployment-package';
protected static ?string $title = 'Deployment csomagoló';
protected static ?string $navigationLabel = 'Deployment csomagoló';
protected static bool $shouldRegisterNavigation = false;
/** @var array<string, mixed>|null */
public ?array $data = [];
public ?string $fromCommit = null;
public ?string $toCommit = null;
public ?string $gitError = 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<int, int> $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<int, int> $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;
}
/**
* @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<int, array{hash:string, short:string, author:string, date:string, subject:string, body:string, file_count:?int}>
*/
#[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<int, array{status:string, path:string, old_path:?string, excluded:bool, action:string}>,
* copied: array<int, string>,
* deleted: array<int, string>,
* counts: array{total:int, copied:int, deleted:int, excluded:int},
* warnings: array<int, array{level:string, title:string, body:string}>
* }|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<int, string>
*/
#[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<string, string>
*/
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;
}
}