纯php实现提交文件到github,用于本地部署php程序,生成静态网站,自动提交到github,实现静态网站。
代码参考如下:
<?php
class GitHubDeployer {
private string $token;
private string $owner;
private string $repo;
private string $branch;
private array $headers;
private int $maxRetries = 3;
private float $retryDelay = 1.0;
private int $concurrentLimit = 20; // 并发数
public function __construct(string $token, string $owner, string $repo, string $branch = 'gh-pages') {
$this->token = $token;
$this->owner = $owner;
$this->repo = $repo;
$this->branch = $branch;
$this->headers = [
'Authorization: token ' . $this->token,
'Accept: application/vnd.github.v3+json',
'User-Agent: PHP-GitHub-Deployer/3.0'
];
}
// ========== HTTP 工具 ==========
private function createCurlHandle(string $url, string $method = 'GET', ?array $data = null): CurlHandle {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_HTTPHEADER => $this->headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true,
]);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_SLASHES));
} elseif ($method === 'PATCH') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_SLASHES));
}
return $ch;
}
private function request(string $url, string $method = 'GET', ?array $data = null, int $retry = 0): array {
$ch = $this->createCurlHandle($url, $method, $data);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($httpCode >= 200 && $httpCode < 300) {
return json_decode($response, true) ?: [];
}
if (($httpCode >= 500 || $httpCode === 0) && $retry < $this->maxRetries) {
$delay = $this->retryDelay * (2 ** $retry);
error_log("⚠️ Retry {$retry}/{$this->maxRetries} for {$url} after {$delay}s");
usleep((int)($delay * 1_000_000));
return $this->request($url, $method, $data, $retry + 1);
}
throw new Exception("API failed [{$httpCode}]: " . ($response ?: $error));
}
// ========== .gitignore 解析 ==========
private function parseGitignore(string $gitignorePath): array {
if (!file_exists($gitignorePath)) return [];
$lines = file($gitignorePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$rules = [];
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#')) continue;
$rules[] = $line;
}
return $rules;
}
private function matchesGitignore(string $relativePath, array $rules): bool {
foreach ($rules as $rule) {
$negate = str_starts_with($rule, '!');
$pattern = $negate ? substr($rule, 1) : $rule;
// 处理目录结尾
$isDirPattern = str_ends_with($pattern, '/');
if ($isDirPattern) $pattern = rtrim($pattern, '/');
// 转换为 glob 模式
if (str_contains($pattern, '/')) {
// 包含路径分隔符:精确匹配或通配
$testPath = $relativePath;
if ($isDirPattern && !str_ends_with($relativePath, '/')) {
$testPath .= '/'; // 临时加 / 便于匹配目录
}
if (fnmatch($pattern, $testPath, FNM_PATHNAME)) {
return !$negate;
}
} else {
// 仅文件名匹配(递归到所有子目录)
$filename = basename($relativePath);
if (fnmatch($pattern, $filename)) {
return !$negate;
}
// 也匹配完整路径(如 *.log)
if (fnmatch($pattern, $relativePath)) {
return !$negate;
}
}
}
return false;
}
// ========== 并发 blob 创建 ==========
private function createBlobsConcurrent(string $apiBase, array $filesToUpload): array {
$results = [];
$total = count($filesToUpload);
$batches = array_chunk($filesToUpload, $this->concurrentLimit);
foreach ($batches as $i => $batch) {
error_log("📤 上传 blob 批次 " . ($i+1) . "/" . count($batches) . " (" . count($batch) . " 个文件)");
$mh = curl_multi_init();
$handles = [];
$map = [];
// 创建并发句柄
foreach ($batch as $relPath => $absPath) {
$content = file_get_contents($absPath);
if ($content === false) continue;
$blobData = [
'content' => base64_encode($content),
'encoding' => 'base64'
];
$ch = $this->createCurlHandle("{$apiBase}/git/blobs", 'POST', $blobData);
curl_multi_add_handle($mh, $ch);
$handles[$relPath] = $ch;
$map[(int)$ch] = $relPath;
}
// 执行并发
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh);
} while ($running > 0);
// 收集结果
foreach ($handles as $relPath => $ch) {
$response = curl_multi_getcontent($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
if ($httpCode >= 200 && $httpCode < 300) {
$blob = json_decode($response, true);
$results[$relPath] = $blob['sha'] ?? null;
} else {
error_log("❌ Blob 创建失败 ({$httpCode}) for {$relPath}");
// 可选:单文件重试(简化起见,此处不重试)
$results[$relPath] = null;
}
}
curl_multi_close($mh);
}
return $results;
}
// ========== Tree 构建(复用之前逻辑)==========
private function getRemoteFiles(string $treeUrl, array &$files = []): void {
$tree = $this->request($treeUrl);
foreach ($tree['tree'] ?? [] as $item) {
if ($item['type'] === 'blob') {
$files[$item['path']] = $item['sha'];
} elseif ($item['type'] === 'tree') {
$this->getRemoteFiles($item['url'], $files);
}
}
}
private function createTreeRecursive(string $apiBase, string $basePath, array $entries, string $baseTreeSha = null): string {
if (count($entries) <= 1000) {
$payload = ['tree' => $entries];
if ($baseTreeSha) $payload['base_tree'] = $baseTreeSha;
$tree = $this->request("{$apiBase}/git/trees", 'POST', $payload);
return $tree['sha'];
}
$groups = [];
$rootFiles = [];
foreach ($entries as $entry) {
if (strpos($entry['path'], '/') === false) {
$rootFiles[] = $entry;
} else {
[$dir] = explode('/', $entry['path'], 2);
$groups[$dir][] = [
'path' => substr($entry['path'], strlen($dir) + 1),
'mode' => $entry['mode'],
'type' => $entry['type'],
'sha' => $entry['sha']
];
}
}
$newEntries = $rootFiles;
foreach ($groups as $dir => $subEntries) {
$subTreeSha = $this->createTreeRecursive($apiBase, "{$basePath}{$dir}/", $subEntries);
$newEntries[] = [
'path' => $dir,
'mode' => '040000',
'type' => 'tree',
'sha' => $subTreeSha
];
}
return $this->createTreeRecursive($apiBase, $basePath, $newEntries, $baseTreeSha);
}
// ========== 主部署逻辑 ==========
public function deploy(string $localDir, string $commitMessage = 'Deploy static site'): string {
$apiBase = "https://api.github.com/repos/{$this->owner}/{$this->repo}";
$localDir = rtrim($localDir, '/\\');
// Step 1: 获取远程状态
$ref = $this->request("{$apiBase}/git/refs/heads/{$this->branch}");
$latestCommitSha = $ref['object']['sha'];
$commit = $this->request("{$apiBase}/git/commits/{$latestCommitSha}");
$baseTreeSha = $commit['tree']['sha'];
$remoteFiles = [];
$this->getRemoteFiles("{$apiBase}/git/trees/{$baseTreeSha}?recursive=1", $remoteFiles);
// Step 2: 加载 .gitignore
$gitignoreRules = $this->parseGitignore("{$localDir}/.gitignore");
// Step 3: 扫描本地文件(应用忽略规则)
$localFiles = [];
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($localDir, RecursiveDirectoryIterator::SKIP_DOTS));
foreach ($iterator as $file) {
if (!$file->isFile()) continue;
$relPath = str_replace('\\', '/', substr($file->getPathname(), strlen($localDir) + 1));
// 应用 .gitignore
if ($this->matchesGitignore($relPath, $gitignoreRules)) {
continue;
}
$localFiles[$relPath] = $file->getPathname();
}
// Step 4: 增量判断(哪些需要上传)
$filesToUpload = [];
$treeEntries = [];
$unchanged = 0;
foreach ($localFiles as $relPath => $absPath) {
$remoteSha = $remoteFiles[$relPath] ?? null;
$localSha1 = sha1_file($absPath);
if ($remoteSha) {
// 尝试获取远程内容 SHA1(简化:直接比对 blob 内容需额外请求,此处用文件内容比对)
// 更优方案:缓存 {path => content_sha},但为性能,我们假设“内容相同则无需上传”
// 实际中可接受轻微冗余上传
try {
$blob = $this->request("{$apiBase}/git/blobs/{$remoteSha}");
if (isset($blob['content']) && base64_decode($blob['content'], true) !== false) {
$remoteContent = base64_decode($blob['content'], true);
if (sha1($remoteContent) === $localSha1) {
$treeEntries[] = ['path' => $relPath, 'mode' => '100644', 'type' => 'blob', 'sha' => $remoteSha];
$unchanged++;
continue;
}
}
} catch (Exception $e) {
// blob 可能被 GC,忽略
}
}
$filesToUpload[$relPath] = $absPath;
}
// Step 5: 并发上传新/修改的文件
if (!empty($filesToUpload)) {
$newBlobs = $this->createBlobsConcurrent($apiBase, $filesToUpload);
foreach ($newBlobs as $relPath => $sha) {
if ($sha) {
$treeEntries[] = ['path' => $relPath, 'mode' => '100644', 'type' => 'blob', 'sha' => $sha];
} else {
error_log("⚠️ 跳过无法上传的文件: {$relPath}");
}
}
}
// Step 6: 创建 tree 和 commit
$newTreeSha = $this->createTreeRecursive($apiBase, '', $treeEntries, $baseTreeSha);
$newCommit = $this->request("{$apiBase}/git/commits", 'POST', [
'message' => $commitMessage,
'tree' => $newTreeSha,
'parents' => [$latestCommitSha]
]);
$this->request("{$apiBase}/git/refs/heads/{$this->branch}", 'PATCH', ['sha' => $newCommit['sha']]);
$deleted = count(array_diff_key($remoteFiles, $localFiles));
$addedOrModified = count($localFiles) - $unchanged;
return sprintf(
"✅ 部署成功!\n" .
" - 新增/修改: %d\n" .
" - 未变更: %d\n" .
" - 已删除: %d\n" .
" - 总文件数: %d\n" .
" - Commit: %s",
$addedOrModified, $unchanged, $deleted, count($localFiles), $newCommit['sha']
);
}
}
// ===== 使用示例 =====
try {
$deployer = new GitHubDeployer(
token: $_ENV['GITHUB_TOKEN'] ?? 'YOUR_TOKEN',
owner: 'your-username',
repo: 'your-static-site',
branch: 'gh-pages'
);
echo $deployer->deploy(__DIR__ . '/dist', 'Auto deploy with .gitignore & concurrency');
} catch (Exception $e) {
fwrite(STDERR, "❌ Error: " . $e->getMessage() . "\n");
exit(1);
}
本代码由千问生成,供参考。
