-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathbot.js
427 lines (373 loc) · 14.7 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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
// Add a deer to the console
const deers = require("deers");
console.log(deers()[3]);
require('dotenv').config();
const path = require("path");
// Importing required modules
const { Octokit } = require("@octokit/rest"); // GitHub API client
// Setting up Octokit with authentication
const octokit = new Octokit({
auth: process.env.ACCESS_TOKEN, // Placeholder token, replace with a valid token
});
// Files to ignore
const filesToIgnore = process.env.IGNORE_FILES.split(",").map(file =>
file.toLowerCase()
);
// Repository and folder paths
const translatedOwner = process.env.TRANSLATED_OWNER; // Owner of the translated repository
const translatedRepo = process.env.TRANSLATED_REPO; // Name of the translated repository
const translatedSubdirectory = process.env.TRANSLATED_SUBDIRECTORY; // Subdirectory of the repository to compare
const translatedBranch = process.env.TRANSLATED_BRANCH; // Name of the branch to compare with
const originalOwner = process.env.ORIGINAL_OWNER; // Owner of the original repository
const originalRepo = process.env.ORIGINAL_REPO; // Name of the original repository
const originalSubdirectory = process.env.ORIGINAL_SUBDIRECTORY; // Subdirectory of the repository to compare
const originalBranch = process.env.ORIGINAL_BRANCH; // Name of the branch to compare with
const issueLabel = JSON.parse(process.env.ISSUE_LABEL);
// Function to check the rate limit status
async function checkRateLimit() {
const { data } = await octokit.rateLimit.get();
console.log(
`API Limit:${data.resources.core.remaining}/5000, Used:${5000-data.resources.core.remaining}, Reset Time:${new Date(data.resources.core.reset * 1000)}`,
); // This will log the core limit status
}
// This is the main function that orchestrates the entire process
async function main() {
try {
await checkRateLimit();
const { originalFilesArray, translatedFilesArray } =
await getFilesArray();
const commonFiles = getCommonFiles(
originalFilesArray,
translatedFilesArray
);
console.log(`Found ${commonFiles.length} common markdown files.`);
if (commonFiles.length === 0) {
console.log("No common markdown files found.");
return;
}
// for performance reasons, fetch all open and closed issues with comments in the translated repository at once
await fetchAllIssuesWithComments();
// Process each common file to check if there are new commits in the original repository
await processCommonFiles(commonFiles);
await checkRateLimit();
} catch (error) {
console.error(`-Error: ${error.message}`);
}
}
main().catch(error => console.error(`Unhandled error: ${error.message}`));
// This function fetches the list of files from both the original and translated repositories and creates arrays for them.
async function getFilesArray() {
const originalFilesArray = await getFilesList(
originalOwner,
originalRepo,
originalSubdirectory,
originalBranch
);
const translatedFilesArray = await getFilesList(
translatedOwner,
translatedRepo,
translatedSubdirectory,
translatedBranch
);
return { originalFilesArray, translatedFilesArray };
}
// Function to get the list of files in a repository
async function getFilesList(owner, repo, subdirectory, branch) {
let files = [];
try {
const { data } = await octokit.git.getTree({
owner: owner,
repo: repo,
tree_sha: branch,
recursive: "1",
});
files = data.tree
.filter(item => item.type === "blob")
.map(item => item.path.replace(subdirectory, "")); // Remove the subdirectory from the file path
console.log(
`Found ${files.length} files in the ${owner}/${repo} repository.`
); // Log the number of files found
} catch (error) {
console.error(`Error fetching files: ${error.message}`);
throw error; // Rethrow the error after logging it
}
return files;
}
// This function finds the common files between the original and translated repositories
function getCommonFiles(originalArray, translatedArray) {
const commonFiles = originalArray.filter(
filePath =>
translatedArray.includes(filePath) &&
filePath.endsWith(".md") &&
!filesToIgnore.includes(filePath.toLowerCase())
);
return commonFiles;
}
// This function processes each common file to check if there are new commits in the original repository
async function processCommonFiles(commonFiles) {
for (const filePath of commonFiles) {
const originalFilePath = originalSubdirectory + filePath;
const translatedFilePath = translatedSubdirectory + filePath;
// Get the date of the last commit for the file in the translated repository
const translatedFileLastCommitDate = await getLastCommitDate(
translatedOwner,
translatedRepo,
translatedFilePath
);
// Fetch the list of commits for the file in the original repository
const { data: originalFileCommits } = await octokit.repos.listCommits({
owner: originalOwner,
repo: originalRepo,
path: originalFilePath,
});
// Filter the list of commits to only include those that are newer than the last commit in the translated repository
const commits = originalFileCommits.filter(
commit =>
new Date(commit.commit.committer.date) >
new Date(translatedFileLastCommitDate)
);
// Filter out the commits that are already resolved in a closed issue
let newCommits = [];
for (const commit of commits) {
const isInIssues = await isCommitInIssues(
commit,
filePath
);
if (!isInIssues) {
newCommits.unshift(commit);
}
}
if (newCommits.length > 0) {
console.log(
`-Found ${newCommits.length} new commits for ${filePath}`
);
await createIssue(
originalFilePath,
translatedFilePath,
newCommits,
filePath
);
} else {
console.log(`-No new commits found for ${filePath}`);
}
}
}
// Function to get the date of the last commit for a file
async function getLastCommitDate(owner, repo, path) {
let date;
try {
const { data: commits } = await octokit.repos.listCommits({
owner: owner,
repo: repo,
path: path,
});
if (commits.length > 0) {
date = commits[0].commit.committer.date;
}
} catch (error) {
console.error(
`Error fetching commits for file ${path} in the ${owner}/${repo} repository: ${error.message}`
);
}
return date;
}
// Define a global variable to store the list of issues with comments
let issuesWithComments = [];
// function to fetch all issues (both open and closed) and their comments in the translated repository
async function fetchAllIssuesWithComments() {
const openIssues = await octokit.paginate(octokit.issues.listForRepo, {
owner: translatedOwner,
repo: translatedRepo,
state: "open",
creator: "filgoBot",
per_page: 100, // 100 is the maximum number of items that can be returned per page
});
const closedIssues = await octokit.paginate(octokit.issues.listForRepo, {
owner: translatedOwner,
repo: translatedRepo,
state: "closed",
creator: "filgoBot",
per_page: 100, // 100 is the maximum number of items that can be returned per page
});
const allIssues = openIssues.concat(closedIssues);
issuesWithComments = await Promise.all(
allIssues.map(async issue => {
const comments = await octokit.paginate(
octokit.issues.listComments,
{
owner: translatedOwner,
repo: translatedRepo,
issue_number: issue.number,
}
);
return {
...issue,
comments,
};
})
);
console.log(`Total number of issues:${issuesWithComments.length}`);
}
// Function to check if a commit is already resolved in a closed issue
async function isCommitInIssues(commit, filePath) {
const commitIdentifier = `${commit.sha}:${filePath}`;
// Check if the commit identifier is present in the body or comments of any closed issue
for (const issue of issuesWithComments) {
const texts = [
issue.body,
...issue.comments.map(comment => comment.body),
];
// If the commit identifier is found in the body or comments of the issue, return true
if (texts.some(text => text && text.includes(commitIdentifier))) {
// console.log(`---Commit is already resolved for ${filePath} in closed issue #${issue.number}`);
return true;
}
// If the commit identifier is not found in the body or comments of the issue, return false
}
return false;
}
// Function to create a GitHub issue for a translation update
async function createIssue(
originalFilePath,
translatedFilePath,
newCommits,
filePath
) {
try {
const fileName = path.basename(filePath);
const originalFileUrl = `https://github.com/${originalOwner}/${originalRepo}/blob/${originalBranch}/${originalFilePath}`;
const translatedFileUrl = `https://github.com/${translatedOwner}/${translatedRepo}/blob/${translatedBranch}/${translatedFilePath}`;
const existingIssue = await getExistingIssue(filePath);
if (existingIssue) {
const commitMessages = await getCommitMessages(
newCommits,
originalFilePath,
filePath
);
await addCommentToIssue(existingIssue, commitMessages);
} else {
// if there is no existing issue, create a new one
const commitMessages = await getCommitMessages(
newCommits,
originalFilePath,
filePath
);
await createNewIssue(
fileName,
originalFileUrl,
translatedFileUrl,
commitMessages
);
}
} catch (error) {
console.error(`--❌Error creating issue: ${error.message}`);
}
}
// function to get an existing issue for a file
async function getExistingIssue(filePath) {
// Search in issuesWithComments
let existingIssue = issuesWithComments.find(issue => issue.body && issue.body.includes(filePath));
// If an issue was found and it's closed, reopen it
if (existingIssue && existingIssue.state === 'closed') {
await octokit.issues.update({
owner: translatedOwner,
repo: translatedRepo,
issue_number: existingIssue.number,
state: 'open'
});
// Update the state of the issue in issuesWithComments
existingIssue.state = 'open';
}
return existingIssue || null;
}
// Define options for date formatting
const DATE_OPTIONS = {
month: "short",
day: "numeric",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "UTC",
timeZoneName: "short",
};
// Function to format a date according to the defined options
function formatDate(date) {
return date
.toLocaleString("en-US", DATE_OPTIONS) // Convert the date to a string using the defined options
.replace(",", "") // Remove commas from the date string
.replace(" GMT", ""); // Remove " GMT" from the date string
}
// Function to get the commit messages for new commits
async function getCommitMessages(newCommits, path, filePath) {
try {
const commitDetails = await Promise.all(
newCommits.map(commit =>
octokit.repos.getCommit({
owner: originalOwner,
repo: originalRepo,
ref: commit.sha,
})
)
);
// Create a list of commit messages
return commitDetails
.map(({ data }) => {
const commitUrl = data.html_url;
const formattedDate = formatDate(
new Date(data.commit.committer.date)
);
// Get the first line of the commit message
const firstLineMessage = data.commit.message.split("\n")[0];
// Get the additions and deletions for the file in the commit by passing commit data and path
const diff = getCommitDiff(data, path);
return `- [${firstLineMessage}](${commitUrl}) (additions: ${diff.additions}, deletions: ${diff.deletions}) on ${formattedDate} <!-- commitIdentifier: ${data.sha}:${filePath} -->`;
})
.join("\n");
} catch (error) {
console.error(`Error getting commit messages: ${error.message}`);
}
}
function getCommitDiff(commit, path) {
// Find the file in the commit that matches the path
const file = commit.files.find(file => file.filename === path);
// Return the additions and deletions for the specific file not the entire commit
return {
additions: file.additions,
deletions: file.deletions,
};
}
// Function to add a comment to an existing issue
async function addCommentToIssue(existingIssue, commitMessages) {
try {
await octokit.issues.createComment({
owner: translatedOwner,
repo: translatedRepo,
issue_number: existingIssue.number,
body: `New commits have been made to the Odin's file. Please update the Kampus' file.\n\n Latest commits:\n${commitMessages}`,
});
console.log(`---✅Comment added to the existing issue`);
} catch (error) {
console.error(`---❌Error adding comment to issue: ${error.message}`);
}
}
// Function to create a new issue in the translated repository
async function createNewIssue(
file,
originalFileUrl,
translatedFileUrl,
commitMessages
) {
try {
await octokit.issues.create({
owner: translatedOwner,
repo: translatedRepo,
title: `Curriculum update needed on \`${file}\``,
body: `The Odin's file, [${file}](${originalFileUrl}) is updated. Please update the Kampus' file, checkout file here [${file}](${translatedFileUrl}) \n\n Latest commits:\n${commitMessages}`,
labels: issueLabel,
});
console.log(`--✅Issue created successfully`);
} catch (error) {
console.error(`--❌Error creating new issue: ${error.message}`);
}
}