diff --git a/package.json b/package.json index 4a75fff0..bb356a29 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,13 @@ "category": "Git Graph", "command": "git-graph.version", "title": "Get Version Information" + }, + { + "category": "Git Graph", + "command": "git-graph.openFile", + "title": "Open File", + "icon": "$(go-to-file)", + "enablement": "isInDiffEditor && resourceScheme == git-graph && git-graph:codiconsSupported" } ], "configuration": { @@ -1422,6 +1429,19 @@ } }, "menus": { + "commandPalette": [ + { + "command": "git-graph.openFile", + "when": "isInDiffEditor && resourceScheme == git-graph && git-graph:codiconsSupported" + } + ], + "editor/title": [ + { + "command": "git-graph.openFile", + "group": "navigation", + "when": "isInDiffEditor && resourceScheme == git-graph && git-graph:codiconsSupported" + } + ], "scm/title": [ { "when": "scmProvider == git && config.git-graph.sourceCodeProviderIntegrationLocation == 'Inline'", diff --git a/src/commands.ts b/src/commands.ts index abf896a4..bc77347c 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -3,11 +3,12 @@ import * as vscode from 'vscode'; import { AvatarManager } from './avatarManager'; import { getConfig } from './config'; import { DataSource } from './dataSource'; +import { DiffDocProvider, decodeDiffDocUri } from './diffDocProvider'; import { CodeReviewData, CodeReviews, ExtensionState } from './extensionState'; import { GitGraphView } from './gitGraphView'; import { Logger } from './logger'; import { RepoManager } from './repoManager'; -import { GitExecutable, UNABLE_TO_FIND_GIT_MSG, abbrevCommit, abbrevText, copyToClipboard, getExtensionVersion, getPathFromUri, getRelativeTimeDiff, getRepoName, isPathInWorkspace, resolveToSymbolicPath, showErrorMessage, showInformationMessage } from './utils'; +import { GitExecutable, UNABLE_TO_FIND_GIT_MSG, abbrevCommit, abbrevText, copyToClipboard, doesVersionMeetRequirement, getExtensionVersion, getPathFromUri, getRelativeTimeDiff, getRepoName, isPathInWorkspace, openFile, resolveToSymbolicPath, showErrorMessage, showInformationMessage } from './utils'; import { Disposable } from './utils/disposable'; import { Event } from './utils/event'; @@ -44,6 +45,7 @@ export class CommandManager extends Disposable { this.repoManager = repoManager; this.gitExecutable = gitExecutable; + // Register Extension Commands this.registerCommand('git-graph.view', (arg) => this.view(arg)); this.registerCommand('git-graph.addGitRepository', () => this.addGitRepository()); this.registerCommand('git-graph.removeGitRepository', () => this.removeGitRepository()); @@ -53,12 +55,20 @@ export class CommandManager extends Disposable { this.registerCommand('git-graph.endSpecificWorkspaceCodeReview', () => this.endSpecificWorkspaceCodeReview()); this.registerCommand('git-graph.resumeWorkspaceCodeReview', () => this.resumeWorkspaceCodeReview()); this.registerCommand('git-graph.version', () => this.version()); + this.registerCommand('git-graph.openFile', (arg) => this.openFile(arg)); this.registerDisposable( onDidChangeGitExecutable((gitExecutable) => { this.gitExecutable = gitExecutable; }) ); + + // Register Extension Contexts + try { + this.registerContext('git-graph:codiconsSupported', doesVersionMeetRequirement(vscode.version, '1.42.0')); + } catch (_) { + this.logger.logError('Unable to set Visual Studio Code Context "git-graph:codiconsSupported"'); + } } /** @@ -72,6 +82,18 @@ export class CommandManager extends Disposable { ); } + /** + * Register a context with Visual Studio Code. + * @param key The Context Key. + * @param value The Context Value. + */ + private registerContext(key: string, value: any) { + return vscode.commands.executeCommand('setContext', key, value).then( + () => this.logger.log('Successfully set Visual Studio Code Context "' + key + '" to "' + JSON.stringify(value) + '"'), + () => this.logger.logError('Failed to set Visual Studio Code Context "' + key + '" to "' + JSON.stringify(value) + '"') + ); + } + /* Commands */ @@ -292,6 +314,26 @@ export class CommandManager extends Disposable { } } + /** + * Opens a file in Visual Studio Code, based on a Git Graph URI (from the Diff View). + * The method run when the `git-graph.openFile` command is invoked. + * @param arg The Git Graph URI. + */ + private openFile(arg?: vscode.Uri) { + const uri = arg || vscode.window.activeTextEditor?.document.uri; + if (typeof uri === 'object' && uri.scheme === DiffDocProvider.scheme) { + // A Git Graph URI has been provided + const request = decodeDiffDocUri(uri); + return openFile(request.repo, request.filePath, vscode.ViewColumn.Active).then((errorInfo) => { + if (errorInfo !== null) { + return showErrorMessage('Unable to Open File: ' + errorInfo); + } + }); + } else { + return showErrorMessage('Unable to Open File: The command was not called with the required arguments.'); + } + } + /* Helper Methods */ diff --git a/src/dataSource.ts b/src/dataSource.ts index 4ee39e21..d1a0be67 100644 --- a/src/dataSource.ts +++ b/src/dataSource.ts @@ -7,7 +7,7 @@ import { AskpassEnvironment, AskpassManager } from './askpass/askpassManager'; import { getConfig } from './config'; import { Logger } from './logger'; import { CommitOrdering, DateType, DeepWriteable, ErrorInfo, GitCommit, GitCommitDetails, GitCommitStash, GitConfigLocation, GitFileChange, GitFileStatus, GitPushBranchMode, GitRepoConfig, GitRepoConfigBranches, GitResetMode, GitSignatureStatus, GitStash, MergeActionOn, RebaseActionOn, SquashMessageFormat, TagType, Writeable } from './types'; -import { GitExecutable, UNABLE_TO_FIND_GIT_MSG, UNCOMMITTED, abbrevCommit, constructIncompatibleGitVersionMessage, getPathFromStr, getPathFromUri, isGitAtLeastVersion, openGitTerminal, pathWithTrailingSlash, realpath, resolveSpawnOutput, showErrorMessage } from './utils'; +import { GitExecutable, UNABLE_TO_FIND_GIT_MSG, UNCOMMITTED, abbrevCommit, constructIncompatibleGitVersionMessage, doesVersionMeetRequirement, getPathFromStr, getPathFromUri, openGitTerminal, pathWithTrailingSlash, realpath, resolveSpawnOutput, showErrorMessage } from './utils'; import { Disposable } from './utils/disposable'; import { Event } from './utils/event'; @@ -88,7 +88,7 @@ export class DataSource extends Disposable { */ public setGitExecutable(gitExecutable: GitExecutable | null) { this.gitExecutable = gitExecutable; - this.gitExecutableSupportsGpgInfo = gitExecutable !== null ? isGitAtLeastVersion(gitExecutable, '2.4.0') : false; + this.gitExecutableSupportsGpgInfo = gitExecutable !== null ? doesVersionMeetRequirement(gitExecutable.version, '2.4.0') : false; this.generateGitCommandFormats(); } @@ -719,7 +719,7 @@ export class DataSource extends Disposable { if (pruneTags) { if (!prune) { return Promise.resolve('In order to Prune Tags, pruning must also be enabled when fetching from ' + (remote !== null ? 'a remote' : 'remote(s)') + '.'); - } else if (this.gitExecutable !== null && !isGitAtLeastVersion(this.gitExecutable, '2.17.0')) { + } else if (this.gitExecutable !== null && !doesVersionMeetRequirement(this.gitExecutable.version, '2.17.0')) { return Promise.resolve(constructIncompatibleGitVersionMessage(this.gitExecutable, '2.17.0', 'pruning tags when fetching')); } args.push('--prune-tags'); @@ -1197,8 +1197,7 @@ export class DataSource extends Disposable { public pushStash(repo: string, message: string, includeUntracked: boolean): Promise { if (this.gitExecutable === null) { return Promise.resolve(UNABLE_TO_FIND_GIT_MSG); - } - if (!isGitAtLeastVersion(this.gitExecutable, '2.13.2')) { + } else if (!doesVersionMeetRequirement(this.gitExecutable.version, '2.13.2')) { return Promise.resolve(constructIncompatibleGitVersionMessage(this.gitExecutable, '2.13.2')); } diff --git a/src/diffDocProvider.ts b/src/diffDocProvider.ts index 9ab521f3..abfce9cd 100644 --- a/src/diffDocProvider.ts +++ b/src/diffDocProvider.ts @@ -47,15 +47,20 @@ export class DiffDocProvider extends Disposable implements vscode.TextDocumentCo * @returns The content of the text document. */ public provideTextDocumentContent(uri: vscode.Uri): string | Thenable { - let document = this.docs.get(uri.toString()); - if (document) return document.value; + const document = this.docs.get(uri.toString()); + if (document) { + return document.value; + } - let request = decodeDiffDocUri(uri); - if (request === null) return ''; // Return empty file (used for one side of added / deleted file diff) + const request = decodeDiffDocUri(uri); + if (!request.exists) { + // Return empty file (used for one side of added / deleted file diff) + return ''; + } return this.dataSource.getCommitFile(request.repo, request.commit, request.filePath).then( (contents) => { - let document = new DiffDocument(contents); + const document = new DiffDocument(contents); this.docs.set(uri.toString(), document); return document.value; }, @@ -99,7 +104,8 @@ type DiffDocUriData = { filePath: string; commit: string; repo: string; -} | null; + exists: boolean; +}; /** * Produce the URI of a file to be used in the Visual Studio Diff View. @@ -115,17 +121,19 @@ export function encodeDiffDocUri(repo: string, filePath: string, commit: string, return vscode.Uri.file(path.join(repo, filePath)); } - let data: DiffDocUriData, extension: string; - if ((diffSide === DiffSide.Old && type === GitFileStatus.Added) || (diffSide === DiffSide.New && type === GitFileStatus.Deleted)) { - data = null; + const fileDoesNotExist = (diffSide === DiffSide.Old && type === GitFileStatus.Added) || (diffSide === DiffSide.New && type === GitFileStatus.Deleted); + const data: DiffDocUriData = { + filePath: getPathFromStr(filePath), + commit: commit, + repo: repo, + exists: !fileDoesNotExist + }; + + let extension: string; + if (fileDoesNotExist) { extension = ''; } else { - data = { - filePath: getPathFromStr(filePath), - commit: commit, - repo: repo - }; - let extIndex = data.filePath.indexOf('.', data.filePath.lastIndexOf('/') + 1); + const extIndex = data.filePath.indexOf('.', data.filePath.lastIndexOf('/') + 1); extension = extIndex > -1 ? data.filePath.substring(extIndex) : ''; } diff --git a/src/utils.ts b/src/utils.ts index 33a30d91..30a48206 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -334,16 +334,17 @@ export function openExternalUrl(url: string, type: string = 'External URL'): The * Open a file within a repository in Visual Studio Code. * @param repo The repository the file is contained in. * @param filePath The relative path of the file within the repository. + * @param viewColumn An optional ViewColumn that the file should be opened in. * @returns A promise resolving to the ErrorInfo of the executed command. */ -export function openFile(repo: string, filePath: string) { +export function openFile(repo: string, filePath: string, viewColumn: vscode.ViewColumn | null = null) { return new Promise(resolve => { const p = path.join(repo, filePath); fs.access(p, fs.constants.R_OK, (err) => { if (err === null) { vscode.commands.executeCommand('vscode.open', vscode.Uri.file(p), { preview: true, - viewColumn: getConfig().openNewTabEditorGroup + viewColumn: viewColumn === null ? getConfig().openNewTabEditorGroup : viewColumn }).then( () => resolve(null), () => resolve('Visual Studio Code was unable to open ' + filePath + '.') @@ -710,17 +711,17 @@ export async function getGitExecutableFromPaths(paths: string[]): Promise `executable` is at least `version`, FALSE => `executable` is older than `version`. + * Checks whether a version is at least a required version. + * @param version The version to check. + * @param requiredVersion The minimum required version. + * @returns TRUE => `version` is at least `requiredVersion`, FALSE => `version` is older than `requiredVersion`. */ -export function isGitAtLeastVersion(executable: GitExecutable, version: string) { - const v1 = parseVersion(executable.version); - const v2 = parseVersion(version); +export function doesVersionMeetRequirement(version: string, requiredVersion: string) { + const v1 = parseVersion(version); + const v2 = parseVersion(requiredVersion); if (v1 === null || v2 === null) { // Unable to parse a version number diff --git a/tests/commands.test.ts b/tests/commands.test.ts index ba4544e3..3ebbf415 100644 --- a/tests/commands.test.ts +++ b/tests/commands.test.ts @@ -15,10 +15,12 @@ import { ConfigurationChangeEvent } from 'vscode'; import { AvatarManager } from '../src/avatarManager'; import { CommandManager } from '../src/commands'; import { DataSource } from '../src/dataSource'; +import { DiffSide, encodeDiffDocUri } from '../src/diffDocProvider'; import { DEFAULT_REPO_STATE, ExtensionState } from '../src/extensionState'; import { GitGraphView } from '../src/gitGraphView'; import { Logger } from '../src/logger'; import { RepoManager } from '../src/repoManager'; +import { GitFileStatus } from '../src/types'; import * as utils from '../src/utils'; import { EventEmitter } from '../src/utils/event'; @@ -69,7 +71,7 @@ describe('CommandManager', () => { it('Should construct a CommandManager, and be disposed', () => { // Assert - expect(commandManager['disposables']).toHaveLength(10); + expect(commandManager['disposables']).toHaveLength(11); expect(commandManager['gitExecutable']).toStrictEqual({ path: '/path/to/git', version: '2.25.0' @@ -96,6 +98,81 @@ describe('CommandManager', () => { }); }); + describe('git-graph:codiconsSupported', () => { + it('Should set git-graph:codiconsSupported to TRUE when vscode.version >= 1.42.0', () => { + // Setup + commandManager.dispose(); + const spyOnExecuteCommand = jest.spyOn(vscode.commands, 'executeCommand'); + const spyOnLog = jest.spyOn(logger, 'log'); + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + commandManager = new CommandManager(vscode.mocks.extensionContext, avatarManager, dataSource, extensionState, repoManager, { path: '/path/to/git', version: '2.25.0' }, onDidChangeGitExecutable.subscribe, logger); + + // Assert + waitForExpect(() => { + expect(spyOnExecuteCommand).toHaveBeenCalledWith('setContext', 'git-graph:codiconsSupported', true); + expect(spyOnLog).toHaveBeenCalledWith('Successfully set Visual Studio Code Context "git-graph:codiconsSupported" to "true"'); + }); + }); + + it('Should set git-graph:codiconsSupported to FALSE when vscode.version < 1.42.0', () => { + // Setup + commandManager.dispose(); + vscode.mockVscodeVersion('1.41.1'); + const spyOnExecuteCommand = jest.spyOn(vscode.commands, 'executeCommand'); + const spyOnLog = jest.spyOn(logger, 'log'); + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + commandManager = new CommandManager(vscode.mocks.extensionContext, avatarManager, dataSource, extensionState, repoManager, { path: '/path/to/git', version: '2.25.0' }, onDidChangeGitExecutable.subscribe, logger); + + // Assert + waitForExpect(() => { + expect(spyOnExecuteCommand).toHaveBeenCalledWith('setContext', 'git-graph:codiconsSupported', false); + expect(spyOnLog).toHaveBeenCalledWith('Successfully set Visual Studio Code Context "git-graph:codiconsSupported" to "false"'); + }); + }); + + it('Should log an error message when vscode.commands.executeCommand rejects', () => { + // Setup + commandManager.dispose(); + const spyOnExecuteCommand = jest.spyOn(vscode.commands, 'executeCommand'); + const spyOnLogError = jest.spyOn(logger, 'logError'); + vscode.commands.executeCommand.mockRejectedValueOnce(null); + + // Run + commandManager = new CommandManager(vscode.mocks.extensionContext, avatarManager, dataSource, extensionState, repoManager, { path: '/path/to/git', version: '2.25.0' }, onDidChangeGitExecutable.subscribe, logger); + + // Assert + waitForExpect(() => { + expect(spyOnExecuteCommand).toHaveBeenCalledWith('setContext', 'git-graph:codiconsSupported', true); + expect(spyOnLogError).toHaveBeenCalledWith('Failed to set Visual Studio Code Context "git-graph:codiconsSupported" to "true"'); + }); + }); + + it('Should log an error message when an exception is thrown', () => { + // Setup + commandManager.dispose(); + const spyOnExecuteCommand = jest.spyOn(vscode.commands, 'executeCommand'); + const spyOnDoesVersionMeetRequirement = jest.spyOn(utils, 'doesVersionMeetRequirement'); + const spyOnLogError = jest.spyOn(logger, 'logError'); + vscode.commands.executeCommand.mockRejectedValueOnce(null); + spyOnDoesVersionMeetRequirement.mockImplementationOnce(() => { + throw new Error(); + }); + + // Run + commandManager = new CommandManager(vscode.mocks.extensionContext, avatarManager, dataSource, extensionState, repoManager, { path: '/path/to/git', version: '2.25.0' }, onDidChangeGitExecutable.subscribe, logger); + + // Assert + waitForExpect(() => { + expect(spyOnExecuteCommand).toHaveBeenCalledWith('setContext', 'git-graph:codiconsSupported', true); + expect(spyOnLogError).toHaveBeenCalledWith('Unable to set Visual Studio Code Context "git-graph:codiconsSupported"'); + }); + }); + }); + describe('git-graph.view', () => { it('Should open the Git Graph View', async () => { // Setup @@ -141,6 +218,7 @@ describe('CommandManager', () => { it('Should open the Git Graph View to the repository containing the active text editor', async () => { // Setup + vscode.window.activeTextEditor = { document: { uri: vscode.Uri.file('/path/to/workspace-folder/active-file.txt') } }; vscode.mockExtensionSettingReturnValue('openToTheRepoOfTheActiveTextEditorDocument', true); jest.spyOn(repoManager, 'getRepoContainingFile').mockReturnValueOnce('/path/to/workspace-folder'); @@ -1118,4 +1196,69 @@ describe('CommandManager', () => { }); }); }); + + describe('git-graph.openFile', () => { + let spyOnOpenFile: jest.SpyInstance; + beforeAll(() => { + spyOnOpenFile = jest.spyOn(utils, 'openFile'); + }); + + it('Should open the provided file', async () => { + spyOnOpenFile.mockResolvedValueOnce(null); + + // Run + await vscode.commands.executeCommand('git-graph.openFile', encodeDiffDocUri('/path/to/repo', 'subfolder/modified.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', GitFileStatus.Modified, DiffSide.New)); + + // Assert + expect(spyOnOpenFile).toHaveBeenCalledWith('/path/to/repo', 'subfolder/modified.txt', vscode.ViewColumn.Active); + }); + + it('Should open the file of the active text editor', async () => { + vscode.window.activeTextEditor = { document: { uri: encodeDiffDocUri('/path/to/repo', 'subfolder/modified.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', GitFileStatus.Modified, DiffSide.New) } }; + spyOnOpenFile.mockResolvedValueOnce(null); + + // Run + await vscode.commands.executeCommand('git-graph.openFile'); + + // Assert + expect(spyOnOpenFile).toHaveBeenCalledWith('/path/to/repo', 'subfolder/modified.txt', vscode.ViewColumn.Active); + }); + + it('Should display an error message when no URI is provided', async () => { + vscode.window.activeTextEditor = undefined; + vscode.window.showErrorMessage.mockResolvedValueOnce(null); + + // Run + await vscode.commands.executeCommand('git-graph.openFile'); + + // Assert + expect(spyOnOpenFile).not.toHaveBeenCalled(); + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith('Unable to Open File: The command was not called with the required arguments.'); + }); + + it('Should display an error message when no Git Graph URI is provided', async () => { + vscode.window.activeTextEditor = { document: { uri: vscode.Uri.file('/path/to/workspace-folder/active-file.txt') } }; + vscode.window.showErrorMessage.mockResolvedValueOnce(null); + + // Run + await vscode.commands.executeCommand('git-graph.openFile'); + + // Assert + expect(spyOnOpenFile).not.toHaveBeenCalled(); + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith('Unable to Open File: The command was not called with the required arguments.'); + }); + + it('Should display an error message when the file can\'t be opened', async () => { + vscode.window.activeTextEditor = { document: { uri: encodeDiffDocUri('/path/to/repo', 'subfolder/modified.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', GitFileStatus.Modified, DiffSide.New) } }; + spyOnOpenFile.mockResolvedValueOnce('Error Message'); + vscode.window.showErrorMessage.mockResolvedValueOnce(null); + + // Run + await vscode.commands.executeCommand('git-graph.openFile'); + + // Assert + expect(spyOnOpenFile).toHaveBeenCalledWith('/path/to/repo', 'subfolder/modified.txt', vscode.ViewColumn.Active); + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith('Unable to Open File: Error Message'); + }); + }); }); diff --git a/tests/diffDocProvider.test.ts b/tests/diffDocProvider.test.ts index 5f036bcd..f04f90cc 100644 --- a/tests/diffDocProvider.test.ts +++ b/tests/diffDocProvider.test.ts @@ -156,7 +156,7 @@ describe('encodeDiffDocUri', () => { // Assert expect(uri.scheme).toBe('git-graph'); expect(uri.fsPath).toBe('file'); - expect(uri.query).toBe('bnVsbA=='); + expect(uri.query).toBe('eyJmaWxlUGF0aCI6InBhdGgvdG8vZmlsZS50eHQiLCJjb21taXQiOiIxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiIiwicmVwbyI6Ii9yZXBvIiwiZXhpc3RzIjpmYWxzZX0='); }); it('Should return an empty file URI if requested on a file displayed on the new side of the diff, and it is deleted', () => { @@ -166,7 +166,7 @@ describe('encodeDiffDocUri', () => { // Assert expect(uri.scheme).toBe('git-graph'); expect(uri.fsPath).toBe('file'); - expect(uri.query).toBe('bnVsbA=='); + expect(uri.query).toBe('eyJmaWxlUGF0aCI6InBhdGgvdG8vZmlsZS50eHQiLCJjb21taXQiOiIxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiIiwicmVwbyI6Ii9yZXBvIiwiZXhpc3RzIjpmYWxzZX0='); }); it('Should return a git-graph URI with the provided file extension', () => { @@ -176,7 +176,7 @@ describe('encodeDiffDocUri', () => { // Assert expect(uri.scheme).toBe('git-graph'); expect(uri.fsPath).toBe('file.txt'); - expect(uri.query).toBe('eyJmaWxlUGF0aCI6InBhdGgvdG8vZmlsZS50eHQiLCJjb21taXQiOiIxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiIiwicmVwbyI6Ii9yZXBvIn0='); + expect(uri.query).toBe('eyJmaWxlUGF0aCI6InBhdGgvdG8vZmlsZS50eHQiLCJjb21taXQiOiIxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiIiwicmVwbyI6Ii9yZXBvIiwiZXhpc3RzIjp0cnVlfQ=='); }); it('Should return a git-graph URI with no file extension when it is not provided', () => { @@ -186,7 +186,7 @@ describe('encodeDiffDocUri', () => { // Assert expect(uri.scheme).toBe('git-graph'); expect(uri.fsPath).toBe('file'); - expect(uri.query).toBe('eyJmaWxlUGF0aCI6InBhdGgvdG8vZmlsZSIsImNvbW1pdCI6IjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZjFhMmIiLCJyZXBvIjoiL3JlcG8ifQ=='); + expect(uri.query).toBe('eyJmaWxlUGF0aCI6InBhdGgvdG8vZmlsZSIsImNvbW1pdCI6IjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZjFhMmIiLCJyZXBvIjoiL3JlcG8iLCJleGlzdHMiOnRydWV9'); }); }); @@ -206,14 +206,15 @@ describe('decodeDiffDocUri', () => { // Run const value = decodeDiffDocUri(vscode.Uri.file('file.txt').with({ scheme: 'git-graph', - query: 'eyJmaWxlUGF0aCI6InBhdGgvdG8vZmlsZS50eHQiLCJjb21taXQiOiIxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiIiwicmVwbyI6Ii9yZXBvIn0=' + query: 'eyJmaWxlUGF0aCI6InBhdGgvdG8vZmlsZS50eHQiLCJjb21taXQiOiIxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiIiwicmVwbyI6Ii9yZXBvIiwiZXhpc3RzIjp0cnVlfQ==' })); // Assert expect(value).toStrictEqual({ filePath: 'path/to/file.txt', commit: '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', - repo: '/repo' + repo: '/repo', + exists: true }); }); }); diff --git a/tests/mocks/vscode.ts b/tests/mocks/vscode.ts index aad5f4db..bbe30aee 100644 --- a/tests/mocks/vscode.ts +++ b/tests/mocks/vscode.ts @@ -119,7 +119,7 @@ export enum StatusBarAlignment { Right = 2 } -export const version = '1.51.0'; +export let version = '1.51.0'; export enum ViewColumn { Active = -1, @@ -136,7 +136,7 @@ export enum ViewColumn { } export const window = { - activeTextEditor: { document: { uri: Uri.file('/path/to/workspace-folder/active-file.txt') } }, + activeTextEditor: { document: { uri: Uri.file('/path/to/workspace-folder/active-file.txt') } } as any, createOutputChannel: jest.fn(() => mocks.outputChannel), createStatusBarItem: jest.fn(() => mocks.statusBarItem), createTerminal: jest.fn(() => mocks.terminal), @@ -170,8 +170,14 @@ beforeEach(() => { Object.keys(mockedExtensionSettingValues).forEach((section) => { delete mockedExtensionSettingValues[section]; }); + + version = '1.51.0'; }); export function mockExtensionSettingReturnValue(section: string, value: any) { mockedExtensionSettingValues[section] = value; } + +export function mockVscodeVersion(newVersion: string) { + version = newVersion; +} diff --git a/tests/utils.test.ts b/tests/utils.test.ts index 73a13bfb..3f88595a 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -24,7 +24,7 @@ import { DataSource } from '../src/dataSource'; import { ExtensionState } from '../src/extensionState'; import { Logger } from '../src/logger'; import { GitFileStatus, PullRequestProvider } from '../src/types'; -import { GitExecutable, UNCOMMITTED, abbrevCommit, abbrevText, archive, constructIncompatibleGitVersionMessage, copyFilePathToClipboard, copyToClipboard, createPullRequest, evalPromises, findGit, getExtensionVersion, getGitExecutable, getGitExecutableFromPaths, getNonce, getPathFromStr, getPathFromUri, getRelativeTimeDiff, getRepoName, isGitAtLeastVersion, isPathInWorkspace, openExtensionSettings, openExternalUrl, openFile, openGitTerminal, pathWithTrailingSlash, realpath, resolveSpawnOutput, resolveToSymbolicPath, showErrorMessage, showInformationMessage, viewDiff, viewDiffWithWorkingFile, viewFileAtRevision, viewScm } from '../src/utils'; +import { GitExecutable, UNCOMMITTED, abbrevCommit, abbrevText, archive, constructIncompatibleGitVersionMessage, copyFilePathToClipboard, copyToClipboard, createPullRequest, doesVersionMeetRequirement, evalPromises, findGit, getExtensionVersion, getGitExecutable, getGitExecutableFromPaths, getNonce, getPathFromStr, getPathFromUri, getRelativeTimeDiff, getRepoName, isPathInWorkspace, openExtensionSettings, openExternalUrl, openFile, openGitTerminal, pathWithTrailingSlash, realpath, resolveSpawnOutput, resolveToSymbolicPath, showErrorMessage, showInformationMessage, viewDiff, viewDiffWithWorkingFile, viewFileAtRevision, viewScm } from '../src/utils'; import { EventEmitter } from '../src/utils/event'; const extensionContext = vscode.mocks.extensionContext; @@ -940,7 +940,7 @@ describe('openExternalUrl', () => { }); describe('openFile', () => { - it('Should open the file in vscode', async () => { + it('Should open the file in vscode (with the user defined ViewColumn)', async () => { // Setup mockedFileSystemModule.access.mockImplementationOnce((_1: fs.PathLike, _2: number | undefined, callback: (err: NodeJS.ErrnoException | null) => void) => callback(null)); vscode.commands.executeCommand.mockResolvedValueOnce(null); @@ -959,6 +959,25 @@ describe('openFile', () => { expect(result).toBe(null); }); + it('Should open the file in vscode (in the specified ViewColumn)', async () => { + // Setup + mockedFileSystemModule.access.mockImplementationOnce((_1: fs.PathLike, _2: number | undefined, callback: (err: NodeJS.ErrnoException | null) => void) => callback(null)); + vscode.commands.executeCommand.mockResolvedValueOnce(null); + + // Run + const result = await openFile('/path/to/repo', 'file.txt', vscode.ViewColumn.Beside); + + // Assert + const [command, uri, config] = vscode.commands.executeCommand.mock.calls[0]; + expect(command).toBe('vscode.open'); + expect(getPathFromUri(uri)).toBe('/path/to/repo/file.txt'); + expect(config).toStrictEqual({ + preview: true, + viewColumn: vscode.ViewColumn.Beside + }); + expect(result).toBe(null); + }); + it('Should return an error message if vscode was unable to open the file', async () => { // Setup mockedFileSystemModule.access.mockImplementationOnce((_1: fs.PathLike, _2: number | undefined, callback: (err: NodeJS.ErrnoException | null) => void) => callback(null)); @@ -989,13 +1008,13 @@ describe('viewDiff', () => { vscode.commands.executeCommand.mockResolvedValueOnce(null); // Run - const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', 'subfolder/added.txt', 'subfolder/added.txt', GitFileStatus.Added); + const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', 'subfolder/added.txt', 'subfolder/added.txt', GitFileStatus.Added); // Assert const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; expect(command).toBe('vscode.diff'); - expect(leftUri.toString()).toBe('git-graph://file?bnVsbA=='); - expect(rightUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9hZGRlZC50eHQiLCJjb21taXQiOiIxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYiLCJyZXBvIjoiL3BhdGgvdG8vcmVwbyJ9'); + expect(leftUri.toString()).toBe(expectedValueGitGraphUri('subfolder/added.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '/path/to/repo', false)); + expect(rightUri.toString()).toBe(expectedValueGitGraphUri('subfolder/added.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '/path/to/repo', true)); expect(title).toBe('added.txt (Added in 1a2b3c4d)'); expect(config).toStrictEqual({ preview: true, @@ -1009,13 +1028,13 @@ describe('viewDiff', () => { vscode.commands.executeCommand.mockResolvedValueOnce(null); // Run - const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', 'subfolder/modified.txt', 'subfolder/modified.txt', GitFileStatus.Modified); + const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', 'subfolder/modified.txt', 'subfolder/modified.txt', GitFileStatus.Modified); // Assert const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; expect(command).toBe('vscode.diff'); - expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9tb2RpZmllZC50eHQiLCJjb21taXQiOiIxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmZeIiwicmVwbyI6Ii9wYXRoL3RvL3JlcG8ifQ=='); - expect(rightUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9tb2RpZmllZC50eHQiLCJjb21taXQiOiIxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYiLCJyZXBvIjoiL3BhdGgvdG8vcmVwbyJ9'); + expect(leftUri.toString()).toBe(expectedValueGitGraphUri('subfolder/modified.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '/path/to/repo', true)); + expect(rightUri.toString()).toBe(expectedValueGitGraphUri('subfolder/modified.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '/path/to/repo', true)); expect(title).toBe('modified.txt (1a2b3c4d^ ↔ 1a2b3c4d)'); expect(config).toStrictEqual({ preview: true, @@ -1029,13 +1048,13 @@ describe('viewDiff', () => { vscode.commands.executeCommand.mockResolvedValueOnce(null); // Run - const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', 'subfolder/deleted.txt', 'subfolder/deleted.txt', GitFileStatus.Deleted); + const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', 'subfolder/deleted.txt', 'subfolder/deleted.txt', GitFileStatus.Deleted); // Assert const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; expect(command).toBe('vscode.diff'); - expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9kZWxldGVkLnR4dCIsImNvbW1pdCI6IjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2Zl4iLCJyZXBvIjoiL3BhdGgvdG8vcmVwbyJ9'); - expect(rightUri.toString()).toBe('git-graph://file?bnVsbA=='); + expect(leftUri.toString()).toBe(expectedValueGitGraphUri('subfolder/deleted.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b^', '/path/to/repo', true)); + expect(rightUri.toString()).toBe(expectedValueGitGraphUri('subfolder/deleted.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '/path/to/repo', false)); expect(title).toBe('deleted.txt (Deleted in 1a2b3c4d)'); expect(config).toStrictEqual({ preview: true, @@ -1049,13 +1068,13 @@ describe('viewDiff', () => { vscode.commands.executeCommand.mockResolvedValueOnce(null); // Run - const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', 'subfolder/added.txt', 'subfolder/added.txt', GitFileStatus.Added); + const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', 'subfolder/added.txt', 'subfolder/added.txt', GitFileStatus.Added); // Assert const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; expect(command).toBe('vscode.diff'); - expect(leftUri.toString()).toBe('git-graph://file?bnVsbA=='); - expect(rightUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9hZGRlZC50eHQiLCJjb21taXQiOiJhMWIyYzNkNGU1ZjZhMWIyYzNkNGU1ZjZhMWIyYzNkNGU1ZjZhMWIyIiwicmVwbyI6Ii9wYXRoL3RvL3JlcG8ifQ=='); + expect(leftUri.toString()).toBe(expectedValueGitGraphUri('subfolder/added.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '/path/to/repo', false)); + expect(rightUri.toString()).toBe(expectedValueGitGraphUri('subfolder/added.txt', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', '/path/to/repo', true)); expect(title).toBe('added.txt (Added between 1a2b3c4d & a1b2c3d4)'); expect(config).toStrictEqual({ preview: true, @@ -1069,13 +1088,13 @@ describe('viewDiff', () => { vscode.commands.executeCommand.mockResolvedValueOnce(null); // Run - const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', 'subfolder/modified.txt', 'subfolder/modified.txt', GitFileStatus.Modified); + const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', 'subfolder/modified.txt', 'subfolder/modified.txt', GitFileStatus.Modified); // Assert const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; expect(command).toBe('vscode.diff'); - expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9tb2RpZmllZC50eHQiLCJjb21taXQiOiIxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYiLCJyZXBvIjoiL3BhdGgvdG8vcmVwbyJ9'); - expect(rightUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9tb2RpZmllZC50eHQiLCJjb21taXQiOiJhMWIyYzNkNGU1ZjZhMWIyYzNkNGU1ZjZhMWIyYzNkNGU1ZjZhMWIyIiwicmVwbyI6Ii9wYXRoL3RvL3JlcG8ifQ=='); + expect(leftUri.toString()).toBe(expectedValueGitGraphUri('subfolder/modified.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '/path/to/repo', true)); + expect(rightUri.toString()).toBe(expectedValueGitGraphUri('subfolder/modified.txt', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', '/path/to/repo', true)); expect(title).toBe('modified.txt (1a2b3c4d ↔ a1b2c3d4)'); expect(config).toStrictEqual({ preview: true, @@ -1089,13 +1108,13 @@ describe('viewDiff', () => { vscode.commands.executeCommand.mockResolvedValueOnce(null); // Run - const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', 'subfolder/deleted.txt', 'subfolder/deleted.txt', GitFileStatus.Deleted); + const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', 'subfolder/deleted.txt', 'subfolder/deleted.txt', GitFileStatus.Deleted); // Assert const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; expect(command).toBe('vscode.diff'); - expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9kZWxldGVkLnR4dCIsImNvbW1pdCI6IjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZiIsInJlcG8iOiIvcGF0aC90by9yZXBvIn0='); - expect(rightUri.toString()).toBe('git-graph://file?bnVsbA=='); + expect(leftUri.toString()).toBe(expectedValueGitGraphUri('subfolder/deleted.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '/path/to/repo', true)); + expect(rightUri.toString()).toBe(expectedValueGitGraphUri('subfolder/deleted.txt', 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', '/path/to/repo', false)); expect(title).toBe('deleted.txt (Deleted between 1a2b3c4d & a1b2c3d4)'); expect(config).toStrictEqual({ preview: true, @@ -1109,12 +1128,12 @@ describe('viewDiff', () => { vscode.commands.executeCommand.mockResolvedValueOnce(null); // Run - const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', UNCOMMITTED, 'subfolder/added.txt', 'subfolder/added.txt', GitFileStatus.Added); + const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', UNCOMMITTED, 'subfolder/added.txt', 'subfolder/added.txt', GitFileStatus.Added); // Assert const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; expect(command).toBe('vscode.diff'); - expect(leftUri.toString()).toBe('git-graph://file?bnVsbA=='); + expect(leftUri.toString()).toBe(expectedValueGitGraphUri('subfolder/added.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '/path/to/repo', false)); expect(getPathFromUri(rightUri)).toBe('/path/to/repo/subfolder/added.txt'); expect(title).toBe('added.txt (Added between 1a2b3c4d & Present)'); expect(config).toStrictEqual({ @@ -1129,12 +1148,12 @@ describe('viewDiff', () => { vscode.commands.executeCommand.mockResolvedValueOnce(null); // Run - const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', UNCOMMITTED, 'subfolder/modified.txt', 'subfolder/modified.txt', GitFileStatus.Modified); + const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', UNCOMMITTED, 'subfolder/modified.txt', 'subfolder/modified.txt', GitFileStatus.Modified); // Assert const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; expect(command).toBe('vscode.diff'); - expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9tb2RpZmllZC50eHQiLCJjb21taXQiOiIxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYiLCJyZXBvIjoiL3BhdGgvdG8vcmVwbyJ9'); + expect(leftUri.toString()).toBe(expectedValueGitGraphUri('subfolder/modified.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '/path/to/repo', true)); expect(getPathFromUri(rightUri)).toBe('/path/to/repo/subfolder/modified.txt'); expect(title).toBe('modified.txt (1a2b3c4d ↔ Present)'); expect(config).toStrictEqual({ @@ -1149,13 +1168,13 @@ describe('viewDiff', () => { vscode.commands.executeCommand.mockResolvedValueOnce(null); // Run - const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', UNCOMMITTED, 'subfolder/deleted.txt', 'subfolder/deleted.txt', GitFileStatus.Deleted); + const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', UNCOMMITTED, 'subfolder/deleted.txt', 'subfolder/deleted.txt', GitFileStatus.Deleted); // Assert const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; expect(command).toBe('vscode.diff'); - expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9kZWxldGVkLnR4dCIsImNvbW1pdCI6IjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZiIsInJlcG8iOiIvcGF0aC90by9yZXBvIn0='); - expect(rightUri.toString()).toBe('git-graph://file?bnVsbA=='); + expect(leftUri.toString()).toBe(expectedValueGitGraphUri('subfolder/deleted.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '/path/to/repo', true)); + expect(rightUri.toString()).toBe(expectedValueGitGraphUri('subfolder/deleted.txt', '*', '/path/to/repo', false)); expect(title).toBe('deleted.txt (Deleted between 1a2b3c4d & Present)'); expect(config).toStrictEqual({ preview: true, @@ -1174,7 +1193,7 @@ describe('viewDiff', () => { // Assert const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; expect(command).toBe('vscode.diff'); - expect(leftUri.toString()).toBe('git-graph://file?bnVsbA=='); + expect(leftUri.toString()).toBe(expectedValueGitGraphUri('subfolder/added.txt', 'HEAD', '/path/to/repo', false)); expect(getPathFromUri(rightUri)).toBe('/path/to/repo/subfolder/added.txt'); expect(title).toBe('added.txt (Uncommitted)'); expect(config).toStrictEqual({ @@ -1194,7 +1213,7 @@ describe('viewDiff', () => { // Assert const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; expect(command).toBe('vscode.diff'); - expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9tb2RpZmllZC50eHQiLCJjb21taXQiOiJIRUFEIiwicmVwbyI6Ii9wYXRoL3RvL3JlcG8ifQ=='); + expect(leftUri.toString()).toBe(expectedValueGitGraphUri('subfolder/modified.txt', 'HEAD', '/path/to/repo', true)); expect(getPathFromUri(rightUri)).toBe('/path/to/repo/subfolder/modified.txt'); expect(title).toBe('modified.txt (Uncommitted)'); expect(config).toStrictEqual({ @@ -1214,8 +1233,8 @@ describe('viewDiff', () => { // Assert const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; expect(command).toBe('vscode.diff'); - expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9kZWxldGVkLnR4dCIsImNvbW1pdCI6IkhFQUQiLCJyZXBvIjoiL3BhdGgvdG8vcmVwbyJ9'); - expect(rightUri.toString()).toBe('git-graph://file?bnVsbA=='); + expect(leftUri.toString()).toBe(expectedValueGitGraphUri('subfolder/deleted.txt', 'HEAD', '/path/to/repo', true)); + expect(rightUri.toString()).toBe(expectedValueGitGraphUri('subfolder/deleted.txt', '*', '/path/to/repo', false)); expect(title).toBe('deleted.txt (Uncommitted)'); expect(config).toStrictEqual({ preview: true, @@ -1229,7 +1248,7 @@ describe('viewDiff', () => { vscode.commands.executeCommand.mockRejectedValueOnce(null); // Run - const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', 'subfolder/modified.txt', 'subfolder/modified.txt', GitFileStatus.Modified); + const result = await viewDiff('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', 'subfolder/modified.txt', 'subfolder/modified.txt', GitFileStatus.Modified); // Assert expect(result).toBe('Visual Studio Code was unable to load the diff editor for subfolder/modified.txt.'); @@ -1262,12 +1281,12 @@ describe('viewDiffWithWorkingFile', () => { vscode.commands.executeCommand.mockResolvedValueOnce(null); // Run - const result = await viewDiffWithWorkingFile('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', 'subfolder/modified.txt'); + const result = await viewDiffWithWorkingFile('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', 'subfolder/modified.txt'); // Assert const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; expect(command).toBe('vscode.diff'); - expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9tb2RpZmllZC50eHQiLCJjb21taXQiOiIxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYxYTJiM2M0ZDVlNmYiLCJyZXBvIjoiL3BhdGgvdG8vcmVwbyJ9'); + expect(leftUri.toString()).toBe(expectedValueGitGraphUri('subfolder/modified.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '/path/to/repo', true)); expect(getPathFromUri(rightUri)).toBe('/path/to/repo/subfolder/modified.txt'); expect(title).toBe('modified.txt (1a2b3c4d ↔ Present)'); expect(config).toStrictEqual({ @@ -1283,13 +1302,13 @@ describe('viewDiffWithWorkingFile', () => { vscode.commands.executeCommand.mockResolvedValueOnce(null); // Run - const result = await viewDiffWithWorkingFile('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', 'subfolder/deleted.txt'); + const result = await viewDiffWithWorkingFile('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', 'subfolder/deleted.txt'); // Assert const [command, leftUri, rightUri, title, config] = vscode.commands.executeCommand.mock.calls[0]; expect(command).toBe('vscode.diff'); - expect(leftUri.toString()).toBe('git-graph://file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9kZWxldGVkLnR4dCIsImNvbW1pdCI6IjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZiIsInJlcG8iOiIvcGF0aC90by9yZXBvIn0='); - expect(rightUri.toString()).toBe('git-graph://file?bnVsbA=='); + expect(leftUri.toString()).toBe(expectedValueGitGraphUri('subfolder/deleted.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '/path/to/repo', true)); + expect(rightUri.toString()).toBe(expectedValueGitGraphUri('subfolder/deleted.txt', '*', '/path/to/repo', false)); expect(title).toBe('deleted.txt (Deleted between 1a2b3c4d & Present)'); expect(config).toStrictEqual({ preview: true, @@ -1304,7 +1323,7 @@ describe('viewDiffWithWorkingFile', () => { vscode.commands.executeCommand.mockRejectedValueOnce(null); // Run - const result = await viewDiffWithWorkingFile('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', 'subfolder/modified.txt'); + const result = await viewDiffWithWorkingFile('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', 'subfolder/modified.txt'); // Assert expect(result).toBe('Visual Studio Code was unable to load the diff editor for subfolder/modified.txt.'); @@ -1317,12 +1336,12 @@ describe('viewFileAtRevision', () => { vscode.commands.executeCommand.mockResolvedValueOnce(null); // Run - const result = await viewFileAtRevision('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', 'subfolder/file.txt'); + const result = await viewFileAtRevision('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', 'subfolder/file.txt'); // Assert const [command, uri, config] = vscode.commands.executeCommand.mock.calls[0]; expect(command).toBe('vscode.open'); - expect(uri.toString()).toBe('git-graph://1a2b3c4d: file.txt?eyJmaWxlUGF0aCI6InN1YmZvbGRlci9maWxlLnR4dCIsImNvbW1pdCI6IjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZjFhMmIzYzRkNWU2ZiIsInJlcG8iOiIvcGF0aC90by9yZXBvIn0='); + expect(uri.toString()).toBe(expectedValueGitGraphUri('subfolder/file.txt', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', '/path/to/repo', true).replace('file.txt', '1a2b3c4d: file.txt')); expect(config).toStrictEqual({ preview: true, viewColumn: vscode.ViewColumn.Active @@ -1335,7 +1354,7 @@ describe('viewFileAtRevision', () => { vscode.commands.executeCommand.mockRejectedValueOnce(null); // Run - const result = await viewFileAtRevision('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f', 'subfolder/file.txt'); + const result = await viewFileAtRevision('/path/to/repo', '1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d5e6f1a2b', 'subfolder/file.txt'); // Assert expect(result).toBe('Visual Studio Code was unable to open subfolder/file.txt at commit 1a2b3c4d.'); @@ -2163,10 +2182,10 @@ describe('getGitExecutableFromPaths', () => { }); }); -describe('isGitAtLeastVersion', () => { +describe('doesVersionMeetRequirement', () => { it('Should correctly determine major newer', () => { // Run - const result = isGitAtLeastVersion({ version: '2.4.6.windows.0', path: '' }, '1.4.6'); + const result = doesVersionMeetRequirement('2.4.6.windows.0', '1.4.6'); // Assert expect(result).toBe(true); @@ -2174,7 +2193,7 @@ describe('isGitAtLeastVersion', () => { it('Should correctly determine major older', () => { // Run - const result = isGitAtLeastVersion({ version: '2.4.6.windows.0', path: '' }, '3.4.6'); + const result = doesVersionMeetRequirement('2.4.6.windows.0', '3.4.6'); // Assert expect(result).toBe(false); @@ -2182,7 +2201,7 @@ describe('isGitAtLeastVersion', () => { it('Should correctly determine minor newer', () => { // Run - const result = isGitAtLeastVersion({ version: '2.4.6 (Apple Git-122.3)', path: '' }, '2.3.6'); + const result = doesVersionMeetRequirement('2.4.6 (Apple Git-122.3)', '2.3.6'); // Assert expect(result).toBe(true); @@ -2190,7 +2209,7 @@ describe('isGitAtLeastVersion', () => { it('Should correctly determine minor older', () => { // Run - const result = isGitAtLeastVersion({ version: '2.4.6 (Apple Git-122.3)', path: '' }, '2.5.6'); + const result = doesVersionMeetRequirement('2.4.6 (Apple Git-122.3)', '2.5.6'); // Assert expect(result).toBe(false); @@ -2198,7 +2217,7 @@ describe('isGitAtLeastVersion', () => { it('Should correctly determine patch newer', () => { // Run - const result = isGitAtLeastVersion({ version: '2.4.6', path: '' }, '2.4.5'); + const result = doesVersionMeetRequirement('2.4.6', '2.4.5'); // Assert expect(result).toBe(true); @@ -2206,7 +2225,7 @@ describe('isGitAtLeastVersion', () => { it('Should correctly determine patch older', () => { // Run - const result = isGitAtLeastVersion({ version: '2.4.6', path: '' }, '2.4.7'); + const result = doesVersionMeetRequirement('2.4.6', '2.4.7'); // Assert expect(result).toBe(false); @@ -2214,7 +2233,7 @@ describe('isGitAtLeastVersion', () => { it('Should correctly determine same version', () => { // Run - const result = isGitAtLeastVersion({ version: '2.4.6', path: '' }, '2.4.6'); + const result = doesVersionMeetRequirement('2.4.6', '2.4.6'); // Assert expect(result).toBe(true); @@ -2222,7 +2241,7 @@ describe('isGitAtLeastVersion', () => { it('Should correctly determine major newer if missing patch version', () => { // Run - const result = isGitAtLeastVersion({ version: '2.4', path: '' }, '1.4'); + const result = doesVersionMeetRequirement('2.4', '1.4'); // Assert expect(result).toBe(true); @@ -2230,7 +2249,7 @@ describe('isGitAtLeastVersion', () => { it('Should correctly determine major newer if missing minor & patch versions', () => { // Run - const result = isGitAtLeastVersion({ version: '2', path: '' }, '1'); + const result = doesVersionMeetRequirement('2', '1'); // Assert expect(result).toBe(true); @@ -2238,13 +2257,13 @@ describe('isGitAtLeastVersion', () => { it('Should only use the valid portion of the version number to compute the result', () => { // Run - const result1 = isGitAtLeastVersion({ version: '2.4..6-windows.0', path: '' }, '2.4.1'); + const result1 = doesVersionMeetRequirement('2.4..6-windows.0', '2.4.1'); // Assert expect(result1).toBe(false); // Run - const result2 = isGitAtLeastVersion({ version: '2.4..6-windows.0', path: '' }, '2.4.0'); + const result2 = doesVersionMeetRequirement('2.4..6-windows.0', '2.4.0'); // Assert expect(result2).toBe(true); @@ -2252,7 +2271,7 @@ describe('isGitAtLeastVersion', () => { it('Should return TRUE if executable version is invalid', () => { // Run - const result = isGitAtLeastVersion({ version: 'a2.4.6', path: '' }, '1.4.6'); + const result = doesVersionMeetRequirement('a2.4.6', '1.4.6'); // Assert expect(result).toBe(true); @@ -2260,7 +2279,7 @@ describe('isGitAtLeastVersion', () => { it('Should return TRUE if version is invalid', () => { // Run - const result = isGitAtLeastVersion({ version: '2.4.6', path: '' }, 'a1.4.6'); + const result = doesVersionMeetRequirement('2.4.6', 'a1.4.6'); // Assert expect(result).toBe(true); @@ -2276,3 +2295,9 @@ describe('constructIncompatibleGitVersionMessage', () => { expect(result).toBe('A newer version of Git (>= 3.0.0) is required for this feature. Git 2.4.5 is currently installed. Please install a newer version of Git to use this feature.'); }); }); + +function expectedValueGitGraphUri(filePath: string, commit: string, repo: string, exists: boolean) { + const extIndex = filePath.indexOf('.', filePath.lastIndexOf('/') + 1); + const extension = exists && extIndex > -1 ? filePath.substring(extIndex) : ''; + return 'git-graph://file' + extension + '?' + Buffer.from(JSON.stringify({ filePath: filePath, commit: commit, repo: repo, exists: exists })).toString('base64'); +}