blob: 8376cfa35fe763464433626291f4f282e7e0932b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { schedule, danger, fail, warn, message, markdown } from "danger";
import fs from "fs";
import path from 'path';
import GitHubApi from 'github';
import parseDiff from 'parse-diff';
// Make sure there are changelog entries
const hasChangelog = danger.git.modified_files.includes("changelog.md")
if (!hasChangelog) { warn("No Changelog changes!") }
const jsFiles = danger.git.created_files.filter(path => path.endsWith("js"));
function absolute (relPath) {
return path.resolve(__dirname, relPath)
}
const flowIgnorePaths = [
'node_modules',
'test',
'build',
'examples',
'android',
'ios',
'bin',
'dist',
'flow-typed'
].map(function (rel) {
return absolute(rel)
});
// new js files should have `@flow` at the top
const unFlowedFiles = jsFiles.filter(filepath => {
let i = 0
const len = flowIgnorePaths.length
while (i < len) {
const p = flowIgnorePaths[i]
if (absolute(filepath).indexOf(p) > -1) {
// ignore this file because it's in the flow-ignore-paths.
return false;
}
i++
}
const content = fs.readFileSync(filepath);
return !content.includes("@flow");
});
if (unFlowedFiles.length > 0) {
warn(
`These new JS files do not have Flow enabled: ${unFlowedFiles.join(", ")}`
);
}
// Error or Warn when delete public interface
var isNotDanger = false;
console.log('pr.title:'+danger.github.pr.title)
if(!isNotDanger && danger.github.pr.title
&& danger.github.pr.title.match(/@notdanger/i)){
isNotDanger = true;
}
console.log('pr.body:'+danger.github.pr.body)
if(!isNotDanger && danger.github.pr.body
&& danger.github.pr.body.match(/@notdanger/i)){
isNotDanger = true;
}
if(!isNotDanger){
for (let c of danger.git.commits) {
// console.log("msg:" + c.message);
if (c.message && c.message.match(/@notdanger/i)) {
isNotDanger = true;
break;
}
}
}
// File name match any of these patterns will be ignored.
function is_ignored_public_check(file) {
var ignored_break_change_pattern = [
/^android\/sdk\/src\/test\/.+/,
/^android\/playground\/.+/
];
for (let p of ignored_break_change_pattern) {
if (file.match(p)) {
return true;
}
}
return false;
}
async function checkBreakChange(file){
var diff = await danger.git.diffForFile(file);
if (diff && diff.removed && diff.removed.match(/^-\s*?public\s+[\s\S]+$/gm)) {
if (isNotDanger) {
warn("Potential BREAK CHANGE. Modify public in " + file);
} else {
warn(
"Potential BREAK CHANGE. Modify public in " +
file +
" without metion it in commit message. You'd better add '@notdanger' in your commit log. "
);
}
}
}
var has_sdk_changes = false;
var has_test_changes = false;
var filesToVerifySrcHeader = [];
var fileCount = 0;
const type_unknown = 0;
const type_ios_sdk = 1;
const type_android_sdk = 2;
const type_ios_test = 3;
const type_android_test = 4;
const type_jsfm = 5;
const type_jsfm_test = 6;
const type_ui_test = 8;
const getFileType = file => {
if (file.match(/WeexSDK\/Sources\/.+\.(m|h|mm)/)) {
return type_ios_sdk;
} else if (file.match(/WeexSDKTests\//)) {
return type_ios_test;
} else if (file.match(/android\/sdk\/src\/test\/.+\.java/)) {
return type_android_test;
} else if (file.match(/android\/sdk\/src\/main\/java\/.+\.java/)) {
return type_android_sdk;
} else if (
file.match(/runtime\/.+\.js/)
) {
return type_jsfm;
} else if (file.match(/test\/js-framework\/.+\.js/)) {
return type_jsfm_test;
} else if(file.match(/test\/scripts\/.+\.js/) || file.match(/test\/pages\/.+\.vue/)){
return type_ui_test
}else{
return type_unknown
}
}
const checkChangedFile = file => {
fileCount++;
console.log("check changed file:"+file)
let fileType = getFileType(file)
has_sdk_changes =
has_sdk_changes ||
fileType == type_android_sdk ||
fileType == type_ios_sdk ||
fileType == type_jsfm;
has_test_changes =
has_test_changes ||
fileType == type_android_test ||
fileType == type_ios_test ||
fileType == type_jsfm_test ||
fileType == type_ui_test
};
const checkAndroidBreakChange = file => {
if (getFileType(file) == type_android_sdk && !is_ignored_public_check(file)) {
schedule(async () => {
await checkBreakChange(file);
});
}
}
const checkFileToVerifySrcHeader = file => {
if (
file.endsWith(".h") ||
file.endsWith(".m") ||
file.endsWith(".mm") ||
file.endsWith(".java") ||
file.endsWith(".js")
) {
filesToVerifySrcHeader.push(file);
}
};
if (danger.git.modified_files) {
danger.git.modified_files.forEach(file => {
checkChangedFile(file);
checkAndroidBreakChange(file);
checkFileToVerifySrcHeader(file);
});
}
console.log('checkFileToVerifySrcHeader')
if (danger.git.created_files) {
danger.git.created_files.forEach(file => {
checkChangedFile(file);
checkFileToVerifySrcHeader(file);
});
}
console.log('checkAndroidBreakChange')
if (danger.git.deleted_files) {
danger.git.deleted_files.forEach(file => {
checkChangedFile(file);
checkAndroidBreakChange(file);
});
}
if (has_sdk_changes && !has_test_changes) {
if(isNotDanger) warn("This PR modify SDK code without add/modify testcases.")
// else fail("This PR modify SDK code. Please add/modify corresponding testcases. If it is ok, please comment about it. Or put '@notdanger' in you commit message.");
}
//check ios copyright
//see scripts/rh/header.template
const copyright_header_components = [
"Licensed to the Apache Software Foundation \\(ASF\\) under one",
"or more contributor license agreements. See the NOTICE file",
"distributed with this work for additional information",
"regarding copyright ownership\\. The ASF licenses this file",
'to you under the Apache License, Version 2\\.0 \\(the',
'"License"\\); you may not use this file except in compliance',
'with the License\\. You may obtain a copy of the License at'
];
//path prefix
const ignoreCopyrightVerifyPath = [
'test',
'packages',
'pre-build',
'runtime/frameworks/legacy/core',
'test/js\-framework/case',
'android/playground/app/src/main/assets',
'android/sdk/assets',
'ios/playground/bundlejs',
'ios/sdk/WeexSDK/Resources',
'ios/sdk/WeexSDK/Sources/Layout',
'ios/sdk/WeexSDK/dependency/SRWebSocket'
]
console.log('copyright_header_components')
filesToVerifySrcHeader.forEach(filepath => {
for(var i=ignoreCopyrightVerifyPath.length-1;i>=0;i--){
if(filepath.startsWith(ignoreCopyrightVerifyPath[i])){
return
}
}
const content = fs.readFileSync(filepath).toString();
for (const line of copyright_header_components) {
if (!content.match(new RegExp(line))) {
fail("Code file "+ filepath +" does not have the copyright header.");
return;
}
}
// check cn for source code
var reg = /[\u4e00-\u9FA5]+/;
var res = reg.test(content);
if(res){
console.error("Code file "+ filepath +" has cn source code.");
console.error("position " + reg.exec(content));
fail("Code file "+ filepath +" has cn source code.");
return ;
}
});
/*
* try to find the appropriate reviewer according to the blame info
* will be seperated to a danger plugin
*/
// console.log('findReviewer')
// schedule(new Promise((resolve, reject) => {
// try {
// findReviewer(resolve, reject)
// } catch (e) {
// console.log(e)
// resolve()
// }
// }));
function findReviewer(resolve, reject) {
var github = new GitHubApi({
protocol: "https",
host: "api.github.com",
});
var fileToDeletedLinesMap = {}
var fileToNormalLinesMap = {}
var fileToBlamesMap = {}
var repoName = danger.github.pr.base.repo && danger.github.pr.base.repo.name
github.pullRequests.get({
owner: danger.github.pr.base.user.login,
repo: repoName,
number: danger.github.pr.number,
headers: {Accept: 'application/vnd.github.diff',"user-agent": "node.js"}
}, function (err, result) {
if ("undefined" === typeof result || "undefined" === typeof result.data || err) {
console.log('result:'+result+', error:'+err);
resolve()
return
}
console.log('result:'+result);
parseDeleteAndNormalLines(result.data, fileToDeletedLinesMap, fileToNormalLinesMap)
console.log('getContent')
var promises = danger.git.modified_files.map(function(file) {
let repoURL = danger.github.pr.base.repo.html_url
let fileName = file.replace(/^.*[\\\/]/, '')
let blameURL = repoURL + '/blame/' + danger.github.pr.base.ref + '/' + file
// console.log("Getting blame html: " + blameURL)
console.log('getContent2')
return getContent(blameURL)
});
console.log('findBlameReviewers')
Promise.all(promises).then(datas => {
datas.forEach(function(data, index) {
fileToBlamesMap[danger.git.modified_files[index]] = parseBlame(data);
});
findBlameReviewers(fileToDeletedLinesMap, fileToNormalLinesMap, fileToBlamesMap)
resolve()
})
});
}
function getContent(url) {
// return new pending promise
return new Promise((resolve, reject) => {
// select http or https module, depending on reqested url
const lib = url.startsWith('https') ? require('https') : require('http');
const request = lib.get(url, (function (url) {
return (response) => {
// handle http errors
console.log('response:', response.statusCode)
if (response.statusCode < 200 || response.statusCode > 299) {
if (response.statusCode === 404 || response.statusCode === 502) {
// ignore this, probably a renamed file,or .so that can't blame
return resolve('')
}
reject(new Error('Failed to load page, status code: ' + response.statusCode + ', '
+ ' url: ' + url));
}
// temporary data holder
const body = [];
// on every content chunk, push it to the data array
response.on('data', (chunk) => body.push(chunk));
// we are done, resolve promise with those joined chunks
response.on('end', () => resolve(body.join('')));
}
})(url));
// handle connection errors of the request
request.on('error', (err) => reject(err))
})
}
function parseDeleteAndNormalLines(diffData, fileToDeletedLinesMap, fileToNormalLinesMap) {
try {
console.log('parseDeleteAndNormalLines')
var diffs = parseDiff(diffData)
console.log('diffs:'+diffs)
if(diffs&&diffs instanceof Array){
diffs.forEach(diff => {
fileToDeletedLinesMap[diff.from] = [];
fileToNormalLinesMap[diff.from] = [];
if(diff&&diff.chunks&&diff.chunks instanceof Array){
diff.chunks.forEach(chunk => {
if(chunk&&chunk.changes&&chunk.changes instanceof Array){
chunk.changes.forEach(change => {
if (change&&change.del) {
fileToDeletedLinesMap[diff.from].push(change.ln)
}
if (change&&change.normal) {
fileToNormalLinesMap[diff.from].push(change.ln1)
}
})
}
})
}
})
}
} catch (error) {
console.log(error)
}
}
function parseBlame(html) {
let regular = /(<img alt="@([^"]+)" class="avatar blame-commit-avatar"|<tr class="blame-line")/g
var match
var currentUser
var lines = []
while ((match = regular.exec(html)) !== null) {
let user = match[2]
if (user) {
currentUser = user
} else {
lines.push(currentUser)
}
}
return lines
}
function findBlameReviewers(fileToDeletedLinesMap, fileToNormalLinesMap, fileToBlamesMap) {
var reviewers = {};
// deleted lines get 3 points, normal line get 1 point
Object.keys(fileToDeletedLinesMap).forEach(function (file) {
let deletedLines = fileToDeletedLinesMap[file]
var blames = fileToBlamesMap[file]
if (!blames) {
console.error(`failed to find blame info for (${file})`)
return;
}
deletedLines.forEach(lineNumber => {
var name = blames[lineNumber]
if (name && !!reviewers) {
reviewers[name] = (reviewers[name] || 0) + 3
}
})
});
Object.keys(fileToNormalLinesMap).forEach(function (file) {
let normalLines = fileToNormalLinesMap[file];
var blames = fileToBlamesMap[file]
if (!blames) {
console.error(`failed to find blame info for (${file})`)
return;
}
normalLines.forEach(lineNumber => {
var name = blames[lineNumber]
if (name && !!reviewers) {
reviewers[name] = (reviewers[name] || 0) + 1
}
})
});
console.log('blame point:', reviewers)
var names = Object.keys(reviewers)
if(!names||!names instanceof Array||names.length<=0)return;
names.sort((name1, name2) => {
return reviewers[name1] > reviewers[name2] ? -1 : 1
})
var prUser = danger.github.pr.user.login
names.splice(names.findIndex(el => {
return el === prUser
}), 1)
if (names.length > 0) {
if (names.length > 2) {
names = names.slice(0, 2)
}
names = names.map(name => {
return '@' + name
})
message("According to the blame info, we recommended " + names.join(' , ') + " to be the reviewers.")
}
}
message('danger test finished.')
/*
* find reviewer end
*/