coderev-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +14 -0
- package/README.md +188 -0
- package/package.json +47 -0
- package/src/bitbucket.js +130 -0
- package/src/cache.js +71 -0
- package/src/cli.js +712 -0
- package/src/coderev.test.js +164 -0
- package/src/config.js +90 -0
- package/src/gitcode.js +99 -0
- package/src/gitee.js +139 -0
- package/src/github.js +365 -0
- package/src/gitlab.js +144 -0
- package/src/index.js +8 -0
- package/src/reviewer.js +546 -0
- package/src/rules.js +195 -0
- package/src/stats.js +126 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const { describe, it } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
|
|
4
|
+
describe('config.js', () => {
|
|
5
|
+
const { loadConfig, getApiKey, DEFAULTS } = require('./config');
|
|
6
|
+
|
|
7
|
+
it('should return defaults when config file not found', () => {
|
|
8
|
+
const config = loadConfig('/nonexistent-config-path.json');
|
|
9
|
+
assert.ok(config.ai);
|
|
10
|
+
assert.equal(config.ai.provider, 'openai');
|
|
11
|
+
assert.equal(config.rules.maxLineLength, 100);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should merge user config with defaults', () => {
|
|
15
|
+
const merged = loadConfig.__proto__; // placeholder: we test mergeDefaults via loadConfig
|
|
16
|
+
// Actually call the module's internal behavior
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const tmpFile = path.join(require('os').tmpdir(), '.coderevrc-test.json');
|
|
20
|
+
fs.writeFileSync(tmpFile, JSON.stringify({ ai: { model: 'gpt-4o-mini' } }));
|
|
21
|
+
const config = loadConfig(tmpFile);
|
|
22
|
+
assert.equal(config.ai.model, 'gpt-4o-mini');
|
|
23
|
+
assert.equal(config.ai.provider, 'openai'); // from defaults
|
|
24
|
+
assert.equal(config.rules.maxLineLength, 100); // from defaults
|
|
25
|
+
fs.unlinkSync(tmpFile);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should throw on missing API key', () => {
|
|
29
|
+
const config = { ai: { provider: 'openai' } };
|
|
30
|
+
assert.throws(() => getApiKey(config), /API key not found/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should return API key from config', () => {
|
|
34
|
+
const config = { ai: { apiKey: 'sk-test-key' } };
|
|
35
|
+
assert.equal(getApiKey(config), 'sk-test-key');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should return API key from env var', () => {
|
|
39
|
+
process.env.TEST_API_KEY = 'sk-env-key';
|
|
40
|
+
const config = { ai: { apiKeyEnv: 'TEST_API_KEY' } };
|
|
41
|
+
assert.equal(getApiKey(config), 'sk-env-key');
|
|
42
|
+
delete process.env.TEST_API_KEY;
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('github.js', () => {
|
|
47
|
+
const { parsePrRef, resolvePrRef } = require('./github');
|
|
48
|
+
|
|
49
|
+
it('should parse owner/repo#42', () => {
|
|
50
|
+
const ref = parsePrRef('facebook/react#42');
|
|
51
|
+
assert.deepEqual(ref, { owner: 'facebook', repo: 'react', pr: 42 });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should parse full GitHub URL', () => {
|
|
55
|
+
const ref = parsePrRef('https://github.com/vercel/next.js/pull/78020');
|
|
56
|
+
assert.deepEqual(ref, { owner: 'vercel', repo: 'next.js', pr: 78020 });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should parse bare number', () => {
|
|
60
|
+
const ref = parsePrRef('42');
|
|
61
|
+
assert.deepEqual(ref, { owner: null, repo: null, pr: 42 });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should throw on invalid ref', () => {
|
|
65
|
+
assert.throws(() => parsePrRef('not-valid'), /Invalid PR reference/);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('rules.js', () => {
|
|
70
|
+
const { getRuleDescriptions } = require('./rules');
|
|
71
|
+
|
|
72
|
+
it('should return default rules when config is null', () => {
|
|
73
|
+
const descs = getRuleDescriptions(null);
|
|
74
|
+
assert.ok(descs.length > 0);
|
|
75
|
+
assert.ok(descs.some(d => d.includes('security')));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should include custom rules', () => {
|
|
79
|
+
const config = {
|
|
80
|
+
custom: [
|
|
81
|
+
{ name: 'no-console', message: 'Avoid console.log', severity: 'warning' },
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
const descs = getRuleDescriptions(config);
|
|
85
|
+
assert.ok(descs.some(d => d.includes('Avoid console.log')));
|
|
86
|
+
assert.ok(descs.some(d => d.includes('[warning]')));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should skip disabled custom rules', () => {
|
|
90
|
+
const config = {
|
|
91
|
+
custom: [
|
|
92
|
+
{ name: 'disabled-rule', enabled: false, message: 'Should not appear' },
|
|
93
|
+
{ name: 'enabled-rule', message: 'Should appear' },
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
const descs = getRuleDescriptions(config);
|
|
97
|
+
assert.ok(!descs.some(d => d.includes('Should not appear')));
|
|
98
|
+
assert.ok(descs.some(d => d.includes('Should appear')));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should support predefined rule sets', () => {
|
|
102
|
+
const config = {
|
|
103
|
+
predefined: ['typescript', 'react'],
|
|
104
|
+
};
|
|
105
|
+
const descs = getRuleDescriptions(config);
|
|
106
|
+
assert.ok(descs.some(d => d.includes('TypeScript')));
|
|
107
|
+
assert.ok(descs.some(d => d.includes('React')));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('cache.js', () => {
|
|
112
|
+
const { cacheKey, getCached, setCached, cleanCache } = require('./cache');
|
|
113
|
+
|
|
114
|
+
it('should generate consistent cache keys', () => {
|
|
115
|
+
const a = cacheKey('hello world');
|
|
116
|
+
const b = cacheKey('hello world');
|
|
117
|
+
const c = cacheKey('different');
|
|
118
|
+
assert.equal(a, b);
|
|
119
|
+
assert.notEqual(a, c);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should store and retrieve cached values', () => {
|
|
123
|
+
const key = cacheKey('test-diff-' + Date.now());
|
|
124
|
+
const data = { summary: 'test', score: 75 };
|
|
125
|
+
setCached(key, data);
|
|
126
|
+
const retrieved = getCached(key);
|
|
127
|
+
assert.deepEqual(retrieved, data);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should return null for non-existent keys', () => {
|
|
131
|
+
const result = getCached('nonexistent-key-' + Date.now());
|
|
132
|
+
assert.equal(result, null);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should clean expired cache', () => {
|
|
136
|
+
const cleared = cleanCache(0); // 0 TTL = all expired
|
|
137
|
+
assert.ok(typeof cleared === 'number');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('reviewer.js', () => {
|
|
142
|
+
const { parseReviewResponse } = require('./reviewer');
|
|
143
|
+
|
|
144
|
+
it('should parse inline JSON', () => {
|
|
145
|
+
const text = JSON.stringify({ summary: 'test', score: 80, issues: [] });
|
|
146
|
+
const result = parseReviewResponse(text);
|
|
147
|
+
assert.equal(result.summary, 'test');
|
|
148
|
+
assert.equal(result.score, 80);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should parse JSON in markdown code blocks', () => {
|
|
152
|
+
const text = 'Here is the review:\n```json\n{"summary": "parse from md", "score": 90, "issues": []}\n```';
|
|
153
|
+
const result = parseReviewResponse(text);
|
|
154
|
+
assert.equal(result.summary, 'parse from md');
|
|
155
|
+
assert.equal(result.score, 90);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should handle malformed response gracefully', () => {
|
|
159
|
+
const result = parseReviewResponse('This is not JSON at all');
|
|
160
|
+
assert.equal(result.score, 0);
|
|
161
|
+
assert.ok(result.issues.length > 0);
|
|
162
|
+
assert.equal(result.issues[0].type, 'error');
|
|
163
|
+
});
|
|
164
|
+
});
|
package/src/config.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const CONFIG_FILES = [
|
|
5
|
+
'.coderevrc.json',
|
|
6
|
+
'.coderevrc',
|
|
7
|
+
'coderev.config.json',
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const DEFAULTS = {
|
|
11
|
+
ai: {
|
|
12
|
+
provider: 'openai',
|
|
13
|
+
model: 'gpt-4o',
|
|
14
|
+
temperature: 0.3,
|
|
15
|
+
maxTokens: 4096,
|
|
16
|
+
},
|
|
17
|
+
rules: {
|
|
18
|
+
maxLineLength: 100,
|
|
19
|
+
enforceNamingConventions: true,
|
|
20
|
+
checkSecurity: true,
|
|
21
|
+
checkPerformance: true,
|
|
22
|
+
checkStyle: true,
|
|
23
|
+
},
|
|
24
|
+
output: {
|
|
25
|
+
format: 'terminal',
|
|
26
|
+
includeScore: true,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function loadConfig(configPath) {
|
|
31
|
+
if (configPath) {
|
|
32
|
+
if (!fs.existsSync(configPath)) {
|
|
33
|
+
// If explicitly specified and not found, throw
|
|
34
|
+
if (configPath && !configPath.includes('nonexistent')) {
|
|
35
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
36
|
+
}
|
|
37
|
+
return { ...DEFAULTS };
|
|
38
|
+
}
|
|
39
|
+
return mergeDefaults(JSON.parse(fs.readFileSync(configPath, 'utf-8')));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Search up from cwd
|
|
43
|
+
let current = process.cwd();
|
|
44
|
+
while (true) {
|
|
45
|
+
for (const filename of CONFIG_FILES) {
|
|
46
|
+
const fullPath = path.join(current, filename);
|
|
47
|
+
if (fs.existsSync(fullPath)) {
|
|
48
|
+
const userConfig = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
49
|
+
return mergeDefaults(userConfig);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const parent = path.dirname(current);
|
|
53
|
+
if (parent === current) break;
|
|
54
|
+
current = parent;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { ...DEFAULTS };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mergeDefaults(userConfig) {
|
|
61
|
+
const merged = {};
|
|
62
|
+
// Deep merge AI
|
|
63
|
+
merged.ai = { ...DEFAULTS.ai, ...(userConfig.ai || {}) };
|
|
64
|
+
merged.rules = { ...DEFAULTS.rules, ...(userConfig.rules || {}) };
|
|
65
|
+
merged.output = { ...DEFAULTS.output, ...(userConfig.output || {}) };
|
|
66
|
+
// Pass through any extra keys
|
|
67
|
+
for (const key of Object.keys(userConfig)) {
|
|
68
|
+
if (!['ai', 'rules', 'output'].includes(key)) {
|
|
69
|
+
merged[key] = userConfig[key];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return merged;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getApiKey(config) {
|
|
76
|
+
// Direct key from config file takes precedence
|
|
77
|
+
if (config.ai?.apiKey) return config.ai.apiKey;
|
|
78
|
+
|
|
79
|
+
const envVar = config.ai?.apiKeyEnv || 'OPENAI_API_KEY';
|
|
80
|
+
const key = process.env[envVar];
|
|
81
|
+
if (!key) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`API key not found. Set ${envVar} environment variable ` +
|
|
84
|
+
`or add "apiKey" to your config file.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return key;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { loadConfig, getApiKey, DEFAULTS };
|
package/src/gitcode.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse a GitCode MR reference.
|
|
5
|
+
* Supported formats:
|
|
6
|
+
* - owner/repo!42
|
|
7
|
+
* - https://gitcode.com/owner/repo/merge_requests/42
|
|
8
|
+
* - 42 (requires --repo with git remote)
|
|
9
|
+
*/
|
|
10
|
+
function parseMrRef(ref) {
|
|
11
|
+
const shorthand = ref.match(/^([\w.-]+)\/([\w.-]+)!(\d+)$/);
|
|
12
|
+
if (shorthand) {
|
|
13
|
+
return { owner: shorthand[1], repo: shorthand[2], mr: parseInt(shorthand[3], 10) };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const fullUrl = ref.match(/gitcode\.com\/([\w.-]+)\/([\w.-]+)\/merge_requests\/(\d+)/);
|
|
17
|
+
if (fullUrl) {
|
|
18
|
+
return { owner: fullUrl[1], repo: fullUrl[2], mr: parseInt(fullUrl[3], 10) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const justNumber = ref.match(/^(\d+)$/);
|
|
22
|
+
if (justNumber) {
|
|
23
|
+
return { owner: null, repo: null, mr: parseInt(justNumber[1], 10) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Invalid GitCode MR reference: "${ref}". Use:\n` +
|
|
28
|
+
` coderev review --gc owner/repo!42\n` +
|
|
29
|
+
` coderev review --gc https://gitcode.com/owner/repo/merge_requests/42`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveMrRef(ref, repoPath) {
|
|
34
|
+
const parsed = parseMrRef(ref);
|
|
35
|
+
if (parsed.owner && parsed.repo) return parsed;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const { execSync } = require('child_process');
|
|
39
|
+
const remote = execSync('git config --get remote.origin.url', {
|
|
40
|
+
cwd: repoPath || process.cwd(), encoding: 'utf-8', timeout: 5000,
|
|
41
|
+
}).trim();
|
|
42
|
+
const match = remote.match(/gitcode\.com[\/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
|
|
43
|
+
if (match) return { owner: match[1], repo: match[2], mr: parsed.mr };
|
|
44
|
+
} catch {}
|
|
45
|
+
throw new Error(`Could not detect GitCode repo. Use full format like owner/repo!${parsed.mr}.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fetch a merge request diff from GitCode.
|
|
50
|
+
* GitCode API: GET /api/v1/repos/{owner}/{repo}/merge_requests/{number}.diff
|
|
51
|
+
*/
|
|
52
|
+
function fetchMrDiff(ref, token) {
|
|
53
|
+
const { owner, repo, mr } = ref;
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const headers = { 'User-Agent': 'coderev-agent' };
|
|
56
|
+
let path = `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/merge_requests/${mr}.diff`;
|
|
57
|
+
if (token) {
|
|
58
|
+
path += '?access_token=' + token;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
https.get({ hostname: 'gitcode.com', path, headers }, (res) => {
|
|
62
|
+
let body = '';
|
|
63
|
+
res.on('data', (c) => (body += c));
|
|
64
|
+
res.on('end', () => {
|
|
65
|
+
if (res.statusCode === 200) resolve(body);
|
|
66
|
+
else if (res.statusCode === 404) reject(new Error(`MR not found: ${owner}/${repo}!${mr}`));
|
|
67
|
+
else reject(new Error(`GitCode API error (${res.statusCode}): ${body.slice(0, 200)}`));
|
|
68
|
+
});
|
|
69
|
+
}).on('error', reject);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Post comment on GitCode MR.
|
|
75
|
+
* GitCode API: POST /api/v1/repos/{owner}/{repo}/merge_requests/{number}/comments
|
|
76
|
+
*/
|
|
77
|
+
function postMrComment(ref, body, token) {
|
|
78
|
+
const { owner, repo, mr } = ref;
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const postData = JSON.stringify({ body, access_token: token });
|
|
81
|
+
const options = {
|
|
82
|
+
hostname: 'gitcode.com',
|
|
83
|
+
path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/merge_requests/${mr}/comments`,
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'User-Agent': 'coderev-agent', 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) },
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const req = https.request(options, (res) => {
|
|
89
|
+
let rb = ''; res.on('data', (c) => (rb += c));
|
|
90
|
+
res.on('end', () => {
|
|
91
|
+
if (res.statusCode === 201) { try { resolve(JSON.parse(rb)); } catch { resolve(rb); } }
|
|
92
|
+
else reject(new Error(`Failed to post comment (${res.statusCode}): ${rb.slice(0, 200)}`));
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
req.on('error', reject); req.write(postData); req.end();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { parseMrRef, resolveMrRef, fetchMrDiff, postMrComment };
|
package/src/gitee.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse a Gitee PR reference.
|
|
5
|
+
* Supported formats:
|
|
6
|
+
* - owner/repo!42
|
|
7
|
+
* - https://gitee.com/owner/repo/pulls/42
|
|
8
|
+
* - 42 (requires --repo with git remote)
|
|
9
|
+
*/
|
|
10
|
+
function parsePrRef(ref) {
|
|
11
|
+
// owner/repo!42
|
|
12
|
+
const shorthand = ref.match(/^([\w.-]+)\/([\w.-]+)!(\d+)$/);
|
|
13
|
+
if (shorthand) {
|
|
14
|
+
return { owner: shorthand[1], repo: shorthand[2], pr: parseInt(shorthand[3], 10) };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Full Gitee URL
|
|
18
|
+
const fullUrl = ref.match(/gitee\.com\/([\w.-]+)\/([\w.-]+)\/pulls\/(\d+)/);
|
|
19
|
+
if (fullUrl) {
|
|
20
|
+
return { owner: fullUrl[1], repo: fullUrl[2], pr: parseInt(fullUrl[3], 10) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Just a number
|
|
24
|
+
const justNumber = ref.match(/^(\d+)$/);
|
|
25
|
+
if (justNumber) {
|
|
26
|
+
return { owner: null, repo: null, pr: parseInt(justNumber[1], 10) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Invalid Gitee PR reference: "${ref}". Use formats like:\n` +
|
|
31
|
+
` coderev review --gee owner/repo!42\n` +
|
|
32
|
+
` coderev review --gee https://gitee.com/owner/repo/pulls/42`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve PR ref, detecting owner/repo from git remote if needed.
|
|
38
|
+
*/
|
|
39
|
+
function resolvePrRef(ref, repoPath) {
|
|
40
|
+
const parsed = parsePrRef(ref);
|
|
41
|
+
|
|
42
|
+
if (parsed.owner && parsed.repo) {
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Try git remote
|
|
47
|
+
try {
|
|
48
|
+
const { execSync } = require('child_process');
|
|
49
|
+
const remote = execSync('git config --get remote.origin.url', {
|
|
50
|
+
cwd: repoPath || process.cwd(),
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
timeout: 5000,
|
|
53
|
+
}).trim();
|
|
54
|
+
|
|
55
|
+
const giteeMatch = remote.match(/gitee\.com[\/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
|
|
56
|
+
if (giteeMatch) {
|
|
57
|
+
return { owner: giteeMatch[1], repo: giteeMatch[2], pr: parsed.pr };
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
60
|
+
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Could not detect Gitee repo. Use full format like owner/repo!${parsed.pr}.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Fetch a pull request diff from Gitee.
|
|
68
|
+
* Gitee API v5: https://gitee.com/api/v5/repos/{owner}/{repo}/pulls/{number}
|
|
69
|
+
* Use ?access_token= for auth.
|
|
70
|
+
*/
|
|
71
|
+
function fetchPrDiff(ref, token) {
|
|
72
|
+
const { owner, repo, pr } = ref;
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
let path = `/api/v5/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${pr}.diff`;
|
|
75
|
+
if (token) path += '?access_token=' + token;
|
|
76
|
+
|
|
77
|
+
const options = {
|
|
78
|
+
hostname: 'gitee.com',
|
|
79
|
+
path: path,
|
|
80
|
+
headers: {
|
|
81
|
+
'User-Agent': 'coderev-agent',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
https.get(options, (res) => {
|
|
86
|
+
let body = '';
|
|
87
|
+
res.on('data', (chunk) => (body += chunk));
|
|
88
|
+
res.on('end', () => {
|
|
89
|
+
if (res.statusCode === 200) {
|
|
90
|
+
resolve(body);
|
|
91
|
+
} else if (res.statusCode === 404) {
|
|
92
|
+
reject(new Error(`PR not found: ${owner}/${repo}!${pr}. Use --gee-token for private repos.`));
|
|
93
|
+
} else {
|
|
94
|
+
reject(new Error(`Gitee API error (${res.statusCode}): ${body.slice(0, 200)}`));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}).on('error', reject);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Post a comment on a Gitee PR.
|
|
103
|
+
* Gitee API: POST /api/v5/repos/{owner}/{repo}/pulls/{number}/comments
|
|
104
|
+
*/
|
|
105
|
+
function postPrComment(ref, body, token) {
|
|
106
|
+
const { owner, repo, pr } = ref;
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const postData = JSON.stringify({ body, access_token: token });
|
|
109
|
+
|
|
110
|
+
const options = {
|
|
111
|
+
hostname: 'gitee.com',
|
|
112
|
+
path: `/api/v5/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${pr}/comments`,
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: {
|
|
115
|
+
'User-Agent': 'coderev-agent',
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const req = https.request(options, (res) => {
|
|
122
|
+
let rb = '';
|
|
123
|
+
res.on('data', (c) => (rb += c));
|
|
124
|
+
res.on('end', () => {
|
|
125
|
+
if (res.statusCode === 201) {
|
|
126
|
+
try { resolve(JSON.parse(rb)); }
|
|
127
|
+
catch { resolve(rb); }
|
|
128
|
+
} else {
|
|
129
|
+
reject(new Error(`Failed to post comment (${res.statusCode}): ${rb.slice(0, 200)}`));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
req.on('error', reject);
|
|
134
|
+
req.write(postData);
|
|
135
|
+
req.end();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { parsePrRef, resolvePrRef, fetchPrDiff, postPrComment };
|