0 && $ahead === 0) { $state = 'update_available'; } elseif ($ahead > 0 && $behind === 0) { $state = 'local_ahead'; } elseif ($ahead > 0 && $behind > 0) { $state = 'diverged'; } return [ 'state' => $state, 'branch' => $branch, 'local_ref' => trim($local['output']), 'remote_ref' => trim($remote['output']), 'behind' => $behind, 'ahead' => $ahead, 'can_deploy' => $state === 'update_available', ]; } function adminRunDeployScript(string $projectRoot, string $branch, string $scriptPath, string $phpBin): array { if (!is_file($scriptPath)) { throw new RuntimeException('Скрипт деплоя не найден: ' . $scriptPath); } $run = adminRunShellCommand('bash ' . escapeshellarg($scriptPath), $projectRoot, [ 'BRANCH' => $branch, 'PHP_BIN' => $phpBin, ]); return [ 'ok' => $run['code'] === 0, 'code' => $run['code'], 'output' => adminTailOutput($run['output']), ]; } function adminRunShellCommand(string $command, string $cwd, array $env = []): array { $envPrefix = ''; foreach ($env as $key => $value) { if (!preg_match('/^[A-Z_][A-Z0-9_]*$/', (string)$key)) { continue; } $envPrefix .= $key . '=' . escapeshellarg((string)$value) . ' '; } $fullCommand = 'cd ' . escapeshellarg($cwd) . ' && ' . $envPrefix . $command . ' 2>&1'; $output = []; $code = 0; exec($fullCommand, $output, $code); return ['code' => $code, 'output' => implode("\n", $output)]; } function adminTailOutput(string $output, int $maxLines = 80): string { $output = trim($output); if ($output === '') { return ''; } $lines = preg_split('/\r\n|\r|\n/', $output); if (!is_array($lines)) { return $output; } return implode("\n", array_slice($lines, -$maxLines)); }