45b15c7fd5
Complete implementation of automated GitHub repository synchronization: - Webhook-based auto-sync from GitHub - Multi-repository support with branch selection - Web dashboard for management - Manual sync and rollback functionality - Comprehensive logging and monitoring Located in /gitpusher/ subdirectory as standalone application.
400 lines
12 KiB
PHP
400 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* GitHandler - Handles all Git operations
|
|
*/
|
|
class GitHandler {
|
|
private $logger;
|
|
private $configManager;
|
|
|
|
public function __construct(Logger $logger, ConfigManager $configManager) {
|
|
$this->logger = $logger;
|
|
$this->configManager = $configManager;
|
|
}
|
|
|
|
/**
|
|
* Execute shell command and return result
|
|
*/
|
|
private function exec($command, $cwd = null) {
|
|
$descriptorspec = [
|
|
0 => ['pipe', 'r'], // stdin
|
|
1 => ['pipe', 'w'], // stdout
|
|
2 => ['pipe', 'w'] // stderr
|
|
];
|
|
|
|
$process = proc_open($command, $descriptorspec, $pipes, $cwd);
|
|
|
|
if (!is_resource($process)) {
|
|
return [
|
|
'success' => false,
|
|
'output' => '',
|
|
'error' => 'Failed to execute command',
|
|
'exit_code' => -1
|
|
];
|
|
}
|
|
|
|
$stdout = stream_get_contents($pipes[1]);
|
|
$stderr = stream_get_contents($pipes[2]);
|
|
|
|
fclose($pipes[0]);
|
|
fclose($pipes[1]);
|
|
fclose($pipes[2]);
|
|
|
|
$exitCode = proc_close($process);
|
|
|
|
return [
|
|
'success' => $exitCode === 0,
|
|
'output' => trim($stdout),
|
|
'error' => trim($stderr),
|
|
'exit_code' => $exitCode
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Build Git URL with authentication token
|
|
*/
|
|
private function buildGitUrl($repoUrl, $token) {
|
|
// Convert https://github.com/user/repo.git to https://TOKEN@github.com/user/repo.git
|
|
$parsed = parse_url($repoUrl);
|
|
|
|
if (!$parsed || !isset($parsed['host'])) {
|
|
return $repoUrl;
|
|
}
|
|
|
|
$scheme = $parsed['scheme'] ?? 'https';
|
|
$host = $parsed['host'];
|
|
$path = $parsed['path'] ?? '';
|
|
|
|
return "$scheme://$token@$host$path";
|
|
}
|
|
|
|
/**
|
|
* Clone repository
|
|
*/
|
|
public function cloneRepository($repoId, $repoUrl, $targetPath, $branch = 'main') {
|
|
$this->logger->info($repoId, "Starting clone of repository to $targetPath");
|
|
|
|
// Check if target path already exists
|
|
if (file_exists($targetPath)) {
|
|
$this->logger->error($repoId, "Target path already exists: $targetPath");
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Target path already exists'
|
|
];
|
|
}
|
|
|
|
// Create parent directory if it doesn't exist
|
|
$parentDir = dirname($targetPath);
|
|
if (!file_exists($parentDir)) {
|
|
mkdir($parentDir, 0755, true);
|
|
}
|
|
|
|
// Get GitHub token
|
|
$token = $this->configManager->getGitHubToken();
|
|
$gitUrl = $token ? $this->buildGitUrl($repoUrl, $token) : $repoUrl;
|
|
|
|
// Clone repository
|
|
$command = sprintf(
|
|
'git clone --branch %s %s %s 2>&1',
|
|
escapeshellarg($branch),
|
|
escapeshellarg($gitUrl),
|
|
escapeshellarg($targetPath)
|
|
);
|
|
|
|
$result = $this->exec($command);
|
|
|
|
if ($result['success']) {
|
|
$this->logger->success($repoId, "Repository cloned successfully", [
|
|
'path' => $targetPath,
|
|
'branch' => $branch
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Repository cloned successfully',
|
|
'output' => $result['output']
|
|
];
|
|
} else {
|
|
$this->logger->error($repoId, "Failed to clone repository", [
|
|
'error' => $result['error'],
|
|
'output' => $result['output']
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Failed to clone repository',
|
|
'error' => $result['error']
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pull latest changes
|
|
*/
|
|
public function pull($repoId, $targetPath, $branch = 'main') {
|
|
$this->logger->info($repoId, "Starting pull for $targetPath");
|
|
|
|
// Check if path exists
|
|
if (!file_exists($targetPath)) {
|
|
$this->logger->error($repoId, "Repository path does not exist: $targetPath");
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Repository path does not exist'
|
|
];
|
|
}
|
|
|
|
// Check if it's a git repository
|
|
if (!file_exists("$targetPath/.git")) {
|
|
$this->logger->error($repoId, "Not a git repository: $targetPath");
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Not a git repository'
|
|
];
|
|
}
|
|
|
|
// Get current commit before pull
|
|
$currentCommit = $this->getCurrentCommit($targetPath);
|
|
|
|
// Pull changes
|
|
$command = sprintf(
|
|
'cd %s && git pull origin %s 2>&1',
|
|
escapeshellarg($targetPath),
|
|
escapeshellarg($branch)
|
|
);
|
|
|
|
$result = $this->exec($command);
|
|
|
|
// Check for merge conflicts
|
|
if (!$result['success'] || strpos($result['output'], 'CONFLICT') !== false) {
|
|
$this->logger->warning($repoId, "Merge conflict detected", [
|
|
'output' => $result['output'],
|
|
'error' => $result['error']
|
|
]);
|
|
|
|
$this->configManager->updateRepository($repoId, ['status' => 'conflict']);
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Merge conflict detected',
|
|
'conflict' => true,
|
|
'output' => $result['output']
|
|
];
|
|
}
|
|
|
|
// Get new commit after pull
|
|
$newCommit = $this->getCurrentCommit($targetPath);
|
|
|
|
// Count changed files
|
|
$changedFiles = $this->getChangedFilesBetweenCommits($targetPath, $currentCommit, $newCommit);
|
|
|
|
if ($result['success']) {
|
|
$this->logger->success($repoId, "Pull completed successfully", [
|
|
'files_changed' => count($changedFiles),
|
|
'old_commit' => substr($currentCommit, 0, 7),
|
|
'new_commit' => substr($newCommit, 0, 7)
|
|
]);
|
|
|
|
$this->configManager->updateRepository($repoId, [
|
|
'status' => 'synced',
|
|
'last_sync' => date('Y-m-d H:i:s'),
|
|
'last_commit' => $newCommit
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Pull completed successfully',
|
|
'files_changed' => count($changedFiles),
|
|
'output' => $result['output']
|
|
];
|
|
} else {
|
|
$this->logger->error($repoId, "Pull failed", [
|
|
'error' => $result['error'],
|
|
'output' => $result['output']
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Pull failed',
|
|
'error' => $result['error']
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Revert to specific commit
|
|
*/
|
|
public function revert($repoId, $targetPath, $commitHash) {
|
|
$this->logger->info($repoId, "Starting revert to commit $commitHash");
|
|
|
|
if (!file_exists($targetPath)) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Repository path does not exist'
|
|
];
|
|
}
|
|
|
|
// Create revert commit
|
|
$command = sprintf(
|
|
'cd %s && git revert --no-edit %s 2>&1',
|
|
escapeshellarg($targetPath),
|
|
escapeshellarg($commitHash)
|
|
);
|
|
|
|
$result = $this->exec($command);
|
|
|
|
if ($result['success']) {
|
|
$this->logger->success($repoId, "Reverted to commit $commitHash", [
|
|
'commit' => $commitHash
|
|
]);
|
|
|
|
$newCommit = $this->getCurrentCommit($targetPath);
|
|
|
|
$this->configManager->updateRepository($repoId, [
|
|
'last_commit' => $newCommit,
|
|
'last_sync' => date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Revert completed successfully',
|
|
'output' => $result['output']
|
|
];
|
|
} else {
|
|
$this->logger->error($repoId, "Revert failed", [
|
|
'error' => $result['error']
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Revert failed',
|
|
'error' => $result['error']
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current commit hash
|
|
*/
|
|
public function getCurrentCommit($targetPath) {
|
|
$result = $this->exec("cd " . escapeshellarg($targetPath) . " && git rev-parse HEAD 2>&1");
|
|
return $result['success'] ? trim($result['output']) : null;
|
|
}
|
|
|
|
/**
|
|
* Get commit history
|
|
*/
|
|
public function getCommitHistory($targetPath, $limit = 20) {
|
|
$command = sprintf(
|
|
'cd %s && git log --pretty=format:"%%H|%%an|%%ae|%%at|%%s" -n %d 2>&1',
|
|
escapeshellarg($targetPath),
|
|
(int)$limit
|
|
);
|
|
|
|
$result = $this->exec($command);
|
|
|
|
if (!$result['success']) {
|
|
return [];
|
|
}
|
|
|
|
$commits = [];
|
|
$lines = explode("\n", $result['output']);
|
|
|
|
foreach ($lines as $line) {
|
|
if (empty($line)) continue;
|
|
|
|
$parts = explode('|', $line);
|
|
if (count($parts) !== 5) continue;
|
|
|
|
$commits[] = [
|
|
'hash' => $parts[0],
|
|
'hash_short' => substr($parts[0], 0, 7),
|
|
'author_name' => $parts[1],
|
|
'author_email' => $parts[2],
|
|
'timestamp' => date('Y-m-d H:i:s', (int)$parts[3]),
|
|
'message' => $parts[4]
|
|
];
|
|
}
|
|
|
|
return $commits;
|
|
}
|
|
|
|
/**
|
|
* Get changed files between commits
|
|
*/
|
|
private function getChangedFilesBetweenCommits($targetPath, $oldCommit, $newCommit) {
|
|
if ($oldCommit === $newCommit) {
|
|
return [];
|
|
}
|
|
|
|
$command = sprintf(
|
|
'cd %s && git diff --name-only %s %s 2>&1',
|
|
escapeshellarg($targetPath),
|
|
escapeshellarg($oldCommit),
|
|
escapeshellarg($newCommit)
|
|
);
|
|
|
|
$result = $this->exec($command);
|
|
|
|
if (!$result['success']) {
|
|
return [];
|
|
}
|
|
|
|
return array_filter(explode("\n", $result['output']));
|
|
}
|
|
|
|
/**
|
|
* Get repository status
|
|
*/
|
|
public function getStatus($targetPath) {
|
|
$command = sprintf(
|
|
'cd %s && git status --porcelain 2>&1',
|
|
escapeshellarg($targetPath)
|
|
);
|
|
|
|
$result = $this->exec($command);
|
|
|
|
return [
|
|
'success' => $result['success'],
|
|
'clean' => $result['success'] && empty($result['output']),
|
|
'output' => $result['output']
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get current branch
|
|
*/
|
|
public function getCurrentBranch($targetPath) {
|
|
$result = $this->exec("cd " . escapeshellarg($targetPath) . " && git branch --show-current 2>&1");
|
|
return $result['success'] ? trim($result['output']) : null;
|
|
}
|
|
|
|
/**
|
|
* Fetch available branches from remote
|
|
*/
|
|
public function getRemoteBranches($repoUrl) {
|
|
$token = $this->configManager->getGitHubToken();
|
|
$gitUrl = $token ? $this->buildGitUrl($repoUrl, $token) : $repoUrl;
|
|
|
|
$command = sprintf(
|
|
'git ls-remote --heads %s 2>&1',
|
|
escapeshellarg($gitUrl)
|
|
);
|
|
|
|
$result = $this->exec($command);
|
|
|
|
if (!$result['success']) {
|
|
return [];
|
|
}
|
|
|
|
$branches = [];
|
|
$lines = explode("\n", $result['output']);
|
|
|
|
foreach ($lines as $line) {
|
|
if (preg_match('/refs\/heads\/(.+)$/', $line, $matches)) {
|
|
$branches[] = $matches[1];
|
|
}
|
|
}
|
|
|
|
return $branches;
|
|
}
|
|
}
|