blob: 9b380e63a9c802ca2ac716c9e86bc84a4f511970 [file] [log] [blame]
/*
* Copyright (C) 2020 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import fs from "fs"
import path from "path"
import RSS from "rss-parser"
import replaceAll from "replaceall"
import storage from "node-persist"
import axios from "axios"
import dotenv from "dotenv"
dotenv.config();
// Change to true when debugging to avoid posting notifications to Slack.
const DEBUG = false;
const contributorsURL = "https://svn.webkit.org/repository/webkit/trunk/Tools/Scripts/webkitpy/common/config/contributors.json";
const commitEndpointURL = "https://git.webkit.org/?p=WebKit-https.git;a=atom";
const defaultInterval = 60 * 1000;
async function sleep(milliseconds)
{
await new Promise(function (resolve) {
setTimeout(resolve, milliseconds);
});
}
class Contributors {
static async create()
{
let response = await axios.get(contributorsURL);
return new Contributors(response.data);
}
constructor(data)
{
this.emails = new Map();
this.entries = Object.values(data);
for (let [fullName, entry] of Object.entries(data)) {
entry.fullName = fullName;
for (let email of entry.emails)
this.emails.set(email, entry);
}
}
queryWithEmail(email)
{
return this.emails.get(email);
}
}
class Commit {
static findAndRemove(change, regExp)
{
let matched = change.match(regExp);
if (matched) {
change = change.replace(regExp, "");
return [matched[1], change];
}
return [null, change];
}
static cleanUpChange(change, contributors)
{
for (let entry of contributors.entries) {
if (!entry.nicks)
continue;
let nameWithNicks = `${entry.fullName} (@${entry.nicks[0]})`;
if (change.includes(entry.fullName)) {
change = replaceAll(entry.fullName, nameWithNicks, change);
for (let email of entry.emails)
change = replaceAll(`<${email}>`, "", change);
} else {
for (let email of entry.emails)
change = replaceAll(` ${email} `, ` ${nameWithNicks} `, change);
}
}
return change;
}
constructor(feedItem, contributors)
{
let originalChange = feedItem.contentSnippet;
let change = Commit.cleanUpChange(originalChange, contributors);
[this.revision, change] = Commit.findAndRemove(change, /^git-svn-id: https:\/\/svn\.webkit\.org\/repository\/webkit\/trunk@(\d+) /im);
[this.patchBy, change] = Commit.findAndRemove(change, /^Patch\s+by\s+(.+?)\s+on(?:\s+\d{4}-\d{2}-\d{2})?\n?/im);
[this.revert, change] = Commit.findAndRemove(change, /(?:rolling out|reverting) (r?\d+(?:(?:,\s*|,?\s*and\s+)?r?\d+)*)\.?\s*/im);
[this.bugzilla, change] = Commit.findAndRemove(change, /https?:\/\/(?:bugs\.webkit\.org\/show_bug\.cgi\?id=|webkit\.org\/b\/)(\d+)/im);
this.email = feedItem.author;
let lines = originalChange.split('\n');
this.title = feedItem.title;
if (lines.length)
this.title = lines[0];
if (this.patchBy)
this.author = this.patchBy;
if (!this.author) {
this.author = this.email;
if (this.email) {
let entry = contributors.queryWithEmail(this.email);
if (entry && entry.nicks && entry.nicks[0])
this.author = `${entry.fullName} (@${entry.nicks[0]})`;
}
}
if (this.revert) {
let matched = change.match(/^\"?(.+?)\"? \(Requested\s+by\s+(.+?)\s+on\s+#webkit\)\./im);
if (matched) {
let reason = matched[0];
let author = matched[1];
// FIXME: Implement more descriptive message when we detect that this commit is revert commit.
}
}
if (!this.revision)
throw new Error(`Canont find revision`);
this.revision = Number.parseInt(this.revision, 10);
this.url = `https://trac.webkit.org/r${this.revision}`;
}
message()
{
let results = [];
results.push(`${this.title}`);
results.push(`${this.url} by ${this.author}`);
if (this.bugzilla)
results.push(`https://webkit.org/b/${this.bugzilla}`);
return results.join('\n');
}
}
class WKR {
constructor(revision)
{
this.revision = revision;
}
async postToSlack(commit)
{
let data = {
"text": commit.message()
};
console.log(data);
if (!DEBUG)
await axios.post(process.env.slackURL, JSON.stringify(data));
await sleep(500);
}
async action(interval)
{
try {
console.log(`${Date.now()}: poll data`);
let contributors = await Contributors.create();
let parser = new RSS;
let response = await parser.parseURL(commitEndpointURL);
let commits = response.items.map((feedItem) => new Commit(feedItem, contributors));
commits.sort((a, b) => a.revision - b.revision);
if (this.revision) {
commits = commits.filter((commit) => commit.revision > this.revision);
for (let commit of commits)
await this.postToSlack(commit);
}
let latestCommit = commits[commits.length - 1];
if (latestCommit) {
this.revision = latestCommit.revision;
await storage.setItem("revision", this.revision);
}
} catch (error) {
console.error(String(error));
}
return defaultInterval;
}
static async create()
{
await storage.init({
dir: 'data',
});
let revision = await storage.getItem("revision");
console.log(`Previous Revision: ${revision}`);
console.log(`Endpoint: ${process.env.slackURL}`);
return new WKR(revision);
}
static async main()
{
let bot = await WKR.create();
let interval = defaultInterval;
for (;;) {
let start = Date.now();
interval = await bot.action(interval);
await sleep(Math.max(interval - (Date.now() - start), 0));
}
}
}
WKR.main();