207 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			207 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | #!/usr/bin/env node
 | ||
|  | /** | ||
|  |  * @license | ||
|  |  * Copyright Google Inc. All Rights Reserved. | ||
|  |  * | ||
|  |  * Use of this source code is governed by an MIT-style license that can be | ||
|  |  * found in the LICENSE file at https://angular.io/license
 | ||
|  |  */ | ||
|  | /* tslint:disable:no-console  */ | ||
|  | const parseYaml = require('yaml').parse; | ||
|  | const readFileSync = require('fs').readFileSync; | ||
|  | const Minimatch = require('minimatch').Minimatch; | ||
|  | const {exec, set, cd} = require('shelljs'); | ||
|  | const path = require('path'); | ||
|  | 
 | ||
|  | // Exit early on shelljs errors
 | ||
|  | set('-e'); | ||
|  | 
 | ||
|  | // Regex Matcher for contains_any_globs conditions
 | ||
|  | const CONTAINS_ANY_GLOBS_REGEX = /^'([^']+)',?$/; | ||
|  | 
 | ||
|  | // Full path of the angular project directory
 | ||
|  | const ANGULAR_PROJECT_DIR = path.resolve(__dirname, '../..'); | ||
|  | // Change to the Angular project directory
 | ||
|  | cd(ANGULAR_PROJECT_DIR); | ||
|  | 
 | ||
|  | // Whether to log verbosely
 | ||
|  | const VERBOSE_MODE = process.argv.includes('-v'); | ||
|  | // Full path to PullApprove config file
 | ||
|  | const PULL_APPROVE_YAML_PATH = path.resolve(ANGULAR_PROJECT_DIR, '.pullapprove.yml'); | ||
|  | // All relative path file names in the git repo, this is retrieved using git rather
 | ||
|  | // that a glob so that we only get files that are checked in, ignoring things like
 | ||
|  | // node_modules, .bazelrc.user, etc
 | ||
|  | const ALL_FILES = exec('git ls-tree --full-tree -r --name-only HEAD', {silent: true}) | ||
|  |                       .trim() | ||
|  |                       .split('\n') | ||
|  |                       .filter(_ => _); | ||
|  | if (!ALL_FILES.length) { | ||
|  |   console.error( | ||
|  |       `No files were found to be in the git tree, did you run this command from \n` + | ||
|  |       `inside the angular repository?`); | ||
|  |   process.exit(1); | ||
|  | } | ||
|  | 
 | ||
|  | /** Gets the glob matching information from each group's condition. */ | ||
|  | function getGlobMatchersFromCondition(groupName, condition) { | ||
|  |   const trimmedCondition = condition.trim(); | ||
|  |   const globMatchers = []; | ||
|  |   const badConditionLines = []; | ||
|  | 
 | ||
|  |   // If the condition starts with contains_any_globs, evaluate all of the globs
 | ||
|  |   if (trimmedCondition.startsWith('contains_any_globs')) { | ||
|  |     trimmedCondition.split('\n') | ||
|  |         .slice(1, -1) | ||
|  |         .map(glob => { | ||
|  |           const trimmedGlob = glob.trim(); | ||
|  |           const match = trimmedGlob.match(CONTAINS_ANY_GLOBS_REGEX); | ||
|  |           if (!match) { | ||
|  |             badConditionLines.push(trimmedGlob); | ||
|  |             return; | ||
|  |           } | ||
|  |           return match[1]; | ||
|  |         }) | ||
|  |         .filter(globString => !!globString) | ||
|  |         .forEach(globString => globMatchers.push({ | ||
|  |           group: groupName, | ||
|  |           glob: globString, | ||
|  |           matcher: new Minimatch(globString, {dot: true}), | ||
|  |           matchCount: 0, | ||
|  |         })); | ||
|  |   } | ||
|  |   return [globMatchers, badConditionLines]; | ||
|  | } | ||
|  | 
 | ||
|  | /** Create logs for each review group. */ | ||
|  | function logGroups(groups) { | ||
|  |   Array.from(groups.entries()).sort().forEach(([groupName, globs]) => { | ||
|  |     console.groupCollapsed(groupName); | ||
|  |     Array.from(globs.values()) | ||
|  |         .sort((a, b) => b.matchCount - a.matchCount) | ||
|  |         .forEach(glob => console.log(`${glob.glob} - ${glob.matchCount}`)); | ||
|  |     console.groupEnd(); | ||
|  |   }); | ||
|  | } | ||
|  | 
 | ||
|  | /** Logs a header within a text drawn box. */ | ||
|  | function logHeader(...params) { | ||
|  |   const totalWidth = 80; | ||
|  |   const fillWidth = totalWidth - 2; | ||
|  |   const headerText = params.join(' ').substr(0, fillWidth); | ||
|  |   const leftSpace = Math.ceil((fillWidth - headerText.length) / 2); | ||
|  |   const rightSpace = fillWidth - leftSpace - headerText.length; | ||
|  |   const fill = (count, content) => content.repeat(count); | ||
|  | 
 | ||
|  |   console.log(`┌${fill(fillWidth, '─')}┐`); | ||
|  |   console.log(`│${fill(leftSpace, ' ')}${headerText}${fill(rightSpace, ' ')}│`); | ||
|  |   console.log(`└${fill(fillWidth, '─')}┘`); | ||
|  | } | ||
|  | 
 | ||
