2020-08-26 16:49:43 -04:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
|
|
|
|
2020-09-11 15:13:36 -04:00
|
|
|
import {existsSync, readFileSync} from 'fs';
|
2020-08-26 16:49:43 -04:00
|
|
|
import * as multimatch from 'multimatch';
|
|
|
|
import {join} from 'path';
|
|
|
|
import {parse as parseYaml} from 'yaml';
|
|
|
|
import {getRepoBaseDir} from '../../utils/config';
|
2020-09-14 14:24:15 -04:00
|
|
|
import {bold, debug, error, info} from '../../utils/console';
|
2020-08-26 16:49:43 -04:00
|
|
|
|
2020-09-14 14:24:15 -04:00
|
|
|
import {BaseModule} from './base';
|
2020-08-26 16:49:43 -04:00
|
|
|
|
2020-09-14 14:24:15 -04:00
|
|
|
/** Information expressing the difference between the master and g3 branches */
|
|
|
|
export interface G3StatsData {
|
|
|
|
insertions: number;
|
|
|
|
deletions: number;
|
|
|
|
files: number;
|
|
|
|
commits: number;
|
|
|
|
}
|
2020-08-26 16:49:43 -04:00
|
|
|
|
2020-09-14 14:24:15 -04:00
|
|
|
export class G3Module extends BaseModule<G3StatsData|void> {
|
|
|
|
async retrieveData() {
|
|
|
|
const toCopyToG3 = this.getG3FileIncludeAndExcludeLists();
|
|
|
|
const latestSha = this.getLatestShas();
|
2020-08-26 16:49:43 -04:00
|
|
|
|
2020-09-14 14:24:15 -04:00
|
|
|
if (toCopyToG3 === null || latestSha === null) {
|
|
|
|
return;
|
|
|
|
}
|
2020-08-26 16:49:43 -04:00
|
|
|
|
2020-09-14 14:24:15 -04:00
|
|
|
return this.getDiffStats(
|
|
|
|
latestSha.g3, latestSha.master, toCopyToG3.include, toCopyToG3.exclude);
|
2020-08-26 16:49:43 -04:00
|
|
|
}
|
|
|
|
|
2020-09-14 14:24:15 -04:00
|
|
|
async printToTerminal() {
|
|
|
|
const stats = await this.data;
|
|
|
|
if (!stats) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
info.group(bold('g3 branch check'));
|
|
|
|
if (stats.files === 0) {
|
|
|
|
info(`${stats.commits} commits between g3 and master`);
|
|
|
|
info('✅ No sync is needed at this time');
|
|
|
|
} else {
|
|
|
|
info(
|
|
|
|
`${stats.files} files changed, ${stats.insertions} insertions(+), ${stats.deletions} ` +
|
|
|
|
`deletions(-) from ${stats.commits} commits will be included in the next sync`);
|
|
|
|
}
|
|
|
|
info.groupEnd();
|
|
|
|
info();
|
|
|
|
}
|
2020-08-26 16:49:43 -04:00
|
|
|
|
2020-10-06 13:13:54 -04:00
|
|
|
/** Fetch and retrieve the latest sha for a specific branch. */
|
2020-09-14 14:24:15 -04:00
|
|
|
private getShaForBranchLatest(branch: string) {
|
|
|
|
const {owner, name} = this.git.remoteConfig;
|
2020-10-06 13:13:54 -04:00
|
|
|
/** The result fo the fetch command. */
|
2020-09-14 14:24:15 -04:00
|
|
|
const fetchResult =
|
|
|
|
this.git.runGraceful(['fetch', '-q', `https://github.com/${owner}/${name}.git`, branch]);
|
2020-10-06 13:13:54 -04:00
|
|
|
|
|
|
|
if (fetchResult.status !== 0 &&
|
|
|
|
fetchResult.stderr.includes(`couldn't find remote ref ${branch}`)) {
|
|
|
|
debug(`No '${branch}' branch exists on upstream, skipping.`);
|
2020-09-14 14:24:15 -04:00
|
|
|
return null;
|
2020-10-06 13:13:54 -04:00
|
|
|
}
|
2020-09-14 14:24:15 -04:00
|
|
|
return this.git.runGraceful(['rev-parse', 'FETCH_HEAD']).stdout.trim();
|
2020-10-06 13:13:54 -04:00
|
|
|
}
|
|
|
|
|
2020-08-26 16:49:43 -04:00
|
|
|
/**
|
|
|
|
* Get git diff stats between master and g3, for all files and filtered to only g3 affecting
|
|
|
|
* files.
|
|
|
|
*/
|
2020-09-14 14:24:15 -04:00
|
|
|
private getDiffStats(
|
|
|
|
g3Ref: string, masterRef: string, includeFiles: string[], excludeFiles: string[]) {
|
2020-08-26 16:49:43 -04:00
|
|
|
/** The diff stats to be returned. */
|
|
|
|
const stats = {
|
|
|
|
insertions: 0,
|
|
|
|
deletions: 0,
|
|
|
|
files: 0,
|
|
|
|
commits: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Determine the number of commits between master and g3 refs. */
|
2020-09-14 14:24:15 -04:00
|
|
|
stats.commits =
|
|
|
|
parseInt(this.git.run(['rev-list', '--count', `${g3Ref}..${masterRef}`]).stdout, 10);
|
2020-08-26 16:49:43 -04:00
|
|
|
|
|
|
|
// Get the numstat information between master and g3
|
2020-09-14 14:24:15 -04:00
|
|
|
this.git.run(['diff', `${g3Ref}...${masterRef}`, '--numstat'])
|
2020-08-26 16:49:43 -04:00
|
|
|
.stdout
|
|
|
|
// Remove the extra space after git's output.
|
|
|
|
.trim()
|
|
|
|
// Split each line of git output into array
|
|
|
|
.split('\n')
|
|
|
|
// Split each line from the git output into components parts: insertions,
|
|
|
|
// deletions and file name respectively
|
2020-09-14 14:24:15 -04:00
|
|
|
.map(line => line.trim().split('\t'))
|
2020-08-26 16:49:43 -04:00
|
|
|
// Parse number value from the insertions and deletions values
|
|
|
|
// Example raw line input:
|
|
|
|
// 10\t5\tsrc/file/name.ts
|
|
|
|
.map(line => [Number(line[0]), Number(line[1]), line[2]] as [number, number, string])
|
|
|
|
// Add each line's value to the diff stats, and conditionally to the g3
|
|
|
|
// stats as well if the file name is included in the files synced to g3.
|
|
|
|
.forEach(([insertions, deletions, fileName]) => {
|
2020-09-14 14:24:15 -04:00
|
|
|
if (this.checkMatchAgainstIncludeAndExclude(fileName, includeFiles, excludeFiles)) {
|
2020-08-26 16:49:43 -04:00
|
|
|
stats.insertions += insertions;
|
|
|
|
stats.deletions += deletions;
|
|
|
|
stats.files += 1;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return stats;
|
|
|
|
}
|
|
|
|
/** Determine whether the file name passes both include and exclude checks. */
|
2020-09-14 14:24:15 -04:00
|
|
|
private checkMatchAgainstIncludeAndExclude(file: string, includes: string[], excludes: string[]) {
|
2020-10-01 19:06:56 -04:00
|
|
|
return (
|
|
|
|
multimatch.call(undefined, file, includes).length >= 1 &&
|
|
|
|
multimatch.call(undefined, file, excludes).length === 0);
|
2020-08-26 16:49:43 -04:00
|
|
|
}
|
2020-09-14 14:24:15 -04:00
|
|
|
|
|
|
|
|
|
|
|
private getG3FileIncludeAndExcludeLists() {
|
|
|
|
const angularRobotFilePath = join(getRepoBaseDir(), '.github/angular-robot.yml');
|
|
|
|
if (!existsSync(angularRobotFilePath)) {
|
|
|
|
debug('No angular robot configuration file exists, skipping.');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
/** The configuration defined for the angular robot. */
|
|
|
|
const robotConfig = parseYaml(readFileSync(angularRobotFilePath).toString());
|
|
|
|
/** The files to be included in the g3 sync. */
|
|
|
|
const include: string[] = robotConfig?.merge?.g3Status?.include || [];
|
|
|
|
/** The files to be expected in the g3 sync. */
|
|
|
|
const exclude: string[] = robotConfig?.merge?.g3Status?.exclude || [];
|
|
|
|
|
|
|
|
if (include.length === 0 && exclude.length === 0) {
|
|
|
|
debug('No g3Status include or exclude lists are defined in the angular robot configuration');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {include, exclude};
|
|
|
|
}
|
|
|
|
|
|
|
|
private getLatestShas() {
|
|
|
|
/** The latest sha for the g3 branch. */
|
|
|
|
const g3 = this.getShaForBranchLatest('g3');
|
|
|
|
/** The latest sha for the master branch. */
|
|
|
|
const master = this.getShaForBranchLatest('master');
|
|
|
|
|
|
|
|
if (g3 === null || master === null) {
|
|
|
|
debug('Either the g3 or master was unable to be retrieved');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {g3, master};
|
|
|
|
}
|
2020-08-26 16:49:43 -04:00
|
|
|
}
|