Skip to content

Commit

Permalink
#392 Support Visual Studio Code's "git.path" Setting containing an ar…
Browse files Browse the repository at this point in the history
…ray of possible Git executable paths (introduced in Visual Studio Code 1.50.0).
  • Loading branch information
mhutchie committed Oct 11, 2020
1 parent e799f69 commit f10111f
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 28 deletions.
17 changes: 13 additions & 4 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,10 +499,19 @@ class Config {
}

/**
* Get the value of the `git.path` Visual Studio Code Setting.
*/
get gitPath() {
return vscode.workspace.getConfiguration('git').get<string | null>('path', null);
* Get the Git executable paths configured by the `git.path` Visual Studio Code Setting.
*/
get gitPaths() {
const configValue = vscode.workspace.getConfiguration('git').get<string | string[] | null>('path', null);
if (configValue === null) {
return [];
} else if (typeof configValue === 'string') {
return [configValue];
} else if (Array.isArray(configValue)) {
return configValue.filter((value) => typeof value === 'string');
} else {
return [];
}
}

/**
Expand Down
10 changes: 5 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { onStartUp } from './life-cycle/startup';
import { Logger } from './logger';
import { RepoManager } from './repoManager';
import { StatusBarItem } from './statusBarItem';
import { GitExecutable, UNABLE_TO_FIND_GIT_MSG, findGit, getGitExecutable, showErrorMessage, showInformationMessage } from './utils';
import { GitExecutable, UNABLE_TO_FIND_GIT_MSG, findGit, getGitExecutableFromPaths, showErrorMessage, showInformationMessage } from './utils';
import { EventEmitter } from './utils/event';

/**
Expand Down Expand Up @@ -52,17 +52,17 @@ export async function activate(context: vscode.ExtensionContext) {
if (event.affectsConfiguration('git-graph')) {
configurationEmitter.emit(event);
} else if (event.affectsConfiguration('git.path')) {
const path = getConfig().gitPath;
if (path === null) return;
const paths = getConfig().gitPaths;
if (paths.length === 0) return;

getGitExecutable(path).then((gitExecutable) => {
getGitExecutableFromPaths(paths).then((gitExecutable) => {
gitExecutableEmitter.emit(gitExecutable);
const msg = 'Git Graph is now using ' + gitExecutable.path + ' (version: ' + gitExecutable.version + ')';
showInformationMessage(msg);
logger.log(msg);
repoManager.searchWorkspaceForRepos();
}, () => {
const msg = 'The new value of "git.path" (' + path + ') does not match the path and filename of a valid Git executable.';
const msg = 'The new value of "git.path" ("' + paths.join('", "') + '") does not ' + (paths.length > 1 ? 'contain a string that matches' : 'match') + ' the path and filename of a valid Git executable.';
showErrorMessage(msg);
logger.logError(msg);
});
Expand Down
22 changes: 18 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,10 +523,10 @@ export async function findGit(extensionState: ExtensionState) {
} catch (_) { }
}

const configGitPath = getConfig().gitPath;
if (configGitPath !== null) {
const configGitPaths = getConfig().gitPaths;
if (configGitPaths.length > 0) {
try {
return await getGitExecutable(configGitPath);
return await getGitExecutableFromPaths(configGitPaths);
} catch (_) { }
}

Expand Down Expand Up @@ -612,7 +612,7 @@ function isExecutable(path: string) {
}

/**
* Gets information about a Git executable.
* Tests whether the specified path corresponds to the path of a Git executable.
* @param path The path of the Git executable.
* @returns The GitExecutable data.
*/
Expand All @@ -628,6 +628,20 @@ export function getGitExecutable(path: string) {
});
}

/**
* Tests whether one of the specified paths corresponds to the path of a Git executable.
* @param paths The paths of possible Git executables.
* @returns The GitExecutable data.
*/
export async function getGitExecutableFromPaths(paths: string[]): Promise<GitExecutable> {
for (let i = 0; i < paths.length; i++) {
try {
return await getGitExecutable(paths[i]);
} catch (_) { }
}
throw new Error('None of the provided paths are a Git executable');
}


/* Git Version Handling */

Expand Down
44 changes: 35 additions & 9 deletions tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3720,44 +3720,70 @@ describe('Config', () => {
});
});

