| /* |
| * 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. |
| */ |
| // Removed import |
| import fs from "fs"; |
| import path from 'path'; |
| import GitHubApi from 'github'; |
| import parseDiff from 'parse-diff'; |
| |
| // check if pr submitted to master branch |
| console.log("checkMasterBranch") |
| const isMergeRefMaster = danger.github.pr.base.ref === 'master'; |
| if(!isMergeRefMaster){ |
| warn("You'd better to submit PR to master branch as the development of Weex is on master branch."); |
| } |
| |
| // match regex line by line |
| function matchRegex(pr_body,regex){ |
| const lines = pr_body.split("\n"); |
| for (let i = 0; i < lines.length; i++) { |
| if(lines[i].match(regex)){ |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| var pr_body = danger.github.pr.body.toLowerCase(); |
| // Because Pr description template include the following lineļ¼ |
| // 1. Write the corresponding [documentation](https://github.com/apache/incubator-weex/blob/master/CONTRIBUTING.md#contribute-code-or-document) |
| // so we should check the documentation below the ### checklist |
| console.log("checkDocumentation"); |
| const index = pr_body.indexOf("checklist") |
| const includeChecklist = (index!=-1) |
| if(includeChecklist && !matchRegex(pr_body.substring(index),/documentation.*http/)){ |
| const msg = "If you update the code, "+ |
| "maybe you should update the documentation and add the documentation link in the PR description. \n" + |
| "here is the guide about how to contribute documentation: <a href='https://github.com/apache/incubator-weex/blob/master/CONTRIBUTING.md#contribute-code-or-document'>https://github.com/apache/incubator-weex/blob/master/CONTRIBUTING.md#contribute-code-or-document</a>"; |
| warn(msg); |
| } |
| |
| // check if pr contains a demo link |
| console.log("checkDemo"); |
| if(!matchRegex(pr_body,/demo.*http/)){ |
| const msg = "If your PR is about fixing a bug excluding crash the code,"+ |
| "you should add the demo link in the PR description. "+ |
| "Demo link: <a href='http://dotwe.org/vue'>http://dotwe.org/vue</a>"; |
| warn(msg); |
| } |
| |
| // check if pr bind the github milestone |
| console.log("checkMileStone"); |
| if(!danger.github.pr.milestone){ |
| warn("Current pr not bind the milestone"); |
| } |
| |
| // Make sure there are changelog entries |
| const hasChangelog = danger.git.modified_files.includes("CHANGELOG.md") |
| if (!hasChangelog) { |
| warn(`No Changelog changes! - <i>Can you add a Changelog? To do so,append your changes to the changelog.md</i>`); |
| } |
| |
| 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', |
| 'android/sdk/src/main/java/org/apache/weex/utils/WXDataStructureUtil.java', |
| 'ios/playground/bundlejs', |
| 'ios/sdk/WeexSDK/Resources', |
| 'ios/sdk/WeexSDK/Sources/Layout', |
| 'ios/sdk/WeexSDK/dependency/SRWebSocket', |
| 'weex_core/Source/include/JavaScriptCore', |
| 'weex_core/Source/include/wtf', |
| 'weex_core/Source/base/third_party/icu', |
| 'weex_core/Source/base/base64/modp_base64' |
| ] |
| |
| 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))) { |
| console.error("Code file "+ filepath +" does not have the copyright header."); |
| 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.") |
| } |
| } |
| /* |
| * find reviewer end |
| */ |