diff --git a/app/Console/Commands/NetgesconGitSyncCommand.php b/app/Console/Commands/NetgesconGitSyncCommand.php index fccb076..50d4ea8 100644 --- a/app/Console/Commands/NetgesconGitSyncCommand.php +++ b/app/Console/Commands/NetgesconGitSyncCommand.php @@ -1,9 +1,8 @@ option('branch')) ?: 'main'; $force = (bool) $this->option('force'); - if (! is_dir(base_path('.git'))) { - $this->error('Repository Git non trovato nella base path corrente.'); - $this->writeProgress(100, 'Repository Git non trovato', 'failed', 1); + $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 non trovato', + 'status' => 'failed', + 'exit_code' => 1, + 'message' => 'Repository Git sorgente non trovato', + 'deploy_path' => $deployPath, ]); return self::FAILURE; @@ -37,35 +41,37 @@ public function handle(): int $this->writeProgress(5, 'Preparazione sync da Gitea', 'running'); - $currentBranch = $this->runGit(['rev-parse', '--abbrev-ref', 'HEAD']); - $currentCommit = $this->runGit(['rev-parse', '--short', 'HEAD']); + $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', + 'status' => 'failed', 'exit_code' => 1, - 'message' => 'Impossibile leggere stato Git locale', + 'message' => 'Impossibile leggere stato Git locale', + 'repo_path' => $repoPath, ]); return self::FAILURE; } - $dirtyResult = Process::path(base_path())->run(['git', 'status', '--porcelain']); + $dirtyResult = Process::path($repoPath)->run(['git', 'status', '--porcelain']); $isDirty = trim((string) $dirtyResult->output()) !== ''; - if ($isDirty && ! $force) { + 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, + '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; @@ -74,7 +80,7 @@ public function handle(): int $this->info("Fetch da {$remote}..."); $this->writeProgress(18, 'Fetch remoto da Gitea', 'running'); - $fetch = Process::path(base_path()) + $fetch = Process::path($repoPath) ->timeout(1800) ->run(['git', 'fetch', $remote]); @@ -82,90 +88,181 @@ public function handle(): int $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, + '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; } - $this->writeProgress(32, 'Allineamento branch locale', 'running'); + $remoteRef = 'refs/remotes/' . $remote . '/' . $branch; + $docsRepoPath = $deployPath; + $newCommit = $currentCommit; + $cleanupWorktree = null; - if ($currentBranch !== $branch) { - $checkout = Process::path(base_path()) + 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(['git', 'checkout', $branch]); + ->run(['git', 'worktree', 'add', '--force', '--detach', $worktreePath, $remoteRef]); - if (! $checkout->successful()) { - $this->line(trim((string) ($checkout->errorOutput() !== '' ? $checkout->errorOutput() : $checkout->output()))); - $this->writeProgress(100, 'Checkout branch fallito', 'failed', 1); + 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' => 'Checkout branch fallito', - 'remote' => $remote, - 'branch' => $branch, - 'before_commit' => $currentCommit, - 'working_tree_dirty' => false, + '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; } - } - $this->info("Pull ff-only da {$remote}/{$branch}..."); - $this->writeProgress(48, 'Pull ff-only da Gitea', 'running'); + $cleanupWorktree = static function () use ($repoPath, $worktreePath): void { + Process::path($repoPath)->timeout(1800)->run(['git', 'worktree', 'remove', '--force', $worktreePath]); + if (is_dir($worktreePath)) { + File::deleteDirectory($worktreePath); + } + }; - $pull = Process::path(base_path()) - ->timeout(1800) - ->run(['git', 'pull', '--ff-only', $remote, $branch]); + $docsRepoPath = $worktreePath; + $newCommit = $this->runGit($worktreePath, ['rev-parse', '--short', 'HEAD']) ?? $currentCommit; - 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, - ]); + $this->writeProgress(48, 'Sync deploy da worktree remoto', 'running'); - return self::FAILURE; + $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(['git', '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(['git', '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(base_path()) + $docsSync = Process::path($deployPath) ->timeout(1800) ->run([ 'bash', 'scripts/ops/netgescon-sync-git-changelog.sh', '--repo', - base_path(), + $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, + '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; @@ -174,15 +271,21 @@ public function handle(): int $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, + '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; @@ -191,19 +294,25 @@ public function handle(): int $this->writeProgress(82, 'Pulizia cache applicativa', 'running'); $this->callSilently('optimize:clear'); - $newCommit = $this->runGit(['rev-parse', '--short', 'HEAD']) ?? $currentCommit; - $remoteRef = $this->runGit(['rev-parse', '--short', 'refs/remotes/' . $remote . '/' . $branch]) ?? $newCommit; + $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' => $remoteRef, - 'working_tree_dirty' => false, + '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.'); @@ -212,9 +321,9 @@ public function handle(): int return self::SUCCESS; } - private function runGit(array $arguments): ?string + private function runGit(string $path, array $arguments): ?string { - $result = Process::path(base_path()) + $result = Process::path($path) ->timeout(1800) ->run(array_merge(['git'], $arguments)); @@ -227,6 +336,25 @@ private function runGit(array $arguments): ?string 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', + '/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')) { + return $candidate; + } + } + + return null; + } + private function writeProgress(int $percent, string $message, string $status, ?int $exitCode = null): void { $path = trim((string) $this->option('progress-file')); @@ -241,9 +369,9 @@ private function writeProgress(int $percent, string $message, string $status, ?i @file_put_contents($path, json_encode([ 'timestamp' => now()->toIso8601String(), - 'percent' => max(0, min(100, $percent)), - 'message' => $message, - 'status' => $status, + 'percent' => max(0, min(100, $percent)), + 'message' => $message, + 'status' => $status, 'exit_code' => $exitCode, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } @@ -259,4 +387,4 @@ private function persistSummary(array $payload): void ], $payload), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ); } -} \ No newline at end of file +}