describe('gitPath', () => {
describe('gitPaths', () => {
it('Should return the configured path', () => {
// Setup
workspaceConfiguration.get.mockReturnValueOnce('/path/to/git');

// Run
const value = config.gitPath;
const value = config.gitPaths;

// Assert
expect(vscode.workspace.getConfiguration).toBeCalledWith('git');
expect(workspaceConfiguration.get).toBeCalledWith('path', null);
expect(value).toBe('/path/to/git');
expect(value).toStrictEqual(['/path/to/git']);
});

it('Should return NULL when the configuration value is NULL', () => {
it('Should return the valid configured paths', () => {
// Setup
workspaceConfiguration.get.mockReturnValueOnce(['/path/to/first/git', '/path/to/second/git', 4, {}, null, '/path/to/third/git']);

// Run
const value = config.gitPaths;

// Assert
expect(vscode.workspace.getConfiguration).toBeCalledWith('git');
expect(workspaceConfiguration.get).toBeCalledWith('path', null);
expect(value).toStrictEqual(['/path/to/first/git', '/path/to/second/git', '/path/to/third/git']);
});

it('Should return an empty array when the configuration value is NULL', () => {
// Setup
workspaceConfiguration.get.mockReturnValueOnce(null);

// Run
const value = config.gitPath;
const value = config.gitPaths;

// Assert
expect(vscode.workspace.getConfiguration).toBeCalledWith('git');
expect(workspaceConfiguration.get).toBeCalledWith('path', null);
expect(value).toStrictEqual([]);
});

it('Should return an empty array when the configuration value is invalid', () => {
// Setup
workspaceConfiguration.get.mockReturnValueOnce(4);

// Run
const value = config.gitPaths;

// Assert
expect(vscode.workspace.getConfiguration).toBeCalledWith('git');
expect(workspaceConfiguration.get).toBeCalledWith('path', null);
expect(value).toBe(null);
expect(value).toStrictEqual([]);
});

it('Should return the default configuration value (NULL)', () => {
it('Should return an empty array when the default configuration value (NULL) is received', () => {
// Setup
workspaceConfiguration.get.mockImplementationOnce((_, defaultValue) => defaultValue);

// Run
const value = config.gitPath;
const value = config.gitPaths;

// Assert
expect(vscode.workspace.getConfiguration).toBeCalledWith('git');
expect(workspaceConfiguration.get).toBeCalledWith('path', null);
expect(value).toBe(null);
expect(value).toStrictEqual([]);
});
});

Expand Down
71 changes: 65 additions & 6 deletions tests/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,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, getGitExecutable, getNonce, getPathFromStr, getPathFromUri, getRelativeTimeDiff, getRepoName, isGitAtLeastVersion, isPathInWorkspace, openExtensionSettings, openFile, openGitTerminal, pathWithTrailingSlash, realpath, resolveSpawnOutput, resolveToSymbolicPath, showErrorMessage, showInformationMessage, viewDiff, viewFileAtRevision, viewScm } from '../src/utils';
import { GitExecutable, UNCOMMITTED, abbrevCommit, abbrevText, archive, constructIncompatibleGitVersionMessage, copyFilePathToClipboard, copyToClipboard, createPullRequest, evalPromises, findGit, getGitExecutable, getGitExecutableFromPaths, getNonce, getPathFromStr, getPathFromUri, getRelativeTimeDiff, getRepoName, isGitAtLeastVersion, isPathInWorkspace, openExtensionSettings, openFile, openGitTerminal, pathWithTrailingSlash, realpath, resolveSpawnOutput, resolveToSymbolicPath, showErrorMessage, showInformationMessage, viewDiff, viewFileAtRevision, viewScm } from '../src/utils';
import { EventEmitter } from '../src/utils/event';

let extensionContext = vscode.mocks.extensionContext;
Expand Down Expand Up @@ -1543,7 +1543,7 @@ describe('findGit', () => {
it('Should use the users git.path if the last known Git executable path no longer exists', async () => {
// Setup
jest.spyOn(extensionState, 'getLastKnownGitPath').mockReturnValueOnce('/path/to/not-git');
workspaceConfiguration.get.mockReturnValueOnce('/path/to/git'); // gitPath
workspaceConfiguration.get.mockReturnValueOnce('/path/to/git'); // git.path
mockSpawnGitVersionThrowingErrorOnce();
mockSpawnGitVersionSuccessOnce();

Expand All @@ -1561,7 +1561,7 @@ describe('findGit', () => {
it('Should use the users git.path if there is no last known Git executable path', async () => {
// Setup
jest.spyOn(extensionState, 'getLastKnownGitPath').mockReturnValueOnce(null);
workspaceConfiguration.get.mockReturnValueOnce('/path/to/git'); // gitPath
workspaceConfiguration.get.mockReturnValueOnce('/path/to/git'); // git.path
mockSpawnGitVersionSuccessOnce();

// Run
Expand All @@ -1579,7 +1579,7 @@ describe('findGit', () => {
let spyOnExec: jest.SpyInstance;
beforeEach(() => {
jest.spyOn(extensionState, 'getLastKnownGitPath').mockReturnValueOnce(null);
workspaceConfiguration.get.mockReturnValueOnce(null); // gitPath
workspaceConfiguration.get.mockReturnValueOnce(null); // git.path
Object.defineProperty(process, 'platform', { value: 'darwin' });
spyOnExec = jest.spyOn(cp, 'exec');
});
Expand Down Expand Up @@ -1699,7 +1699,7 @@ describe('findGit', () => {
let programW6432: string | undefined, programFilesX86: string | undefined, programFiles: string | undefined, localAppData: string | undefined, envPath: string | undefined;
beforeEach(() => {
jest.spyOn(extensionState, 'getLastKnownGitPath').mockReturnValueOnce(null);
workspaceConfiguration.get.mockReturnValueOnce(null); // gitPath
workspaceConfiguration.get.mockReturnValueOnce([]); // git.path
programW6432 = process.env['ProgramW6432'];
programFilesX86 = process.env['ProgramFiles(x86)'];
programFiles = process.env['ProgramFiles'];
Expand Down Expand Up @@ -1856,7 +1856,7 @@ describe('findGit', () => {
describe('process.platform === \'unknown\'', () => {
beforeEach(() => {
jest.spyOn(extensionState, 'getLastKnownGitPath').mockReturnValueOnce(null);
workspaceConfiguration.get.mockReturnValueOnce(null); // gitPath
workspaceConfiguration.get.mockReturnValueOnce(null); // git.path
Object.defineProperty(process, 'platform', { value: 'unknown' });
});

Expand Down Expand Up @@ -1948,6 +1948,65 @@ describe('getGitExecutable', () => {
});
});

describe('getGitExecutableFromPaths', () => {
it('Should return the git version information from the first valid path (all valid)', async () => {
// Setup
mockSpawnGitVersionSuccessOnce();

// Run
const result = await getGitExecutableFromPaths(['/path/to/first/git', '/path/to/second/git']);

// Assert
expect(result).toStrictEqual({
path: '/path/to/first/git',
version: '1.2.3'
});
expect(spyOnSpawn).toBeCalledTimes(1);
});

it('Should return the git version information from the first valid path (first invalid)', async () => {
// Setup
mockSpawnGitVersionThrowingErrorOnce();
mockSpawnGitVersionSuccessOnce();

// Run
const result = await getGitExecutableFromPaths(['/path/to/first/git', '/path/to/second/git']);

// Assert
expect(result).toStrictEqual({
path: '/path/to/second/git',
version: '1.2.3'
});
expect(spyOnSpawn).toBeCalledTimes(2);
});

it('Should reject when none of the provided paths are valid Git executables', async () => {
// Setup
let rejected = false;
mockSpawnGitVersionThrowingErrorOnce();
mockSpawnGitVersionThrowingErrorOnce();

// Run
await getGitExecutableFromPaths(['/path/to/first/git', '/path/to/second/git']).catch(() => rejected = true);

// Assert
expect(rejected).toBe(true);
expect(spyOnSpawn).toBeCalledTimes(2);
});

it('Should reject when no paths are provided', async () => {
// Setup
let rejected = false;

// Run
await getGitExecutableFromPaths([]).catch(() => rejected = true);

// Assert
expect(rejected).toBe(true);
expect(spyOnSpawn).toBeCalledTimes(0);
});
});

describe('isGitAtLeastVersion', () => {
it('Should correctly determine major newer', () => {
// Run
Expand Down

0 comments on commit f10111f

Please sign in to comment.