prscan 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/.vscode/launch.json +14 -0
- package/README.MD +32 -0
- package/dist/bot/lark.d.ts +2 -0
- package/dist/bot/lark.d.ts.map +1 -0
- package/dist/bot/lark.js +156 -0
- package/dist/bot/lark.js.map +1 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +77 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/report/index.d.ts +7 -0
- package/dist/report/index.d.ts.map +1 -0
- package/dist/report/index.js +45 -0
- package/dist/report/index.js.map +1 -0
- package/dist/tool/prscan.d.ts +72 -0
- package/dist/tool/prscan.d.ts.map +1 -0
- package/dist/tool/prscan.js +477 -0
- package/dist/tool/prscan.js.map +1 -0
- package/dist/util/analyze.d.ts +4 -0
- package/dist/util/analyze.d.ts.map +1 -0
- package/dist/util/analyze.js +213 -0
- package/dist/util/analyze.js.map +1 -0
- package/dist/util/archive.d.ts +34 -0
- package/dist/util/archive.d.ts.map +1 -0
- package/dist/util/archive.js +110 -0
- package/dist/util/archive.js.map +1 -0
- package/dist/util/memory-archive.d.ts +37 -0
- package/dist/util/memory-archive.d.ts.map +1 -0
- package/dist/util/memory-archive.js +128 -0
- package/dist/util/memory-archive.js.map +1 -0
- package/dist/util/npm.d.ts +46 -0
- package/dist/util/npm.d.ts.map +1 -0
- package/dist/util/npm.js +35 -0
- package/dist/util/npm.js.map +1 -0
- package/dist/util/parse.d.ts +18 -0
- package/dist/util/parse.d.ts.map +1 -0
- package/dist/util/parse.js +92 -0
- package/dist/util/parse.js.map +1 -0
- package/dist/util/proxy.d.ts +45 -0
- package/dist/util/proxy.d.ts.map +1 -0
- package/dist/util/proxy.js +143 -0
- package/dist/util/proxy.js.map +1 -0
- package/dist/util/repo.d.ts +103 -0
- package/dist/util/repo.d.ts.map +1 -0
- package/dist/util/repo.js +170 -0
- package/dist/util/repo.js.map +1 -0
- package/package.json +35 -0
- package/report.png +0 -0
- package/src/bot/lark.ts +184 -0
- package/src/cli/cli.ts +80 -0
- package/src/index.ts +67 -0
- package/src/report/index.ts +50 -0
- package/src/tool/prscan.ts +634 -0
- package/src/util/analyze.ts +248 -0
- package/src/util/memory-archive.ts +184 -0
- package/src/util/npm.ts +100 -0
- package/src/util/parse.ts +103 -0
- package/src/util/repo.ts +224 -0
- package/tsconfig.json +43 -0
package/src/util/repo.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { Octokit } from "octokit";
|
|
2
|
+
import got from "got";
|
|
3
|
+
|
|
4
|
+
export class GitHubRepo {
|
|
5
|
+
private octokit: Octokit;
|
|
6
|
+
private maxRetries: number;
|
|
7
|
+
|
|
8
|
+
constructor(authToken?: string, maxRetries = 3) {
|
|
9
|
+
this.maxRetries = maxRetries;
|
|
10
|
+
this.octokit = new Octokit(authToken ? { auth: authToken } : {});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public async getPRInfo(owner: string, repo: string, pull_number: number) {
|
|
14
|
+
for (let i = 0; i < this.maxRetries; i++) {
|
|
15
|
+
try {
|
|
16
|
+
console.info(
|
|
17
|
+
`Fetching PR info ${owner}/${repo}#${pull_number}`
|
|
18
|
+
);
|
|
19
|
+
const { data } = await this.octokit.rest.pulls.get({
|
|
20
|
+
owner,
|
|
21
|
+
repo,
|
|
22
|
+
pull_number,
|
|
23
|
+
});
|
|
24
|
+
return data;
|
|
25
|
+
} catch (error) {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw new Error("读取PR信息失败");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async getPRChangedFiles(
|
|
32
|
+
owner: string,
|
|
33
|
+
repo: string,
|
|
34
|
+
pull_number: number,
|
|
35
|
+
per_page = 30
|
|
36
|
+
) {
|
|
37
|
+
const files = [];
|
|
38
|
+
|
|
39
|
+
let page = 1;
|
|
40
|
+
while (true) {
|
|
41
|
+
let data;
|
|
42
|
+
for (let i = 0; i < this.maxRetries; i++) {
|
|
43
|
+
try {
|
|
44
|
+
console.info(
|
|
45
|
+
`Fetching PR files ${owner}/${repo}#${pull_number} page ${page}`
|
|
46
|
+
);
|
|
47
|
+
data = await this.octokit.rest.pulls.listFiles({
|
|
48
|
+
owner,
|
|
49
|
+
repo,
|
|
50
|
+
pull_number,
|
|
51
|
+
per_page,
|
|
52
|
+
page,
|
|
53
|
+
});
|
|
54
|
+
break; // 成功获取数据,跳出重试循环
|
|
55
|
+
} catch (error) {}
|
|
56
|
+
}
|
|
57
|
+
if (data) {
|
|
58
|
+
files.push(...data.data);
|
|
59
|
+
page++;
|
|
60
|
+
if (data.data.length < per_page) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error("读取PR变更文件失败");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return files;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public async getTextFileContent(
|
|
72
|
+
owner: string,
|
|
73
|
+
repo: string,
|
|
74
|
+
path: string,
|
|
75
|
+
ref: string
|
|
76
|
+
): Promise<string | null> {
|
|
77
|
+
// 使用多种方式获取文件内容,解决原API经常超时的问题
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
console.info(
|
|
81
|
+
`Fetching ${owner}/${repo}/${path}@${ref} via Git Data API`
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// 先获取commit的tree
|
|
85
|
+
const { data: commit } = await this.octokit.rest.git.getCommit({
|
|
86
|
+
owner,
|
|
87
|
+
repo,
|
|
88
|
+
commit_sha: ref,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// 递归查找文件
|
|
92
|
+
const blob = await this.findFileInTree(
|
|
93
|
+
owner,
|
|
94
|
+
repo,
|
|
95
|
+
commit.tree.sha,
|
|
96
|
+
path
|
|
97
|
+
);
|
|
98
|
+
if (blob) {
|
|
99
|
+
const { data: blobData } = await this.octokit.rest.git.getBlob({
|
|
100
|
+
owner,
|
|
101
|
+
repo,
|
|
102
|
+
file_sha: blob.sha!,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (blobData.encoding === "base64") {
|
|
106
|
+
return Buffer.from(blobData.content, "base64").toString(
|
|
107
|
+
"utf-8"
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.warn(`Git Data API failed for ${path}:`, error);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < this.maxRetries; i++) {
|
|
116
|
+
try {
|
|
117
|
+
console.info(
|
|
118
|
+
`Fetching ${owner}/${repo}/${path}@${ref} via Contents API (attempt ${
|
|
119
|
+
i + 1
|
|
120
|
+
})`
|
|
121
|
+
);
|
|
122
|
+
const { data } = await this.octokit.rest.repos.getContent({
|
|
123
|
+
owner,
|
|
124
|
+
repo,
|
|
125
|
+
path,
|
|
126
|
+
ref,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if ("content" in data && data.content) {
|
|
130
|
+
// 使用 Buffer 解码 base64 内容 (替代 atob,更可靠)
|
|
131
|
+
return Buffer.from(data.content, "base64").toString(
|
|
132
|
+
"utf-8"
|
|
133
|
+
);
|
|
134
|
+
} else if ("download_url" in data && data.download_url) {
|
|
135
|
+
// 文件过大时:使用 download_url 获取原始内容
|
|
136
|
+
const response = await got(data.download_url);
|
|
137
|
+
if (response.statusCode === 200) {
|
|
138
|
+
return response.body;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.warn(
|
|
143
|
+
`Contents API attempt ${i + 1} failed for ${path}:`,
|
|
144
|
+
error
|
|
145
|
+
);
|
|
146
|
+
if (i === this.maxRetries - 1) {
|
|
147
|
+
// 最后一次重试失败,尝试其他方法
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
// 等待一段时间再重试
|
|
151
|
+
await new Promise((resolve) =>
|
|
152
|
+
setTimeout(resolve, 200 * (i + 1))
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
console.info(
|
|
159
|
+
`Fetching ${owner}/${repo}/${path}@${ref} via raw URL`
|
|
160
|
+
);
|
|
161
|
+
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path}`;
|
|
162
|
+
const response = await got(rawUrl);
|
|
163
|
+
if (response.statusCode === 200) {
|
|
164
|
+
return response.body;
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.warn(`Raw URL failed for ${path}:`, error);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.error(
|
|
171
|
+
`All methods failed to fetch ${owner}/${repo}/${path}@${ref}`
|
|
172
|
+
);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 在Git树中递归查找文件
|
|
178
|
+
*/
|
|
179
|
+
private async findFileInTree(
|
|
180
|
+
owner: string,
|
|
181
|
+
repo: string,
|
|
182
|
+
treeSha: string,
|
|
183
|
+
filePath: string
|
|
184
|
+
): Promise<{ sha: string } | null> {
|
|
185
|
+
const pathParts = filePath.split("/");
|
|
186
|
+
let currentTreeSha = treeSha;
|
|
187
|
+
let currentPath = "";
|
|
188
|
+
|
|
189
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
190
|
+
const part = pathParts[i]!;
|
|
191
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const { data: tree } = await this.octokit.rest.git.getTree({
|
|
195
|
+
owner,
|
|
196
|
+
repo,
|
|
197
|
+
tree_sha: currentTreeSha,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const item = tree.tree.find((item) => item.path === part);
|
|
201
|
+
if (!item) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (i === pathParts.length - 1) {
|
|
206
|
+
// 这是最后一个部分,应该是文件
|
|
207
|
+
return item.type === "blob" ? { sha: item.sha! } : null;
|
|
208
|
+
} else {
|
|
209
|
+
// 这是一个目录,继续递归
|
|
210
|
+
if (item.type === "tree") {
|
|
211
|
+
currentTreeSha = item.sha!;
|
|
212
|
+
} else {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.warn(`Failed to get tree ${currentTreeSha}:`, error);
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Visit https://aka.ms/tsconfig to read more about this file
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
// File Layout
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
|
|
8
|
+
// Environment Settings
|
|
9
|
+
// See also https://aka.ms/tsconfig/module
|
|
10
|
+
"module": "nodenext",
|
|
11
|
+
"target": "esnext",
|
|
12
|
+
"lib": ["esnext"],
|
|
13
|
+
// For nodejs with fetch support:
|
|
14
|
+
"types": ["node"],
|
|
15
|
+
// and npm install -D @types/node
|
|
16
|
+
|
|
17
|
+
// Other Outputs
|
|
18
|
+
"sourceMap": true,
|
|
19
|
+
"declaration": true,
|
|
20
|
+
"declarationMap": true,
|
|
21
|
+
|
|
22
|
+
// Stricter Typechecking Options
|
|
23
|
+
"noUncheckedIndexedAccess": true,
|
|
24
|
+
"exactOptionalPropertyTypes": true,
|
|
25
|
+
|
|
26
|
+
// Style Options
|
|
27
|
+
// "noImplicitReturns": true,
|
|
28
|
+
// "noImplicitOverride": true,
|
|
29
|
+
// "noUnusedLocals": true,
|
|
30
|
+
// "noUnusedParameters": true,
|
|
31
|
+
// "noFallthroughCasesInSwitch": true,
|
|
32
|
+
// "noPropertyAccessFromIndexSignature": true,
|
|
33
|
+
|
|
34
|
+
// Recommended Options
|
|
35
|
+
"strict": true,
|
|
36
|
+
"jsx": "react-jsx",
|
|
37
|
+
"verbatimModuleSyntax": true,
|
|
38
|
+
"isolatedModules": true,
|
|
39
|
+
"noUncheckedSideEffectImports": true,
|
|
40
|
+
"moduleDetection": "force",
|
|
41
|
+
"skipLibCheck": true,
|
|
42
|
+
}
|
|
43
|
+
}
|