-
-
- Something went wrong with that request. Please try again.
-
-
-
-
-
-
-
-
-
- You signed in with another tab or window. Reload to refresh your session.
- You signed out in another tab or window. Reload to refresh your session.
-
-
-
- Something went wrong with that request. Please try again.
-
-
-
-
-
-
-
-
-
-
- You signed in with another tab or window. Reload to refresh your session.
- You signed out in another tab or window. Reload to refresh your session.
-
-
-
- Something went wrong with that request. Please try again.
-
-
-
-
-
-
-
-
-
-
- You signed in with another tab or window. Reload to refresh your session.
- You signed out in another tab or window. Reload to refresh your session.
-
// Jest fatals for the following statement (minimal repro case)
-
-
-
-
2
-
//
-
-
-
-
3
-
// exports.something = Symbol;
-
-
-
-
4
-
//
-
-
-
-
5
-
// Until it is fixed, mocking the entire node module makes the
-
-
-
-
6
-
// problem go away.
-
-
-
-
7
-
-
-
-
-
8
-
'use strict';
-
-
-
-
9
-
module.exports=function() {};
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Something went wrong with that request. Please try again.
-
-
-
-
-
-
-
-
-
-
- You signed in with another tab or window. Reload to refresh your session.
- You signed out in another tab or window. Reload to refresh your session.
-
-
-
- Something went wrong with that request. Please try again.
-
-
-
-
-
-
-
-
-
-
- You signed in with another tab or window. Reload to refresh your session.
- You signed out in another tab or window. Reload to refresh your session.
-
-
-
-
diff --git a/cache/https---github.com-facebook-react-native-pull-3229.diff b/cache/https---github.com-facebook-react-native-pull-3229.diff
deleted file mode 100644
index d7d2850..0000000
--- a/cache/https---github.com-facebook-react-native-pull-3229.diff
+++ /dev/null
@@ -1,366 +0,0 @@
-diff --git a/Libraries/WebSocket/WebSocket.ios.js b/Libraries/WebSocket/WebSocket.ios.js
-index c2318b4..c09b931 100644
---- a/Libraries/WebSocket/WebSocket.ios.js
-+++ b/Libraries/WebSocket/WebSocket.ios.js
-@@ -16,16 +16,12 @@ var RCTWebSocketManager = require('NativeModules').WebSocketManager;
-
- var WebSocketBase = require('WebSocketBase');
-
--class Event {
-- constructor(type) {
-- this.type = type.toString();
-- }
--}
--
--class MessageEvent extends Event {
-+class WebsocketEvent {
- constructor(type, eventInitDict) {
-- super(type);
-- Object.assign(this, eventInitDict);
-+ this.type = type.toString();
-+ if (typeof eventInitDict === 'object') {
-+ Object.assign(this, eventInitDict);
-+ }
- }
- }
-
-@@ -67,56 +63,60 @@ class WebSocket extends WebSocketBase {
- this._subs = [
- RCTDeviceEventEmitter.addListener(
- 'websocketMessage',
-- function(ev) {
-+ ev => {
- if (ev.id !== id) {
- return;
- }
-- var event = new MessageEvent('message', {
-- data: ev.data
-+ var event = new WebsocketEvent('message', {
-+ data: ev.data,
- });
-- this.onmessage && this.onmessage(event);
- this.dispatchEvent(event);
-- }.bind(this)
-+ },
- ),
- RCTDeviceEventEmitter.addListener(
- 'websocketOpen',
-- function(ev) {
-+ ev => {
- if (ev.id !== id) {
- return;
- }
- this.readyState = this.OPEN;
-- var event = new Event('open');
-- this.onopen && this.onopen(event);
-+ var event = new WebsocketEvent('open');
- this.dispatchEvent(event);
-- }.bind(this)
-+ }
- ),
- RCTDeviceEventEmitter.addListener(
- 'websocketClosed',
-- function(ev) {
-+ (ev) => {
- if (ev.id !== id) {
- return;
- }
- this.readyState = this.CLOSED;
-- var event = new Event('close');
-- this.onclose && this.onclose(event);
-+ var event = new WebsocketEvent('close');
- this.dispatchEvent(event);
-+
- this._unregisterEvents();
- RCTWebSocketManager.close(id);
-- }.bind(this)
-+ }
- ),
- RCTDeviceEventEmitter.addListener(
- 'websocketFailed',
-- function(ev) {
-+ (ev) => {
- if (ev.id !== id) {
- return;
- }
-- var event = new Event('error');
-- event.message = ev.message;
-- this.onerror && this.onerror(event);
-- this.dispatchEvent(event);
-+ this.readyState = this.CLOSED;
-+
-+ var closeEvent = new WebsocketEvent('close');
-+ this.dispatchEvent(closeEvent);
-+
-+ var errorEvent = new WebsocketEvent('error', {
-+ message: ev.message,
-+ });
-+ this.dispatchEvent(errorEvent);
-+
- this._unregisterEvents();
- RCTWebSocketManager.close(id);
-- }.bind(this)
-+ }
- )
- ];
- }
-diff --git a/Libraries/WebSocket/WebSocketBase.js b/Libraries/WebSocket/WebSocketBase.js
-index a4cca41..f6cff0e 100644
---- a/Libraries/WebSocket/WebSocketBase.js
-+++ b/Libraries/WebSocket/WebSocketBase.js
-@@ -16,17 +16,12 @@ var EventTarget = require('event-target-shim');
- /**
- * Shared base for platform-specific WebSocket implementations.
- */
--class WebSocketBase extends EventTarget {
-+class WebSocketBase extends EventTarget('close', 'error', 'message', 'open') {
- CONNECTING: number;
- OPEN: number;
- CLOSING: number;
- CLOSED: number;
-
-- onclose: ?Function;
-- onerror: ?Function;
-- onmessage: ?Function;
-- onopen: ?Function;
--
- binaryType: ?string;
- bufferedAmount: number;
- extension: ?string;
-@@ -41,6 +36,8 @@ class WebSocketBase extends EventTarget {
- this.CLOSING = 2;
- this.CLOSED = 3;
-
-+ this.readyState = this.CONNECTING;
-+
- if (!protocols) {
- protocols = [];
- }
-diff --git a/Libraries/WebSocket/__mocks__/event-target-shim.js b/Libraries/WebSocket/__mocks__/event-target-shim.js
-deleted file mode 100644
-index 3b566fc..0000000
---- a/Libraries/WebSocket/__mocks__/event-target-shim.js
-+++ /dev/null
-@@ -1,9 +0,0 @@
--// Jest fatals for the following statement (minimal repro case)
--//
--// exports.something = Symbol;
--//
--// Until it is fixed, mocking the entire node module makes the
--// problem go away.
--
--'use strict';
--module.exports = function() {};
-diff --git a/Libraries/WebSocket/__tests__/Websocket-test.js b/Libraries/WebSocket/__tests__/Websocket-test.js
-new file mode 100644
-index 0000000..d062ed9
---- /dev/null
-+++ b/Libraries/WebSocket/__tests__/Websocket-test.js
-@@ -0,0 +1,207 @@
-+/**
-+ * Copyright (c) 2015-present, Facebook, Inc.
-+ * All rights reserved.
-+ *
-+ * This source code is licensed under the BSD-style license found in the
-+ * LICENSE file in the root directory of this source tree. An additional grant
-+ * of patent rights can be found in the PATENTS file in the same directory.
-+ */
-+'use strict';
-+
-+jest
-+ .autoMockOff()
-+ .mock('ErrorUtils')
-+ .setMock('NativeModules', {
-+ WebSocketManager: {
-+ connect: jest.genMockFunction(),
-+ close: jest.genMockFunction(),
-+ send: jest.genMockFunction(),
-+ },
-+ });
-+
-+var readyStates = {
-+ CONNECTING: 0,
-+ OPEN: 1,
-+ CLOSING: 2,
-+ CLOSED: 3,
-+};
-+
-+var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
-+var RCTWebSocketManager = require('NativeModules').WebSocketManager;
-+
-+var WebSocket = require('WebSocket');
-+
-+// Small utils to keep it DRY
-+var simulateEvent = (ws, event, data = {}) => {
-+ RCTDeviceEventEmitter.emit(event, {id: ws._socketId, ...data});
-+};
-+var expectOneCall = fn => expect(fn.mock.calls.length).toBe(1);
-+var getSocket = () => new WebSocket('ws://echo.websocket.org');
-+
-+describe('WebSockets', () => {
-+ beforeEach(() => {
-+ // Reset RCTWebSocketManager calls
-+ Object.assign(RCTWebSocketManager, {
-+ connect: jest.genMockFunction(),
-+ close: jest.genMockFunction(),
-+ send: jest.genMockFunction(),
-+ });
-+ });
-+
-+ it('should have readyState CONNECTING initialy', () => {
-+ var ws = getSocket();
-+ expect(ws.readyState).toBe(readyStates.CONNECTING);
-+ });
-+
-+ // Open event
-+ it('Should call native connect when connecting', () => {
-+ var ws = getSocket();
-+ ws.onopen = jest.genMockFunction();
-+
-+ expectOneCall(RCTWebSocketManager.connect);
-+ });
-+
-+ it('should have readyState OPEN when connected', () => {
-+ var ws = getSocket();
-+ simulateEvent(ws, 'websocketOpen');
-+ expect(ws.readyState).toBe(readyStates.OPEN);
-+ });
-+
-+ it('should call onopen when connected', () => {
-+ var ws = getSocket();
-+ ws.onopen = jest.genMockFunction();
-+
-+ simulateEvent(ws, 'websocketOpen');
-+
-+ expectOneCall(ws.onopen);
-+ });
-+
-+ it('should trigger listener when connected', () => {
-+ var ws = getSocket();
-+ var listener = jest.genMockFunction();
-+ ws.addEventListener('open', listener);
-+
-+ simulateEvent(ws, 'websocketOpen');
-+
-+ expectOneCall(listener);
-+ });
-+
-+ // Sending message
-+ it('should call native send when sending a message', () => {
-+ var ws = getSocket();
-+ simulateEvent(ws, 'websocketOpen');
-+ var message = 'Hello websocket!';
-+
-+ ws.send(message);
-+
-+ expectOneCall(RCTWebSocketManager.send);
-+ expect(RCTWebSocketManager.send.mock.calls[0])
-+ .toEqual([message, ws._socketId]);
-+ });
-+
-+ it('should call onmessage when receiving a message', () => {
-+ var ws = getSocket();
-+ var message = 'Hello listener!';
-+ ws.onmessage = jest.genMockFunction();
-+
-+ simulateEvent(ws, 'websocketOpen');
-+ simulateEvent(ws, 'websocketMessage', {data: message});
-+
-+ expectOneCall(ws.onmessage);
-+ expect(ws.onmessage.mock.calls[0][0].data).toBe(message);
-+ });
-+
-+ it('should trigger listeners when recieving a message', () => {
-+ var ws = getSocket();
-+ var message = 'Hello listener!';
-+ var listener = jest.genMockFunction();
-+ ws.addEventListener('message', listener);
-+
-+ simulateEvent(ws, 'websocketOpen');
-+ simulateEvent(ws, 'websocketMessage', {data: message});
-+
-+ expectOneCall(listener);
-+ expect(listener.mock.calls[0][0].data).toBe(message);
-+ });
-+
-+ // Close event
-+ it('should have readyState CLOSED when closed', () => {
-+ var ws = getSocket();
-+
-+ simulateEvent(ws, 'websocketOpen');
-+ simulateEvent(ws, 'websocketClosed');
-+
-+ expect(ws.readyState).toBe(readyStates.CLOSED);
-+ });
-+
-+ it('should call onclose when closed', () => {
-+ var ws = getSocket();
-+ ws.onclose = jest.genMockFunction();
-+
-+ simulateEvent(ws, 'websocketOpen');
-+ simulateEvent(ws, 'websocketClosed');
-+
-+ expectOneCall(ws.onclose);
-+ });
-+
-+ it('should trigger listeners when closed', () => {
-+ var ws = getSocket();
-+ var listener = jest.genMockFunction();
-+ ws.addEventListener('close', listener);
-+
-+ simulateEvent(ws, 'websocketOpen');
-+ simulateEvent(ws, 'websocketClosed');
-+
-+ expectOneCall(listener);
-+ });
-+
-+ it('should call native close when closed', () => {
-+ var ws = getSocket();
-+
-+ simulateEvent(ws, 'websocketOpen');
-+ simulateEvent(ws, 'websocketClosed');
-+
-+ expectOneCall(RCTWebSocketManager.close);
-+ expect(RCTWebSocketManager.close.mock.calls[0]).toEqual([ws._socketId]);
-+ });
-+
-+ // Fail event
-+ it('should have readyState CLOSED when failed', () => {
-+ var ws = getSocket();
-+ simulateEvent(ws, 'websocketFailed');
-+ expect(ws.readyState).toBe(readyStates.CLOSED);
-+ });
-+
-+ it('should call native close when failed', () => {
-+ var ws = getSocket();
-+
-+ simulateEvent(ws, 'websocketFailed');
-+
-+ expectOneCall(RCTWebSocketManager.close);
-+ expect(RCTWebSocketManager.close.mock.calls[0]).toEqual([ws._socketId]);
-+ });
-+
-+ it('should call onerror and onclose when failed', () => {
-+ var ws = getSocket();
-+ ws.onclose = jest.genMockFunction();
-+ ws.onerror = jest.genMockFunction();
-+
-+ simulateEvent(ws, 'websocketFailed');
-+
-+ expectOneCall(ws.onclose);
-+ expectOneCall(ws.onerror);
-+ });
-+
-+ it('should call onerror and onclose when failed', () => {
-+ var ws = getSocket();
-+ var onclose = jest.genMockFunction();
-+ var onerror = jest.genMockFunction();
-+ ws.addEventListener('close', onclose);
-+ ws.addEventListener('error', onerror);
-+
-+ simulateEvent(ws, 'websocketFailed');
-+
-+ expectOneCall(onclose);
-+ expectOneCall(onerror);
-+ });
-+});
diff --git a/cache/https---github.com-facebook-react-native-pull-3238.diff b/cache/https---github.com-facebook-react-native-pull-3238.diff
deleted file mode 100644
index 8b0507c..0000000
--- a/cache/https---github.com-facebook-react-native-pull-3238.diff
+++ /dev/null
@@ -1,99 +0,0 @@
-diff --git a/Libraries/ReactIOS/NativeMethodsMixin.js b/Libraries/ReactIOS/NativeMethodsMixin.js
-index afc79f7..59de859 100644
---- a/Libraries/ReactIOS/NativeMethodsMixin.js
-+++ b/Libraries/ReactIOS/NativeMethodsMixin.js
-@@ -37,8 +37,38 @@ type MeasureLayoutOnSuccessCallback = (
- height: number
- ) => void
-
-+/**
-+ * NativeMethodsMixin provides methods that bypass the React rendering process
-+ * and are passed through directly to the underlying native component. This
-+ * allows side effects to be performed and actions to be taken that don't fit
-+ * into the React model directly.
-+ *
-+ * The methods described here are available on most of the default components
-+ * provided by React Native, and components that extend them. Note however that
-+ * they are *not* available on composite components that aren't directly backed
-+ * by a native view. This will generally include most components that you define
-+ * in your own app. For more information, see [Direct
-+ * Manipulation](/react-native/docs/direct-manipulation.html).
-+ */
-
- var NativeMethodsMixin = {
-+ /**
-+ * Determines the location on screen, width, and height of the given view and
-+ * returns the values via an async callback. If successful, the callback will
-+ * be called with the following arguments:
-+ *
-+ * - x
-+ * - y
-+ * - width
-+ * - height
-+ * - pageX
-+ * - pageY
-+ *
-+ * Note that these measurements are currently not available until after the
-+ * first rendering has been completed. If you need the measurements as soon as
-+ * possible, consider instead using the [`onLayout`
-+ * prop](/react-native/docs/view.html#onlayout).
-+ */
- measure: function(callback: MeasureOnSuccessCallback) {
- RCTUIManager.measure(
- findNodeHandle(this),
-@@ -46,6 +76,11 @@ var NativeMethodsMixin = {
- );
- },
-
-+ /**
-+ * Like [`measure()`](#measure), but measures the view specified by tag
-+ * relative to the given `relativeToNativeNode`. This means that the returned
-+ * x, y are relative to the origin x, y of the ancestor view.
-+ */
- measureLayout: function(
- relativeToNativeNode: number,
- onSuccess: MeasureLayoutOnSuccessCallback,
-@@ -60,9 +95,10 @@ var NativeMethodsMixin = {
- },
-
- /**
-- * This function sends props straight to native. They will not participate
-- * in future diff process, this means that if you do not include them in the
-- * next render, they will remain active.
-+ * This function sends props straight to native. They will not participate in
-+ * future diff process -- this means that if you do not include them in the
-+ * next render, they will remain active (see [Direct
-+ * Manipulation](/react-native/docs/direct-manipulation.html)).
- */
- setNativeProps: function(nativeProps: Object) {
- // nativeProps contains a style attribute that's going to be flattened
-@@ -114,10 +150,17 @@ var NativeMethodsMixin = {
- );
- },
-
-+ /**
-+ * Requests focus for the given input or view. The exact behavior triggered
-+ * will depend on the platform and type of view.
-+ */
- focus: function() {
- TextInputState.focusTextInput(findNodeHandle(this));
- },
-
-+ /**
-+ * Blurs the given input or view. This is the opposite of `focus()`.
-+ */
- blur: function() {
- TextInputState.blurTextInput(findNodeHandle(this));
- }
-diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js
-index 867fdfa..7ce11d5 100644
---- a/website/server/extractDocs.js
-+++ b/website/server/extractDocs.js
-@@ -228,6 +228,7 @@ var apis = [
- '../Libraries/Interaction/InteractionManager.js',
- '../Libraries/LayoutAnimation/LayoutAnimation.js',
- '../Libraries/LinkingIOS/LinkingIOS.js',
-+ '../Libraries/ReactIOS/NativeMethodsMixin.js',
- '../Libraries/Network/NetInfo.js',
- '../Libraries/vendor/react/browser/eventPlugins/PanResponder.js',
- '../Libraries/Utilities/PixelRatio.js',
diff --git a/cache/https---github.com-fbsamples-bot-testing-blame-master-hello-world.js b/cache/https---github.com-fbsamples-bot-testing-blame-master-hello-world.js
deleted file mode 100644
index 59f0bef..0000000
--- a/cache/https---github.com-fbsamples-bot-testing-blame-master-hello-world.js
+++ /dev/null
@@ -1 +0,0 @@
-{"error":"Not Found"}
\ No newline at end of file
diff --git a/cache/https---github.com-fbsamples-bot-testing-pull-95.diff b/cache/https---github.com-fbsamples-bot-testing-pull-95.diff
deleted file mode 100644
index d787882..0000000
--- a/cache/https---github.com-fbsamples-bot-testing-pull-95.diff
+++ /dev/null
@@ -1,7 +0,0 @@
-diff --git a/hello-world.js b/hello-world.js
-new file mode 100644
-index 0000000..a420803
---- /dev/null
-+++ b/hello-world.js
-@@ -0,0 +1 @@
-+console.log('Hello World!');
diff --git a/cookieJar.js b/cookieJar.js
deleted file mode 100644
index 691fe5b..0000000
--- a/cookieJar.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * Copyright (c) 2015-present, Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- *
- * @flow
- */
-
-'use strict';
-
-class CookieJar {
- cookies: { [key: string]: string };
-
- constructor() {
- this.cookies = {};
- }
-
- /**
- * Serializes stored cookies into http header format
- */
- get(): string {
- return Object.keys(this.cookies)
- .map(function(key) {
- let str = key;
- if (this.cookies[key]) {
- str += '=' + this.cookies[key];
- }
- return str;
- }, this)
- .join('; ');
- }
-
- /**
- * Parses a single header line of cookie, parses them, and stores them in a hash
- */
- parseCookieHeader(headerLine: string) {
- headerLine.split(';')
- .forEach(function(cookie) {
- var pair = cookie.split('=');
- this.cookies[pair[0]] = pair[1];
- }, this);
- }
-
- /**
- * Finds Set-Cookie header in resp headers and adds them to the jar
- * Returns all stored cookies where the new cookies overwrite previous cookies
- */
-
- parseHeaders(headers: string) {
- if (!headers) {
- return;
- }
- headers.split('\n')
- .filter(function(line) {
- return line.split('Set-Cookie').length > 1;
- })
- .forEach(function(line) {
- this.parseCookieHeader(line.split('Set-Cookie: ')[1].trim());
- }, this);
- return this.get();
- }
-}
-
-module.exports = CookieJar;
diff --git a/githubAuthCookies.js b/githubAuthCookies.js
deleted file mode 100644
index 03e262f..0000000
--- a/githubAuthCookies.js
+++ /dev/null
@@ -1,95 +0,0 @@
-/**
- * Copyright (c) 2015-present, Facebook, Inc.
- * All rights reserved.
- *
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- *
- * @flow
- */
-
-'use strict';
-var USERNAME = process.env.GITHUB_USER;
-var PASSWORD = process.env.GITHUB_PASSWORD;
-var config = require('./config');
-var childProcess = require('child_process');
-var CookieJar = require('./cookieJar');
-var jar = new CookieJar();
-
-var ghHost = config.github.host
-var ghProtocol = config.github.protocol
-
-/**
- * Scrape github login page
- */
-var githubLogin = function() {
- var output = childProcess.execSync(
- `curl -v -L ${ghProtocol}://${ghHost}/login 2>&1`,
- {encoding: 'utf8'}
- ).toString().split('');
-
- return {
- headers: output[0],
- body: output[1]
- };
-};
-
-
-/**
- * gets a CSRF token by hitting github's login form
- * returns CSRF token
- */
-var getAuthenticityToken = function() {
- var login = githubLogin();
- jar.parseHeaders(login.headers);
- return encodeURIComponent(
- (login.body.match(/name="authenticity_token".*value="([^"]+)"/) || [''])[1]
- );
-};
-
-/**
- * runs curl request to perform login action
- * returns github response headers
- */
-var getGithubLoginResponseHeaders = function(): string {
- var authenticity_token = getAuthenticityToken();
- var commandArr = [
- `${ghProtocol}://${ghHost}/session`,
- `--silent`,
- `-v`,
- `-d`, `utf8=%E2%9C%93&authenticity_token=${authenticity_token}`,
- `-d`, `login=${USERNAME}`,
- `--data-urlencode`, `password=${PASSWORD}`,
- `-H`, `Pragma: no-cache`,
- `-H`, `Origin: ${ghProtocol}://${ghHost}`,
- `-H`, `Accept-Encoding: gzip, deflate`,
- `-H`, `Accept-Language: en-US,en;q=0.8`,
- `-H`, `Upgrade-Insecure-Requests: 1`,
- `-H`, `User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36`,
- `-H`, `Content-Type: application/x-www-form-urlencoded`,
- `-H`, `Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8`,
- `-H`, `Cache-Control: no-cache`,
- `-H`, `Referer: ${ghProtocol}://${ghHost}/`,
- `-H`, `Cookie: ${jar.get()}`,
- `-H`, `Connection: keep-alive`,
- ];
-
- return childProcess.spawnSync(
- 'curl',
- commandArr,
- {encoding: 'utf8'}
- ).stderr.toString();
-};
-
-if (USERNAME) {
- var headers = getGithubLoginResponseHeaders();
- jar.parseHeaders(headers);
- if (jar.cookies['logged_in'] === 'no') {
- console.error(`Login to ${USERNAME} failed`);
- }
-
- module.exports = jar.get();
-} else {
- module.exports = null;
-}
diff --git a/mention-bot.js b/mention-bot.js
index fd5bae4..7454809 100644
--- a/mention-bot.js
+++ b/mention-bot.js
@@ -11,234 +11,13 @@
'use strict';
-var githubAuthCookies = require('./githubAuthCookies');
-var fs = require('fs');
var minimatch = require('minimatch');
-async function downloadFileAsync(url: string, cookies: ?string): Promise {
- return new Promise(function(resolve, reject) {
- var args = ['--silent', '-L', url];
-
- if (cookies) {
- args.push('-H', `Cookie: ${cookies}`);
- }
-
- require('child_process')
- .execFile('curl', args, {encoding: 'utf8', maxBuffer: 1000 * 1024 * 10}, function(error, stdout, stderr) {
- if (error) {
- reject(error);
- } else {
- resolve(stdout.toString());
- }
- });
- });
-}
-
-async function readFileAsync(name: string, encoding: string): Promise {
- return new Promise(function(resolve, reject) {
- fs.readFile(name, encoding, function(err, data) {
- if (err) {
- reject(err);
- } else {
- resolve(data);
- }
- });
- });
-}
-
-type FileInfo = {
- path: string,
- deletedLines: Array,
-};
-
type WhitelistUser = {
name: string,
files: Array
};
-function startsWith(str, start) {
- return str.substr(0, start.length) === start;
-}
-
-function parseDiffFile(lines: Array): FileInfo {
- var deletedLines = [];
-
- // diff --git a/path b/path
- var line = lines.pop();
- if (!line.match(/^diff --git a\//)) {
- throw new Error('Invalid line, should start with `diff --git a/`, instead got \n' + line + '\n');
- }
- var fromFile = line.replace(/^diff --git a\/(.+) b\/.+/g, '$1');
-
- // index sha..sha mode
- line = lines.pop();
- if (startsWith(line, 'deleted file') ||
- startsWith(line, 'new file')) {
- line = lines.pop();
- }
-
- line = lines.pop();
- if (!line) {
- // If the diff ends in an empty file with 0 additions or deletions, line will be null
- } else if (startsWith(line, 'diff --git')) {
- lines.push(line);
- } else if (startsWith(line, 'Binary files')) {
- // We just ignore binary files (mostly images). If we want to improve the
- // precision in the future, we could look at the history of those files
- // to get more names.
- } else if (startsWith(line, '--- ')) {
- // +++ path
- line = lines.pop();
- if (!line.match(/^\+\+\+ /)) {
- throw new Error('Invalid line, should start with `+++`, instead got \n' + line + '\n');
- }
-
- var currentFromLine = 0;
- while (lines.length > 0) {
- line = lines.pop();
- if (startsWith(line, 'diff --git')) {
- lines.push(line);
- break;
- }
-
- // @@ -from_line,from_count +to_line,to_count @@ first line
- if (startsWith(line, '@@')) {
- var matches = line.match(/^\@\@ -([0-9]+),?([0-9]+)? \+([0-9]+),?([0-9]+)? \@\@/);
- if (!matches) {
- continue;
- }
-
- var from_line = matches[1];
- var from_count = matches[2];
- var to_line = matches[3];
- var to_count = matches[4];
-
- currentFromLine = +from_line;
- continue;
- }
-
- if (startsWith(line, '-')) {
- deletedLines.push(currentFromLine);
- }
- if (!startsWith(line, '+')) {
- currentFromLine++;
- }
- }
- }
-
- return {
- path: fromFile,
- deletedLines: deletedLines,
- };
-}
-
-function parseDiff(diff: string): Array {
- var files = [];
- // The algorithm is designed to be best effort. If the http request failed
- // for some reason and we get an empty file, we should not crash.
- if (!diff || !diff.match(/^diff/)) {
- return files;
- }
-
- var lines = diff.trim().split('\n');
- // Hack Array doesn't have shift/unshift to work from the beginning of the
- // array, so we reverse the entire array in order to be able to use pop/add.
- lines.reverse();
-
- while (lines.length > 0) {
- files.push(parseDiffFile(lines));
- }
-
- return files;
-}
-
-/**
- * Sadly, github doesn't have an API to get line by line blame for a file.
- * We could git clone the repo and blame, but it's annoying to have to
- * maintain a local git repo and the blame is going to be name + email instead
- * of the github username, so we'll have to do a http request anyway.
- *
- * There are two main ways to extract information from an HTML file:
- * - First is to work like a browser: parse the html, build a DOM tree and
- * use a jQuery-like API to traverse the DOM and extract what you need.
- * The big issue is that creating the DOM is --extremely-- slow.
- * - Second is to use regex to analyze the outputted html and find whatever
- * we want.
- *
- * I(vjeux)'ve scraped hundreds of websites using both techniques and both of
- * them have the same reliability when it comes to the website updating their
- * markup. If they change what you are extracting you are screwed and if they
- * change something around, both are resistant to it when written properly.
- * So, might as well use the fastest one of the two: regex :)
- */
-function parseBlame(blame: string): Array {
- // The way the document is structured is that commits and lines are
- // interleaved. So every time we see a commit we grab the author's name
- // and every time we see a line we log the last seen author.
- var re = /(rel="(?:author|contributor)">([^<]+)<\/a> authored|
)/g;
-
- var currentAuthor = 'none';
- var lines = [];
- var match;
- while (match = re.exec(blame)) {
- if (match[2]) {
- currentAuthor = match[2];
- } else {
- lines.push(currentAuthor);
- }
- }
-
- return lines;
-}
-
-function getDeletedOwners(
- files: Array,
- blames: { [key: string]: Array }
-): { [key: string]: number } {
- var owners = {};
- files.forEach(function(file) {
- var blame = blames[file['path']];
- if (!blame) {
- return;
- }
- file.deletedLines.forEach(function (line) {
- // In a perfect world, this should never fail. However, in practice, the
- // blame request may fail, the blame is checking against master and the
- // pull request isn't, the blame file was too big and the curl wrapper
- // only read the first n bytes...
- // Since the output of the algorithm is best effort, it's better to just
- // swallow errors and have a less accurate implementation than to crash.
- var name = blame[line - 1];
- if (!name) {
- return;
- }
- owners[name] = (owners[name] || 0) + 1;
- });
- });
- return owners;
-}
-
-function getAllOwners(
- files: Array,
- blames: { [key: string]: Array }
-): { [key: string]: number } {
- var owners = {};
- files.forEach(function(file) {
- var blame = blames[file.path];
- if (!blame) {
- return;
- }
- for (var i = 0; i < blame.length; ++i) {
- var name = blame[i];
- if (!name) {
- return;
- }
- owners[name] = (owners[name] || 0) + 1;
- }
- });
- return owners;
-}
-
function getSortedOwners(
owners: { [key: string]: number }
): Array {
@@ -252,7 +31,7 @@ function getSortedOwners(
}
function getMatchingOwners(
- files: Array,
+ files: Array,
whitelist: Array
): Array {
var owners = [];
@@ -264,7 +43,7 @@ function getMatchingOwners(
user.files.forEach(function(pattern) {
if (!userHasChangedFile) {
userHasChangedFile = files.find(function(file) {
- return minimatch(file.path, pattern);
+ return minimatch(file, pattern);
});
}
});
@@ -277,28 +56,55 @@ function getMatchingOwners(
return owners;
}
-/**
- * While developing/debugging the algorithm itself, it's very important not to
- * make http requests to github. Not only it's going to make the reload cycle
- * much slower, it's also going to temporary/permanently ban your ip and
- * you won't be able to get anymore work done when it happens :(
- */
-async function fetch(url: string): Promise {
- if (!module.exports.enableCachingForDebugging) {
- return downloadFileAsync(url, githubAuthCookies);
- }
-
- var cacheDir = __dirname + '/cache/';
+async function getFilesPath(
+ user: string,
+ repo: string,
+ number: number,
+ github: Object
+): Promise> {
+ return new Promise(function(resolve, reject) {
+ github.pullRequests.getFiles({
+ user: user, repo: repo, number: number
+ }, function(err, result) {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(result.map(function(file) {
+ return file.filename;
+ }));
+ }
+ });
+ });
+}
- if (!fs.existsSync(cacheDir)) {
- fs.mkdir(cacheDir);
- }
- var cache_key = cacheDir + url.replace(/[^a-zA-Z0-9-_\.]/g, '-');
- if (!fs.existsSync(cache_key)) {
- var file = await downloadFileAsync(url, githubAuthCookies);
- fs.writeFileSync(cache_key, file);
- }
- return readFileAsync(cache_key, 'utf8');
+async function getCommitsAuthors(
+ user: string,
+ repo: string,
+ path: string,
+ github: Object
+): Promise> {
+ return new Promise(function(resolve, reject) {
+ github.repos.getCommits({
+ user: user, repo: repo, path: path
+ }, function (err, result) {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(result.reduce(function(acc, commit) {
+ if (!commit.author) {
+ // Safeguard against missing authors
+ return acc;
+ }
+ if (acc.has(commit.author.login)) {
+ acc.set(commit.author.login, acc.get(commit.author.login) + 1);
+ } else {
+ acc.set(commit.author.login, 1);
+ }
+ return acc;
+ }, new Map()));
+ }
+ });
+ });
}
async function getOwnerOrgs(
@@ -418,8 +224,7 @@ async function filterPrivateRepo(
* them, concat them and finally take the first 3 names.
*/
async function guessOwners(
- files: Array,
- blames: { [key: string]: Array },
+ owners: { [key: string]: number },
creator: string,
defaultOwners: Array,
fallbackOwners: Array,
@@ -428,25 +233,9 @@ async function guessOwners(
config: Object,
github: Object
): Promise> {
- var deletedOwners = getDeletedOwners(files, blames);
- var allOwners = getAllOwners(files, blames);
-
- deletedOwners = getSortedOwners(deletedOwners);
- allOwners = getSortedOwners(allOwners);
-
- // Remove owners that are also in deletedOwners
- var deletedOwnersSet = new Set(deletedOwners);
- var allOwners = allOwners.filter(function(element) {
- return !deletedOwnersSet.has(element);
- });
-
- var owners = []
- .concat(deletedOwners)
- .concat(allOwners)
- .filter(function(owner) {
- return owner !== 'none';
- })
- .filter(function(owner) {
+ var guess = Array.from(owners.keys()).sort(function(a, b) {
+ return owners.get(b) - owners.get(a);
+ }).filter(function(owner) {
return owner !== creator;
})
.filter(function(owner) {
@@ -454,18 +243,18 @@ async function guessOwners(
});
if (config.requiredOrgs.length > 0) {
- owners = await filterRequiredOrgs(owners, config, github);
+ guess = await filterRequiredOrgs(guess, config, github);
}
if (privateRepo && org != null) {
- owners = await filterPrivateRepo(owners, org, github);
+ guess = await filterPrivateRepo(guess, org, github);
}
- if (owners.length === 0) {
+ if (guess.length === 0) {
defaultOwners = defaultOwners.concat(fallbackOwners);
}
- return owners
+ return guess
.slice(0, config.maxReviewers)
.concat(defaultOwners)
.filter(function(owner, index, ownersFound) {
@@ -474,7 +263,8 @@ async function guessOwners(
}
async function guessOwnersForPullRequest(
- repoURL: string,
+ repo: string,
+ user: string,
id: number,
creator: string,
targetBranch: string,
@@ -483,52 +273,44 @@ async function guessOwnersForPullRequest(
config: Object,
github: Object
): Promise> {
- var diff = await fetch(repoURL + '/pull/' + id + '.diff');
- var files = parseDiff(diff);
+ var files = await getFilesPath(user, repo, id, github);
var defaultOwners = getMatchingOwners(files, config.alwaysNotifyForPaths);
var fallbackOwners = getMatchingOwners(files, config.fallbackNotifyForPaths);
if (!config.findPotentialReviewers) {
return defaultOwners;
}
- // There are going to be degenerated changes that end up modifying hundreds
- // of files. In theory, it would be good to actually run the algorithm on
- // all of them to get the best set of reviewers. In practice, we don't
- // want to do hundreds of http requests. Using the top 5 files is enough
- // to get us 3 people that may have context.
- files.sort(function(a, b) {
- var countA = a.deletedLines.length;
- var countB = b.deletedLines.length;
- return countA > countB ? -1 : (countA < countB ? 1 : 0);
- });
// remove files that match any of the globs in the file blacklist config
config.fileBlacklist.forEach(function(glob) {
files = files.filter(function(file) {
- return !minimatch(file.path, glob);
+ return !minimatch(file, glob);
});
});
files = files.slice(0, config.numFilesToCheck);
- var blames = {};
- // create blame promises (allows concurrent loading)
- var promises = files.map(function(file) {
- return fetch(repoURL + '/blame/' + targetBranch + '/' + file.path);
+ // create authors promises (allows concurrent loading)
+ let promises = files.map(function(file) {
+ return getCommitsAuthors(user, repo, file, github);
});
// wait for all promises to resolve
- var results = await Promise.all(promises);
- results.forEach(function(result, index) {
- blames[files[index].path] = parseBlame(result);
- });
+ let results = await Promise.all(promises);
+ let owners = results.reduce(function(acc, owners) {
+ for (let [owner, commits] of owners) {
+ if (acc.has(owner)) {
+ acc.set(owner, acc.get(owner) + commits);
+ } else {
+ acc.set(owner, commits);
+ }
+ }
+ return acc;
+ }, new Map());
// This is the line that implements the actual algorithm, all the lines
// before are there to fetch and extract the data needed.
- return guessOwners(files, blames, creator, defaultOwners, fallbackOwners, privateRepo, org, config, github);
+ return guessOwners(owners, creator, defaultOwners, fallbackOwners, privateRepo, org, config, github);
}
module.exports = {
- enableCachingForDebugging: false,
- parseDiff: parseDiff,
- parseBlame: parseBlame,
guessOwnersForPullRequest: guessOwnersForPullRequest,
};
diff --git a/package.json b/package.json
index 23a1421..78a371d 100644
--- a/package.json
+++ b/package.json
@@ -4,12 +4,12 @@
"license": "BSD-3-Clause",
"repository": "facebook/mention-bot",
"scripts": {
- "test": "flow && jest",
+ "test": "flow & jest",
"start": "node run-server.js"
},
"main": "./run-mention-bot.js",
"engines": {
- "node": ">=4"
+ "node": ">=6"
},
"jest": {
"scriptPreprocessor": "/node_modules/babel-jest",
@@ -27,7 +27,6 @@
"babel-polyfill": "^6.1.4",
"babel-preset-react": "^6.1.2",
"bl": "^1.0.0",
- "download-file-sync": "^1.0.3",
"express": "^4.13.3",
"flow-bin": "^0.18.1",
"github": "^2.1.0",
diff --git a/server.js b/server.js
index 49be8f5..5655230 100644
--- a/server.js
+++ b/server.js
@@ -12,7 +12,6 @@
var bl = require('bl');
var config = require('./config');
var express = require('express');
-var fs = require('fs');
var mentionBot = require('./mention-bot.js');
var messageGenerator = require('./message.js');
var util = require('util');
@@ -36,19 +35,6 @@ if (!process.env.GITHUB_TOKEN) {
process.exit(1);
}
-if (!process.env.GITHUB_USER) {
- console.warn(
- 'There was no GitHub user detected.',
- 'This is fine, but mention-bot won\'t work with private repos.'
- );
- console.warn(
- 'To make mention-bot work with private repos, please expose',
- 'GITHUB_USER and GITHUB_PASSWORD as environment variables.',
- 'The username and password must have access to the private repo',
- 'you want to use.'
- );
-}
-
var github = new GitHubApi({
host: config.github.apiHost,
pathPrefix: config.github.pathPrefix,
@@ -138,7 +124,7 @@ async function work(body) {
withLabel: '',
skipCollaboratorPR: false,
};
-
+
if (process.env.MENTION_BOT_CONFIG) {
try {
repoConfig = {
@@ -247,7 +233,8 @@ async function work(body) {
}
var reviewers = await mentionBot.guessOwnersForPullRequest(
- data.repository.html_url, // 'https://github.com/fbsamples/bot-testing'
+ data.repository.name, // 'bot-testing'
+ data.repository.owner.login, // 'facebook'
data.pull_request.number, // 23
data.pull_request.user.login, // 'mention-bot'
data.pull_request.base.ref, // 'master'