Update the license headers throughout the repository to reference Google LLC rather than Google Inc, for the required license headers. PR Close #37205
		
			
				
	
	
		
			188 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			188 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env node
 | |
| 
 | |
| /**
 | |
|  * @license
 | |
|  * Copyright Google LLC 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
 | |
|  */
 | |
| 
 | |
| 'use strict';
 | |
| 
 | |
| /**
 | |
|  * This script compares commits in master and patch branches to find the delta between them. This is
 | |
|  * useful for release reviews, to make sure all the necessary commits were included into the patch
 | |
|  * branch and there is no discrepancy.
 | |
|  *
 | |
|  * Additionally, lists all 'feat' commits that were merged to the patch branch to aid in ensuring
 | |
|  * features are only released to master.
 | |
|  */
 | |
| 
 | |
| const {exec} = require('shelljs');
 | |
| const semver = require('semver');
 | |
| 
 | |
| // Ignore commits that have specific patterns in commit message, it's ok for these commits to be
 | |
| // present only in one branch. Ignoring them reduced the "noise" in the final output.
 | |
| const ignoreCommitPatterns = [
 | |
|   'release:',
 | |
|   'docs: release notes',
 | |
|   // These commits are created to update cli command docs sources with the most recent sha (stored
 | |
|   // in `aio/package.json`). Separate commits are generated for master and patch branches and since
 | |
|   // it's purely an infrastructure-related change, we ignore these commits while comparing master
 | |
|   // and patch diffs to look for delta.
 | |
|   'build(docs-infra): upgrade cli command docs sources',
 | |
| ];
 | |
| 
 | |
| // Ignore feature commits that have specific patterns in commit message, it's ok for these commits
 | |
| // to be present in patch branch.
 | |
| const ignoreFeatureCheckPatterns = [
 | |
|   // It is ok and in fact desirable for dev-infra features to be on the patch branch.
 | |
|   'feat(dev-infra):'
 | |
| ];
 | |
| 
 | |
| // String to be displayed as a version for initial commits in a branch
 | |
| // (before first release from that branch).
 | |
| const initialVersion = 'initial';
 | |
| 
 | |
| // Helper methods
 | |
| 
 | |
| function execGitCommand(gitCommand) {
 | |
|   const output = exec(gitCommand, {silent: true});
 | |
|   if (output.code !== 0) {
 | |
|     console.error(`Error: git command "${gitCommand}" failed: \n\n ${output.stderr}`);
 | |
|     process.exit(1);
 | |
|   }
 | |
|   return output;
 | |
| }
 | |
| 
 | |
| function toArray(rawGitCommandOutput) {
 | |
|   return rawGitCommandOutput.trim().split('\n');
 | |
| }
 | |
| 
 | |
| function maybeExtractReleaseVersion(commit) {
 | |
|   const versionRegex = /release: cut the (.*?) release/;
 | |
|   const matches = commit.match(versionRegex);
 | |
|   return matches ? matches[1] || matches[2] : null;
 | |
| }
 | |
| 
 | |
| // Checks whether commit message matches any patterns in ignore list.
 | |
