0 && $ahead === 0) { $state = 'update_available'; } elseif ($ahead > 0 && $behind === 0) { $state = 'local_ahead'; } elseif ($ahead > 0 && $behind > 0) { $state = 'diverged'; } return [ 'state' => $state, 'remote_name' => $remoteName, '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, string $remoteName = 'origin', string $remoteUrl = ''): array { if (!is_file($scriptPath)) { throw new RuntimeException('Скрипт деплоя не найден: ' . $scriptPath); } $remoteName = adminNormalizeRemoteName($remoteName); $run = adminRunShellCommand('bash ' . escapeshellarg($scriptPath), $projectRoot, [ 'BRANCH' => $branch, 'PHP_BIN' => $phpBin, 'REMOTE_NAME' => $remoteName, 'REMOTE_URL' => $remoteUrl, ]); return [ 'ok' => $run['code'] === 0, 'code' => $run['code'], 'output' => adminTailOutput($run['output']), ]; } function adminEnsureRemote(string $projectRoot, string $remoteName, string $remoteUrl): void { $getRemote = adminRunShellCommand('git remote get-url ' . escapeshellarg($remoteName), $projectRoot); if ($getRemote['code'] !== 0) { if ($remoteUrl === '') { throw new RuntimeException('Remote ' . $remoteName . ' не найден'); } $add = adminRunShellCommand('git remote add ' . escapeshellarg($remoteName) . ' ' . escapeshellarg($remoteUrl), $projectRoot); if ($add['code'] !== 0) { throw new RuntimeException('Не удалось добавить remote ' . $remoteName . ': ' . adminTailOutput($add['output'])); } return; } if ($remoteUrl === '') { return; } $currentUrl = trim($getRemote['output']); if ($currentUrl === $remoteUrl) { return; } $set = adminRunShellCommand('git remote set-url ' . escapeshellarg($remoteName) . ' ' . escapeshellarg($remoteUrl), $projectRoot); if ($set['code'] !== 0) { throw new RuntimeException('Не удалось обновить remote ' . $remoteName . ': ' . adminTailOutput($set['output'])); } } function adminNormalizeRemoteName(string $remoteName): string { $remoteName = trim($remoteName); if ($remoteName === '') { return 'origin'; } if (!preg_match('/^[A-Za-z0-9._-]+$/', $remoteName)) { throw new RuntimeException('Некорректное имя remote'); } return $remoteName; } 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)); }