From fa3088054757f8ef2fd8cea4d78fed91188b20ad Mon Sep 17 00:00:00 2001
From: Eric Amodio <eamodio@gmail.com>
Date: Mon, 20 Nov 2023 15:54:01 -0500
Subject: [PATCH] Fixes #3023 unifies file & dirty blame Adds don't prompt
 again to error prompts Adds specific error handling for invalid revision vs
 invalid file Adds better fallback handling of user entered custom args

---
 CHANGELOG.md                         |  1 +
 package.json                         | 14 +++++-
 src/config.ts                        |  4 +-
 src/env/node/git/git.ts              | 96 ++++++++++++++++--------------------
 src/env/node/git/localGitProvider.ts | 26 ++++++----
 src/git/errors.ts                    | 22 +++++++--
 src/messages.ts                      | 20 ++++++++
 7 files changed, 112 insertions(+), 71 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d861372..47539dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
 
 ### Fixed
 
+- Fixes [#3023](https://github.com/gitkraken/vscode-gitlens/issues/3023) - "Unable to show blame. Invalid or missing blame.ignoreRevsFile" with valid ignore revs file
 - Fixes [#3018](https://github.com/gitkraken/vscode-gitlens/issues/3018) - Line blame overlay is broken when commit message contains a `)`
 - Fixes [#2625](https://github.com/gitkraken/vscode-gitlens/issues/2625) - full issue ref has escape characters that break hover links
 - Fixes stuck busy state of the _Commit Details_ Explain AI panel after canceling a request
diff --git a/package.json b/package.json
index 39adc98..abe9b4d 100644
--- a/package.json
+++ b/package.json
@@ -4031,7 +4031,9 @@
 							"suppressRebaseSwitchToTextWarning": false,
 							"suppressIntegrationDisconnectedTooManyFailedRequestsWarning": false,
 							"suppressIntegrationRequestFailed500Warning": false,
-							"suppressIntegrationRequestTimedOutWarning": false
+							"suppressIntegrationRequestTimedOutWarning": false,
+							"suppressBlameInvalidIgnoreRevsFileWarning": false,
+							"suppressBlameInvalidIgnoreRevsFileBadRevisionWarning": false
 						},
 						"properties": {
 							"suppressCommitHasNoPreviousCommitWarning": {
@@ -4103,6 +4105,16 @@
 								"type": "boolean",
 								"default": false,
 								"description": "Integration Request Timed Out Warning"
+							},
+							"suppressBlameInvalidIgnoreRevsFileWarning": {
+								"type": "boolean",
+								"default": false,
+								"description": "Invalid Blame IgnoreRevs File Warning"
+							},
+							"suppressBlameInvalidIgnoreRevsFileBadRevisionWarning": {
+								"type": "boolean",
+								"default": false,
+								"description": "Invalid Revision in Blame IgnoreRevs File Warning"
 							}
 						},
 						"additionalProperties": false,
diff --git a/src/config.ts b/src/config.ts
index 3199a48..d2eca10 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -537,7 +537,9 @@ export type SuppressedMessages =
 	| 'suppressRebaseSwitchToTextWarning'
 	| 'suppressIntegrationDisconnectedTooManyFailedRequestsWarning'
 	| 'suppressIntegrationRequestFailed500Warning'
-	| 'suppressIntegrationRequestTimedOutWarning';
+	| 'suppressIntegrationRequestTimedOutWarning'
+	| 'suppressBlameInvalidIgnoreRevsFileWarning'
+	| 'suppressBlameInvalidIgnoreRevsFileBadRevisionWarning';
 
 export interface ViewsCommonConfig {
 	readonly defaultItemLimit: number;
diff --git a/src/env/node/git/git.ts b/src/env/node/git/git.ts
index 8378d14..71b7808 100644
--- a/src/env/node/git/git.ts
+++ b/src/env/node/git/git.ts
@@ -10,6 +10,7 @@ import { GlyphChars } from '../../../constants';
 import type { GitCommandOptions, GitSpawnOptions } from '../../../git/commandOptions';
 import { GitErrorHandling } from '../../../git/commandOptions';
 import {
+	BlameIgnoreRevsFileBadRevisionError,
 	BlameIgnoreRevsFileError,
 	CherryPickError,
 	CherryPickErrorReason,
@@ -35,7 +36,7 @@ import { parseGitTagsDefaultFormat } from '../../../git/parsers/tagParser';
 import { splitAt } from '../../../system/array';
 import { configuration } from '../../../system/configuration';
 import { log } from '../../../system/decorators/log';
-import { join } from '../../../system/iterable';
+import { count, join } from '../../../system/iterable';
 import { Logger } from '../../../system/logger';
 import { slowCallWarningThreshold } from '../../../system/logger.constants';
 import { getLogScope } from '../../../system/logger.scope';
@@ -70,12 +71,13 @@ const textDecoder = new TextDecoder('utf8');
 const rootSha = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
 
 export const GitErrors = {
-	badIgnoreRevsFile: /could not open object name list: (.*)\s/i,
 	badRevision: /bad revision '(.*?)'/i,
 	cantLockRef: /cannot lock ref|unable to update local ref/i,
 	changesWouldBeOverwritten: /Your local changes to the following files would be overwritten/i,
 	commitChangesFirst: /Please, commit your changes before you can/i,
 	conflict: /^CONFLICT \([^)]+\): \b/m,
+	invalidObjectName: /invalid object name: (.*)\s/i,
+	invalidObjectNameList: /could not open object name list: (.*)\s/i,
 	noFastForward: /\(non-fast-forward\)/i,
 	noMergeBase: /no merge base/i,
 	noRemoteRepositorySpecified: /No remote repository specified\./i,
@@ -386,20 +388,37 @@ export class Git {
 	async blame(
 		repoPath: string | undefined,
 		fileName: string,
-		ref?: string,
-		options: { args?: string[] | null; ignoreWhitespace?: boolean; startLine?: number; endLine?: number } = {},
+		options?: ({ ref: string | undefined; contents?: never } | { contents: string; ref?: never }) & {
+			args?: string[] | null;
+			correlationKey?: string;
+			ignoreWhitespace?: boolean;
+			startLine?: number;
+			endLine?: number;
+		},
 	) {
 		const [file, root] = splitPath(fileName, repoPath, true);
 
 		const params = ['blame', '--root', '--incremental'];
 
-		if (options.ignoreWhitespace) {
+		if (options?.ignoreWhitespace) {
 			params.push('-w');
 		}
-		if (options.startLine != null && options.endLine != null) {
+		if (options?.startLine != null && options.endLine != null) {
 			params.push(`-L ${options.startLine},${options.endLine}`);
 		}
-		if (options.args != null) {
+		if (options?.args != null) {
+			// See if the args contains a value like: `--ignore-revs-file <file>` or `--ignore-revs-file=<file>` to account for user error
+			// If so split it up into two args
+			const argIndex = options.args.findIndex(
+				arg => arg !== '--ignore-revs-file' && arg.startsWith('--ignore-revs-file'),
+			);
+			if (argIndex !== -1) {
+				const match = /^--ignore-revs-file\s*=?\s*(.*)$/.exec(options.args[argIndex]);
+				if (match != null) {
+					options.args.splice(argIndex, 1, '--ignore-revs-file', match[1]);
+				}
+			}
+
 			params.push(...options.args);
 		}
 
@@ -444,63 +463,26 @@ export class Git {
 		}
 
 		let stdin;
-		if (ref) {
-			if (isUncommittedStaged(ref)) {
+		if (options?.contents != null) {
+			// Pipe the blame contents to stdin
+			params.push('--contents', '-');
+
+			stdin = options.contents;
+		} else if (options?.ref) {
+			if (isUncommittedStaged(options.ref)) {
 				// Pipe the blame contents to stdin
 				params.push('--contents', '-');
 
 				// Get the file contents for the staged version using `:`
 				stdin = await this.show<string>(repoPath, fileName, ':');
 			} else {
-				params.push(ref);
+				params.push(options.ref);
 			}
 		}
 
 		try {
-			const blame = await this.git<string>({ cwd: root, stdin: stdin }, ...params, '--', file);
-			return blame;
-		} catch (ex) {
-			// Since `-c blame.ignoreRevsFile=` doesn't seem to work (unlike as the docs suggest), try to detect the error and throw a more helpful one
-			const match = GitErrors.badIgnoreRevsFile.exec(ex.message);
-			if (match != null) {
-				throw new BlameIgnoreRevsFileError(match[1], ex);
-			}
-			throw ex;
-		}
-	}
-
-	async blame__contents(
-		repoPath: string | undefined,
-		fileName: string,
-		contents: string,
-		options: {
-			args?: string[] | null;
-			correlationKey?: string;
-			ignoreWhitespace?: boolean;
-			startLine?: number;
-			endLine?: number;
-		} = {},
-	) {
-		const [file, root] = splitPath(fileName, repoPath, true);
-
-		const params = ['blame', '--root', '--incremental'];
-
-		if (options.ignoreWhitespace) {
-			params.push('-w');
-		}
-		if (options.startLine != null && options.endLine != null) {
-			params.push(`-L ${options.startLine},${options.endLine}`);
-		}
-		if (options.args != null) {
-			params.push(...options.args);
-		}
-
-		// Pipe the blame contents to stdin
-		params.push('--contents', '-');
-
-		try {
 			const blame = await this.git<string>(
-				{ cwd: root, stdin: contents, correlationKey: options.correlationKey },
+				{ cwd: root, stdin: stdin, correlationKey: options?.correlationKey },
 				...params,
 				'--',
 				file,
@@ -508,10 +490,16 @@ export class Git {
 			return blame;
 		} catch (ex) {
 			// Since `-c blame.ignoreRevsFile=` doesn't seem to work (unlike as the docs suggest), try to detect the error and throw a more helpful one
-			const match = GitErrors.badIgnoreRevsFile.exec(ex.message);
+			let match = GitErrors.invalidObjectNameList.exec(ex.message);
 			if (match != null) {
 				throw new BlameIgnoreRevsFileError(match[1], ex);
 			}
+
+			match = GitErrors.invalidObjectName.exec(ex.message);
+			if (match != null) {
+				throw new BlameIgnoreRevsFileBadRevisionError(match[1], ex);
+			}
+
 			throw ex;
 		}
 	}
diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts
index 0c5c618..bafd22f 100644
--- a/src/env/node/git/localGitProvider.ts
+++ b/src/env/node/git/localGitProvider.ts
@@ -17,6 +17,7 @@ import { emojify } from '../../../emojis';
 import { Features } from '../../../features';
 import { GitErrorHandling } from '../../../git/commandOptions';
 import {
+	BlameIgnoreRevsFileBadRevisionError,
 	BlameIgnoreRevsFileError,
 	CherryPickError,
 	CherryPickErrorReason,
@@ -150,6 +151,7 @@ import { getRemoteProviderMatcher, loadRemoteProviders } from '../../../git/remo
 import type { GitSearch, GitSearchResultData, GitSearchResults, SearchQuery } from '../../../git/search';
 import { getGitArgsFromSearchQuery, getSearchQueryComparisonKey } from '../../../git/search';
 import {
+	showBlameInvalidIgnoreRevsFileWarningMessage,
 	showGenericErrorMessage,
 	showGitDisabledErrorMessage,
 	showGitInvalidConfigErrorMessage,
@@ -1748,7 +1750,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
 		const [relativePath, root] = paths;
 
 		try {
-			const data = await this.git.blame(root, relativePath, uri.sha, {
+			const data = await this.git.blame(root, relativePath, {
+				ref: uri.sha,
 				args: configuration.get('advanced.blame.customArguments'),
 				ignoreWhitespace: configuration.get('blame.ignoreWhitespace'),
 			});
@@ -1769,8 +1772,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
 				document.state.setBlame(key, value);
 				document.setBlameFailure(ex);
 
-				if (ex instanceof BlameIgnoreRevsFileError) {
-					void window.showErrorMessage(ex.friendlyMessage);
+				if (ex instanceof BlameIgnoreRevsFileError || ex instanceof BlameIgnoreRevsFileBadRevisionError) {
+					void showBlameInvalidIgnoreRevsFileWarningMessage(ex);
 				}
 
 				return emptyPromise as Promise<GitBlame>;
@@ -1831,7 +1834,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
 		const [relativePath, root] = paths;
 
 		try {
-			const data = await this.git.blame__contents(root, relativePath, contents, {
+			const data = await this.git.blame(root, relativePath, {
+				contents: contents,
 				args: configuration.get('advanced.blame.customArguments'),
 				correlationKey: `:${key}`,
 				ignoreWhitespace: configuration.get('blame.ignoreWhitespace'),
@@ -1853,8 +1857,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
 				document.state.setBlame(key, value);
 				document.setBlameFailure(ex);
 
-				if (ex instanceof BlameIgnoreRevsFileError) {
-					void window.showErrorMessage(ex.friendlyMessage);
+				if (ex instanceof BlameIgnoreRevsFileError || ex instanceof BlameIgnoreRevsFileBadRevisionError) {
+					void showBlameInvalidIgnoreRevsFileWarningMessage(ex);
 				}
 
 				return emptyPromise as Promise<GitBlame>;
@@ -1903,7 +1907,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
 		const [relativePath, root] = splitPath(uri, uri.repoPath);
 
 		try {
-			const data = await this.git.blame(root, relativePath, uri.sha, {
+			const data = await this.git.blame(root, relativePath, {
+				ref: uri.sha,
 				args: configuration.get('advanced.blame.customArguments'),
 				ignoreWhitespace: configuration.get('blame.ignoreWhitespace'),
 				startLine: lineToBlame,
@@ -1919,8 +1924,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
 			};
 		} catch (ex) {
 			Logger.error(ex, scope);
-			if (ex instanceof BlameIgnoreRevsFileError) {
-				void window.showErrorMessage(ex.friendlyMessage);
+			if (ex instanceof BlameIgnoreRevsFileError || ex instanceof BlameIgnoreRevsFileBadRevisionError) {
+				void showBlameInvalidIgnoreRevsFileWarningMessage(ex);
 			}
 
 			return undefined;
@@ -1959,7 +1964,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
 		const [relativePath, root] = splitPath(uri, uri.repoPath);
 
 		try {
-			const data = await this.git.blame__contents(root, relativePath, contents, {
+			const data = await this.git.blame(root, relativePath, {
+				contents: contents,
 				args: configuration.get('advanced.blame.customArguments'),
 				ignoreWhitespace: configuration.get('blame.ignoreWhitespace'),
 				startLine: lineToBlame,
diff --git a/src/git/errors.ts b/src/git/errors.ts
index dc01aa1..ba9696b 100644
--- a/src/git/errors.ts
+++ b/src/git/errors.ts
@@ -11,19 +11,31 @@ export class BlameIgnoreRevsFileError extends Error {
 		return ex instanceof BlameIgnoreRevsFileError;
 	}
 
-	readonly friendlyMessage: string;
-
 	constructor(
-		filename: string,
+		public readonly fileName: string,
 		public readonly original?: Error,
 	) {
-		super(`Invalid blame.ignoreRevsFile: '${filename}'`);
+		super(`Invalid blame.ignoreRevsFile: '${fileName}'`);
 
-		this.friendlyMessage = `Unable to show blame. Invalid or missing blame.ignoreRevsFile (${filename}) specified in your Git config.`;
 		Error.captureStackTrace?.(this, BlameIgnoreRevsFileError);
 	}
 }
 
+export class BlameIgnoreRevsFileBadRevisionError extends Error {
+	static is(ex: unknown): ex is BlameIgnoreRevsFileBadRevisionError {
+		return ex instanceof BlameIgnoreRevsFileBadRevisionError;
+	}
+
+	constructor(
+		public readonly revision: string,
+		public readonly original?: Error,
+	) {
+		super(`Invalid revision in blame.ignoreRevsFile: '${revision}'`);
+
+		Error.captureStackTrace?.(this, BlameIgnoreRevsFileBadRevisionError);
+	}
+}
+
 export const enum StashApplyErrorReason {
 	WorkingChanges = 1,
 }
diff --git a/src/messages.ts b/src/messages.ts
index 70b6140..38736fb 100644
--- a/src/messages.ts
+++ b/src/messages.ts
@@ -2,11 +2,31 @@ import type { MessageItem } from 'vscode';
 import { ConfigurationTarget, window } from 'vscode';
 import type { SuppressedMessages } from './config';
 import { Commands } from './constants';
+import type { BlameIgnoreRevsFileError } from './git/errors';
+import { BlameIgnoreRevsFileBadRevisionError } from './git/errors';
 import type { GitCommit } from './git/models/commit';
 import { executeCommand } from './system/command';
 import { configuration } from './system/configuration';
 import { Logger } from './system/logger';
 
+export function showBlameInvalidIgnoreRevsFileWarningMessage(
+	ex: BlameIgnoreRevsFileError | BlameIgnoreRevsFileBadRevisionError,
+): Promise<MessageItem | undefined> {
+	if (ex instanceof BlameIgnoreRevsFileBadRevisionError) {
+		return showMessage(
+			'error',
+			`Unable to show blame. Invalid revision (${ex.revision}) specified in the blame.ignoreRevsFile in your Git config.`,
+			'suppressBlameInvalidIgnoreRevsFileBadRevisionWarning',
+		);
+	}
+
+	return showMessage(
+		'error',
+		`Unable to show blame. Invalid or missing blame.ignoreRevsFile (${ex.fileName}) specified in your Git config.`,
+		'suppressBlameInvalidIgnoreRevsFileWarning',
+	);
+}
+
 export function showCommitHasNoPreviousCommitWarningMessage(commit?: GitCommit): Promise<MessageItem | undefined> {
 	if (commit == null) {
 		return showMessage('info', 'There is no previous commit.', 'suppressCommitHasNoPreviousCommitWarning');