Modifiche'; public function handle(): int { $remote = trim((string) $this->option('remote')) ?: 'origin'; $branch = trim((string) $this->option('branch')) ?: 'main'; $force = (bool) $this->option('force'); $deployPath = base_path(); $repoPath = is_dir(base_path('.git')) ? $deployPath : $this->resolveSourceRepositoryPath(); $deploySync = $repoPath !== null && realpath($repoPath) !== realpath($deployPath); if ($repoPath === null) { $this->error('Repository Git sorgente non trovato.'); $this->writeProgress(100, 'Repository Git sorgente non trovato', 'failed', 1); $this->persistSummary([ 'status' => 'failed', 'exit_code' => 1, 'message' => 'Repository Git sorgente non trovato', 'deploy_path' => $deployPath, ]); return self::FAILURE; } $this->writeProgress(5, 'Preparazione sync da Gitea', 'running'); $currentBranch = $this->runGit($repoPath, ['rev-parse', '--abbrev-ref', 'HEAD']); $currentCommit = $this->runGit($repoPath, ['rev-parse', '--short', 'HEAD']); if ($currentBranch === null || $currentCommit === null) { $this->error('Impossibile leggere lo stato Git locale.'); $this->writeProgress(100, 'Impossibile leggere stato Git locale', 'failed', 1); $this->persistSummary([ 'status' => 'failed', 'exit_code' => 1, 'message' => 'Impossibile leggere stato Git locale', 'repo_path' => $repoPath, ]); return self::FAILURE; } $dirtyResult = Process::path($repoPath)->run($this->gitCommandArgs($repoPath, ['status', '--porcelain'])); $isDirty = trim((string) $dirtyResult->output()) !== ''; if ($isDirty && ! $force && ! $deploySync) { $this->error('Working tree sporco: sync bloccata per evitare conflitti locali.'); $this->writeProgress(100, 'Working tree sporco: sync bloccata', 'failed', 1); $this->persistSummary([ 'status' => 'failed', 'exit_code' => 1, 'message' => 'Working tree sporco: sync bloccata', 'remote' => $remote, 'branch' => $branch, 'before_commit' => $currentCommit, 'working_tree_dirty' => true, 'repo_path' => $repoPath, ]); return self::FAILURE; } $this->info("Fetch da {$remote}..."); $this->writeProgress(18, 'Fetch remoto da Gitea', 'running'); $fetch = Process::path($repoPath) ->timeout(1800) ->run($this->gitCommandArgs($repoPath, ['fetch', $remote])); if (! $fetch->successful()) { $this->line(trim((string) ($fetch->errorOutput() !== '' ? $fetch->errorOutput() : $fetch->output()))); $this->writeProgress(100, 'Fetch remoto fallito', 'failed', 1); $this->persistSummary([ 'status' => 'failed', 'exit_code' => 1, 'message' => 'Fetch remoto fallito', 'remote' => $remote, 'branch' => $branch, 'before_commit' => $currentCommit, 'working_tree_dirty' => false, 'repo_path' => $repoPath, ]); return self::FAILURE; } $remoteRef = 'refs/remotes/' . $remote . '/' . $branch; $docsRepoPath = $deployPath; $newCommit = $currentCommit; $cleanupWorktree = null; if ($deploySync) { $this->writeProgress(32, 'Creazione sorgente remota pulita', 'running'); $worktreePath = storage_path('app/support/git-sync-worktrees/' . now()->format('YmdHis') . '-' . uniqid()); File::ensureDirectoryExists(dirname($worktreePath)); $worktreeAdd = Process::path($repoPath) ->timeout(1800) ->run($this->gitCommandArgs($repoPath, ['worktree', 'add', '--force', '--detach', $worktreePath, $remoteRef])); if (! $worktreeAdd->successful()) { $this->line(trim((string) ($worktreeAdd->errorOutput() !== '' ? $worktreeAdd->errorOutput() : $worktreeAdd->output()))); $this->writeProgress(100, 'Creazione worktree remoto fallita', 'failed', 1); $this->persistSummary([ 'status' => 'failed', 'exit_code' => 1, 'message' => 'Creazione worktree remoto fallita', 'remote' => $remote, 'branch' => $branch, 'before_commit' => $currentCommit, 'working_tree_dirty' => $isDirty, 'repo_path' => $repoPath, 'deploy_path' => $deployPath, ]); return self::FAILURE; } $cleanupWorktree = static function () use ($repoPath, $worktreePath): void { Process::path($repoPath)->timeout(1800)->run($this->gitCommandArgs($repoPath, ['worktree', 'remove', '--force', $worktreePath])); if (is_dir($worktreePath)) { File::deleteDirectory($worktreePath); } }; $docsRepoPath = $worktreePath; $newCommit = $this->runGit($worktreePath, ['rev-parse', '--short', 'HEAD']) ?? $currentCommit; $this->writeProgress(48, 'Sync deploy da worktree remoto', 'running'); $sync = Process::path($deployPath) ->timeout(1800) ->run([ 'bash', 'scripts/ops/netgescon-sync-prod-update.sh', '--src', $worktreePath, '--dst', $deployPath, ]); if (! $sync->successful()) { $this->line(trim((string) ($sync->errorOutput() !== '' ? $sync->errorOutput() : $sync->output()))); if ($cleanupWorktree instanceof \Closure) { $cleanupWorktree(); } $this->writeProgress(100, 'Sync deploy fallita', 'failed', 1); $this->persistSummary([ 'status' => 'failed', 'exit_code' => 1, 'message' => 'Sync deploy fallita', 'remote' => $remote, 'branch' => $branch, 'before_commit' => $currentCommit, 'after_commit' => $newCommit, 'working_tree_dirty' => $isDirty, 'repo_path' => $repoPath, 'deploy_path' => $deployPath, ]); return self::FAILURE; } } else { $this->writeProgress(32, 'Allineamento branch locale', 'running'); if ($currentBranch !== $branch) { $checkout = Process::path($repoPath) ->timeout(1800) ->run($this->gitCommandArgs($repoPath, ['checkout', $branch])); if (! $checkout->successful()) { $this->line(trim((string) ($checkout->errorOutput() !== '' ? $checkout->errorOutput() : $checkout->output()))); $this->writeProgress(100, 'Checkout branch fallito', 'failed', 1); $this->persistSummary([ 'status' => 'failed', 'exit_code' => 1, 'message' => 'Checkout branch fallito', 'remote' => $remote, 'branch' => $branch, 'before_commit' => $currentCommit, 'working_tree_dirty' => false, 'repo_path' => $repoPath, ]); return self::FAILURE; } } $this->info("Pull ff-only da {$remote}/{$branch}..."); $this->writeProgress(48, 'Pull ff-only da Gitea', 'running'); $pull = Process::path($repoPath) ->timeout(1800) ->run($this->gitCommandArgs($repoPath, ['pull', '--ff-only', $remote, $branch])); if (! $pull->successful()) { $this->line(trim((string) ($pull->errorOutput() !== '' ? $pull->errorOutput() : $pull->output()))); $this->writeProgress(100, 'Pull da Gitea fallito', 'failed', 1); $this->persistSummary([ 'status' => 'failed', 'exit_code' => 1, 'message' => 'Pull da Gitea fallito', 'remote' => $remote, 'branch' => $branch, 'before_commit' => $currentCommit, 'working_tree_dirty' => false, 'repo_path' => $repoPath, ]); return self::FAILURE; } $docsRepoPath = $deployPath; $newCommit = $this->runGit($repoPath, ['rev-parse', '--short', 'HEAD']) ?? $currentCommit; } $this->writeProgress(68, 'Rigenerazione changelog Git automatico', 'running'); File::ensureDirectoryExists(storage_path('app/support/generated')); $docsSync = Process::path($deployPath) ->timeout(1800) ->run([ 'bash', 'scripts/ops/netgescon-sync-git-changelog.sh', '--repo', $docsRepoPath, '--output', storage_path('app/support/generated/NETGESCON-GIT-AUTOCHANGELOG.md'), ]); if (! $docsSync->successful()) { $this->line(trim((string) ($docsSync->errorOutput() !== '' ? $docsSync->errorOutput() : $docsSync->output()))); if ($cleanupWorktree instanceof \Closure) { $cleanupWorktree(); } $this->writeProgress(100, 'Rigenerazione changelog Git fallita', 'failed', 1); $this->persistSummary([ 'status' => 'failed', 'exit_code' => 1, 'message' => 'Rigenerazione changelog Git fallita', 'remote' => $remote, 'branch' => $branch, 'before_commit' => $currentCommit, 'after_commit' => $newCommit, 'working_tree_dirty' => false, 'repo_path' => $repoPath, 'deploy_path' => $deployPath, ]); return self::FAILURE; } $this->writeProgress(76, 'Rigenerazione snapshot continuita sviluppo', 'running'); $snapshotExit = Artisan::call('netgescon:development-snapshot'); if ($snapshotExit !== 0) { if ($cleanupWorktree instanceof \Closure) { $cleanupWorktree(); } $this->writeProgress(100, 'Rigenerazione snapshot sviluppo fallita', 'failed', 1); $this->persistSummary([ 'status' => 'failed', 'exit_code' => 1, 'message' => 'Rigenerazione snapshot sviluppo fallita', 'remote' => $remote, 'branch' => $branch, 'before_commit' => $currentCommit, 'after_commit' => $newCommit, 'working_tree_dirty' => false, 'repo_path' => $repoPath, 'deploy_path' => $deployPath, ]); return self::FAILURE; } $this->writeProgress(82, 'Pulizia cache applicativa', 'running'); $this->callSilently('optimize:clear'); $remoteCommit = $this->runGit($repoPath, ['rev-parse', '--short', $remoteRef]) ?? $newCommit; if ($cleanupWorktree instanceof \Closure) { $cleanupWorktree(); } $this->persistSummary([ 'status' => 'completed', 'exit_code' => 0, 'message' => 'Sync Git completata', 'remote' => $remote, 'branch' => $branch, 'before_commit' => $currentCommit, 'after_commit' => $newCommit, 'remote_commit' => $remoteCommit, 'working_tree_dirty' => $isDirty, 'repo_path' => $repoPath, 'deploy_path' => $deployPath, 'mode' => $deploySync ? 'external-repo-rsync' : 'in-place-git', ]); $this->info('Sync Git completata con successo.'); $this->writeProgress(100, 'Sync Git completata', 'completed', 0); return self::SUCCESS; } private function runGit(string $path, array $arguments): ?string { $result = Process::path($path) ->timeout(1800) ->run($this->gitCommandArgs($path, $arguments)); if (! $result->successful()) { return null; } $output = trim((string) $result->output()); return $output !== '' ? $output : null; } private function resolveSourceRepositoryPath(): ?string { $candidates = array_values(array_filter(array_unique([ trim((string) env('NETGESCON_GIT_SYNC_SOURCE_REPO', '')), '/home/michele/netgescon/netgescon-day0', '/home/michele/netgescon/netgescon-laravel', storage_path('app/support/git-source'), '/var/www/netgescon-git-source', '/var/www/netgescon_backup_20250722_220433', '/var/www/netgescon_backup_20250722_215454', ]), static fn(string $path): bool => $path !== '')); foreach ($candidates as $candidate) { if (! is_dir($candidate . '/.git')) { continue; } if ($this->runGit($candidate, ['rev-parse', '--git-dir']) !== null) { return $candidate; } } return null; } private function gitCommandArgs(string $path, array $arguments): array { return array_merge(['git', '-c', 'safe.directory=' . $path], $arguments); } private function writeProgress(int $percent, string $message, string $status, ?int $exitCode = null): void { $path = trim((string) $this->option('progress-file')); if ($path === '') { return; } $dir = dirname($path); if (! is_dir($dir)) { @mkdir($dir, 0775, true); } @file_put_contents($path, json_encode([ 'timestamp' => now()->toIso8601String(), 'percent' => max(0, min(100, $percent)), 'message' => $message, 'status' => $status, 'exit_code' => $exitCode, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } private function persistSummary(array $payload): void { File::ensureDirectoryExists(storage_path('app/support/generated')); @file_put_contents( storage_path('app/support/generated/git-sync-last.json'), json_encode(array_merge([ 'synced_at' => now()->toIso8601String(), ], $payload), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ); } }