forked from angular/angular
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcompare-master-to-patch.js
executable file
·187 lines (159 loc) · 6.62 KB
/
compare-master-to-patch.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#!/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();