refactor(dev-infra): move `getRepoBaseDir()` to `GitClient` (#41527)
As `getRepoBaseDir()` relies on git, it should be a method on `GitClient` for retrieval rather than its own utility outside of the common GitClient used for all git ineractions. PR Close #41527
This commit is contained in:
parent
5332a4a919
commit
dd4c3dba3f
|
@ -7,7 +7,461 @@ var fs = require('fs');
|
|||
var path = require('path');
|
||||
var chalk = _interopDefault(require('chalk'));
|
||||
require('inquirer');
|
||||
var shelljs = require('shelljs');
|
||||
var child_process = require('child_process');
|
||||
var semver = require('semver');
|
||||
var graphql = require('@octokit/graphql');
|
||||
var Octokit = require('@octokit/rest');
|
||||
var typedGraphqlify = require('typed-graphqlify');
|
||||
var url = require('url');
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
/** Whether the current environment is in dry run mode. */
|
||||
function isDryRun() {
|
||||
return process.env['DRY_RUN'] !== undefined;
|
||||
}
|
||||
/** Error to be thrown when a function or method is called in dryRun mode and shouldn't be. */
|
||||
var DryRunError = /** @class */ (function (_super) {
|
||||
tslib.__extends(DryRunError, _super);
|
||||
function DryRunError() {
|
||||
var _this = _super.call(this, 'Cannot call this function in dryRun mode.') || this;
|
||||
// Set the prototype explicitly because in ES5, the prototype is accidentally lost due to
|
||||
// a limitation in down-leveling.
|
||||
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work.
|
||||
Object.setPrototypeOf(_this, DryRunError.prototype);
|
||||
return _this;
|
||||
}
|
||||
return DryRunError;
|
||||
}(Error));
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
/** Error for failed Github API requests. */
|
||||
var GithubApiRequestError = /** @class */ (function (_super) {
|
||||
tslib.__extends(GithubApiRequestError, _super);
|
||||
function GithubApiRequestError(status, message) {
|
||||
var _this = _super.call(this, message) || this;
|
||||
_this.status = status;
|
||||
return _this;
|
||||
}
|
||||
return GithubApiRequestError;
|
||||
}(Error));
|
||||
/** Error for failed Github API requests. */
|
||||
var GithubGraphqlClientError = /** @class */ (function (_super) {
|
||||
tslib.__extends(GithubGraphqlClientError, _super);
|
||||
function GithubGraphqlClientError() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
return GithubGraphqlClientError;
|
||||
}(Error));
|
||||
/**
|
||||
* A Github client for interacting with the Github APIs.
|
||||
*
|
||||
* Additionally, provides convenience methods for actions which require multiple requests, or
|
||||
* would provide value from memoized style responses.
|
||||
**/
|
||||
var GithubClient = /** @class */ (function (_super) {
|
||||
tslib.__extends(GithubClient, _super);
|
||||
/**
|
||||
* @param token The github authentication token for Github Rest and Graphql API requests.
|
||||
*/
|
||||
function GithubClient(token) {
|
||||
var _this =
|
||||
// Pass in authentication token to base Octokit class.
|
||||
_super.call(this, { auth: token }) || this;
|
||||
_this.token = token;
|
||||
/** The current user based on checking against the Github API. */
|
||||
_this._currentUser = null;
|
||||
/** The graphql instance with authentication set during construction. */
|
||||
_this._graphql = graphql.graphql.defaults({ headers: { authorization: "token " + _this.token } });
|
||||
_this.hook.error('request', function (error) {
|
||||
// Wrap API errors in a known error class. This allows us to
|
||||
// expect Github API errors better and in a non-ambiguous way.
|
||||
throw new GithubApiRequestError(error.status, error.message);
|
||||
});
|
||||
// Note: The prototype must be set explictly as Github's Octokit class is a non-standard class
|
||||
// definition which adjusts the prototype chain.
|
||||
// See:
|
||||
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work
|
||||
// https://github.com/octokit/rest.js/blob/7b51cee4a22b6e52adcdca011f93efdffa5df998/lib/constructor.js
|
||||
Object.setPrototypeOf(_this, GithubClient.prototype);
|
||||
return _this;
|
||||
}
|
||||
/** Perform a query using Github's Graphql API. */
|
||||
GithubClient.prototype.graphql = function (queryObject, params) {
|
||||
if (params === void 0) { params = {}; }
|
||||
return tslib.__awaiter(this, void 0, void 0, function () {
|
||||
return tslib.__generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
if (this.token === undefined) {
|
||||
throw new GithubGraphqlClientError('Cannot query via graphql without an authentication token set, use the authenticated ' +
|
||||
'`GitClient` by calling `GitClient.getAuthenticatedInstance()`.');
|
||||
}
|
||||
return [4 /*yield*/, this._graphql(typedGraphqlify.query(queryObject), params)];
|
||||
case 1: return [2 /*return*/, (_a.sent())];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
/** Retrieve the login of the current user from Github. */
|
||||
GithubClient.prototype.getCurrentUser = function () {
|
||||
return tslib.__awaiter(this, void 0, void 0, function () {
|
||||
var result;
|
||||
return tslib.__generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
// If the current user has already been retrieved return the current user value again.
|
||||
if (this._currentUser !== null) {
|
||||
return [2 /*return*/, this._currentUser];
|
||||
}
|
||||
return [4 /*yield*/, this.graphql({
|
||||
viewer: {
|
||||
login: typedGraphqlify.types.string,
|
||||
}
|
||||
})];
|
||||
case 1:
|
||||
result = _a.sent();
|
||||
return [2 /*return*/, this._currentUser = result.viewer.login];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
return GithubClient;
|
||||
}(Octokit));
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
/** URL to the Github page where personal access tokens can be managed. */
|
||||
var GITHUB_TOKEN_SETTINGS_URL = 'https://github.com/settings/tokens';
|
||||
/** URL to the Github page where personal access tokens can be generated. */
|
||||
var GITHUB_TOKEN_GENERATE_URL = 'https://github.com/settings/tokens/new';
|
||||
/** Adds the provided token to the given Github HTTPs remote url. */
|
||||
function addTokenToGitHttpsUrl(githubHttpsUrl, token) {
|
||||
var url$1 = new url.URL(githubHttpsUrl);
|
||||
url$1.username = token;
|
||||
return url$1.href;
|
||||
}
|
||||
/** Gets the repository Git URL for the given github config. */
|
||||
function getRepositoryGitUrl(config, githubToken) {
|
||||
if (config.useSsh) {
|
||||
return "git@github.com:" + config.owner + "/" + config.name + ".git";
|
||||
}
|
||||
var baseHttpUrl = "https://github.com/" + config.owner + "/" + config.name + ".git";
|
||||
if (githubToken !== undefined) {
|
||||
return addTokenToGitHttpsUrl(baseHttpUrl, githubToken);
|
||||
}
|
||||
return baseHttpUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
/** Error for failed Git commands. */
|
||||
var GitCommandError = /** @class */ (function (_super) {
|
||||
tslib.__extends(GitCommandError, _super);
|
||||
function GitCommandError(client, args) {
|
||||
var _this =
|
||||
// Errors are not guaranteed to be caught. To ensure that we don't
|
||||
// accidentally leak the Github token that might be used in a command,
|
||||
// we sanitize the command that will be part of the error message.
|
||||
_super.call(this, "Command failed: git " + client.omitGithubTokenFromMessage(args.join(' '))) || this;
|
||||
_this.args = args;
|
||||
return _this;
|
||||
}
|
||||
return GitCommandError;
|
||||
}(Error));
|
||||
/**
|
||||
* Common client for performing Git interactions with a given remote.
|
||||
*
|
||||
* Takes in two optional arguments:
|
||||
* `githubToken`: the token used for authentication in Github interactions, by default empty
|
||||
* allowing readonly actions.
|
||||
* `config`: The dev-infra configuration containing information about the remote. By default
|
||||
* the dev-infra configuration is loaded with its Github configuration.
|
||||
**/
|
||||
var GitClient = /** @class */ (function () {
|
||||
/**
|
||||
* @param githubToken The github token used for authentication, if provided.
|
||||
* @param _config The configuration, containing the github specific configuration.
|
||||
* @param baseDir The full path to the root of the repository base.
|
||||
*/
|
||||
function GitClient(githubToken, config, baseDir) {
|
||||
this.githubToken = githubToken;
|
||||
/** Whether verbose logging of Git actions should be used. */
|
||||
this.verboseLogging = true;
|
||||
/** The OAuth scopes available for the provided Github token. */
|
||||
this._cachedOauthScopes = null;
|
||||
/**
|
||||
* Regular expression that matches the provided Github token. Used for
|
||||
* sanitizing the token from Git child process output.
|
||||
*/
|
||||
this._githubTokenRegex = null;
|
||||
/** Instance of the Github octokit API. */
|
||||
this.github = new GithubClient(this.githubToken);
|
||||
this.baseDir = baseDir || this.determineBaseDir();
|
||||
this.config = config || getConfig(this.baseDir);
|
||||
this.remoteConfig = this.config.github;
|
||||
this.remoteParams = { owner: this.remoteConfig.owner, repo: this.remoteConfig.name };
|
||||
// If a token has been specified (and is not empty), pass it to the Octokit API and
|
||||
// also create a regular expression that can be used for sanitizing Git command output
|
||||
// so that it does not print the token accidentally.
|
||||
if (typeof githubToken === 'string') {
|
||||
this._githubTokenRegex = new RegExp(githubToken, 'g');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Static method to get the singleton instance of the unauthorized GitClient, creating it if it
|
||||
* has not yet been created.
|
||||
*/
|
||||
GitClient.getInstance = function () {
|
||||
if (!GitClient.unauthenticated) {
|
||||
GitClient.unauthenticated = new GitClient(undefined);
|
||||
}
|
||||
return GitClient.unauthenticated;
|
||||
};
|
||||
/**
|
||||
* Static method to get the singleton instance of the authenticated GitClient if it has been
|
||||
* generated.
|
||||
*/
|
||||
GitClient.getAuthenticatedInstance = function () {
|
||||
if (!GitClient.authenticated) {
|
||||
throw Error('The authenticated GitClient has not yet been generated.');
|
||||
}
|
||||
return GitClient.authenticated;
|
||||
};
|
||||
/** Build the authenticated GitClient instance. */
|
||||
GitClient.authenticateWithToken = function (token) {
|
||||
if (GitClient.authenticated) {
|
||||
throw Error('Cannot generate new authenticated GitClient after one has already been generated.');
|
||||
}
|
||||
GitClient.authenticated = new GitClient(token);
|
||||
};
|
||||
/** Set the verbose logging state of the GitClient instance. */
|
||||
GitClient.prototype.setVerboseLoggingState = function (verbose) {
|
||||
this.verboseLogging = verbose;
|
||||
return this;
|
||||
};
|
||||
/** Executes the given git command. Throws if the command fails. */
|
||||
GitClient.prototype.run = function (args, options) {
|
||||
var result = this.runGraceful(args, options);
|
||||
if (result.status !== 0) {
|
||||
throw new GitCommandError(this, args);
|
||||
}
|
||||
// Omit `status` from the type so that it's obvious that the status is never
|
||||
// non-zero as explained in the method description.
|
||||
return result;
|
||||
};
|
||||
/**
|
||||
* Spawns a given Git command process. Does not throw if the command fails. Additionally,
|
||||
* if there is any stderr output, the output will be printed. This makes it easier to
|
||||
* info failed commands.
|
||||
*/
|
||||
GitClient.prototype.runGraceful = function (args, options) {
|
||||
if (options === void 0) { options = {}; }
|
||||
/** The git command to be run. */
|
||||
var gitCommand = args[0];
|
||||
if (isDryRun() && gitCommand === 'push') {
|
||||
debug("\"git push\" is not able to be run in dryRun mode.");
|
||||
throw new DryRunError();
|
||||
}
|
||||
// To improve the debugging experience in case something fails, we print all executed Git
|
||||
// commands to better understand the git actions occuring. Depending on the command being
|
||||
// executed, this debugging information should be logged at different logging levels.
|
||||
var printFn = (!this.verboseLogging || options.stdio === 'ignore') ? debug : info;
|
||||
// Note that we do not want to print the token if it is contained in the command. It's common
|
||||
// to share errors with others if the tool failed, and we do not want to leak tokens.
|
||||
printFn('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
|
||||
var result = child_process.spawnSync('git', args, tslib.__assign(tslib.__assign({ cwd: this.baseDir, stdio: 'pipe' }, options), {
|
||||
// Encoding is always `utf8` and not overridable. This ensures that this method
|
||||
// always returns `string` as output instead of buffers.
|
||||
encoding: 'utf8' }));
|
||||
if (result.stderr !== null) {
|
||||
// Git sometimes prints the command if it failed. This means that it could
|
||||
// potentially leak the Github token used for accessing the remote. To avoid
|
||||
// printing a token, we sanitize the string before printing the stderr output.
|
||||
process.stderr.write(this.omitGithubTokenFromMessage(result.stderr));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
/** Git URL that resolves to the configured repository. */
|
||||
GitClient.prototype.getRepoGitUrl = function () {
|
||||
return getRepositoryGitUrl(this.remoteConfig, this.githubToken);
|
||||
};
|
||||
/** Whether the given branch contains the specified SHA. */
|
||||
GitClient.prototype.hasCommit = function (branchName, sha) {
|
||||
return this.run(['branch', branchName, '--contains', sha]).stdout !== '';
|
||||
};
|
||||
/** Gets the currently checked out branch or revision. */
|
||||
GitClient.prototype.getCurrentBranchOrRevision = function () {
|
||||
var branchName = this.run(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
|
||||
// If no branch name could be resolved. i.e. `HEAD` has been returned, then Git
|
||||
// is currently in a detached state. In those cases, we just want to return the
|
||||
// currently checked out revision/SHA.
|
||||
if (branchName === 'HEAD') {
|
||||
return this.run(['rev-parse', 'HEAD']).stdout.trim();
|
||||
}
|
||||
return branchName;
|
||||
};
|
||||
/** Gets whether the current Git repository has uncommitted changes. */
|
||||
GitClient.prototype.hasUncommittedChanges = function () {
|
||||
return this.runGraceful(['diff-index', '--quiet', 'HEAD']).status !== 0;
|
||||
};
|
||||
/** Whether the repo has any local changes. */
|
||||
GitClient.prototype.hasLocalChanges = function () {
|
||||
return this.runGraceful(['diff-index', '--quiet', 'HEAD']).status !== 0;
|
||||
};
|
||||
/** Sanitizes a given message by omitting the provided Github token if present. */
|
||||
GitClient.prototype.omitGithubTokenFromMessage = function (value) {
|
||||
// If no token has been defined (i.e. no token regex), we just return the
|
||||
// value as is. There is no secret value that needs to be omitted.
|
||||
if (this._githubTokenRegex === null) {
|
||||
return value;
|
||||
}
|
||||
return value.replace(this._githubTokenRegex, '<TOKEN>');
|
||||
};
|
||||
/**
|
||||
* Checks out a requested branch or revision, optionally cleaning the state of the repository
|
||||
* before attempting the checking. Returns a boolean indicating whether the branch or revision
|
||||
* was cleanly checked out.
|
||||
*/
|
||||
GitClient.prototype.checkout = function (branchOrRevision, cleanState) {
|
||||
if (cleanState) {
|
||||
// Abort any outstanding ams.
|
||||
this.runGraceful(['am', '--abort'], { stdio: 'ignore' });
|
||||
// Abort any outstanding cherry-picks.
|
||||
this.runGraceful(['cherry-pick', '--abort'], { stdio: 'ignore' });
|
||||
// Abort any outstanding rebases.
|
||||
this.runGraceful(['rebase', '--abort'], { stdio: 'ignore' });
|
||||
// Clear any changes in the current repo.
|
||||
this.runGraceful(['reset', '--hard'], { stdio: 'ignore' });
|
||||
}
|
||||
return this.runGraceful(['checkout', branchOrRevision], { stdio: 'ignore' }).status === 0;
|
||||
};
|
||||
/** Gets the latest git tag on the current branch that matches SemVer. */
|
||||
GitClient.prototype.getLatestSemverTag = function () {
|
||||
var semVerOptions = { loose: true };
|
||||
var tags = this.runGraceful(['tag', '--sort=-committerdate', '--merged']).stdout.split('\n');
|
||||
var latestTag = tags.find(function (tag) { return semver.parse(tag, semVerOptions); });
|
||||
if (latestTag === undefined) {
|
||||
throw new Error("Unable to find a SemVer matching tag on \"" + this.getCurrentBranchOrRevision() + "\"");
|
||||
}
|
||||
return new semver.SemVer(latestTag, semVerOptions);
|
||||
};
|
||||
/** Gets the path of the directory for the repository base. */
|
||||
GitClient.prototype.getBaseDir = function () {
|
||||
var previousVerboseLoggingState = this.verboseLogging;
|
||||
this.setVerboseLoggingState(false);
|
||||
var _a = this.runGraceful(['rev-parse', '--show-toplevel']), stdout = _a.stdout, stderr = _a.stderr, status = _a.status;
|
||||
this.setVerboseLoggingState(previousVerboseLoggingState);
|
||||
if (status !== 0) {
|
||||
throw Error("Unable to find the path to the base directory of the repository.\n" +
|
||||
"Was the command run from inside of the repo?\n\n" +
|
||||
("ERROR:\n " + stderr));
|
||||
}
|
||||
return stdout.trim();
|
||||
};
|
||||
/** Retrieve a list of all files in the repostitory changed since the provided shaOrRef. */
|
||||
GitClient.prototype.allChangesFilesSince = function (shaOrRef) {
|
||||
if (shaOrRef === void 0) { shaOrRef = 'HEAD'; }
|
||||
return Array.from(new Set(tslib.__spreadArray(tslib.__spreadArray([], tslib.__read(gitOutputAsArray(this.runGraceful(['diff', '--name-only', '--diff-filter=d', shaOrRef])))), tslib.__read(gitOutputAsArray(this.runGraceful(['ls-files', '--others', '--exclude-standard']))))));
|
||||
};
|
||||
/** Retrieve a list of all files currently staged in the repostitory. */
|
||||
GitClient.prototype.allStagedFiles = function () {
|
||||
return gitOutputAsArray(this.runGraceful(['diff', '--name-only', '--diff-filter=ACM', '--staged']));
|
||||
};
|
||||
/** Retrieve a list of all files tracked in the repostitory. */
|
||||
GitClient.prototype.allFiles = function () {
|
||||
return gitOutputAsArray(this.runGraceful(['ls-files']));
|
||||
};
|
||||
/**
|
||||
* Assert the GitClient instance is using a token with permissions for the all of the
|
||||
* provided OAuth scopes.
|
||||
*/
|
||||
GitClient.prototype.hasOauthScopes = function (testFn) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function () {
|
||||
var scopes, missingScopes, error;
|
||||
return tslib.__generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, this.getAuthScopesForToken()];
|
||||
case 1:
|
||||
scopes = _a.sent();
|
||||
missingScopes = [];
|
||||
// Test Github OAuth scopes and collect missing ones.
|
||||
testFn(scopes, missingScopes);
|
||||
// If no missing scopes are found, return true to indicate all OAuth Scopes are available.
|
||||
if (missingScopes.length === 0) {
|
||||
return [2 /*return*/, true];
|
||||
}
|
||||
error = "The provided <TOKEN> does not have required permissions due to missing scope(s): " +
|
||||
(yellow(missingScopes.join(', ')) + "\n\n") +
|
||||
"Update the token in use at:\n" +
|
||||
(" " + GITHUB_TOKEN_SETTINGS_URL + "\n\n") +
|
||||
("Alternatively, a new token can be created at: " + GITHUB_TOKEN_GENERATE_URL + "\n");
|
||||
return [2 /*return*/, { error: error }];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Retrieve the OAuth scopes for the loaded Github token.
|
||||
**/
|
||||
GitClient.prototype.getAuthScopesForToken = function () {
|
||||
// If the OAuth scopes have already been loaded, return the Promise containing them.
|
||||
if (this._cachedOauthScopes !== null) {
|
||||
return this._cachedOauthScopes;
|
||||
}
|
||||
// OAuth scopes are loaded via the /rate_limit endpoint to prevent
|
||||
// usage of a request against that rate_limit for this lookup.
|
||||
return this._cachedOauthScopes = this.github.rateLimit.get().then(function (_response) {
|
||||
var response = _response;
|
||||
var scopes = response.headers['x-oauth-scopes'] || '';
|
||||
return scopes.split(',').map(function (scope) { return scope.trim(); });
|
||||
});
|
||||
};
|
||||
GitClient.prototype.determineBaseDir = function () {
|
||||
this.setVerboseLoggingState(false);
|
||||
var _a = this.runGraceful(['rev-parse', '--show-toplevel']), stdout = _a.stdout, stderr = _a.stderr, status = _a.status;
|
||||
if (status !== 0) {
|
||||
throw Error("Unable to find the path to the base directory of the repository.\n" +
|
||||
"Was the command run from inside of the repo?\n\n" +
|
||||
("ERROR:\n " + stderr));
|
||||
}
|
||||
this.setVerboseLoggingState(true);
|
||||
return stdout.trim();
|
||||
};
|
||||
return GitClient;
|
||||
}());
|
||||
/**
|
||||
* Takes the output from `GitClient.run` and `GitClient.runGraceful` and returns an array of strings
|
||||
* for each new line. Git commands typically return multiple output values for a command a set of
|
||||
* strings separated by new lines.
|
||||
*
|
||||
* Note: This is specifically created as a locally available function for usage as convience utility
|
||||
* within `GitClient`'s methods to create outputs as array.
|
||||
*/
|
||||
function gitOutputAsArray(gitCommandResult) {
|
||||
return gitCommandResult.stdout.split('\n').map(function (x) { return x.trim(); }).filter(function (x) { return !!x; });
|
||||
}
|
||||
|
||||
/**
|
||||
* @license
|
||||
|
@ -117,21 +571,6 @@ function printToLogFile(logLevel) {
|
|||
var logLevelText = (LOG_LEVELS[logLevel] + ":").padEnd(LOG_LEVEL_COLUMNS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
/**
|
||||
* Runs an given command as child process. By default, child process
|
||||
* output will not be printed.
|
||||
*/
|
||||
function exec(cmd, opts) {
|
||||
return shelljs.exec(cmd, tslib.__assign(tslib.__assign({ silent: true }, opts), { async: false }));
|
||||
}
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
|
@ -164,15 +603,12 @@ function isTsNodeAvailable() {
|
|||
var CONFIG_FILE_PATH = '.ng-dev/config';
|
||||
/** The configuration for ng-dev. */
|
||||
var cachedConfig = null;
|
||||
/**
|
||||
* Get the configuration from the file system, returning the already loaded
|
||||
* copy if it is defined.
|
||||
*/
|
||||
function getConfig() {
|
||||
function getConfig(baseDir) {
|
||||
// If the global config is not defined, load it from the file system.
|
||||
if (cachedConfig === null) {
|
||||
baseDir = baseDir || GitClient.getInstance().getBaseDir();
|
||||
// The full path to the configuration file.
|
||||
var configPath = path.join(getRepoBaseDir(), CONFIG_FILE_PATH);
|
||||
var configPath = path.join(baseDir, CONFIG_FILE_PATH);
|
||||
// Read the configuration and validate it before caching it for the future.
|
||||
cachedConfig = validateCommonConfig(readConfigFile(configPath));
|
||||
}
|
||||
|
@ -253,16 +689,6 @@ function assertNoErrors(errors) {
|
|||
}
|
||||
process.exit(1);
|
||||
}
|
||||
/** Gets the path of the directory for the repository base. */
|
||||
function getRepoBaseDir() {
|
||||
var baseRepoDir = exec("git rev-parse --show-toplevel");
|
||||
if (baseRepoDir.code) {
|
||||
throw Error("Unable to find the path to the base directory of the repository.\n" +
|
||||
"Was the command run from inside of the repo?\n\n" +
|
||||
("ERROR:\n " + baseRepoDir.stderr));
|
||||
}
|
||||
return baseRepoDir.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* @license
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import {SpawnSyncReturns} from 'child_process';
|
||||
|
||||
import * as console from '../../utils/console';
|
||||
import {GitClient} from '../../utils/git';
|
||||
import {GitClient} from '../../utils/git/index';
|
||||
import {installVirtualGitClientSpies, mockNgDevConfig} from '../../utils/testing';
|
||||
|
||||
import {G3Module, G3StatsData} from './g3';
|
||||
|
|
|
@ -10,7 +10,6 @@ import {existsSync, readFileSync} from 'fs';
|
|||
import * as multimatch from 'multimatch';
|
||||
import {join} from 'path';
|
||||
import {parse as parseYaml} from 'yaml';
|
||||
import {getRepoBaseDir} from '../../utils/config';
|
||||
import {bold, debug, error, info} from '../../utils/console';
|
||||
|
||||
import {BaseModule} from './base';
|
||||
|
@ -121,7 +120,7 @@ export class G3Module extends BaseModule<G3StatsData|void> {
|
|||
|
||||
|
||||
private getG3FileIncludeAndExcludeLists() {
|
||||
const angularRobotFilePath = join(getRepoBaseDir(), '.github/angular-robot.yml');
|
||||
const angularRobotFilePath = join(this.git.baseDir, '.github/angular-robot.yml');
|
||||
if (!existsSync(angularRobotFilePath)) {
|
||||
debug('No angular robot configuration file exists, skipping.');
|
||||
return null;
|
||||
|
|
|
@ -8,15 +8,16 @@
|
|||
import {readFileSync} from 'fs';
|
||||
import {resolve} from 'path';
|
||||
|
||||
import {getRepoBaseDir} from '../../utils/config';
|
||||
import {error, green, info, log, red, yellow} from '../../utils/console';
|
||||
import {GitClient} from '../../utils/git/index';
|
||||
|
||||
import {deleteCommitMessageDraft, saveCommitMessageDraft} from '../restore-commit-message/commit-message-draft';
|
||||
import {printValidationErrors, validateCommitMessage} from '../validate';
|
||||
|
||||
/** Validate commit message at the provided file path. */
|
||||
export function validateFile(filePath: string, isErrorMode: boolean) {
|
||||
const commitMessage = readFileSync(resolve(getRepoBaseDir(), filePath), 'utf8');
|
||||
const git = GitClient.getInstance();
|
||||
const commitMessage = readFileSync(resolve(git.baseDir, filePath), 'utf8');
|
||||
const {valid, errors} = validateCommitMessage(commitMessage);
|
||||
if (valid) {
|
||||
info(`${green('√')} Valid commit message`);
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
import {allChangedFilesSince, allFiles, allStagedFiles} from '../utils/repo-files';
|
||||
import {GitClient} from '../utils/git/index';
|
||||
|
||||
import {checkFiles, formatFiles} from './format';
|
||||
|
||||
|
@ -25,7 +24,8 @@ export function buildFormatParser(localYargs: yargs.Argv) {
|
|||
'all', 'Run the formatter on all files in the repository', args => args,
|
||||
({check}) => {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(allFiles());
|
||||
const allFiles = GitClient.getInstance().allFiles();
|
||||
executionCmd(allFiles);
|
||||
})
|
||||
.command(
|
||||
'changed [shaOrRef]', 'Run the formatter on files changed since the provided sha/ref',
|
||||
|
@ -33,13 +33,15 @@ export function buildFormatParser(localYargs: yargs.Argv) {
|
|||
({shaOrRef, check}) => {
|
||||
const sha = shaOrRef || 'master';
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(allChangedFilesSince(sha));
|
||||
const allChangedFilesSince = GitClient.getInstance().allChangesFilesSince(sha);
|
||||
executionCmd(allChangedFilesSince);
|
||||
})
|
||||
.command(
|
||||
'staged', 'Run the formatter on all staged files', args => args,
|
||||
({check}) => {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(allStagedFiles());
|
||||
const allStagedFiles = GitClient.getInstance().allStagedFiles();
|
||||
executionCmd(allStagedFiles);
|
||||
})
|
||||
.command(
|
||||
'files <files..>', 'Run the formatter on provided files',
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {GitClient} from '../../utils/git/index';
|
||||
import {FormatConfig} from '../config';
|
||||
|
||||
// A callback to determine if the formatter run found a failure in formatting.
|
||||
|
@ -24,6 +25,7 @@ interface FormatterActionMetadata {
|
|||
* The base class for formatters to run against provided files.
|
||||
*/
|
||||
export abstract class Formatter {
|
||||
protected git = GitClient.getInstance();
|
||||
/**
|
||||
* The name of the formatter, this is used for identification in logging and for enabling and
|
||||
* configuring the formatter in the config.
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import {join} from 'path';
|
||||
|
||||
import {getRepoBaseDir} from '../../utils/config';
|
||||
import {error} from '../../utils/console';
|
||||
|
||||
import {Formatter} from './base-formatter';
|
||||
|
@ -19,7 +18,7 @@ import {Formatter} from './base-formatter';
|
|||
export class Buildifier extends Formatter {
|
||||
name = 'buildifier';
|
||||
|
||||
binaryFilePath = join(getRepoBaseDir(), 'node_modules/.bin/buildifier');
|
||||
binaryFilePath = join(this.git.baseDir, 'node_modules/.bin/buildifier');
|
||||
|
||||
defaultFileMatcher = ['**/*.bzl', '**/BUILD.bazel', '**/WORKSPACE', '**/BUILD'];
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import {join} from 'path';
|
||||
|
||||
import {getRepoBaseDir} from '../../utils/config';
|
||||
import {error} from '../../utils/console';
|
||||
|
||||
import {Formatter} from './base-formatter';
|
||||
|
@ -19,7 +18,7 @@ import {Formatter} from './base-formatter';
|
|||
export class ClangFormat extends Formatter {
|
||||
name = 'clang-format';
|
||||
|
||||
binaryFilePath = join(getRepoBaseDir(), 'node_modules/.bin/clang-format');
|
||||
binaryFilePath = join(this.git.baseDir, 'node_modules/.bin/clang-format');
|
||||
|
||||
defaultFileMatcher = ['**/*.{t,j}s'];
|
||||
|
||||
|
|
|
@ -9,13 +9,12 @@ var chalk = _interopDefault(require('chalk'));
|
|||
var fs = require('fs');
|
||||
var inquirer = require('inquirer');
|
||||
var path = require('path');
|
||||
var shelljs = require('shelljs');
|
||||
var url = require('url');
|
||||
var child_process = require('child_process');
|
||||
var semver = require('semver');
|
||||
var graphql = require('@octokit/graphql');
|
||||
var Octokit = require('@octokit/rest');
|
||||
var typedGraphqlify = require('typed-graphqlify');
|
||||
var url = require('url');
|
||||
var fetch = _interopDefault(require('node-fetch'));
|
||||
var multimatch = require('multimatch');
|
||||
var yaml = require('yaml');
|
||||
|
@ -23,27 +22,13 @@ var conventionalCommitsParser = require('conventional-commits-parser');
|
|||
var gitCommits_ = require('git-raw-commits');
|
||||
var cliProgress = require('cli-progress');
|
||||
var os = require('os');
|
||||
var shelljs = require('shelljs');
|
||||
var minimatch = require('minimatch');
|
||||
var ora = require('ora');
|
||||
require('ejs');
|
||||
var glob = require('glob');
|
||||
var ts = require('typescript');
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
/**
|
||||
* Runs an given command as child process. By default, child process
|
||||
* output will not be printed.
|
||||
*/
|
||||
function exec(cmd, opts) {
|
||||
return shelljs.exec(cmd, tslib.__assign(tslib.__assign({ silent: true }, opts), { async: false }));
|
||||
}
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
|
@ -83,15 +68,12 @@ var cachedConfig = null;
|
|||
var USER_CONFIG_FILE_PATH = '.ng-dev.user';
|
||||
/** The local user configuration for ng-dev. */
|
||||
var userConfig = null;
|
||||
/**
|
||||
* Get the configuration from the file system, returning the already loaded
|
||||
* copy if it is defined.
|
||||
*/
|
||||
function getConfig() {
|
||||
function getConfig(baseDir) {
|
||||
// If the global config is not defined, load it from the file system.
|
||||
if (cachedConfig === null) {
|
||||
baseDir = baseDir || GitClient.getInstance().getBaseDir();
|
||||
// The full path to the configuration file.
|
||||
var configPath = path.join(getRepoBaseDir(), CONFIG_FILE_PATH);
|
||||
var configPath = path.join(baseDir, CONFIG_FILE_PATH);
|
||||
// Read the configuration and validate it before caching it for the future.
|
||||
cachedConfig = validateCommonConfig(readConfigFile(configPath));
|
||||
}
|
||||
|
@ -172,16 +154,6 @@ function assertNoErrors(errors) {
|
|||
}
|
||||
process.exit(1);
|
||||
}
|
||||
/** Gets the path of the directory for the repository base. */
|
||||
function getRepoBaseDir() {
|
||||
var baseRepoDir = exec("git rev-parse --show-toplevel");
|
||||
if (baseRepoDir.code) {
|
||||
throw Error("Unable to find the path to the base directory of the repository.\n" +
|
||||
"Was the command run from inside of the repo?\n\n" +
|
||||
("ERROR:\n " + baseRepoDir.stderr));
|
||||
}
|
||||
return baseRepoDir.trim();
|
||||
}
|
||||
/**
|
||||
* Get the local user configuration from the file system, returning the already loaded copy if it is
|
||||
* defined.
|
||||
|
@ -192,8 +164,9 @@ function getRepoBaseDir() {
|
|||
function getUserConfig() {
|
||||
// If the global config is not defined, load it from the file system.
|
||||
if (userConfig === null) {
|
||||
var git = GitClient.getInstance();
|
||||
// The full path to the configuration file.
|
||||
var configPath = path.join(getRepoBaseDir(), USER_CONFIG_FILE_PATH);
|
||||
var configPath = path.join(git.baseDir, USER_CONFIG_FILE_PATH);
|
||||
// Set the global config object.
|
||||
userConfig = readConfigFile(configPath, true);
|
||||
}
|
||||
|
@ -202,209 +175,6 @@ function getUserConfig() {
|
|||
return tslib.__assign({}, userConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
/** Reexport of chalk colors for convenient access. */
|
||||
var red = chalk.red;
|
||||
var green = chalk.green;
|
||||
var yellow = chalk.yellow;
|
||||
var bold = chalk.bold;
|
||||
var blue = chalk.blue;
|
||||
/** Prompts the user with a confirmation question and a specified message. */
|
||||
function promptConfirm(message, defaultValue) {
|
||||
if (defaultValue === void 0) { defaultValue = false; }
|
||||
return tslib.__awaiter(this, void 0, void 0, function () {
|
||||
return tslib.__generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'result',
|
||||
message: message,
|
||||
default: defaultValue,
|
||||
})];
|
||||
case 1: return [2 /*return*/, (_a.sent())
|
||||
.result];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Supported levels for logging functions.
|
||||
*
|
||||
* Levels are mapped to numbers to represent a hierarchy of logging levels.
|
||||
*/
|
||||
var LOG_LEVELS;
|
||||
(function (LOG_LEVELS) {
|
||||
LOG_LEVELS[LOG_LEVELS["SILENT"] = 0] = "SILENT";
|
||||
LOG_LEVELS[LOG_LEVELS["ERROR"] = 1] = "ERROR";
|
||||
LOG_LEVELS[LOG_LEVELS["WARN"] = 2] = "WARN";
|
||||
LOG_LEVELS[LOG_LEVELS["LOG"] = 3] = "LOG";
|
||||
LOG_LEVELS[LOG_LEVELS["INFO"] = 4] = "INFO";
|
||||
LOG_LEVELS[LOG_LEVELS["DEBUG"] = 5] = "DEBUG";
|
||||
})(LOG_LEVELS || (LOG_LEVELS = {}));
|
||||
/** Default log level for the tool. */
|
||||
var DEFAULT_LOG_LEVEL = LOG_LEVELS.INFO;
|
||||
/** Write to the console for at INFO logging level */
|
||||
var info = buildLogLevelFunction(function () { return console.info; }, LOG_LEVELS.INFO);
|
||||
/** Write to the console for at ERROR logging level */
|
||||
var error = buildLogLevelFunction(function () { return console.error; }, LOG_LEVELS.ERROR);
|
||||
/** Write to the console for at DEBUG logging level */
|
||||
var debug = buildLogLevelFunction(function () { return console.debug; }, LOG_LEVELS.DEBUG);
|
||||
/** Write to the console for at LOG logging level */
|
||||
// tslint:disable-next-line: no-console
|
||||
var log = buildLogLevelFunction(function () { return console.log; }, LOG_LEVELS.LOG);
|
||||
/** Write to the console for at WARN logging level */
|
||||
var warn = buildLogLevelFunction(function () { return console.warn; }, LOG_LEVELS.WARN);
|
||||
/** Build an instance of a logging function for the provided level. */
|
||||
function buildLogLevelFunction(loadCommand, level) {
|
||||
/** Write to stdout for the LOG_LEVEL. */
|
||||
var loggingFunction = function () {
|
||||
var text = [];
|
||||
for (var _i = 0; _i < arguments.length; _i++) {
|
||||
text[_i] = arguments[_i];
|
||||
}
|
||||
runConsoleCommand.apply(void 0, tslib.__spreadArray([loadCommand, level], tslib.__read(text)));
|
||||
};
|
||||
/** Start a group at the LOG_LEVEL, optionally starting it as collapsed. */
|
||||
loggingFunction.group = function (text, collapsed) {
|
||||
if (collapsed === void 0) { collapsed = false; }
|
||||
var command = collapsed ? console.groupCollapsed : console.group;
|
||||
runConsoleCommand(function () { return command; }, level, text);
|
||||
};
|
||||
/** End the group at the LOG_LEVEL. */
|
||||
loggingFunction.groupEnd = function () {
|
||||
runConsoleCommand(function () { return console.groupEnd; }, level);
|
||||
};
|
||||
return loggingFunction;
|
||||
}
|
||||
/**
|
||||
* Run the console command provided, if the environments logging level greater than the
|
||||
* provided logging level.
|
||||
*
|
||||
* The loadCommand takes in a function which is called to retrieve the console.* function
|
||||
* to allow for jasmine spies to still work in testing. Without this method of retrieval
|
||||
* the console.* function, the function is saved into the closure of the created logging
|
||||
* function before jasmine can spy.
|
||||
*/
|
||||
function runConsoleCommand(loadCommand, logLevel) {
|
||||
var text = [];
|
||||
for (var _i = 2; _i < arguments.length; _i++) {
|
||||
text[_i - 2] = arguments[_i];
|
||||
}
|
||||
if (getLogLevel() >= logLevel) {
|
||||
loadCommand().apply(void 0, tslib.__spreadArray([], tslib.__read(text)));
|
||||
}
|
||||
printToLogFile.apply(void 0, tslib.__spreadArray([logLevel], tslib.__read(text)));
|
||||
}
|
||||
/**
|
||||
* Retrieve the log level from environment variables, if the value found
|
||||
* based on the LOG_LEVEL environment variable is undefined, return the default
|
||||
* logging level.
|
||||
*/
|
||||
function getLogLevel() {
|
||||
var logLevelEnvValue = (process.env["LOG_LEVEL"] || '').toUpperCase();
|
||||
var logLevel = LOG_LEVELS[logLevelEnvValue];
|
||||
if (logLevel === undefined) {
|
||||
return DEFAULT_LOG_LEVEL;
|
||||
}
|
||||
return logLevel;
|
||||
}
|
||||
/** All text to write to the log file. */
|
||||
var LOGGED_TEXT = '';
|
||||
/** Whether file logging as been enabled. */
|
||||
var FILE_LOGGING_ENABLED = false;
|
||||
/**
|
||||
* The number of columns used in the prepended log level information on each line of the logging
|
||||
* output file.
|
||||
*/
|
||||
var LOG_LEVEL_COLUMNS = 7;
|
||||
/**
|
||||
* Enable writing the logged outputs to the log file on process exit, sets initial lines from the
|
||||
* command execution, containing information about the timing and command parameters.
|
||||
*
|
||||
* This is expected to be called only once during a command run, and should be called by the
|
||||
* middleware of yargs to enable the file logging before the rest of the command parsing and
|
||||
* response is executed.
|
||||
*/
|
||||
function captureLogOutputForCommand(argv) {
|
||||
if (FILE_LOGGING_ENABLED) {
|
||||
throw Error('`captureLogOutputForCommand` cannot be called multiple times');
|
||||
}
|
||||
/** The date time used for timestamping when the command was invoked. */
|
||||
var now = new Date();
|
||||
/** Header line to separate command runs in log files. */
|
||||
var headerLine = Array(100).fill('#').join('');
|
||||
LOGGED_TEXT += headerLine + "\nCommand: " + argv.$0 + " " + argv._.join(' ') + "\nRan at: " + now + "\n";
|
||||
// On process exit, write the logged output to the appropriate log files
|
||||
process.on('exit', function (code) {
|
||||
LOGGED_TEXT += headerLine + "\n";
|
||||
LOGGED_TEXT += "Command ran in " + (new Date().getTime() - now.getTime()) + "ms\n";
|
||||
LOGGED_TEXT += "Exit Code: " + code + "\n";
|
||||
/** Path to the log file location. */
|
||||
var logFilePath = path.join(getRepoBaseDir(), '.ng-dev.log');
|
||||
// Strip ANSI escape codes from log outputs.
|
||||
LOGGED_TEXT = LOGGED_TEXT.replace(/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]/g, '');
|
||||
fs.writeFileSync(logFilePath, LOGGED_TEXT);
|
||||
// For failure codes greater than 1, the new logged lines should be written to a specific log
|
||||
// file for the command run failure.
|
||||
if (code > 1) {
|
||||
var logFileName = ".ng-dev.err-" + now.getTime() + ".log";
|
||||
console.error("Exit code: " + code + ". Writing full log to " + logFileName);
|
||||
fs.writeFileSync(path.join(getRepoBaseDir(), logFileName), LOGGED_TEXT);
|
||||
}
|
||||
});
|
||||
// Mark file logging as enabled to prevent the function from executing multiple times.
|
||||
FILE_LOGGING_ENABLED = true;
|
||||
}
|
||||
/** Write the provided text to the log file, prepending each line with the log level. */
|
||||
function printToLogFile(logLevel) {
|
||||
var text = [];
|
||||
for (var _i = 1; _i < arguments.length; _i++) {
|
||||
text[_i - 1] = arguments[_i];
|
||||
}
|
||||
var logLevelText = (LOG_LEVELS[logLevel] + ":").padEnd(LOG_LEVEL_COLUMNS);
|
||||
LOGGED_TEXT += text.join(' ').split('\n').map(function (l) { return logLevelText + " " + l + "\n"; }).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
/** URL to the Github page where personal access tokens can be managed. */
|
||||
var GITHUB_TOKEN_SETTINGS_URL = 'https://github.com/settings/tokens';
|
||||
/** URL to the Github page where personal access tokens can be generated. */
|
||||
var GITHUB_TOKEN_GENERATE_URL = 'https://github.com/settings/tokens/new';
|
||||
/** Adds the provided token to the given Github HTTPs remote url. */
|
||||
function addTokenToGitHttpsUrl(githubHttpsUrl, token) {
|
||||
var url$1 = new url.URL(githubHttpsUrl);
|
||||
url$1.username = token;
|
||||
return url$1.href;
|
||||
}
|
||||
/** Gets the repository Git URL for the given github config. */
|
||||
function getRepositoryGitUrl(config, githubToken) {
|
||||
if (config.useSsh) {
|
||||
return "git@github.com:" + config.owner + "/" + config.name + ".git";
|
||||
}
|
||||
var baseHttpUrl = "https://github.com/" + config.owner + "/" + config.name + ".git";
|
||||
if (githubToken !== undefined) {
|
||||
return addTokenToGitHttpsUrl(baseHttpUrl, githubToken);
|
||||
}
|
||||
return baseHttpUrl;
|
||||
}
|
||||
/** Gets a Github URL that refers to a list of recent commits within a specified branch. */
|
||||
function getListCommitsInBranchUrl(_a, branchName) {
|
||||
var remoteParams = _a.remoteParams;
|
||||
return "https://github.com/" + remoteParams.owner + "/" + remoteParams.repo + "/commits/" + branchName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
|
@ -531,6 +301,40 @@ var GithubClient = /** @class */ (function (_super) {
|
|||
return GithubClient;
|
||||
}(Octokit));
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
/** URL to the Github page where personal access tokens can be managed. */
|
||||
var GITHUB_TOKEN_SETTINGS_URL = 'https://github.com/settings/tokens';
|
||||
/** URL to the Github page where personal access tokens can be generated. */
|
||||
var GITHUB_TOKEN_GENERATE_URL = 'https://github.com/settings/tokens/new';
|
||||
/** Adds the provided token to the given Github HTTPs remote url. */
|
||||
function addTokenToGitHttpsUrl(githubHttpsUrl, token) {
|
||||
var url$1 = new url.URL(githubHttpsUrl);
|
||||
url$1.username = token;
|
||||
return url$1.href;
|
||||
}
|
||||
/** Gets the repository Git URL for the given github config. */
|
||||
function getRepositoryGitUrl(config, githubToken) {
|
||||
if (config.useSsh) {
|
||||
return "git@github.com:" + config.owner + "/" + config.name + ".git";
|
||||
}
|
||||
var baseHttpUrl = "https://github.com/" + config.owner + "/" + config.name + ".git";
|
||||
if (githubToken !== undefined) {
|
||||
return addTokenToGitHttpsUrl(baseHttpUrl, githubToken);
|
||||
}
|
||||
return baseHttpUrl;
|
||||
}
|
||||
/** Gets a Github URL that refers to a list of recent commits within a specified branch. */
|
||||
function getListCommitsInBranchUrl(_a, branchName) {
|
||||
var remoteParams = _a.remoteParams;
|
||||
return "https://github.com/" + remoteParams.owner + "/" + remoteParams.repo + "/commits/" + branchName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
|
@ -565,14 +369,10 @@ var GitClient = /** @class */ (function () {
|
|||
/**
|
||||
* @param githubToken The github token used for authentication, if provided.
|
||||
* @param _config The configuration, containing the github specific configuration.
|
||||
* @param _projectRoot The full path to the root of the repository base.
|
||||
* @param baseDir The full path to the root of the repository base.
|
||||
*/
|
||||
function GitClient(githubToken, _config, _projectRoot) {
|
||||
if (_config === void 0) { _config = getConfig(); }
|
||||
if (_projectRoot === void 0) { _projectRoot = getRepoBaseDir(); }
|
||||
function GitClient(githubToken, config, baseDir) {
|
||||
this.githubToken = githubToken;
|
||||
this._config = _config;
|
||||
this._projectRoot = _projectRoot;
|
||||
/** Whether verbose logging of Git actions should be used. */
|
||||
this.verboseLogging = true;
|
||||
/** The OAuth scopes available for the provided Github token. */
|
||||
|
@ -582,12 +382,12 @@ var GitClient = /** @class */ (function () {
|
|||
* sanitizing the token from Git child process output.
|
||||
*/
|
||||
this._githubTokenRegex = null;
|
||||
/** Short-hand for accessing the default remote configuration. */
|
||||
this.remoteConfig = this._config.github;
|
||||
/** Octokit request parameters object for targeting the configured remote. */
|
||||
this.remoteParams = { owner: this.remoteConfig.owner, repo: this.remoteConfig.name };
|
||||
/** Instance of the authenticated Github octokit API. */
|
||||
/** Instance of the Github octokit API. */
|
||||
this.github = new GithubClient(this.githubToken);
|
||||
this.baseDir = baseDir || this.determineBaseDir();
|
||||
this.config = config || getConfig(this.baseDir);
|
||||
this.remoteConfig = this.config.github;
|
||||
this.remoteParams = { owner: this.remoteConfig.owner, repo: this.remoteConfig.name };
|
||||
// If a token has been specified (and is not empty), pass it to the Octokit API and
|
||||
// also create a regular expression that can be used for sanitizing Git command output
|
||||
// so that it does not print the token accidentally.
|
||||
|
@ -657,7 +457,7 @@ var GitClient = /** @class */ (function () {
|
|||
// Note that we do not want to print the token if it is contained in the command. It's common
|
||||
// to share errors with others if the tool failed, and we do not want to leak tokens.
|
||||
printFn('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
|
||||
var result = child_process.spawnSync('git', args, tslib.__assign(tslib.__assign({ cwd: this._projectRoot, stdio: 'pipe' }, options), {
|
||||
var result = child_process.spawnSync('git', args, tslib.__assign(tslib.__assign({ cwd: this.baseDir, stdio: 'pipe' }, options), {
|
||||
// Encoding is always `utf8` and not overridable. This ensures that this method
|
||||
// always returns `string` as output instead of buffers.
|
||||
encoding: 'utf8' }));
|
||||
|
@ -733,6 +533,32 @@ var GitClient = /** @class */ (function () {
|
|||
}
|
||||
return new semver.SemVer(latestTag, semVerOptions);
|
||||
};
|
||||
/** Gets the path of the directory for the repository base. */
|
||||
GitClient.prototype.getBaseDir = function () {
|
||||
var previousVerboseLoggingState = this.verboseLogging;
|
||||
this.setVerboseLoggingState(false);
|
||||
var _a = this.runGraceful(['rev-parse', '--show-toplevel']), stdout = _a.stdout, stderr = _a.stderr, status = _a.status;
|
||||
this.setVerboseLoggingState(previousVerboseLoggingState);
|
||||
if (status !== 0) {
|
||||
throw Error("Unable to find the path to the base directory of the repository.\n" +
|
||||
"Was the command run from inside of the repo?\n\n" +
|
||||
("ERROR:\n " + stderr));
|
||||
}
|
||||
return stdout.trim();
|
||||
};
|
||||
/** Retrieve a list of all files in the repostitory changed since the provided shaOrRef. */
|
||||
GitClient.prototype.allChangesFilesSince = function (shaOrRef) {
|
||||
if (shaOrRef === void 0) { shaOrRef = 'HEAD'; }
|
||||
return Array.from(new Set(tslib.__spreadArray(tslib.__spreadArray([], tslib.__read(gitOutputAsArray(this.runGraceful(['diff', '--name-only', '--diff-filter=d', shaOrRef])))), tslib.__read(gitOutputAsArray(this.runGraceful(['ls-files', '--others', '--exclude-standard']))))));
|
||||
};
|
||||
/** Retrieve a list of all files currently staged in the repostitory. */
|
||||
GitClient.prototype.allStagedFiles = function () {
|
||||
return gitOutputAsArray(this.runGraceful(['diff', '--name-only', '--diff-filter=ACM', '--staged']));
|
||||
};
|
||||
/** Retrieve a list of all files tracked in the repostitory. */
|
||||
GitClient.prototype.allFiles = function () {
|
||||
return gitOutputAsArray(this.runGraceful(['ls-files']));
|
||||
};
|
||||
/**
|
||||
* Assert the GitClient instance is using a token with permissions for the all of the
|
||||
* provided OAuth scopes.
|
||||
|
@ -778,8 +604,200 @@ var GitClient = /** @class */ (function () {
|
|||
return scopes.split(',').map(function (scope) { return scope.trim(); });
|
||||
});
|
||||
};
|
||||
GitClient.prototype.determineBaseDir = function () {
|
||||
this.setVerboseLoggingState(false);
|
||||
var _a = this.runGraceful(['rev-parse', '--show-toplevel']), stdout = _a.stdout, stderr = _a.stderr, status = _a.status;
|
||||
if (status !== 0) {
|
||||
throw Error("Unable to find the path to the base directory of the repository.\n" +
|
||||
"Was the command run from inside of the repo?\n\n" +
|
||||
("ERROR:\n " + stderr));
|
||||
}
|
||||
this.setVerboseLoggingState(true);
|
||||
return stdout.trim();
|
||||
};
|
||||
return GitClient;
|
||||
}());
|
||||
/**
|
||||
* Takes the output from `GitClient.run` and `GitClient.runGraceful` and returns an array of strings
|
||||
* for each new line. Git commands typically return multiple output values for a command a set of
|
||||
* strings separated by new lines.
|
||||
*
|
||||
* Note: This is specifically created as a locally available function for usage as convience utility
|
||||
* within `GitClient`'s methods to create outputs as array.
|
||||
*/
|
||||
function gitOutputAsArray(gitCommandResult) {
|
||||
return gitCommandResult.stdout.split('\n').map(function (x) { return x.trim(); }).filter(function (x) { return !!x; });
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
/** Reexport of chalk colors for convenient access. */
|
||||
var red = chalk.red;
|
||||
var green = chalk.green;
|
||||
var yellow = chalk.yellow;
|
||||
var bold = chalk.bold;
|
||||
var blue = chalk.blue;
|
||||
/** Prompts the user with a confirmation question and a specified message. */
|
||||
function promptConfirm(message, defaultValue) {
|
||||
if (defaultValue === void 0) { defaultValue = false; }
|
||||
return tslib.__awaiter(this, void 0, void 0, function () {
|
||||
return tslib.__generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'result',
|
||||
message: message,
|
||||
default: defaultValue,
|
||||
})];
|
||||
case 1: return [2 /*return*/, (_a.sent())
|
||||
.result];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Supported levels for logging functions.
|
||||
*
|
||||
* Levels are mapped to numbers to represent a hierarchy of logging levels.
|
||||
*/
|
||||
var LOG_LEVELS;
|
||||
(function (LOG_LEVELS) {
|
||||
LOG_LEVELS[LOG_LEVELS["SILENT"] = 0] = "SILENT";
|
||||
LOG_LEVELS[LOG_LEVELS["ERROR"] = 1] = "ERROR";
|
||||
LOG_LEVELS[LOG_LEVELS["WARN"] = 2] = "WARN";
|
||||
LOG_LEVELS[LOG_LEVELS["LOG"] = 3] = "LOG";
|
||||
LOG_LEVELS[LOG_LEVELS["INFO"] = 4] = "INFO";
|
||||
LOG_LEVELS[LOG_LEVELS["DEBUG"] = 5] = "DEBUG";
|
||||
})(LOG_LEVELS || (LOG_LEVELS = {}));
|
||||
/** Default log level for the tool. */
|
||||
var DEFAULT_LOG_LEVEL = LOG_LEVELS.INFO;
|
||||
/** Write to the console for at INFO logging level */
|
||||
var info = buildLogLevelFunction(function () { return console.info; }, LOG_LEVELS.INFO);
|
||||
/** Write to the console for at ERROR logging level */
|
||||
var error = buildLogLevelFunction(function () { return console.error; }, LOG_LEVELS.ERROR);
|
||||
/** Write to the console for at DEBUG logging level */
|
||||
var debug = buildLogLevelFunction(function () { return console.debug; }, LOG_LEVELS.DEBUG);
|
||||
/** Write to the console for at LOG logging level */
|
||||
// tslint:disable-next-line: no-console
|
||||
var log = buildLogLevelFunction(function () { return console.log; }, LOG_LEVELS.LOG);
|
||||
/** Write to the console for at WARN logging level */
|
||||
var warn = buildLogLevelFunction(function () { return console.warn; }, LOG_LEVELS.WARN);
|
||||
/** Build an instance of a logging function for the provided level. */
|
||||
function buildLogLevelFunction(loadCommand, level) {
|
||||
/** Write to stdout for the LOG_LEVEL. */
|
||||
var loggingFunction = function () {
|
||||
var text = [];
|
||||
for (var _i = 0; _i < arguments.length; _i++) {
|
||||
text[_i] = arguments[_i];
|
||||
}
|
||||
runConsoleCommand.apply(void 0, tslib.__spreadArray([loadCommand, level], tslib.__read(text)));
|
||||
};
|
||||
/** Start a group at the LOG_LEVEL, optionally starting it as collapsed. */
|
||||
loggingFunction.group = function (text, collapsed) {
|
||||
if (collapsed === void 0) { collapsed = false; }
|
||||
var command = collapsed ? console.groupCollapsed : console.group;
|
||||
runConsoleCommand(function () { return command; }, level, text);
|
||||
};
|
||||
/** End the group at the LOG_LEVEL. */
|
||||
loggingFunction.groupEnd = function () {
|
||||
runConsoleCommand(function () { return console.groupEnd; }, level);
|
||||
};
|
||||
return loggingFunction;
|
||||
}
|
||||
/**
|
||||
* Run the console command provided, if the environments logging level greater than the
|
||||
* provided logging level.
|
||||
*
|
||||
* The loadCommand takes in a function which is called to retrieve the console.* function
|
||||
* to allow for jasmine spies to still work in testing. Without this method of retrieval
|
||||
* the console.* function, the function is saved into the closure of the created logging
|
||||
* function before jasmine can spy.
|
||||
*/
|
||||
function runConsoleCommand(loadCommand, logLevel) {
|
||||
var text = [];
|
||||
for (var _i = 2; _i < arguments.length; _i++) {
|
||||
text[_i - 2] = arguments[_i];
|
||||
}
|
||||
if (getLogLevel() >= logLevel) {
|
||||
loadCommand().apply(void 0, tslib.__spreadArray([], tslib.__read(text)));
|
||||
}
|
||||
printToLogFile.apply(void 0, tslib.__spreadArray([logLevel], tslib.__read(text)));
|
||||
}
|
||||
/**
|
||||
* Retrieve the log level from environment variables, if the value found
|
||||
* based on the LOG_LEVEL environment variable is undefined, return the default
|
||||
* logging level.
|
||||
*/
|
||||
function getLogLevel() {
|
||||
var logLevelEnvValue = (process.env["LOG_LEVEL"] || '').toUpperCase();
|
||||
var logLevel = LOG_LEVELS[logLevelEnvValue];
|
||||
if (logLevel === undefined) {
|
||||
return DEFAULT_LOG_LEVEL;
|
||||
}
|
||||
return logLevel;
|
||||
}
|
||||
/** All text to write to the log file. */
|
||||
var LOGGED_TEXT = '';
|
||||
/** Whether file logging as been enabled. */
|
||||
var FILE_LOGGING_ENABLED = false;
|
||||
/**
|
||||
* The number of columns used in the prepended log level information on each line of the logging
|
||||
* output file.
|
||||
*/
|
||||
var LOG_LEVEL_COLUMNS = 7;
|
||||
/**
|
||||
* Enable writing the logged outputs to the log file on process exit, sets initial lines from the
|
||||
* command execution, containing information about the timing and command parameters.
|
||||
*
|
||||
* This is expected to be called only once during a command run, and should be called by the
|
||||
* middleware of yargs to enable the file logging before the rest of the command parsing and
|
||||
* response is executed.
|
||||
*/
|
||||
function captureLogOutputForCommand(argv) {
|
||||
if (FILE_LOGGING_ENABLED) {
|
||||
throw Error('`captureLogOutputForCommand` cannot be called multiple times');
|
||||
}
|
||||
var git = GitClient.getInstance();
|
||||
/** The date time used for timestamping when the command was invoked. */
|
||||
var now = new Date();
|
||||
/** Header line to separate command runs in log files. */
|
||||
var headerLine = Array(100).fill('#').join('');
|
||||
LOGGED_TEXT += headerLine + "\nCommand: " + argv.$0 + " " + argv._.join(' ') + "\nRan at: " + now + "\n";
|
||||
// On process exit, write the logged output to the appropriate log files
|
||||
process.on('exit', function (code) {
|
||||
LOGGED_TEXT += headerLine + "\n";
|
||||
LOGGED_TEXT += "Command ran in " + (new Date().getTime() - now.getTime()) + "ms\n";
|
||||
LOGGED_TEXT += "Exit Code: " + code + "\n";
|
||||
/** Path to the log file location. */
|
||||
var logFilePath = path.join(git.baseDir, '.ng-dev.log');
|
||||
// Strip ANSI escape codes from log outputs.
|
||||
LOGGED_TEXT = LOGGED_TEXT.replace(/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]/g, '');
|
||||
fs.writeFileSync(logFilePath, LOGGED_TEXT);
|
||||
// For failure codes greater than 1, the new logged lines should be written to a specific log
|
||||
// file for the command run failure.
|
||||
if (code > 1) {
|
||||
var logFileName = ".ng-dev.err-" + now.getTime() + ".log";
|
||||
console.error("Exit code: " + code + ". Writing full log to " + logFileName);
|
||||
fs.writeFileSync(path.join(git.baseDir, logFileName), LOGGED_TEXT);
|
||||
}
|
||||
});
|
||||
// Mark file logging as enabled to prevent the function from executing multiple times.
|
||||
FILE_LOGGING_ENABLED = true;
|
||||
}
|
||||
/** Write the provided text to the log file, prepending each line with the log level. */
|
||||
function printToLogFile(logLevel) {
|
||||
var text = [];
|
||||
for (var _i = 1; _i < arguments.length; _i++) {
|
||||
text[_i - 1] = arguments[_i];
|
||||
}
|
||||
var logLevelText = (LOG_LEVELS[logLevel] + ":").padEnd(LOG_LEVEL_COLUMNS);
|
||||
LOGGED_TEXT += text.join(' ').split('\n').map(function (l) { return logLevelText + " " + l + "\n"; }).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @license
|
||||
|
@ -1318,7 +1336,7 @@ class G3Module extends BaseModule {
|
|||
}
|
||||
getG3FileIncludeAndExcludeLists() {
|
||||
var _a, _b, _c, _d;
|
||||
const angularRobotFilePath = path.join(getRepoBaseDir(), '.github/angular-robot.yml');
|
||||
const angularRobotFilePath = path.join(this.git.baseDir, '.github/angular-robot.yml');
|
||||
if (!fs.existsSync(angularRobotFilePath)) {
|
||||
debug('No angular robot configuration file exists, skipping.');
|
||||
return null;
|
||||
|
@ -2059,7 +2077,8 @@ function printValidationErrors(errors, print = error) {
|
|||
*/
|
||||
/** Validate commit message at the provided file path. */
|
||||
function validateFile(filePath, isErrorMode) {
|
||||
const commitMessage = fs.readFileSync(path.resolve(getRepoBaseDir(), filePath), 'utf8');
|
||||
const git = GitClient.getInstance();
|
||||
const commitMessage = fs.readFileSync(path.resolve(git.baseDir, filePath), 'utf8');
|
||||
const { valid, errors } = validateCommitMessage(commitMessage);
|
||||
if (valid) {
|
||||
info(`${green('√')} Valid commit message`);
|
||||
|
@ -2260,47 +2279,6 @@ function buildCommitMessageParser(localYargs) {
|
|||
.command(ValidateRangeModule);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
/**
|
||||
* A list of all files currently in the repo which have been modified since the provided sha.
|
||||
*
|
||||
* git diff
|
||||
* Deleted files (--diff-filter=d) are not included as they are not longer present in the repo
|
||||
* and can not be checked anymore.
|
||||
*
|
||||
* git ls-files
|
||||
* Untracked files (--others), which are not matched by .gitignore (--exclude-standard)
|
||||
* as they are expected to become tracked files.
|
||||
*/
|
||||
function allChangedFilesSince(sha) {
|
||||
if (sha === void 0) { sha = 'HEAD'; }
|
||||
var diffFiles = gitOutputAsArray("git diff --name-only --diff-filter=d " + sha);
|
||||
var untrackedFiles = gitOutputAsArray("git ls-files --others --exclude-standard");
|
||||
// Use a set to deduplicate the list as its possible for a file to show up in both lists.
|
||||
return Array.from(new Set(tslib.__spreadArray(tslib.__spreadArray([], tslib.__read(diffFiles)), tslib.__read(untrackedFiles))));
|
||||
}
|
||||
/**
|
||||
* A list of all staged files which have been modified.
|
||||
*
|
||||
* Only added, created and modified files are listed as others (deleted, renamed, etc) aren't
|
||||
* changed or available as content to act upon.
|
||||
*/
|
||||
function allStagedFiles() {
|
||||
return gitOutputAsArray("git diff --staged --name-only --diff-filter=ACM");
|
||||
}
|
||||
function allFiles() {
|
||||
return gitOutputAsArray("git ls-files");
|
||||
}
|
||||
function gitOutputAsArray(cmd) {
|
||||
return exec(cmd, { cwd: getRepoBaseDir() }).split('\n').map(function (x) { return x.trim(); }).filter(function (x) { return !!x; });
|
||||
}
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
|
@ -2351,6 +2329,7 @@ function checkFormatterConfig(key, config, errors) {
|
|||
class Formatter {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.git = GitClient.getInstance();
|
||||
}
|
||||
/**
|
||||
* Retrieve the command to execute the provided action, including both the binary
|
||||
|
@ -2414,7 +2393,7 @@ class Buildifier extends Formatter {
|
|||
constructor() {
|
||||
super(...arguments);
|
||||
this.name = 'buildifier';
|
||||
this.binaryFilePath = path.join(getRepoBaseDir(), 'node_modules/.bin/buildifier');
|
||||
this.binaryFilePath = path.join(this.git.baseDir, 'node_modules/.bin/buildifier');
|
||||
this.defaultFileMatcher = ['**/*.bzl', '**/BUILD.bazel', '**/WORKSPACE', '**/BUILD'];
|
||||
this.actions = {
|
||||
check: {
|
||||
|
@ -2459,7 +2438,7 @@ class ClangFormat extends Formatter {
|
|||
constructor() {
|
||||
super(...arguments);
|
||||
this.name = 'clang-format';
|
||||
this.binaryFilePath = path.join(getRepoBaseDir(), 'node_modules/.bin/clang-format');
|
||||
this.binaryFilePath = path.join(this.git.baseDir, 'node_modules/.bin/clang-format');
|
||||
this.defaultFileMatcher = ['**/*.{t,j}s'];
|
||||
this.actions = {
|
||||
check: {
|
||||
|
@ -2674,16 +2653,19 @@ function buildFormatParser(localYargs) {
|
|||
})
|
||||
.command('all', 'Run the formatter on all files in the repository', args => args, ({ check }) => {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(allFiles());
|
||||
const allFiles = GitClient.getInstance().allFiles();
|
||||
executionCmd(allFiles);
|
||||
})
|
||||
.command('changed [shaOrRef]', 'Run the formatter on files changed since the provided sha/ref', args => args.positional('shaOrRef', { type: 'string' }), ({ shaOrRef, check }) => {
|
||||
const sha = shaOrRef || 'master';
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(allChangedFilesSince(sha));
|
||||
const allChangedFilesSince = GitClient.getInstance().allChangesFilesSince(sha);
|
||||
executionCmd(allChangedFilesSince);
|
||||
})
|
||||
.command('staged', 'Run the formatter on all staged files', args => args, ({ check }) => {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
executionCmd(allStagedFiles());
|
||||
const allStagedFiles = GitClient.getInstance().allStagedFiles();
|
||||
executionCmd(allStagedFiles);
|
||||
})
|
||||
.command('files <files..>', 'Run the formatter on provided files', args => args.positional('files', { array: true, type: 'string' }), ({ check, files }) => {
|
||||
const executionCmd = check ? checkFiles : formatFiles;
|
||||
|
@ -2699,8 +2681,9 @@ function buildFormatParser(localYargs) {
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
function verify() {
|
||||
const git = GitClient.getInstance();
|
||||
/** Full path to NgBot config file */
|
||||
const NGBOT_CONFIG_YAML_PATH = path.resolve(getRepoBaseDir(), '.github/angular-robot.yml');
|
||||
const NGBOT_CONFIG_YAML_PATH = path.resolve(git.baseDir, '.github/angular-robot.yml');
|
||||
/** The NgBot config file */
|
||||
const ngBotYaml = fs.readFileSync(NGBOT_CONFIG_YAML_PATH, 'utf8');
|
||||
try {
|
||||
|
@ -3176,6 +3159,21 @@ const CheckoutCommandModule = {
|
|||
describe: 'Checkout a PR from the upstream repo',
|
||||
};
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
/**
|
||||
* Runs an given command as child process. By default, child process
|
||||
* output will not be printed.
|
||||
*/
|
||||
function exec(cmd, opts) {
|
||||
return shelljs.exec(cmd, tslib.__assign(tslib.__assign({ silent: true }, opts), { async: false }));
|
||||
}
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
|
@ -4965,10 +4963,11 @@ function getGroupsFromYaml(pullApproveYamlRaw) {
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
function verify$1() {
|
||||
const git = GitClient.getInstance();
|
||||
/** Full path to PullApprove config file */
|
||||
const PULL_APPROVE_YAML_PATH = path.resolve(getRepoBaseDir(), '.pullapprove.yml');
|
||||
const PULL_APPROVE_YAML_PATH = path.resolve(git.baseDir, '.pullapprove.yml');
|
||||
/** All tracked files in the repository. */
|
||||
const REPO_FILES = allFiles();
|
||||
const REPO_FILES = git.allFiles();
|
||||
/** The pull approve config file. */
|
||||
const pullApproveYamlRaw = fs.readFileSync(PULL_APPROVE_YAML_PATH, 'utf8');
|
||||
/** All of the groups defined in the pullapprove yaml. */
|
||||
|
@ -6787,9 +6786,10 @@ function builder$8(argv) {
|
|||
/** Yargs command handler for staging a release. */
|
||||
function handler$8(args) {
|
||||
return tslib.__awaiter(this, void 0, void 0, function* () {
|
||||
const git = GitClient.getInstance();
|
||||
const config = getConfig();
|
||||
const releaseConfig = getReleaseConfig(config);
|
||||
const projectDir = getRepoBaseDir();
|
||||
const projectDir = git.baseDir;
|
||||
const task = new ReleaseTool(releaseConfig, config.github, args.githubToken, projectDir);
|
||||
const result = yield task.run();
|
||||
switch (result) {
|
||||
|
@ -6916,7 +6916,8 @@ function hasLocalChanges() {
|
|||
*/
|
||||
function getSCMVersion(mode) {
|
||||
if (mode === 'release') {
|
||||
const packageJsonPath = path.join(getRepoBaseDir(), 'package.json');
|
||||
const git = GitClient.getInstance();
|
||||
const packageJsonPath = path.join(git.baseDir, 'package.json');
|
||||
const { version } = require(packageJsonPath);
|
||||
return version;
|
||||
}
|
||||
|
|
|
@ -9,12 +9,13 @@ import {readFileSync} from 'fs';
|
|||
import {resolve} from 'path';
|
||||
import {parse as parseYaml} from 'yaml';
|
||||
|
||||
import {getRepoBaseDir} from '../utils/config';
|
||||
import {error, green, info, red} from '../utils/console';
|
||||
import {GitClient} from '../utils/git/index';
|
||||
|
||||
export function verify() {
|
||||
const git = GitClient.getInstance();
|
||||
/** Full path to NgBot config file */
|
||||
const NGBOT_CONFIG_YAML_PATH = resolve(getRepoBaseDir(), '.github/angular-robot.yml');
|
||||
const NGBOT_CONFIG_YAML_PATH = resolve(git.baseDir, '.github/angular-robot.yml');
|
||||
|
||||
/** The NgBot config file */
|
||||
const ngBotYaml = readFileSync(NGBOT_CONFIG_YAML_PATH, 'utf8');
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
|
||||
import {getConfig, getRepoBaseDir} from '../../utils/config';
|
||||
import {getConfig} from '../../utils/config';
|
||||
import {error, green, info, promptConfirm, red, yellow} from '../../utils/console';
|
||||
import {GithubApiRequestError} from '../../utils/git/github';
|
||||
import {GITHUB_TOKEN_GENERATE_URL} from '../../utils/git/github-urls';
|
||||
|
|
|
@ -8,17 +8,17 @@
|
|||
import {readFileSync} from 'fs';
|
||||
import {resolve} from 'path';
|
||||
|
||||
import {getRepoBaseDir} from '../utils/config';
|
||||
import {debug, info} from '../utils/console';
|
||||
import {allFiles} from '../utils/repo-files';
|
||||
import {GitClient} from '../utils/git/index';
|
||||
import {logGroup, logHeader} from './logging';
|
||||
import {getGroupsFromYaml} from './parse-yaml';
|
||||
|
||||
export function verify() {
|
||||
const git = GitClient.getInstance();
|
||||
/** Full path to PullApprove config file */
|
||||
const PULL_APPROVE_YAML_PATH = resolve(getRepoBaseDir(), '.pullapprove.yml');
|
||||
const PULL_APPROVE_YAML_PATH = resolve(git.baseDir, '.pullapprove.yml');
|
||||
/** All tracked files in the repository. */
|
||||
const REPO_FILES = allFiles();
|
||||
const REPO_FILES = git.allFiles();
|
||||
/** The pull approve config file. */
|
||||
const pullApproveYamlRaw = readFileSync(PULL_APPROVE_YAML_PATH, 'utf8');
|
||||
/** All of the groups defined in the pullapprove yaml. */
|
||||
|
|
|
@ -8,9 +8,10 @@
|
|||
|
||||
import {Arguments, Argv, CommandModule} from 'yargs';
|
||||
|
||||
import {getConfig, getRepoBaseDir} from '../../utils/config';
|
||||
import {getConfig} from '../../utils/config';
|
||||
import {error, green, info, red, yellow} from '../../utils/console';
|
||||
import {addGithubTokenOption} from '../../utils/git/github-yargs';
|
||||
import {GitClient} from '../../utils/git/index';
|
||||
import {getReleaseConfig} from '../config/index';
|
||||
|
||||
import {CompletionState, ReleaseTool} from './index';
|
||||
|
@ -27,9 +28,10 @@ function builder(argv: Argv): Argv<ReleasePublishOptions> {
|
|||
|
||||
/** Yargs command handler for staging a release. */
|
||||
async function handler(args: Arguments<ReleasePublishOptions>) {
|
||||
const git = GitClient.getInstance();
|
||||
const config = getConfig();
|
||||
const releaseConfig = getReleaseConfig(config);
|
||||
const projectDir = getRepoBaseDir();
|
||||
const projectDir = git.baseDir;
|
||||
const task = new ReleaseTool(releaseConfig, config.github, args.githubToken, projectDir);
|
||||
const result = await task.run();
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
*/
|
||||
|
||||
import {join} from 'path';
|
||||
import {GitClient} from '../../utils/git/index';
|
||||
|
||||
import {getRepoBaseDir} from '../../utils/config';
|
||||
import {exec as _exec} from '../../utils/shelljs';
|
||||
|
||||
export type EnvStampMode = 'snapshot'|'release';
|
||||
|
@ -51,7 +51,8 @@ function hasLocalChanges() {
|
|||
*/
|
||||
function getSCMVersion(mode: EnvStampMode) {
|
||||
if (mode === 'release') {
|
||||
const packageJsonPath = join(getRepoBaseDir(), 'package.json');
|
||||
const git = GitClient.getInstance();
|
||||
const packageJsonPath = join(git.baseDir, 'package.json');
|
||||
const {version} = require(packageJsonPath);
|
||||
return version;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import {existsSync} from 'fs';
|
|||
import {dirname, join} from 'path';
|
||||
|
||||
import {debug, error} from './console';
|
||||
import {exec} from './shelljs';
|
||||
import {GitClient} from './git/index';
|
||||
import {isTsNodeAvailable} from './ts-node';
|
||||
|
||||
/** Configuration for Git client interactions. */
|
||||
|
@ -64,11 +64,14 @@ let userConfig: {[key: string]: any}|null = null;
|
|||
* Get the configuration from the file system, returning the already loaded
|
||||
* copy if it is defined.
|
||||
*/
|
||||
export function getConfig(): NgDevConfig {
|
||||
export function getConfig(): NgDevConfig;
|
||||
export function getConfig(baseDir?: string): NgDevConfig;
|
||||
export function getConfig(baseDir?: string): NgDevConfig {
|
||||
// If the global config is not defined, load it from the file system.
|
||||
if (cachedConfig === null) {
|
||||
baseDir = baseDir || GitClient.getInstance().getBaseDir();
|
||||
// The full path to the configuration file.
|
||||
const configPath = join(getRepoBaseDir(), CONFIG_FILE_PATH);
|
||||
const configPath = join(baseDir, CONFIG_FILE_PATH);
|
||||
// Read the configuration and validate it before caching it for the future.
|
||||
cachedConfig = validateCommonConfig(readConfigFile(configPath));
|
||||
}
|
||||
|
@ -141,18 +144,6 @@ export function assertNoErrors(errors: string[]) {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
/** Gets the path of the directory for the repository base. */
|
||||
export function getRepoBaseDir() {
|
||||
const baseRepoDir = exec(`git rev-parse --show-toplevel`);
|
||||
if (baseRepoDir.code) {
|
||||
throw Error(
|
||||
`Unable to find the path to the base directory of the repository.\n` +
|
||||
`Was the command run from inside of the repo?\n\n` +
|
||||
`ERROR:\n ${baseRepoDir.stderr}`);
|
||||
}
|
||||
return baseRepoDir.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the local user configuration from the file system, returning the already loaded copy if it is
|
||||
* defined.
|
||||
|
@ -163,8 +154,9 @@ export function getRepoBaseDir() {
|
|||
export function getUserConfig() {
|
||||
// If the global config is not defined, load it from the file system.
|
||||
if (userConfig === null) {
|
||||
const git = GitClient.getInstance();
|
||||
// The full path to the configuration file.
|
||||
const configPath = join(getRepoBaseDir(), USER_CONFIG_FILE_PATH);
|
||||
const configPath = join(git.baseDir, USER_CONFIG_FILE_PATH);
|
||||
// Set the global config object.
|
||||
userConfig = readConfigFile(configPath, true);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import {prompt} from 'inquirer';
|
|||
import {join} from 'path';
|
||||
import {Arguments} from 'yargs';
|
||||
|
||||
import {getRepoBaseDir} from './config';
|
||||
import {GitClient} from './git/index';
|
||||
|
||||
/** Reexport of chalk colors for convenient access. */
|
||||
export const red: typeof chalk = chalk.red;
|
||||
|
@ -143,6 +143,8 @@ export function captureLogOutputForCommand(argv: Arguments) {
|
|||
if (FILE_LOGGING_ENABLED) {
|
||||
throw Error('`captureLogOutputForCommand` cannot be called multiple times');
|
||||
}
|
||||
|
||||
const git = GitClient.getInstance();
|
||||
/** The date time used for timestamping when the command was invoked. */
|
||||
const now = new Date();
|
||||
/** Header line to separate command runs in log files. */
|
||||
|
@ -155,7 +157,7 @@ export function captureLogOutputForCommand(argv: Arguments) {
|
|||
LOGGED_TEXT += `Command ran in ${new Date().getTime() - now.getTime()}ms\n`;
|
||||
LOGGED_TEXT += `Exit Code: ${code}\n`;
|
||||
/** Path to the log file location. */
|
||||
const logFilePath = join(getRepoBaseDir(), '.ng-dev.log');
|
||||
const logFilePath = join(git.baseDir, '.ng-dev.log');
|
||||
|
||||
// Strip ANSI escape codes from log outputs.
|
||||
LOGGED_TEXT = LOGGED_TEXT.replace(/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]/g, '');
|
||||
|
@ -167,7 +169,7 @@ export function captureLogOutputForCommand(argv: Arguments) {
|
|||
if (code > 1) {
|
||||
const logFileName = `.ng-dev.err-${now.getTime()}.log`;
|
||||
console.error(`Exit code: ${code}. Writing full log to ${logFileName}`);
|
||||
writeFileSync(join(getRepoBaseDir(), logFileName), LOGGED_TEXT);
|
||||
writeFileSync(join(git.baseDir, logFileName), LOGGED_TEXT);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import * as Octokit from '@octokit/rest';
|
|||
import {spawnSync, SpawnSyncOptions, SpawnSyncReturns} from 'child_process';
|
||||
import {Options as SemVerOptions, parse, SemVer} from 'semver';
|
||||
|
||||
import {getConfig, getRepoBaseDir} from '../config';
|
||||
import {getConfig, GithubConfig, NgDevConfig} from '../config';
|
||||
import {debug, info, yellow} from '../console';
|
||||
import {DryRunError, isDryRun} from '../dry-run';
|
||||
import {GithubClient} from './github';
|
||||
|
@ -83,7 +83,8 @@ export class GitClient<Authenticated extends boolean> {
|
|||
GitClient.authenticated = new GitClient(token);
|
||||
}
|
||||
|
||||
|
||||
/** The configuration, containing the github specific configuration. */
|
||||
private config: NgDevConfig;
|
||||
/** Whether verbose logging of Git actions should be used. */
|
||||
private verboseLogging = true;
|
||||
/** The OAuth scopes available for the provided Github token. */
|
||||
|
@ -94,21 +95,27 @@ export class GitClient<Authenticated extends boolean> {
|
|||
*/
|
||||
private _githubTokenRegex: RegExp|null = null;
|
||||
/** Short-hand for accessing the default remote configuration. */
|
||||
remoteConfig = this._config.github;
|
||||
remoteConfig: GithubConfig;
|
||||
/** Octokit request parameters object for targeting the configured remote. */
|
||||
remoteParams = {owner: this.remoteConfig.owner, repo: this.remoteConfig.name};
|
||||
/** Instance of the authenticated Github octokit API. */
|
||||
remoteParams: {owner: string, repo: string};
|
||||
/** Instance of the Github octokit API. */
|
||||
github = new GithubClient(this.githubToken);
|
||||
/** The full path to the root of the repository base. */
|
||||
baseDir: string;
|
||||
|
||||
/**
|
||||
* @param githubToken The github token used for authentication, if provided.
|
||||
* @param _config The configuration, containing the github specific configuration.
|
||||
* @param _projectRoot The full path to the root of the repository base.
|
||||
* @param baseDir The full path to the root of the repository base.
|
||||
*/
|
||||
protected constructor(public githubToken:
|
||||
Authenticated extends true? string: undefined,
|
||||
private _config = getConfig(),
|
||||
private _projectRoot = getRepoBaseDir()) {
|
||||
protected constructor(public githubToken: Authenticated extends true? string: undefined,
|
||||
config?: NgDevConfig,
|
||||
baseDir?: string) {
|
||||
this.baseDir = baseDir || this.determineBaseDir();
|
||||
this.config = config || getConfig(this.baseDir);
|
||||
this.remoteConfig = this.config.github;
|
||||
this.remoteParams = {owner: this.remoteConfig.owner, repo: this.remoteConfig.name};
|
||||
|
||||
// If a token has been specified (and is not empty), pass it to the Octokit API and
|
||||
// also create a regular expression that can be used for sanitizing Git command output
|
||||
// so that it does not print the token accidentally.
|
||||
|
@ -157,7 +164,7 @@ export class GitClient<Authenticated extends boolean> {
|
|||
printFn('Executing: git', this.omitGithubTokenFromMessage(args.join(' ')));
|
||||
|
||||
const result = spawnSync('git', args, {
|
||||
cwd: this._projectRoot,
|
||||
cwd: this.baseDir,
|
||||
stdio: 'pipe',
|
||||
...options,
|
||||
// Encoding is always `utf8` and not overridable. This ensures that this method
|
||||
|
@ -249,6 +256,40 @@ export class GitClient<Authenticated extends boolean> {
|
|||
return new SemVer(latestTag, semVerOptions);
|
||||
}
|
||||
|
||||
/** Gets the path of the directory for the repository base. */
|
||||
getBaseDir(): string {
|
||||
const previousVerboseLoggingState = this.verboseLogging;
|
||||
this.setVerboseLoggingState(false);
|
||||
const {stdout, stderr, status} = this.runGraceful(['rev-parse', '--show-toplevel']);
|
||||
this.setVerboseLoggingState(previousVerboseLoggingState);
|
||||
if (status !== 0) {
|
||||
throw Error(
|
||||
`Unable to find the path to the base directory of the repository.\n` +
|
||||
`Was the command run from inside of the repo?\n\n` +
|
||||
`ERROR:\n ${stderr}`);
|
||||
}
|
||||
return stdout.trim();
|
||||
}
|
||||
|
||||
/** Retrieve a list of all files in the repostitory changed since the provided shaOrRef. */
|
||||
allChangesFilesSince(shaOrRef = 'HEAD'): string[] {
|
||||
return Array.from(new Set([
|
||||
...gitOutputAsArray(this.runGraceful(['diff', '--name-only', '--diff-filter=d', shaOrRef])),
|
||||
...gitOutputAsArray(this.runGraceful(['ls-files', '--others', '--exclude-standard'])),
|
||||
]));
|
||||
}
|
||||
|
||||
/** Retrieve a list of all files currently staged in the repostitory. */
|
||||
allStagedFiles(): string[] {
|
||||
return gitOutputAsArray(
|
||||
this.runGraceful(['diff', '--name-only', '--diff-filter=ACM', '--staged']));
|
||||
}
|
||||
|
||||
/** Retrieve a list of all files tracked in the repostitory. */
|
||||
allFiles(): string[] {
|
||||
return gitOutputAsArray(this.runGraceful(['ls-files']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the GitClient instance is using a token with permissions for the all of the
|
||||
* provided OAuth scopes.
|
||||
|
@ -293,4 +334,29 @@ export class GitClient<Authenticated extends boolean> {
|
|||
return scopes.split(',').map(scope => scope.trim());
|
||||
});
|
||||
}
|
||||
|
||||
private determineBaseDir() {
|
||||
this.setVerboseLoggingState(false);
|
||||
const {stdout, stderr, status} = this.runGraceful(['rev-parse', '--show-toplevel']);
|
||||
if (status !== 0) {
|
||||
throw Error(
|
||||
`Unable to find the path to the base directory of the repository.\n` +
|
||||
`Was the command run from inside of the repo?\n\n` +
|
||||
`ERROR:\n ${stderr}`);
|
||||
}
|
||||
this.setVerboseLoggingState(true);
|
||||
return stdout.trim();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the output from `GitClient.run` and `GitClient.runGraceful` and returns an array of strings
|
||||
* for each new line. Git commands typically return multiple output values for a command a set of
|
||||
* strings separated by new lines.
|
||||
*
|
||||
* Note: This is specifically created as a locally available function for usage as convience utility
|
||||
* within `GitClient`'s methods to create outputs as array.
|
||||
*/
|
||||
function gitOutputAsArray(gitCommandResult: SpawnSyncReturns<string>): string[] {
|
||||
return gitCommandResult.stdout.split('\n').map(x => x.trim()).filter(x => !!x);
|
||||
}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {getRepoBaseDir} from './config';
|
||||
import {exec} from './shelljs';
|
||||
|
||||
/**
|
||||
* A list of all files currently in the repo which have been modified since the provided sha.
|
||||
*
|
||||
* git diff
|
||||
* Deleted files (--diff-filter=d) are not included as they are not longer present in the repo
|
||||
* and can not be checked anymore.
|
||||
*
|
||||
* git ls-files
|
||||
* Untracked files (--others), which are not matched by .gitignore (--exclude-standard)
|
||||
* as they are expected to become tracked files.
|
||||
*/
|
||||
export function allChangedFilesSince(sha = 'HEAD') {
|
||||
const diffFiles = gitOutputAsArray(`git diff --name-only --diff-filter=d ${sha}`);
|
||||
const untrackedFiles = gitOutputAsArray(`git ls-files --others --exclude-standard`);
|
||||
// Use a set to deduplicate the list as its possible for a file to show up in both lists.
|
||||
return Array.from(new Set([...diffFiles, ...untrackedFiles]));
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of all staged files which have been modified.
|
||||
*
|
||||
* Only added, created and modified files are listed as others (deleted, renamed, etc) aren't
|
||||
* changed or available as content to act upon.
|
||||
*/
|
||||
export function allStagedFiles() {
|
||||
return gitOutputAsArray(`git diff --staged --name-only --diff-filter=ACM`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function allFiles() {
|
||||
return gitOutputAsArray(`git ls-files`);
|
||||
}
|
||||
|
||||
|
||||
function gitOutputAsArray(cmd: string) {
|
||||
return exec(cmd, {cwd: getRepoBaseDir()}).split('\n').map(x => x.trim()).filter(x => !!x);
|
||||
}
|
Loading…
Reference in New Issue