207 lines
5.0 KiB
JavaScript
207 lines
5.0 KiB
JavaScript
|
|
/**
|
|||
|
|
* services/zhuyuan-bridge/lib/repo-operator.js
|
|||
|
|
*
|
|||
|
|
* 仓库操作封装 — 通过 GitHub API 读写代码、创建分支和 PR
|
|||
|
|
*
|
|||
|
|
* 纯 Node.js,无外部依赖。
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const https = require('https');
|
|||
|
|
|
|||
|
|
class RepoOperator {
|
|||
|
|
/**
|
|||
|
|
* @param {string} token — GitHub access token
|
|||
|
|
* @param {string} owner — Repository owner
|
|||
|
|
* @param {string} repo — Repository name
|
|||
|
|
*/
|
|||
|
|
constructor(token, owner, repo) {
|
|||
|
|
this.token = token;
|
|||
|
|
this.owner = owner;
|
|||
|
|
this.repo = repo;
|
|||
|
|
this.baseUrl = `/repos/${owner}/${repo}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* GitHub API 请求
|
|||
|
|
*/
|
|||
|
|
_request(method, apiPath, body) {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
const options = {
|
|||
|
|
hostname: 'api.github.com',
|
|||
|
|
path: apiPath,
|
|||
|
|
method: method,
|
|||
|
|
headers: {
|
|||
|
|
'User-Agent': 'zhuyuan-bridge/1.0',
|
|||
|
|
'Accept': 'application/vnd.github+json',
|
|||
|
|
'Authorization': `token ${this.token}`,
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
if (body) {
|
|||
|
|
options.headers['Content-Type'] = 'application/json';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const req = https.request(options, (res) => {
|
|||
|
|
let data = '';
|
|||
|
|
res.on('data', chunk => data += chunk);
|
|||
|
|
res.on('end', () => {
|
|||
|
|
try {
|
|||
|
|
resolve({ status: res.statusCode, data: JSON.parse(data) });
|
|||
|
|
} catch (_) {
|
|||
|
|
resolve({ status: res.statusCode, data: data });
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
req.on('error', reject);
|
|||
|
|
if (body) req.write(JSON.stringify(body));
|
|||
|
|
req.end();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 读取文件内容
|
|||
|
|
* @param {string} filePath
|
|||
|
|
* @param {string} ref — branch or commit SHA
|
|||
|
|
* @returns {Promise<{content: string, sha: string}|null>}
|
|||
|
|
*/
|
|||
|
|
async readFile(filePath, ref = 'main') {
|
|||
|
|
const res = await this._request(
|
|||
|
|
'GET',
|
|||
|
|
`${this.baseUrl}/contents/${encodeURIComponent(filePath)}?ref=${encodeURIComponent(ref)}`
|
|||
|
|
);
|
|||
|
|
if (res.status !== 200) return null;
|
|||
|
|
return {
|
|||
|
|
content: Buffer.from(res.data.content, 'base64').toString('utf8'),
|
|||
|
|
sha: res.data.sha
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建或更新文件
|
|||
|
|
* @param {string} filePath
|
|||
|
|
* @param {string} content
|
|||
|
|
* @param {string} message — commit message
|
|||
|
|
* @param {string} branch
|
|||
|
|
* @returns {Promise<object>}
|
|||
|
|
*/
|
|||
|
|
async writeFile(filePath, content, message, branch = 'main') {
|
|||
|
|
let sha;
|
|||
|
|
try {
|
|||
|
|
const existing = await this.readFile(filePath, branch);
|
|||
|
|
if (existing) sha = existing.sha;
|
|||
|
|
} catch (_) { /* new file */ }
|
|||
|
|
|
|||
|
|
const body = {
|
|||
|
|
message,
|
|||
|
|
content: Buffer.from(content).toString('base64'),
|
|||
|
|
branch,
|
|||
|
|
committer: {
|
|||
|
|
name: '铸渊 (ZhùYuān)',
|
|||
|
|
email: 'zhuyuan@guanghulab.com'
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
if (sha) body.sha = sha;
|
|||
|
|
|
|||
|
|
const res = await this._request(
|
|||
|
|
'PUT',
|
|||
|
|
`${this.baseUrl}/contents/${encodeURIComponent(filePath)}`,
|
|||
|
|
body
|
|||
|
|
);
|
|||
|
|
return res.data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建分支
|
|||
|
|
* @param {string} branchName
|
|||
|
|
* @param {string} fromBranch
|
|||
|
|
* @returns {Promise<object>}
|
|||
|
|
*/
|
|||
|
|
async createBranch(branchName, fromBranch = 'main') {
|
|||
|
|
const refRes = await this._request(
|
|||
|
|
'GET',
|
|||
|
|
`${this.baseUrl}/git/ref/heads/${encodeURIComponent(fromBranch)}`
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (refRes.status !== 200) {
|
|||
|
|
throw new Error(`获取分支 ${fromBranch} 失败: ${refRes.status}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const res = await this._request(
|
|||
|
|
'POST',
|
|||
|
|
`${this.baseUrl}/git/refs`,
|
|||
|
|
{
|
|||
|
|
ref: `refs/heads/${branchName}`,
|
|||
|
|
sha: refRes.data.object.sha
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
return res.data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建 Pull Request
|
|||
|
|
* @param {string} title
|
|||
|
|
* @param {string} body
|
|||
|
|
* @param {string} head — source branch
|
|||
|
|
* @param {string} base — target branch
|
|||
|
|
* @returns {Promise<object>}
|
|||
|
|
*/
|
|||
|
|
async createPR(title, body, head, base = 'main') {
|
|||
|
|
const res = await this._request(
|
|||
|
|
'POST',
|
|||
|
|
`${this.baseUrl}/pulls`,
|
|||
|
|
{ title, body, head, base }
|
|||
|
|
);
|
|||
|
|
return res.data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 评论 Issue
|
|||
|
|
* @param {number} issueNumber
|
|||
|
|
* @param {string} body
|
|||
|
|
* @returns {Promise<object>}
|
|||
|
|
*/
|
|||
|
|
async commentOnIssue(issueNumber, body) {
|
|||
|
|
const res = await this._request(
|
|||
|
|
'POST',
|
|||
|
|
`${this.baseUrl}/issues/${issueNumber}/comments`,
|
|||
|
|
{ body }
|
|||
|
|
);
|
|||
|
|
return res.data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建 Issue
|
|||
|
|
* @param {string} title
|
|||
|
|
* @param {string} body
|
|||
|
|
* @param {string[]} labels
|
|||
|
|
* @returns {Promise<object>}
|
|||
|
|
*/
|
|||
|
|
async createIssue(title, body, labels = []) {
|
|||
|
|
const res = await this._request(
|
|||
|
|
'POST',
|
|||
|
|
`${this.baseUrl}/issues`,
|
|||
|
|
{ title, body, labels }
|
|||
|
|
);
|
|||
|
|
return res.data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 列出目录文件
|
|||
|
|
* @param {string} dirPath
|
|||
|
|
* @param {string} ref
|
|||
|
|
* @returns {Promise<Array>}
|
|||
|
|
*/
|
|||
|
|
async listDir(dirPath, ref = 'main') {
|
|||
|
|
const encodedPath = dirPath ? encodeURIComponent(dirPath) : '';
|
|||
|
|
const res = await this._request(
|
|||
|
|
'GET',
|
|||
|
|
`${this.baseUrl}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`
|
|||
|
|
);
|
|||
|
|
if (res.status !== 200) return [];
|
|||
|
|
return Array.isArray(res.data) ? res.data : [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = { RepoOperator };
|