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);
							 |