From 7ace9cb152f78be5d5393df4d7356ff1c803ed13 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Thu, 10 Nov 2016 18:33:28 -0500 Subject: [PATCH] Adds support for custom git installation locations Also gracefully deals with the times when git isn't in the PATH --- .vscode/settings.json | 8 ++-- package.json | 9 ++++- src/@types/spawn-rx/index.d.ts | 10 ++--- src/configuration.ts | 1 + src/extension.ts | 61 ++++++++++++++++------------ src/git/git.ts | 24 ++++++----- src/git/gitLocator.ts | 74 ++++++++++++++++++++++++++++++++++ src/gitCodeLensProvider.ts | 2 +- 8 files changed, 142 insertions(+), 47 deletions(-) create mode 100644 src/git/gitLocator.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index ca6b108..2e8f7db 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,11 @@ -// Place your settings in this file to overwrite default and user settings. { "files.exclude": { - "out": false, // set this to true to hide the "out" folder with the compiled JS files - "node_modules": true + "node_modules": true, + "out": true }, "search.exclude": { - "out": true, // set this to false to include "out" folder in search results "node_modules": true, - "typings": true + "out": true }, "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version } \ No newline at end of file diff --git a/package.json b/package.json index 917761d..9685718 100644 --- a/package.json +++ b/package.json @@ -221,6 +221,11 @@ "default": false, "description": "Specifies debug mode" }, + "gitlens.advanced.git": { + "type": "string", + "default": null, + "description": "Specifies a git path to use" + }, "gitlens.advanced.output.level": { "type": "string", "default": "silent", @@ -346,8 +351,8 @@ "lodash.debounce": "^4.0.8", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.4.0", - "moment": "^2.15.2", - "spawn-rx": "^2.0.3", + "moment": "^2.16.0", + "spawn-rx": "^2.0.6", "tmp": "^0.0.30" }, "devDependencies": { diff --git a/src/@types/spawn-rx/index.d.ts b/src/@types/spawn-rx/index.d.ts index c5b8179..46a79b0 100644 --- a/src/@types/spawn-rx/index.d.ts +++ b/src/@types/spawn-rx/index.d.ts @@ -3,11 +3,11 @@ declare module "spawn-rx" { import { Observable } from 'rxjs/Observable'; namespace spawnrx { - function findActualExecutable(exe: string, args: Array): { cmd: string, args: Array }; - function spawnDetached(exe: string, params: Array, opts: Object|undefined): Observable; - function spawn(exe: string, params: Array, opts: Object|undefined): Observable; - function spawnDetachedPromise(exe: string, params: Array, opts: Object|undefined): Promise; - function spawnPromise(exe: string, params: Array, opts: Object|undefined): Promise; + function findActualExecutable(exe: string, args?: Array | undefined): { cmd: string, args: Array }; + function spawnDetached(exe: string, params?: Array | undefined, opts?: Object | undefined): Observable; + function spawn(exe: string, params?: Array | undefined, opts?: Object | undefined): Observable; + function spawnDetachedPromise(exe: string, params?: Array | undefined, opts?: Object | undefined): Promise; + function spawnPromise(exe: string, params?: Array | undefined, opts?: Object | undefined): Promise; } export = spawnrx; } \ No newline at end of file diff --git a/src/configuration.ts b/src/configuration.ts index 6770e77..3941b6b 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -87,6 +87,7 @@ export interface IAdvancedConfig { enabled: boolean; }; debug: boolean; + git: string; output: { level: OutputLevel; }; diff --git a/src/extension.ts b/src/extension.ts index b30753b..0f6f18e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,5 @@ 'use strict'; -import { ExtensionContext, extensions, languages, workspace } from 'vscode'; +import { ExtensionContext, extensions, languages, window, workspace } from 'vscode'; import BlameAnnotationController from './blameAnnotationController'; import BlameStatusBarController from './blameStatusBarController'; import GitContentProvider from './gitContentProvider'; @@ -7,6 +7,8 @@ import GitBlameCodeLensProvider from './gitBlameCodeLensProvider'; import GitBlameContentProvider from './gitBlameContentProvider'; import GitProvider, { Git } from './gitProvider'; import { WorkspaceState } from './constants'; +import { IAdvancedConfig } from './configuration'; +import { Logger } from './logger'; import DiffWithPreviousCommand from './commands/diffWithPrevious'; import DiffLineWithPreviousCommand from './commands/diffLineWithPrevious'; import DiffWithWorkingCommand from './commands/diffWithWorking'; @@ -16,10 +18,9 @@ import ShowBlameHistoryCommand from './commands/showBlameHistory'; import ShowHistoryCommand from './commands/showHistory'; import ToggleBlameCommand from './commands/toggleBlame'; import ToggleCodeLensCommand from './commands/toggleCodeLens'; -import { Logger } from './logger'; // this method is called when your extension is activated -export function activate(context: ExtensionContext) { +export async function activate(context: ExtensionContext) { // Workspace not using a folder. No access to git repo. if (!workspace.rootPath) { Logger.warn('GitLens inactive: no rootPath'); @@ -30,34 +31,44 @@ export function activate(context: ExtensionContext) { const rootPath = workspace.rootPath.replace(/\\/g, '/'); Logger.log(`GitLens active: ${rootPath}`); - Git.repoPath(rootPath).then(repoPath => { - context.workspaceState.update(WorkspaceState.RepoPath, repoPath); - context.workspaceState.update(WorkspaceState.HasGitHistoryExtension, extensions.getExtension('donjayamanne.githistory') !== undefined); + const gitPath = workspace.getConfiguration('gitlens').get('advanced').git; - const git = new GitProvider(context); - context.subscriptions.push(git); + let repoPath: string; + try { + repoPath = await Git.repoPath(rootPath, gitPath); + } + catch (ex) { + Logger.error(ex); + await window.showErrorMessage(`GitLens: Unable to find Git. Please make sure Git is installed. Also ensure that Git is either in the PATH, or that 'gitlens.advanced.git' is pointed to its installed location.`); + return; + } - context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitContentProvider.scheme, new GitContentProvider(context, git))); - context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitBlameContentProvider.scheme, new GitBlameContentProvider(context, git))); + context.workspaceState.update(WorkspaceState.RepoPath, repoPath); + context.workspaceState.update(WorkspaceState.HasGitHistoryExtension, extensions.getExtension('donjayamanne.githistory') !== undefined); - context.subscriptions.push(languages.registerCodeLensProvider(GitBlameCodeLensProvider.selector, new GitBlameCodeLensProvider(context, git))); + const git = new GitProvider(context); + context.subscriptions.push(git); - const annotationController = new BlameAnnotationController(context, git); - context.subscriptions.push(annotationController); + context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitContentProvider.scheme, new GitContentProvider(context, git))); + context.subscriptions.push(workspace.registerTextDocumentContentProvider(GitBlameContentProvider.scheme, new GitBlameContentProvider(context, git))); - const statusBarController = new BlameStatusBarController(context, git); - context.subscriptions.push(statusBarController); + context.subscriptions.push(languages.registerCodeLensProvider(GitBlameCodeLensProvider.selector, new GitBlameCodeLensProvider(context, git))); - context.subscriptions.push(new DiffWithWorkingCommand(git)); - context.subscriptions.push(new DiffLineWithWorkingCommand(git)); - context.subscriptions.push(new DiffWithPreviousCommand(git)); - context.subscriptions.push(new DiffLineWithPreviousCommand(git)); - context.subscriptions.push(new ShowBlameCommand(git, annotationController)); - context.subscriptions.push(new ToggleBlameCommand(git, annotationController)); - context.subscriptions.push(new ShowBlameHistoryCommand(git)); - context.subscriptions.push(new ShowHistoryCommand(git)); - context.subscriptions.push(new ToggleCodeLensCommand(git)); - }).catch(reason => Logger.warn(reason)); + const annotationController = new BlameAnnotationController(context, git); + context.subscriptions.push(annotationController); + + const statusBarController = new BlameStatusBarController(context, git); + context.subscriptions.push(statusBarController); + + context.subscriptions.push(new DiffWithWorkingCommand(git)); + context.subscriptions.push(new DiffLineWithWorkingCommand(git)); + context.subscriptions.push(new DiffWithPreviousCommand(git)); + context.subscriptions.push(new DiffLineWithPreviousCommand(git)); + context.subscriptions.push(new ShowBlameCommand(git, annotationController)); + context.subscriptions.push(new ToggleBlameCommand(git, annotationController)); + context.subscriptions.push(new ShowBlameHistoryCommand(git)); + context.subscriptions.push(new ShowHistoryCommand(git)); + context.subscriptions.push(new ToggleCodeLensCommand(git)); } // this method is called when your extension is deactivated diff --git a/src/git/git.ts b/src/git/git.ts index ee10d68..26958fb 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -1,28 +1,30 @@ 'use strict'; +import { findGitPath, IGit } from './gitLocator'; +import { Logger } from '../logger'; +import { spawnPromise } from 'spawn-rx'; import * as fs from 'fs'; import * as path from 'path'; import * as tmp from 'tmp'; -import { spawnPromise } from 'spawn-rx'; -import { Logger } from '../logger'; export * from './gitEnrichment'; export * from './enrichers/blameParserEnricher'; export * from './enrichers/logParserEnricher'; +let git: IGit; const UncommittedRegex = /^[0]+$/; async function gitCommand(cwd: string, ...args: any[]) { try { - const s = await spawnPromise('git', args, { cwd: cwd }); - Logger.log('git', ...args, cwd); + const s = await spawnPromise(git.path, args, { cwd: cwd }); + Logger.log('git', ...args, ` cwd='${cwd}'`); return s; } catch (ex) { const msg = ex && ex.toString(); - if (msg && (msg.includes('is outside repository') || msg.includes('no such path'))) { - Logger.warn('git', ...args, cwd, msg && msg.replace(/\r?\n|\r/g, ' ')); + if (msg && (msg.includes('Not a git repository') || msg.includes('is outside repository') || msg.includes('no such path'))) { + Logger.warn('git', ...args, ` cwd='${cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`); } else { - Logger.error('git', ...args, cwd, msg && msg.replace(/\r?\n|\r/g, ' ')); + Logger.error('git', ...args, ` cwd='${cwd}'`, msg && `\n ${msg.replace(/\r?\n|\r/g, ' ')}`); } throw ex; } @@ -52,8 +54,12 @@ export default class Git { } } - static repoPath(cwd: string) { - return gitCommand(cwd, 'rev-parse', '--show-toplevel').then(data => data.replace(/\r?\n|\r/g, '').replace(/\\/g, '/')); + static async repoPath(cwd: string, gitPath?: string) { + git = await findGitPath(gitPath); + + let data = await gitCommand(cwd, 'rev-parse', '--show-toplevel'); + data = data.replace(/\r?\n|\r/g, '').replace(/\\/g, '/'); + return data; } static blame(format: GitBlameFormat, fileName: string, sha?: string, repoPath?: string) { diff --git a/src/git/gitLocator.ts b/src/git/gitLocator.ts new file mode 100644 index 0000000..e4c59b7 --- /dev/null +++ b/src/git/gitLocator.ts @@ -0,0 +1,74 @@ +import { spawnPromise } from 'spawn-rx'; +import * as path from 'path'; + +export interface IGit { + path: string; + version: string; +} + +function parseVersion(raw: string): string { + return raw.replace(/^git version /, ''); +} + +async function findSpecificGit(path: string): Promise { + const version = await spawnPromise(path, ['--version']); + return { + path, + version: parseVersion(version.trim()) + }; +} + +async function findGitDarwin(): Promise { + try { + let path = await spawnPromise('which', ['git']); + path = path.replace(/^\s+|\s+$/g, ''); + + if (path !== '/usr/bin/git') { + return findSpecificGit(path); + } + + try { + await spawnPromise('xcode-select', ['-p']); + return findSpecificGit(path); + } + catch (ex) { + if (ex.code === 2) { + return Promise.reject('Unable to find git'); + } + return findSpecificGit(path); + } + } + catch (ex) { + return Promise.reject('Unable to find git'); + } +} + +function findSystemGitWin32(basePath: string): Promise { + if (!basePath) return Promise.reject('Unable to find git'); + return findSpecificGit(path.join(basePath, 'Git', 'cmd', 'git.exe')); +} + +function findGitWin32(): Promise { + return findSystemGitWin32(process.env['ProgramW6432']) + .then(null, () => findSystemGitWin32(process.env['ProgramFiles(x86)'])) + .then(null, () => findSystemGitWin32(process.env['ProgramFiles'])) + .then(null, () => findSpecificGit('git')); +} + +export async function findGitPath(path?: string): Promise { + try { + return await findSpecificGit(path || 'git'); + } + catch (ex) { + try { + switch (process.platform) { + case 'darwin': return await findGitDarwin(); + case 'win32': return await findGitWin32(); + default: return Promise.reject('Unable to find git'); + } + } + catch (ex) { + return Promise.reject('Unable to find git'); + } + } +} \ No newline at end of file diff --git a/src/gitCodeLensProvider.ts b/src/gitCodeLensProvider.ts index 4132abb..d87cc24 100644 --- a/src/gitCodeLensProvider.ts +++ b/src/gitCodeLensProvider.ts @@ -3,7 +3,7 @@ import { Iterables, Strings } from './system'; import { CancellationToken, CodeLens, CodeLensProvider, commands, DocumentSelector, ExtensionContext, Position, Range, SymbolInformation, SymbolKind, TextDocument, Uri, workspace } from 'vscode'; import { BuiltInCommands, Commands, DocumentSchemes, WorkspaceState } from './constants'; import { CodeLensCommand, CodeLensLocation, IConfig, ICodeLensLanguageLocation } from './configuration'; -import GitProvider, { GitCommit, IGitBlame, IGitBlameLines, IGitLog } from './gitProvider'; +import GitProvider, { GitCommit, IGitBlame, IGitBlameLines } from './gitProvider'; import * as moment from 'moment'; export class GitRecentChangeCodeLens extends CodeLens {