diff --git a/src/constants.ts b/src/constants.ts index 67f53b3..8a71ed8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -258,6 +258,7 @@ export interface Usage { export const enum WorkspaceState { AssumeRepositoriesOnStartup = 'gitlens:assumeRepositoriesOnStartup', + GitPath = 'gitlens:gitPath', BranchComparisons = 'gitlens:branch:comparisons', ConnectedPrefix = 'gitlens:connected:', diff --git a/src/container.ts b/src/container.ts index e2403e3..b36391a 100644 --- a/src/container.ts +++ b/src/container.ts @@ -139,9 +139,7 @@ export class Container { this._ready = true; this.registerGitProviders(); - queueMicrotask(() => { - this._onReady.fire(); - }); + queueMicrotask(() => this._onReady.fire()); } @log() diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index 53d5880..ace8b61 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -23,7 +23,7 @@ import type { GitExtension, } from '../../../@types/vscode.git'; import { configuration } from '../../../configuration'; -import { BuiltInGitConfiguration, DocumentSchemes, GlyphChars } from '../../../constants'; +import { BuiltInGitConfiguration, DocumentSchemes, GlyphChars, WorkspaceState } from '../../../constants'; import type { Container } from '../../../container'; import { StashApplyError, StashApplyErrorReason } from '../../../git/errors'; import { @@ -94,7 +94,7 @@ import { LogCorrelationContext, Logger } from '../../../logger'; import { Messages } from '../../../messages'; import { Arrays, debug, Functions, gate, Iterables, log, Promises, Strings, Versions } from '../../../system'; import { isFolderGlob, normalizePath, splitPath } from '../../../system/path'; -import { PromiseOrValue } from '../../../system/promise'; +import { any, PromiseOrValue } from '../../../system/promise'; import { CachedBlame, CachedDiff, @@ -215,16 +215,6 @@ export class LocalGitProvider implements GitProvider, Disposable { const scmPromise = this.getScmGitApi(); - // Try to use the same git as the built-in vscode git extension, but only wait for a bit - const timeout = 100; - let gitPath; - try { - const gitApi = await Promises.cancellable(scmPromise, timeout); - gitPath = gitApi?.git.path; - } catch { - Logger.log(cc, `Stopped waiting for built-in Git, after ${timeout} ms...`); - } - async function subscribeToScmOpenCloseRepository( container: Container, apiPromise: Promise, @@ -249,8 +239,34 @@ export class LocalGitProvider implements GitProvider, Disposable { } void subscribeToScmOpenCloseRepository(this.container, scmPromise); + const potentialGitPaths = + configuration.getAny('git.path') ?? + this.container.context.workspaceState.get(WorkspaceState.GitPath, undefined); + const start = hrtime(); - const location = await findGitPath(gitPath ?? configuration.getAny('git.path')); + + const findGitPromise = findGitPath(potentialGitPaths); + // Try to use the same git as the built-in vscode git extension, but don't wait for it if we find something faster + const findGitFromSCMPromise = scmPromise.then(gitApi => { + const path = gitApi?.git.path; + if (!path) return findGitPromise; + + if (potentialGitPaths != null) { + if (typeof potentialGitPaths === 'string') { + if (path === potentialGitPaths) return findGitPromise; + } else if (potentialGitPaths.includes(path)) { + return findGitPromise; + } + } + + return findGitPath(path, false); + }); + + const location = await any(findGitPromise, findGitFromSCMPromise); + // Save the found git path, but let things settle first to not impact startup performance + setTimeout(() => { + void this.container.context.workspaceState.update(WorkspaceState.GitPath, location.path); + }, 1000); if (cc != null) { cc.exitDetails = ` ${GlyphChars.Dot} Git found (${Strings.getDurationMilliseconds(start)} ms): ${ diff --git a/src/env/node/git/locator.ts b/src/env/node/git/locator.ts index 909be21..820fa3d 100644 --- a/src/env/node/git/locator.ts +++ b/src/env/node/git/locator.ts @@ -3,6 +3,7 @@ import { join as joinPaths } from 'path'; import { GlyphChars } from '../../../constants'; import { LogLevel } from '../../../logger'; import { Stopwatch } from '../../../system'; +import { any } from '../../../system/promise'; import { findExecutable, run } from './shell'; export class UnableToFindGitError extends Error { @@ -103,20 +104,29 @@ function findGitWin32(): Promise { .then(null, () => findSpecificGit('git')); } -export async function findGitPath(paths?: string | string[]): Promise { +export async function findGitPath( + paths: string | string[] | null | undefined, + search: boolean = true, +): Promise { try { if (paths == null || typeof paths === 'string') { return await findSpecificGit(paths ?? 'git'); } - for (const path of paths) { - try { - return await findSpecificGit(path); - } catch {} + try { + return any(...paths.map(p => findSpecificGit(p))); + } catch (ex) { + throw new UnableToFindGitError(ex); + } + } catch (ex) { + if (!search) { + return Promise.reject( + ex instanceof InvalidGitConfigError || ex instanceof UnableToFindGitError + ? ex + : new UnableToFindGitError(ex), + ); } - throw new UnableToFindGitError(); - } catch { try { switch (process.platform) { case 'darwin': diff --git a/src/system/promise.ts b/src/system/promise.ts index 9590469..3ac4c70 100644 --- a/src/system/promise.ts +++ b/src/system/promise.ts @@ -4,6 +4,35 @@ import { map } from './iterable'; export type PromiseOrValue = Promise | T; +export function any(...promises: Promise[]): Promise { + return new Promise((resolve, reject) => { + const errors: Error[] = []; + let settled = false; + + for (const promise of promises) { + // eslint-disable-next-line no-loop-func + void (async () => { + try { + const result = await promise; + if (settled) return; + + resolve(result); + settled = true; + } catch (ex) { + errors.push(ex); + } finally { + if (!settled) { + if (promises.length - errors.length < 1) { + reject(new AggregateError(errors)); + settled = true; + } + } + } + })(); + } + }); +} + export class CancellationError = Promise> extends Error { constructor(public readonly promise: T, message: string) { super(message); @@ -73,21 +102,6 @@ export function cancellable( }); } -export function first(promises: Promise[], predicate: (value: T) => boolean): Promise { - const newPromises: Promise[] = promises.map( - p => - new Promise((resolve, reject) => - p.then(value => { - if (predicate(value)) { - resolve(value); - } - }, reject), - ), - ); - newPromises.push(Promise.all(promises).then(() => undefined)); - return Promise.race(newPromises); -} - export function is(obj: PromiseLike | T): obj is Promise { return obj instanceof Promise || typeof (obj as PromiseLike)?.then === 'function'; } @@ -154,3 +168,11 @@ export async function raceAll( ), ); } + +export class AggregateError extends Error { + constructor(readonly errors: Error[]) { + super(`AggregateError(${errors.length})\n${errors.map(e => `\t${String(e)}`).join('\n')}`); + + Error.captureStackTrace?.(this, AggregateError); + } +}