| function shouldIgnoreCommit(commitMessage, ignorePatterns) {
 | |
|   return ignorePatterns.some(pattern => commitMessage.indexOf(pattern) > -1);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @param rawGitCommits
 | |
|  * @returns {Map<string, [string, string]>} - Map of commit message to [commit info, version]
 | |
|  */
 | |
| function collectCommitsAsMap(rawGitCommits) {
 | |
|   const commits = toArray(rawGitCommits);
 | |
|   const commitsMap = new Map();
 | |
|   let version = initialVersion;
 | |
|   commits.reverse().forEach((commit) => {
 | |
|     const ignore = shouldIgnoreCommit(commit, ignoreCommitPatterns);
 | |
|     // Keep track of the current version while going though the list of commits, so that we can use
 | |
|     // this information in the output (i.e. display a version when a commit was introduced).
 | |
|     version = maybeExtractReleaseVersion(commit) || version;
 | |
|     if (!ignore) {
 | |
|       // Extract original commit description from commit message, so that we can find matching
 | |
|       // commit in other commit range. For example, for the following commit message:
 | |
|       //
 | |
|       //   15d3e741e9 feat: update the locale files (#33556)
 | |
|       //
 | |
|       // we extract only "feat: update the locale files" part and use it as a key, since commit SHA
 | |
|       // and PR number may be different for the same commit in master and patch branches.
 | |
|       const key = commit.slice(11).replace(/\(\#\d+\)/g, '').trim();
 | |
|       commitsMap.set(key, [commit, version]);
 | |
|     }
 | |
|   });
 | |
|   return commitsMap;
 | |
| }
 | |
| 
 | |
| function getCommitInfoAsString(version, commitInfo) {
 | |
|   const formattedVersion = version === initialVersion ? version : `${version}+`;
 | |
|   return `[${formattedVersion}] ${commitInfo}`;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Returns a list of items present in `mapA`, but *not* present in `mapB`.
 | |
|  * This function is needed to compare 2 sets of commits and return the list of unique commits in the
 | |
|  * first set.
 | |
|  */
 | |
| function diff(mapA, mapB) {
 | |
|   const result = [];
 | |
|   mapA.forEach((value, key) => {
 | |
|     if (!mapB.has(key)) {
 | |
|       result.push(getCommitInfoAsString(value[1], value[0]));
 | |
|     }
 | |
|   });
 | |
|   return result;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @param {Map<string, [string, string]>} commitsMap - commit map from collectCommitsAsMap
 | |
|  * @returns {string[]} List of commits with commit messages that start with 'feat'
 | |
|  */
 | |
| function listFeatures(commitsMap) {
 | |
|   return Array.from(commitsMap.keys()).reduce((result, key) => {
 | |
|     if (key.startsWith('feat') && !shouldIgnoreCommit(key, ignoreFeatureCheckPatterns)) {
 | |
|       const value = commitsMap.get(key);
 | |
|       result.push(getCommitInfoAsString(value[1], value[0]));
 | |
|     }
 | |
|     return result;
 | |
|   }, []);
 | |
| }
 | |
| 
 | |
| function getBranchByTag(tag) {
 | |
|   const version = semver(tag);
 | |
|   return `${version.major}.${version.minor}.x`;  // e.g. 9.0.x
 | |
| }
 | |
| 
 | |
| function getLatestTag(tags) {
 | |
|   // Exclude Next releases, since we cut them from master, so there is nothing to compare.
 | |
|   const isNotNextVersion = version => version.indexOf('-next') === -1;
 | |
|   return tags.filter(semver.valid)
 | |
|       .filter(isNotNextVersion)
 | |
|       .map(semver.clean)
 | |
|       .sort(semver.rcompare)[0];
 | |
| }
 | |
| 
 | |
| // Main program
 | |
| function main() {
 | |
|   execGitCommand('git fetch upstream');
 | |
| 
 | |
|   // Extract tags information and pick the most recent version
 | |
|   // that we'll use later to compare with master.
 | |
|   const tags = toArray(execGitCommand('git tag'));
 | |
|   const latestTag = getLatestTag(tags);
 | |
| 
 | |
|   // Based on the latest tag, generate the name of the patch branch.
 | |
|   const branch = getBranchByTag(latestTag);
 | |
| 
 | |
|   // Extract master-only and patch-only commits using `git log` command.
 | |
|   const masterCommits = execGitCommand(
 | |
|       `git log --cherry-pick --oneline --right-only upstream/${branch}...upstream/master`);
 | |
|   const patchCommits = execGitCommand(
 | |
|       `git log --cherry-pick --oneline --left-only upstream/${branch}...upstream/master`);
 | |
| 
 | |
|   // Post-process commits and convert raw data into a Map, so that we can diff it easier.
 | |
|   const masterCommitsMap = collectCommitsAsMap(masterCommits);
 | |
|   const patchCommitsMap = collectCommitsAsMap(patchCommits);
 | |
| 
 | |
|   // tslint:disable-next-line:no-console
 | |
|   console.log(`
 | |
| Comparing branches "${branch}" and master.
 | |
| 
 | |
| ***** Only in MASTER *****
 | |
| ${diff(masterCommitsMap, patchCommitsMap).join('\n') || 'No extra commits'}
 | |
| 
 | |
| ***** Only in PATCH (${branch}) *****
 | |
| ${diff(patchCommitsMap, masterCommitsMap).join('\n') || 'No extra commits'}
 | |
| 
 | |
| ***** Features in PATCH (${branch}) - should always be empty *****
 | |
| ${listFeatures(patchCommitsMap).join('\n') || 'No extra commits'}
 | |
| `);
 | |
| }
 | |
| 
 | |
| main();
 |