From f4ced74e3a617267051ce423e58101457bc1e99a Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Thu, 30 Jul 2020 14:01:03 -0700 Subject: [PATCH] feat(dev-infra): save invalid commit message attempts to be restored on next commit attempt (#38304) When a commit message fails validation, rather than throwing out the commit message entirely the commit message is saved into a draft file and restored on the next commit attempt. PR Close #38304 --- dev-infra/commit-message/BUILD.bazel | 2 + dev-infra/commit-message/cli.ts | 23 +++++++++ .../commit-message/commit-message-draft.ts | 30 ++++++++++++ .../commit-message/restore-commit-message.ts | 48 +++++++++++++++++++ dev-infra/commit-message/validate-file.ts | 5 ++ package.json | 3 +- 6 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 dev-infra/commit-message/commit-message-draft.ts create mode 100644 dev-infra/commit-message/restore-commit-message.ts diff --git a/dev-infra/commit-message/BUILD.bazel b/dev-infra/commit-message/BUILD.bazel index 333c62ba99..c8e7898a4b 100644 --- a/dev-infra/commit-message/BUILD.bazel +++ b/dev-infra/commit-message/BUILD.bazel @@ -5,8 +5,10 @@ ts_library( name = "commit-message", srcs = [ "cli.ts", + "commit-message-draft.ts", "config.ts", "parse.ts", + "restore-commit-message.ts", "validate.ts", "validate-file.ts", "validate-range.ts", diff --git a/dev-infra/commit-message/cli.ts b/dev-infra/commit-message/cli.ts index f7b2cc8936..ebdde827e4 100644 --- a/dev-infra/commit-message/cli.ts +++ b/dev-infra/commit-message/cli.ts @@ -9,6 +9,7 @@ import * as yargs from 'yargs'; import {info} from '../utils/console'; +import {restoreCommitMessage} from './restore-commit-message'; import {validateFile} from './validate-file'; import {validateCommitRange} from './validate-range'; @@ -16,6 +17,28 @@ import {validateCommitRange} from './validate-range'; export function buildCommitMessageParser(localYargs: yargs.Argv) { return localYargs.help() .strict() + .command( + 'restore-commit-message-draft', false, { + 'file-env-variable': { + type: 'string', + conflicts: ['file'], + required: true, + description: + 'The key for the environment variable which holds the arguments for the ' + + 'prepare-commit-msg hook as described here: ' + + 'https://git-scm.com/docs/githooks#_prepare_commit_msg', + coerce: arg => { + const [file, source] = (process.env[arg] || '').split(' '); + if (!file) { + throw new Error(`Provided environment variable "${arg}" was not found.`); + } + return [file, source]; + }, + } + }, + args => { + restoreCommitMessage(args.fileEnvVariable[0], args.fileEnvVariable[1]); + }) .command( 'pre-commit-validate', 'Validate the most recent commit message', { 'file': { diff --git a/dev-infra/commit-message/commit-message-draft.ts b/dev-infra/commit-message/commit-message-draft.ts new file mode 100644 index 0000000000..86a5655fd0 --- /dev/null +++ b/dev-infra/commit-message/commit-message-draft.ts @@ -0,0 +1,30 @@ +/** + * @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 {existsSync, readFileSync, unlinkSync, writeFileSync} from 'fs'; + +/** Load the commit message draft from the file system if it exists. */ +export function loadCommitMessageDraft(basePath: string) { + const commitMessageDraftPath = `${basePath}.ngDevSave`; + if (existsSync(commitMessageDraftPath)) { + return readFileSync(commitMessageDraftPath).toString(); + } + return ''; +} + +/** Remove the commit message draft from the file system. */ +export function deleteCommitMessageDraft(basePath: string) { + const commitMessageDraftPath = `${basePath}.ngDevSave`; + if (existsSync(commitMessageDraftPath)) { + unlinkSync(commitMessageDraftPath); + } +} + +/** Save the commit message draft to the file system for later retrieval. */ +export function saveCommitMessageDraft(basePath: string, commitMessage: string) { + writeFileSync(`${basePath}.ngDevSave`, commitMessage); +} diff --git a/dev-infra/commit-message/restore-commit-message.ts b/dev-infra/commit-message/restore-commit-message.ts new file mode 100644 index 0000000000..bcbf302b98 --- /dev/null +++ b/dev-infra/commit-message/restore-commit-message.ts @@ -0,0 +1,48 @@ +/** + * @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 {info} from 'console'; +import {writeFileSync} from 'fs'; + +import {loadCommitMessageDraft} from './commit-message-draft'; + +/** + * Restore the commit message draft to the git to be used as the default commit message. + * + * The source provided may be one of the sources described in + * https://git-scm.com/docs/githooks#_prepare_commit_msg + */ +export function restoreCommitMessage( + filePath: string, source?: 'message'|'template'|'squash'|'commit') { + if (!!source) { + info('Skipping commit message restoration attempt'); + if (source === 'message') { + info('A commit message was already provided via the command with a -m or -F flag'); + } + if (source === 'template') { + info('A commit message was already provided via the -t flag or config.template setting'); + } + if (source === 'squash') { + info('A commit message was already provided as a merge action or via .git/MERGE_MSG'); + } + if (source === 'commit') { + info('A commit message was already provided through a revision specified via --fixup, -c,'); + info('-C or --amend flag'); + } + process.exit(0); + } + /** A draft of a commit message. */ + const commitMessage = loadCommitMessageDraft(filePath); + + // If the commit message draft has content, restore it into the provided filepath. + if (commitMessage) { + writeFileSync(filePath, commitMessage); + } + // Exit the process + process.exit(0); +} diff --git a/dev-infra/commit-message/validate-file.ts b/dev-infra/commit-message/validate-file.ts index f4cd5ed830..50b8b72afc 100644 --- a/dev-infra/commit-message/validate-file.ts +++ b/dev-infra/commit-message/validate-file.ts @@ -11,6 +11,7 @@ import {resolve} from 'path'; import {getRepoBaseDir} from '../utils/config'; import {info} from '../utils/console'; +import {deleteCommitMessageDraft, saveCommitMessageDraft} from './commit-message-draft'; import {validateCommitMessage} from './validate'; /** Validate commit message at the provided file path. */ @@ -18,8 +19,12 @@ export function validateFile(filePath: string) { const commitMessage = readFileSync(resolve(getRepoBaseDir(), filePath), 'utf8'); if (validateCommitMessage(commitMessage)) { info('√ Valid commit message'); + deleteCommitMessageDraft(filePath); return; } + // On all invalid commit messages, the commit message should be saved as a draft to be + // restored on the next commit attempt. + saveCommitMessageDraft(filePath, commitMessage); // If the validation did not return true, exit as a failure. process.exit(1); } diff --git a/package.json b/package.json index ca078c921d..f57e005bbe 100644 --- a/package.json +++ b/package.json @@ -210,7 +210,8 @@ "husky": { "hooks": { "pre-commit": "yarn -s ng-dev format staged", - "commit-msg": "yarn -s ng-dev commit-message pre-commit-validate --file-env-variable HUSKY_GIT_PARAMS" + "commit-msg": "yarn -s ng-dev commit-message pre-commit-validate --file-env-variable HUSKY_GIT_PARAMS", + "prepare-commit-msg": "yarn -s ng-dev commit-message restore-commit-message-draft --file-env-variable HUSKY_GIT_PARAMS" } } }