|  | /** Runs the pull approve verification check on provided files. */ | ||
|  | function runVerification(files) { | ||
|  |   // All of the globs created for each group's conditions.
 | ||
|  |   const allGlobs = []; | ||
|  |   // The pull approve config file.
 | ||
|  |   const pullApprove = readFileSync(PULL_APPROVE_YAML_PATH, {encoding: 'utf8'}); | ||
|  |   // All of the PullApprove groups, parsed from the PullApprove yaml file.
 | ||
|  |   const parsedPullApproveGroups = parseYaml(pullApprove).groups; | ||
|  |   // All files which were found to match a condition in PullApprove.
 | ||
|  |   const matchedFiles = new Set(); | ||
|  |   // All files which were not found to match a condition in PullApprove.
 | ||
|  |   const unmatchedFiles = new Set(); | ||
|  |   // All PullApprove groups which matched at least one file.
 | ||
|  |   const matchedGroups = new Map(); | ||
|  |   // All PullApprove groups which did not match at least one file.
 | ||
|  |   const unmatchedGroups = new Map(); | ||
|  |   // All condition lines which were not able to be correctly parsed, by group.
 | ||
|  |   const badConditionLinesByGroup = new Map(); | ||
|  |   // Total number of condition lines which were not able to be correctly parsed.
 | ||
|  |   let badConditionLineCount = 0; | ||
|  | 
 | ||
|  |   // Get all of the globs from the PullApprove group conditions.
 | ||
|  |   Object.entries(parsedPullApproveGroups).map(([groupName, group]) => { | ||
|  |     for (const condition of group.conditions) { | ||
|  |       const [matchers, badConditions] = getGlobMatchersFromCondition(groupName, condition); | ||
|  |       if (badConditions.length) { | ||
|  |         badConditionLinesByGroup.set(groupName, badConditions); | ||
|  |         badConditionLineCount += badConditions.length; | ||
|  |       } | ||
|  |       allGlobs.push(...matchers); | ||
|  |     } | ||
|  |   }); | ||
|  | 
 | ||
|  |   if (badConditionLineCount) { | ||
|  |     console.log(`Discovered ${badConditionLineCount} parsing errors in PullApprove conditions`); | ||
|  |     console.log(`Attempted parsing using: ${CONTAINS_ANY_GLOBS_REGEX}`); | ||
|  |     console.log(); | ||
|  |     console.log(`Unable to properly parse the following line(s) by group:`); | ||
|  |     for (const [groupName, badConditionLines] of badConditionLinesByGroup.entries()) { | ||
|  |       console.log(`- ${groupName}:`); | ||
|  |       badConditionLines.forEach(line => console.log(`    ${line}`)); | ||
|  |     } | ||
|  |     console.log(); | ||
|  |     console.log( | ||
|  |         `Please correct the invalid conditions, before PullApprove verification can be completed`); | ||
|  |     process.exit(1); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Check each file for if it is matched by a PullApprove condition.
 | ||
|  |   for (let file of files) { | ||
|  |     const matched = allGlobs.filter(glob => glob.matcher.match(file)); | ||
|  |     matched.length ? matchedFiles.add(file) : unmatchedFiles.add(file); | ||
|  |     matched.forEach(glob => glob.matchCount++); | ||
|  |   } | ||
|  | 
 | ||
|  |   // Add each glob for each group to a map either matched or unmatched.
 | ||
|  |   allGlobs.forEach(glob => { | ||
|  |     const groups = glob.matchCount ? matchedGroups : unmatchedGroups; | ||
|  |     const globs = groups.get(glob.group) || new Map(); | ||
|  |     // Set the globs map in the groups map
 | ||
|  |     groups.set(glob.group, globs); | ||
|  |     // Set the glob in the globs map
 | ||
|  |     globs.set(glob.glob, glob); | ||
|  |   }); | ||
|  | 
 | ||
|  |   // PullApprove is considered verified if no files or groups are found to be unsed.
 | ||
|  |   const verificationSucceeded = !(unmatchedFiles.size || unmatchedGroups.size); | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Overall result | ||
|  |    */ | ||
|  |   logHeader('Result'); | ||
|  |   if (verificationSucceeded) { | ||
|  |     console.log('PullApprove verification succeeded!'); | ||
|  |   } else { | ||
|  |     console.log(`PullApprove verification failed.\n`); | ||
|  |     console.log(`Please update '.pullapprove.yml' to ensure that all necessary`); | ||
|  |     console.log(`files/directories have owners and all patterns that appear in`); | ||
|  |     console.log(`the file correspond to actual files/directories in the repo.`); | ||
|  |   } | ||
|  |   /** | ||
|  |    * File by file Summary | ||
|  |    */ | ||
|  |   logHeader('PullApprove file match results'); | ||
|  |   console.groupCollapsed(`Matched Files (${matchedFiles.size} files)`); | ||
|  |   VERBOSE_MODE && matchedFiles.forEach(file => console.log(file)); | ||
|  |   console.groupEnd(); | ||
|  |   console.groupCollapsed(`Unmatched Files (${unmatchedFiles.size} files)`); | ||
|  |   unmatchedFiles.forEach(file => console.log(file)); | ||
|  |   console.groupEnd(); | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Group by group Summary | ||
|  |    */ | ||
|  |   logHeader('PullApprove group matches'); | ||
|  |   console.groupCollapsed(`Matched Groups (${matchedGroups.size} groups)`); | ||
|  |   VERBOSE_MODE && logGroups(matchedGroups); | ||
|  |   console.groupEnd(); | ||
|  |   console.groupCollapsed(`Unmatched Groups (${unmatchedGroups.size} groups)`); | ||
|  |   logGroups(unmatchedGroups); | ||
|  |   console.groupEnd(); | ||
|  | 
 | ||
|  |   // Provide correct exit code based on verification success.
 | ||
|  |   process.exit(verificationSucceeded ? 0 : 1); | ||
|  | } | ||
|  | 
 | ||
|  | runVerification(ALL_FILES); |