-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbot.js
253 lines (225 loc) · 8.68 KB
/
bot.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
const fs = require("fs");
const { execSync } = require("child_process");
const axios = require("axios");
const { Octokit } = require("@octokit/rest");
const { retry } = require("@octokit/plugin-retry");
const { throttling } = require("@octokit/plugin-throttling");
const config = require("./config.json");
const pollInterval = config.pollInterval * 1000;
const ourRepo = "Fluffy-Frontier/FluffySTG".split("/");
const ourRepoOwner = ourRepo[0];
const ourRepoName = ourRepo[1];
const upstreamPath = "https://github.com/NovaSector/NovaSector.git";
const upstreamRepo = "NovaSector/NovaSector".split("/");
const upstreamRepoOwner = upstreamRepo[0];
const upstreamRepoName = upstreamRepo[1];
//only used to get original PR author for changelogs
const TGRepo = "tgstation/tgstation".split("/");
const repoPath = config.repoPath;
const MyOctokit = Octokit.plugin(retry, throttling);
const githubClient = new MyOctokit({
auth: config.key,
userAgent: "FFMirrorBot",
throttle: {
onRateLimit: (retryAfter, options) => {
if (options.request.retryCount <= 10) {
console.info(`Primary quota reached. Retrying after ${retryAfter} seconds!`);
return true;
}
console.warn(`Request ${options.method} ${options.url} failed after 5 retries.`);
screamOutLoud(`Request ${options.method} ${options.url} failed after 5 retries.`);
},
onSecondaryRateLimit: (retryAfter, options) => {
if (options.request.retryCount <= 10) {
console.info(`Secondary quota reached. Retrying after ${retryAfter} seconds!`);
return true;
}
console.warn(`Request ${options.method} ${options.url} failed after 5 retries.`);
screamOutLoud(`Request ${options.method} ${options.url} failed after 5 retries.`);
},
},
});
async function getCommitsToPoint(sha) {
let commits = [];
let iterator;
iterator = githubClient.paginate.iterator(githubClient.rest.repos.listCommits,{
owner: upstreamRepoOwner,
repo: upstreamRepoName,
per_page: 100,
});
paginateLoop:
for await (const { data: rawCommits } of iterator) {
for(let rawCommit of rawCommits){
let commitsha = rawCommit.sha;
if (commitsha.startsWith(sha)) break paginateLoop;
let info = rawCommit.commit.message;
if(info.includes("Automatic changelog")) continue;
let commit = new Commit(commitsha, info);
if (!commit.PR) {screamOutLoud("Commit: " + commit.info + "\ndoesn't have attached PR to mirror."); continue;}
await commit.PR.resolvePR()
.catch((error) => {
screamOutLoud("Commit: " + commit.info + "\ndoesn't have attached PR to mirror.")
})
.then(()=>{
commits.unshift(commit);
})
}
}
if (commits.length === 0) {return {commits: commits, lastSHA: sha}}
let lastSHA = commits[commits.length - 1].SHA
return {commits: commits, lastSHA: lastSHA}
}
async function getPRdata(id, repo) {
let PR = await githubClient.rest.pulls.get({
owner: repo[0],
repo: repo[1],
pull_number: id,
})
return PR.data
}
function mirrorPR(PR){
let labels = [];
let prCreateResponse;
if(PR.configUpdate) labels.push("Configs");
PR.urlTG ? labels.push("TG Mirror") : labels.push("Nova Mirror")
//updates local repo from target remote and cleans it
execSync("git checkout master && git pull --depth 1000 origin master && git fetch --depth 1000 mirror master && git reset --hard origin/master", { cwd: repoPath });
try{
execSync(`git checkout -b upstream-mirror-${PR.id} && git cherry-pick ${PR.mergeCommit.SHA} --allow-empty --keep-redundant-commits`, { cwd: repoPath });
} catch{
try{
execSync("git add -A . && git -c core.editor=true cherry-pick --continue", { cwd: repoPath }); //theres conflicts, proceed regardless. No way to see where's exactly
} catch {
screamOutLoud(`${PR.title} failed to cherry-pick!`); // probably a merge commit instead of squash
execSync(`git checkout master && git branch -D upstream-mirror-${PR.id}`, { cwd: repoPath });
return
}
console.info(`Conflict while merging with ${PR.id}`);
labels.push("Mirroring conflict");
}
let title = PR.title.replace(/"/g, `"'"'"`) // replace double quotes to prevent collision between command and commit name
execSync(`git commit --allow-empty -m "${title}"`, { cwd: repoPath }); // empty commit to remove redundant PR id when merging on github
execSync(`git push origin upstream-mirror-${PR.id}`, { cwd: repoPath });
execSync(`git checkout master && git branch -D upstream-mirror-${PR.id}`, { cwd: repoPath });
githubClient.rest.pulls.create({
owner: ourRepoOwner,
repo: ourRepoName,
head: `upstream-mirror-${PR.id}`,
base: "master",
title: PR.title,
body: PR.info,
}).then((result) => {
prCreateResponse = result.data
if(labels.length > 0){
let mirrorID = prCreateResponse?.number;
githubClient.rest.issues.addLabels({
owner: ourRepoOwner,
repo: ourRepoName,
issue_number: mirrorID,
labels: labels,
}).catch((error) => {
screamOutLoud(`Error while labeling PR #${PR.id}\n` + error.message);
console.log(`Error while labeling PR #${PR.id}\n`, error.message);
})
}
}).catch((error) => {
screamOutLoud(`Error while mirroring PR #${PR.id}\n` + error.message);
console.log(`Error while mirroring PR #${PR.id}\n`, error.message);
})
}
//executes once just to make sure our local repo is properly set
function gitPreCheck(){
try {
execSync(`git remote add mirror ${upstreamPath}`, { cwd: repoPath })}
catch{
console.info("Remote already set or URL is invalid")
}
}
function screamOutLoud(message){
axios.post(config.webhookURL, {
content: `<@${config.pingID}>\n ${message}`,
})
}
gitPreCheck();
screamOutLoud("Я живое");
(function pollLoop() {
setTimeout(() => {
let lastCommit
try{
lastCommit = fs.readFileSync("./lastSha.txt", "utf8");
} catch {
fs.writeFileSync("./lastSha.txt", "add last commit here");
throw Error("lastSHA created. Add last commit hash to it and restart.")
}
getCommitsToPoint(lastCommit)
.catch((error) => console.log(`${error?.message}`))
.then((result) => {
if(result.commits){
for(let commit of result.commits){
let PR = commit.PR;
console.log(`Mirroring #${PR.id}:"${PR.title}" with its commit sha ${commit.SHA}`);
mirrorPR(PR);
fs.writeFileSync("./lastSha.txt", commit.SHA);
}
//if (lastSHA != result.lastSHA) console.log("Failed to mirror all PRs");
}
});
pollLoop();
}, pollInterval);
})();
process.on("uncaughtException", (err, origin) => {
screamOutLoud(`${err}\n\n${origin}`);
setTimeout(() => process.exit(1), 100)
})
class Commit{
constructor(SHA, info){
this.SHA = SHA;
this.info = info;
this.PRid = info.match(/\(#[0-9]+\)/g);
if(this.PRid) {
this.PR = new PullRequest(this);
}
}
}
class PullRequest{
constructor(commit){
this.mergeCommit = commit;
this.id = commit.PRid;
};
async resolvePR(){
let data
let i = 0
do {
let id = this.id[i].replace(/[(|#)]/g, "");
data = await getPRdata(id, upstreamRepo).catch(() => { });//catching 404 error since thats what the purpose of entire do while loop here
i++
} while (!data)
this.id = this.id[i-1].replace(/[(|#)]/g, "");
this.title = data.title;
this.url = data.html_url;
this.info = data.body;
this.creator = data.user.login;
if (this.title.startsWith("[MIRROR]") || this.title.startsWith("[MISSED MIRROR]")){ //true => TG mirror, so we need to get some additional info
let urlLine = this.info.split("\n")[0];
this.urlTG = urlLine.slice(13);
this.idTG = this.urlTG.match(/[0-9]+/);
await getPRdata(this.idTG, TGRepo).then((data) => this.creator = data.user.login);
}
this.compileData();
};
compileData(){
if(this.title.toLowerCase().includes("mirror") && !this.urlTG) console.warn('PR', this.id, 'had "mirror" in its name but missed original url.' );
//try to get author name from :cl: thingy
let clBody = this.info.split(":cl:");
if(clBody[1]){
let authors = clBody[1].split("\n")[0];
if (authors.length > 1) this.creator = ""; // then we dont need to add anything, it's already here
clBody[1] = " " + this.creator + clBody[1];
this.info = clBody.join(":cl:");
}
this.configUpdate = this.info.includes("config: ");
if(this.title.search(/\[MIRROR]/) < 0) this.title = `[MIRROR] ${this.title}`
this.title = this.title.replace(/(\[(MDB IGNORE|NO GBP|MISSED MIRROR)])/g, "");
this.info = (this.urlTG ? `Mirrored on Nova: ${this.url}\n` : `## **Original PR: ${this.url}**\n`) + this.info;
}
}