diff --git a/src/constants.ts b/src/constants.ts index 25bd454..c72eb6d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -255,6 +255,8 @@ export interface Usage { } export enum WorkspaceState { + AssumeRepositoriesOnStartup = 'gitlens:assumeRepositoriesOnStartup', + BranchComparisons = 'gitlens:branch:comparisons', ConnectedPrefix = 'gitlens:connected:', DefaultRemote = 'gitlens:remote:default', diff --git a/src/container.ts b/src/container.ts index 7e3c4c1..af5191f 100644 --- a/src/container.ts +++ b/src/container.ts @@ -24,8 +24,7 @@ import { FileAnnotationType, } from './configuration'; import { GitFileSystemProvider } from './git/fsProvider'; -import { GitProviderId } from './git/gitProvider'; -import { GitProviderService } from './git/gitProviderService'; +import { GitProviderId, GitProviderService } from './git/gitProviderService'; import { LocalGitProvider } from './git/providers/localGitProvider'; import { LineHoverController } from './hovers/lineHoverController'; import { Keyboard } from './keyboard'; @@ -147,14 +146,18 @@ export class Container { if (this._ready) throw new Error('Container is already ready'); this._ready = true; - this.registerGitProviders(); - this._onReady.fire(); + queueMicrotask(() => { + this.registerGitProviders(); + this._onReady.fire(); + }); } private registerGitProviders() { if (env.uiKind !== UIKind.Web) { this._context.subscriptions.push(this._git.register(GitProviderId.Git, new LocalGitProvider(this))); } + + this._git.registrationComplete(); } private onConfigurationChanging(e: ConfigurationWillChangeEvent) { diff --git a/src/extension.ts b/src/extension.ts index 2a2b4d0..b097c31 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -31,9 +31,6 @@ export function activate(context: ExtensionContext): Promise(GlobalState.Version) ?? context.globalState.get(GlobalState.Deprecated_Version); - let previousVersion; + let previousVersion: string | undefined; if (localVersion == null || syncedVersion == null) { previousVersion = syncedVersion ?? localVersion; } else if (Versions.compare(syncedVersion, localVersion) === 1) { @@ -95,46 +92,38 @@ export function activate(context: ExtensionContext): Promise('enabled', true); - if (!enabled) { - Logger.log(`GitLens (v${gitlensVersion}) was NOT activated -- "git.enabled": false`); - void setEnabled(false); - - void Messages.showGitDisabledErrorMessage(); - - return undefined; - } - Configuration.configure(context); const cfg = configuration.get(); // await migrateSettings(context, previousVersion); const container = Container.create(context, cfg); - // Signal that the container is now ready - container.ready(); + container.onReady(() => { + registerCommands(context); + registerBuiltInActionRunners(container); + registerPartnerActionRunners(context); - registerCommands(context); - registerBuiltInActionRunners(container); - registerPartnerActionRunners(context); + void showWelcomeOrWhatsNew(container, gitlensVersion, previousVersion); - void showWelcomeOrWhatsNew(container, gitlensVersion, previousVersion); + void context.globalState.update(GlobalState.Version, gitlensVersion); - void context.globalState.update(GlobalState.Version, gitlensVersion); + // Only update our synced version if the new version is greater + if (syncedVersion == null || Versions.compare(gitlensVersion, syncedVersion) === 1) { + void context.globalState.update(SyncedState.Version, gitlensVersion); + } - // Only update our synced version if the new version is greater - if (syncedVersion == null || Versions.compare(gitlensVersion, syncedVersion) === 1) { - void context.globalState.update(SyncedState.Version, gitlensVersion); - } + if (cfg.outputLevel === TraceLevel.Debug) { + setTimeout(async () => { + if (cfg.outputLevel !== TraceLevel.Debug) return; - if (cfg.outputLevel === TraceLevel.Debug) { - setTimeout(async () => { - if (cfg.outputLevel !== TraceLevel.Debug) return; + if (await Messages.showDebugLoggingWarningMessage()) { + void commands.executeCommand(Commands.DisableDebugLogging); + } + }, 60000); + } + }); - if (await Messages.showDebugLoggingWarningMessage()) { - void commands.executeCommand(Commands.DisableDebugLogging); - } - }, 60000); - } + // Signal that the container is now ready + container.ready(); Logger.log( `GitLens (v${gitlensVersion}${cfg.mode.active ? `, mode: ${cfg.mode.active}` : ''}) activated ${ @@ -163,10 +152,6 @@ export function deactivate() { // } // } -export async function setEnabled(enabled: boolean): Promise { - await Promise.all([setContext(ContextKeys.Enabled, enabled), setContext(ContextKeys.Disabled, !enabled)]); -} - function setKeysForSync(context: ExtensionContext, ...keys: (SyncedState | string)[]) { return context.globalState?.setKeysForSync([...keys, SyncedState.Version, SyncedState.WelcomeViewVisible]); } diff --git a/src/git/git.ts b/src/git/git.ts index b899c3f..ddb58b7 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -1,14 +1,12 @@ /* eslint-disable @typescript-eslint/naming-convention */ 'use strict'; import * as paths from 'path'; -import * as iconv from 'iconv-lite'; import { Uri, window, workspace } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; import { Logger } from '../logger'; -import { Messages } from '../messages'; -import { Paths, Strings, Versions } from '../system'; -import { findGitPath, GitLocation } from './locator'; +import { Paths, Strings } from '../system'; +import { GitLocation } from './locator'; import { GitRevision } from './models/models'; import { GitBranchParser, GitLogParser, GitReflogParser, GitStashParser, GitTagParser } from './parsers/parsers'; import { fsExists, run, RunError, RunOptions } from './shell'; @@ -130,7 +128,7 @@ export async function git(options: GitCommandOptio args.splice(0, 0, '-c', 'core.longpaths=true'); } - promise = run(Git.getGitPath(), args, encoding ?? 'utf8', runOpts); + promise = run(await Git.path(), args, encoding ?? 'utf8', runOpts); pendingCommands.set(command, promise); } else { @@ -208,43 +206,21 @@ function defaultExceptionHandler(ex: Error, cwd: string | undefined, start?: [nu } export namespace Git { - let gitInfo: GitLocation | undefined; - - export function getEncoding(encoding: string | undefined) { - return encoding !== undefined && iconv.encodingExists(encoding) ? encoding : 'utf8'; - } - - export function getGitPath(): string { - return gitInfo?.path ?? ''; - } - - export function getGitVersion(): string { - return gitInfo?.version ?? ''; + let gitLocator!: () => Promise; + export function setLocator(locator: () => Promise): void { + gitLocator = locator; } - export function hasGitPath(): boolean { - return Boolean(gitInfo?.path); + export async function path(): Promise { + return (await gitLocator()).path; } - export async function setOrFindGitPath(gitPath?: string | string[]): Promise { - const start = process.hrtime(); - - gitInfo = await findGitPath(gitPath); - - Logger.log( - `Git found: ${gitInfo.version} @ ${gitInfo.path === 'git' ? 'PATH' : gitInfo.path} ${ - GlyphChars.Dot - } ${Strings.getDurationMilliseconds(start)} ms`, - ); - - // Warn if git is less than v2.7.2 - if (Versions.compare(Versions.fromString(gitInfo.version), Versions.fromString('2.7.2')) === -1) { - void Messages.showGitVersionUnsupportedErrorMessage(gitInfo.version, '2.7.2'); - } + export async function version(): Promise { + return (await gitLocator()).version; } - export function validateVersion(major: number, minor: number): boolean { - const [gitMajor, gitMinor] = getGitVersion().split('.'); + export async function validateVersion(major: number, minor: number): Promise { + const [gitMajor, gitMinor] = (await version()).split('.'); return parseInt(gitMajor, 10) >= major && parseInt(gitMinor, 10) >= minor; } @@ -286,7 +262,7 @@ export namespace Git { const index = params.indexOf('--ignore-revs-file'); if (index !== -1) { // Ensure the version of Git supports the --ignore-revs-file flag, otherwise the blame will fail - let supported = Git.validateVersion(2, 23); + let supported = await Git.validateVersion(2, 23); if (supported) { let ignoreRevsFile = params[index + 1]; if (!paths.isAbsolute(ignoreRevsFile)) { @@ -1493,7 +1469,7 @@ export namespace Git { void (await git({ cwd: repoPath }, ...params)); } - export function status( + export async function status( repoPath: string, porcelainVersion: number = 1, { similarityThreshold }: { similarityThreshold?: number | null } = {}, @@ -1504,7 +1480,7 @@ export namespace Git { '--branch', '-u', ]; - if (Git.validateVersion(2, 18)) { + if (await Git.validateVersion(2, 18)) { params.push(`--find-renames${similarityThreshold == null ? '' : `=${similarityThreshold}%`}`); } @@ -1515,7 +1491,7 @@ export namespace Git { ); } - export function status__file( + export async function status__file( repoPath: string, fileName: string, porcelainVersion: number = 1, @@ -1524,7 +1500,7 @@ export namespace Git { const [file, root] = Paths.splitPath(fileName, repoPath); const params = ['status', porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain']; - if (Git.validateVersion(2, 18)) { + if (await Git.validateVersion(2, 18)) { params.push(`--find-renames${similarityThreshold == null ? '' : `=${similarityThreshold}%`}`); } diff --git a/src/git/gitProvider.ts b/src/git/gitProvider.ts index a18b886..d4f90f2 100644 --- a/src/git/gitProvider.ts +++ b/src/git/gitProvider.ts @@ -396,15 +396,7 @@ export interface GitProvider { hasTrackingBranch(repoPath: string | undefined): Promise; isActiveRepoPath(repoPath: string | undefined, editor?: TextEditor): Promise; - isTrackable(scheme: string): boolean; isTrackable(uri: Uri): boolean; - isTrackable(schemeOruri: string | Uri): boolean; - isTracked( - fileName: string, - repoPath?: string, - options?: { ref?: string | undefined; skipCacheUpdate?: boolean | undefined }, - ): Promise; - isTracked(uri: GitUri): Promise; isTracked( fileNameOrUri: string | GitUri, repoPath?: string, diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index 0adf398..c4c6cb7 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -1,4 +1,5 @@ 'use strict'; +import { encodingExists } from 'iconv-lite'; import { ConfigurationChangeEvent, Disposable, @@ -16,9 +17,15 @@ import { } from 'vscode'; import { resetAvatarCache } from '../avatars'; import { configuration } from '../configuration'; -import { BuiltInGitConfiguration, ContextKeys, DocumentSchemes, GlyphChars, setContext } from '../constants'; +import { + BuiltInGitConfiguration, + ContextKeys, + DocumentSchemes, + GlyphChars, + setContext, + WorkspaceState, +} from '../constants'; import { Container } from '../container'; -import { setEnabled } from '../extension'; import { Logger } from '../logger'; import { Arrays, debug, gate, Iterables, log, Paths, Promises, Strings } from '../system'; import { vslsUriPrefixRegex } from '../vsls/vsls'; @@ -28,7 +35,6 @@ import { BranchDateFormatting, BranchSortOptions, CommitDateFormatting, - Git, GitBlame, GitBlameLine, GitBlameLines, @@ -68,7 +74,7 @@ import { GitProvider, GitProviderDescriptor, GitProviderId, PagedResult, ScmRepo import { GitUri } from './gitUri'; import { RemoteProvider, RemoteProviders, RichRemoteProvider } from './remotes/factory'; -export type { GitProviderDescriptor, GitProviderId }; +export { type GitProviderDescriptor, GitProviderId }; const slash = '/'; @@ -122,7 +128,7 @@ export class GitProviderService implements Disposable { } this.resetCaches('providers'); - void this.updateContext(this._repositories); + void this.updateContext(); }), ); @@ -130,7 +136,7 @@ export class GitProviderService implements Disposable { CommitDateFormatting.reset(); PullRequestDateFormatting.reset(); - void this.updateContext(this._repositories); + void this.updateContext(); } dispose() { @@ -192,7 +198,7 @@ export class GitProviderService implements Disposable { } if (removed.length) { - void this.updateContext(this._repositories); + void this.updateContext(); // Defer the event trigger enough to let everything unwind queueMicrotask(() => { @@ -203,70 +209,6 @@ export class GitProviderService implements Disposable { } } - private async updateContext(repositories: Map) { - const hasRepositories = this.openRepositoryCount !== 0; - await setEnabled(hasRepositories); - - // Don't block for the remote context updates (because it can block other downstream requests during initialization) - async function updateRemoteContext() { - let hasRemotes = false; - let hasRichRemotes = false; - let hasConnectedRemotes = false; - if (hasRepositories) { - for (const repo of repositories.values()) { - if (!hasConnectedRemotes) { - hasConnectedRemotes = await repo.hasRichRemote(true); - - if (hasConnectedRemotes) { - hasRichRemotes = true; - hasRemotes = true; - } - } - - if (!hasRichRemotes) { - hasRichRemotes = await repo.hasRichRemote(); - } - - if (!hasRemotes) { - hasRemotes = await repo.hasRemotes(); - } - - if (hasRemotes && hasRichRemotes && hasConnectedRemotes) break; - } - } - - await Promise.all([ - setContext(ContextKeys.HasRemotes, hasRemotes), - setContext(ContextKeys.HasRichRemotes, hasRichRemotes), - setContext(ContextKeys.HasConnectedRemotes, hasConnectedRemotes), - ]); - } - - void updateRemoteContext(); - - // If we have no repositories setup a watcher in case one is initialized - if (!hasRepositories) { - for (const provider of this._providers.values()) { - const watcher = provider.createRepositoryInitWatcher?.(); - if (watcher != null) { - const disposable = Disposable.from( - watcher, - watcher.onDidCreate(uri => { - const f = workspace.getWorkspaceFolder(uri); - if (f == null) return; - - void this.discoverRepositories([f], { force: true }).then(() => { - if (Iterables.some(this.repositories, r => r.folder === f)) { - disposable.dispose(); - } - }); - }), - ); - } - } - } - } - get hasProviders(): boolean { return this._providers.size !== 0; } @@ -330,20 +272,42 @@ export class GitProviderService implements Disposable { if (this._providers.has(id)) throw new Error(`Provider '${id}' has already been registered`); this._providers.set(id, provider); - const disposable = provider.onDidChangeRepository(e => { - if (e.changed(RepositoryChange.Closed, RepositoryChangeComparisonMode.Any)) { - void this.updateContext(this._repositories); - // Send a notification that the repositories changed - queueMicrotask(() => this._onDidChangeRepositories.fire({ added: [], removed: [e.repository] })); - } + const disposables = []; + + const watcher = provider.createRepositoryInitWatcher?.(); + if (watcher != null) { + disposables.push( + watcher, + watcher.onDidCreate(uri => { + const f = workspace.getWorkspaceFolder(uri); + if (f == null) return; + + void this.discoverRepositories([f], { force: true }); + }), + ); + } - this._onDidChangeRepository.fire(e); - }); + const disposable = Disposable.from( + ...disposables, + provider.onDidChangeRepository(e => { + if (e.changed(RepositoryChange.Closed, RepositoryChangeComparisonMode.Any)) { + void this.updateContext(); + + // Send a notification that the repositories changed + queueMicrotask(() => this._onDidChangeRepositories.fire({ added: [], removed: [e.repository] })); + } + + this._onDidChangeRepository.fire(e); + }), + ); this._onDidChangeProviders.fire({ added: [provider], removed: [] }); - void this.onWorkspaceFoldersChanged({ added: workspace.workspaceFolders ?? [], removed: [] }); + // Don't kick off the discovery if we're still initializing (we'll do it at the end for all "known" providers) + if (!this._initializing) { + this.onWorkspaceFoldersChanged({ added: workspace.workspaceFolders ?? [], removed: [] }); + } return { dispose: () => { @@ -359,7 +323,7 @@ export class GitProviderService implements Disposable { } } - void this.updateContext(this._repositories); + void this.updateContext(); if (removed.length) { // Defer the event trigger enough to let everything unwind @@ -374,6 +338,27 @@ export class GitProviderService implements Disposable { }; } + private _initializing: boolean = true; + registrationComplete() { + this._initializing = false; + + const { workspaceFolders } = workspace; + if (workspaceFolders?.length) { + const autoRepositoryDetection = + configuration.getAny( + BuiltInGitConfiguration.AutoRepositoryDetection, + ) ?? true; + + if (autoRepositoryDetection !== false) { + void this.discoverRepositories(workspaceFolders); + + return; + } + } + + void this.updateContext(); + } + private _discoveredWorkspaceFolders = new Map>(); async discoverRepositories(folders: readonly WorkspaceFolder[], options?: { force?: boolean }): Promise { @@ -408,9 +393,10 @@ export class GitProviderService implements Disposable { this._repositories.set(repository.path, repository); } + void this.updateContext(); + if (added.length === 0) return; - void this.updateContext(this._repositories); // Defer the event trigger enough to let everything unwind queueMicrotask(() => this._onDidChangeRepositories.fire({ added: added, removed: [] })); } @@ -434,16 +420,85 @@ export class GitProviderService implements Disposable { } } - static getProviderId(repoPath: string | Uri): GitProviderId { - if (typeof repoPath !== 'string' && repoPath.scheme === DocumentSchemes.VirtualFS) { - if (repoPath.authority.startsWith('github')) { - return GitProviderId.GitHub; + private _context: { enabled: boolean; disabled: boolean } = { enabled: false, disabled: false }; + + async setEnabledContext(enabled: boolean): Promise { + let disabled = !enabled; + // If we think we should be disabled during startup, check if we have a saved value from the last time this repo was loaded + if (!enabled && this._initializing) { + disabled = !( + this.container.context.workspaceState.get(WorkspaceState.AssumeRepositoriesOnStartup) ?? true + ); + } + + if (this._context.enabled === enabled && this._context.disabled === disabled) return; + + const promises = []; + + if (this._context.enabled !== enabled) { + this._context.enabled = enabled; + promises.push(setContext(ContextKeys.Enabled, enabled)); + } + + if (this._context.disabled !== disabled) { + this._context.disabled = disabled; + promises.push(setContext(ContextKeys.Disabled, disabled)); + } + + await Promise.all(promises); + + if (!this._initializing) { + void this.container.context.workspaceState.update(WorkspaceState.AssumeRepositoriesOnStartup, enabled); + } + } + + private async updateContext() { + const hasRepositories = this.openRepositoryCount !== 0; + await this.setEnabledContext(hasRepositories); + + // Don't bother trying to set the values if we're still starting up + if (!hasRepositories && this._initializing) return; + + // Don't block for the remote context updates (because it can block other downstream requests during initialization) + async function updateRemoteContext(this: GitProviderService) { + let hasRemotes = false; + let hasRichRemotes = false; + let hasConnectedRemotes = false; + if (hasRepositories) { + for (const repo of this._repositories.values()) { + if (!hasConnectedRemotes) { + hasConnectedRemotes = await repo.hasRichRemote(true); + + if (hasConnectedRemotes) { + hasRichRemotes = true; + hasRemotes = true; + } + } + + if (!hasRichRemotes) { + hasRichRemotes = await repo.hasRichRemote(); + + if (hasRichRemotes) { + hasRemotes = true; + } + } + + if (!hasRemotes) { + hasRemotes = await repo.hasRemotes(); + } + + if (hasRemotes && hasRichRemotes && hasConnectedRemotes) break; + } } - throw new Error(`Unsupported scheme: ${repoPath.scheme}`); + await Promise.all([ + setContext(ContextKeys.HasRemotes, hasRemotes), + setContext(ContextKeys.HasRichRemotes, hasRichRemotes), + setContext(ContextKeys.HasConnectedRemotes, hasConnectedRemotes), + ]); } - return GitProviderId.Git; + void updateRemoteContext.call(this); } private getProvider(repoPath: string | Uri): { provider: GitProvider; path: string } { @@ -467,6 +522,18 @@ export class GitProviderService implements Disposable { } } + static getProviderId(repoPath: string | Uri): GitProviderId { + if (typeof repoPath !== 'string' && repoPath.scheme === DocumentSchemes.VirtualFS) { + if (repoPath.authority.startsWith('github')) { + return GitProviderId.GitHub; + } + + throw new Error(`Unsupported scheme: ${repoPath.scheme}`); + } + + return GitProviderId.Git; + } + @log() addRemote(repoPath: string | Uri, name: string, url: string): Promise { const { provider, path } = this.getProvider(repoPath); @@ -1396,7 +1463,7 @@ export class GitProviderService implements Disposable { repo = provider.createRepository(folder, rp, false); this._repositories.set(rp, repo); - void this.updateContext(this._repositories); + void this.updateContext(); // Send a notification that the repositories changed queueMicrotask(() => this._onDidChangeRepositories.fire({ added: [repo!], removed: [] })); @@ -1638,41 +1705,20 @@ export class GitProviderService implements Disposable { return repoPath === doc?.uri.repoPath; } - isTrackable(scheme: string): boolean; - isTrackable(uri: Uri): boolean; - isTrackable(schemeOruri: string | Uri): boolean { - const scheme = typeof schemeOruri === 'string' ? schemeOruri : schemeOruri.scheme; - return ( - scheme === DocumentSchemes.File || - scheme === DocumentSchemes.Git || - scheme === DocumentSchemes.GitLens || - scheme === DocumentSchemes.PRs || - scheme === DocumentSchemes.Vsls || - scheme === DocumentSchemes.VirtualFS - ); + isTrackable(uri: Uri): boolean { + const { provider } = this.getProvider(uri); + return provider.isTrackable(uri); } - async isTracked(uri: GitUri): Promise; - async isTracked( + private async isTracked( fileName: string, repoPath: string | Uri, options?: { ref?: string; skipCacheUpdate?: boolean }, - ): Promise; - @log({ - exit: tracked => `returned ${tracked}`, - singleLine: true, - }) - async isTracked( - fileNameOrUri: string | GitUri, - repoPath?: string | Uri, - options?: { ref?: string; skipCacheUpdate?: boolean }, ): Promise { if (options?.ref === GitRevision.deletedOrMissing) return false; - const { provider, path } = this.getProvider( - repoPath ?? (typeof fileNameOrUri === 'string' ? undefined! : fileNameOrUri), - ); - return provider.isTracked(fileNameOrUri, path, options); + const { provider, path } = this.getProvider(repoPath); + return provider.isTracked(fileName, path, options); } @log() @@ -1814,7 +1860,8 @@ export class GitProviderService implements Disposable { static getEncoding(uri: Uri): string; static getEncoding(repoPathOrUri: string | Uri, fileName?: string): string { const uri = typeof repoPathOrUri === 'string' ? GitUri.resolveToUri(fileName!, repoPathOrUri) : repoPathOrUri; - return Git.getEncoding(configuration.getAny('files.encoding', uri)); + const encoding = configuration.getAny('files.encoding', uri); + return encoding != null && encodingExists(encoding) ? encoding : 'utf8'; } } diff --git a/src/git/providers/localGitProvider.ts b/src/git/providers/localGitProvider.ts index f6afbfb..071a0a6 100644 --- a/src/git/providers/localGitProvider.ts +++ b/src/git/providers/localGitProvider.ts @@ -17,7 +17,7 @@ import { } from 'vscode'; import type { API as BuiltInGitApi, Repository as BuiltInGitRepository, GitExtension } from '../../@types/vscode.git'; import { configuration } from '../../configuration'; -import { BuiltInGitConfiguration, DocumentSchemes } from '../../constants'; +import { BuiltInGitConfiguration, DocumentSchemes, GlyphChars } from '../../constants'; import { Container } from '../../container'; import { LogCorrelationContext, Logger } from '../../logger'; import { Messages } from '../../messages'; @@ -84,7 +84,7 @@ import { import { GitProvider, GitProviderId, PagedResult, RepositoryInitWatcher, ScmRepository } from '../gitProvider'; import { GitProviderService } from '../gitProviderService'; import { GitUri } from '../gitUri'; -import { InvalidGitConfigError, UnableToFindGitError } from '../locator'; +import { findGitPath, GitLocation, InvalidGitConfigError, UnableToFindGitError } from '../locator'; import { GitReflogParser, GitShortLogParser } from '../parsers/parsers'; import { RemoteProvider, RemoteProviderFactory, RemoteProviders, RichRemoteProvider } from '../remotes/factory'; import { fsExists, isWindows } from '../shell'; @@ -121,7 +121,9 @@ export class LocalGitProvider implements GitProvider, Disposable { private readonly _trackedCache = new Map>(); private readonly _userMapCache = new Map(); - constructor(private readonly container: Container) {} + constructor(private readonly container: Container) { + Git.setLocator(this.ensureGit.bind(this)); + } dispose() {} @@ -167,16 +169,24 @@ export class LocalGitProvider implements GitProvider, Disposable { this._onDidChangeRepository.fire(e); } - private _initialized: Promise | undefined; - private async ensureInitialized(): Promise { - if (this._initialized == null) { - this._initialized = this.initializeCore(); + private _gitLocator: Promise | undefined; + private async ensureGit(): Promise { + if (this._gitLocator == null) { + this._gitLocator = this.findGit(); } - return this._initialized; + return this._gitLocator; } - private async initializeCore() { + @log() + private async findGit(): Promise { + if (!configuration.getAny('git.enabled', null, true)) { + Logger.log('Built-in Git is disabled ("git.enabled": false)'); + void Messages.showGitDisabledErrorMessage(); + + throw new UnableToFindGitError(); + } + // Try to use the same git as the built-in vscode git extension const gitApi = await this.getScmGitApi(); if (gitApi != null) { @@ -196,14 +206,23 @@ export class LocalGitProvider implements GitProvider, Disposable { ); } - if (Git.hasGitPath()) return; + const gitPath = gitApi?.git.path ?? configuration.getAny('git.path'); - await Git.setOrFindGitPath(gitApi?.git.path ?? configuration.getAny('git.path')); + const start = process.hrtime(); + const location = await findGitPath(gitPath); + Logger.log( + `Git found: ${location.version} @ ${location.path === 'git' ? 'PATH' : location.path} ${ + GlyphChars.Dot + } ${Strings.getDurationMilliseconds(start)} ms`, + ); // Warn if git is less than v2.7.2 - if (this.compareGitVersion('2.7.2') === -1) { - void Messages.showGitVersionUnsupportedErrorMessage(Git.getGitVersion(), '2.7.2'); + if (Versions.compare(Versions.fromString(location.version), Versions.fromString('2.7.2')) === -1) { + Logger.log(`Git version (${location.version}) is outdated`); + void Messages.showGitVersionUnsupportedErrorMessage(location.version, '2.7.2'); } + + return location; } async discoverRepositories(uri: Uri): Promise { @@ -216,7 +235,7 @@ export class LocalGitProvider implements GitProvider, Disposable { if (autoRepositoryDetection === false) return []; try { - await this.ensureInitialized(); + void (await this.ensureGit()); const repositories = await this.repositorySearch(workspace.getWorkspaceFolder(uri)!); if (autoRepositoryDetection === true || autoRepositoryDetection === 'subFolders') { @@ -3143,7 +3162,7 @@ export class LocalGitProvider implements GitProvider, Disposable { @log() async getStatusForFile(repoPath: string, fileName: string): Promise { - const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1; + const porcelainVersion = (await Git.validateVersion(2, 11)) ? 2 : 1; const data = await Git.status__file(repoPath, fileName, porcelainVersion, { similarityThreshold: this.container.config.advanced.similarityThreshold, @@ -3156,7 +3175,7 @@ export class LocalGitProvider implements GitProvider, Disposable { @log() async getStatusForFiles(repoPath: string, path: string): Promise { - const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1; + const porcelainVersion = (await Git.validateVersion(2, 11)) ? 2 : 1; const data = await Git.status__file(repoPath, path, porcelainVersion, { similarityThreshold: this.container.config.advanced.similarityThreshold, @@ -3171,7 +3190,7 @@ export class LocalGitProvider implements GitProvider, Disposable { async getStatusForRepo(repoPath: string | undefined): Promise { if (repoPath == null) return undefined; - const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1; + const porcelainVersion = (await Git.validateVersion(2, 11)) ? 2 : 1; const data = await Git.status(repoPath, porcelainVersion, { similarityThreshold: this.container.config.advanced.similarityThreshold, @@ -3396,10 +3415,8 @@ export class LocalGitProvider implements GitProvider, Disposable { return repoPath === doc?.uri.repoPath; } - isTrackable(scheme: string): boolean; - isTrackable(uri: Uri): boolean; - isTrackable(schemeOruri: string | Uri): boolean { - const scheme = typeof schemeOruri === 'string' ? schemeOruri : schemeOruri.scheme; + isTrackable(uri: Uri): boolean { + const { scheme } = uri; return ( scheme === DocumentSchemes.File || scheme === DocumentSchemes.Git || @@ -3409,12 +3426,6 @@ export class LocalGitProvider implements GitProvider, Disposable { ); } - async isTracked( - fileName: string, - repoPath?: string, - options?: { ref?: string; skipCacheUpdate?: boolean }, - ): Promise; - async isTracked(uri: GitUri): Promise; @log({ exit: tracked => `returned ${tracked}`, singleLine: true, @@ -3687,7 +3698,7 @@ export class LocalGitProvider implements GitProvider, Disposable { } @log() - stashSave( + async stashSave( repoPath: string, message?: string, uris?: Uri[], @@ -3695,7 +3706,7 @@ export class LocalGitProvider implements GitProvider, Disposable { ): Promise { if (uris == null) return Git.stash__push(repoPath, message, options); - this.ensureGitVersion( + await this.ensureGitVersion( '2.13.2', 'Stashing individual files', ' Please retry by stashing everything or install a more recent version of Git.', @@ -3704,10 +3715,10 @@ export class LocalGitProvider implements GitProvider, Disposable { const pathspecs = uris.map(u => `./${Paths.splitPath(u.fsPath, repoPath)[0]}`); const stdinVersion = '2.30.0'; - const stdin = this.compareGitVersion(stdinVersion) !== -1; + const stdin = (await this.compareGitVersion(stdinVersion)) !== -1; // If we don't support stdin, then error out if we are over the maximum allowed git cli length if (!stdin && Arrays.countStringLength(pathspecs) > maxGitCliLength) { - this.ensureGitVersion( + await this.ensureGitVersion( stdinVersion, `Stashing so many files (${pathspecs.length}) at once`, ' Please retry by stashing fewer files or install a more recent version of Git.', @@ -3774,14 +3785,14 @@ export class LocalGitProvider implements GitProvider, Disposable { } } - private compareGitVersion(version: string) { - return Versions.compare(Versions.fromString(Git.getGitVersion()), Versions.fromString(version)); + private async compareGitVersion(version: string) { + return Versions.compare(Versions.fromString(await Git.version()), Versions.fromString(version)); } - private ensureGitVersion(version: string, prefix: string, suffix: string): void { - if (this.compareGitVersion(version) === -1) { + private async ensureGitVersion(version: string, prefix: string, suffix: string): Promise { + if ((await this.compareGitVersion(version)) === -1) { throw new Error( - `${prefix} requires a newer version of Git (>= ${version}) than is currently installed (${Git.getGitVersion()}).${suffix}`, + `${prefix} requires a newer version of Git (>= ${version}) than is currently installed (${await Git.version()}).${suffix}`, ); } } diff --git a/src/vsls/guest.ts b/src/vsls/guest.ts index f4accfc..fe0a25e 100644 --- a/src/vsls/guest.ts +++ b/src/vsls/guest.ts @@ -2,7 +2,6 @@ import { CancellationToken, Disposable, window, WorkspaceFolder } from 'vscode'; import type { LiveShare, SharedServiceProxy } from '../@types/vsls'; import { Container } from '../container'; -import { setEnabled } from '../extension'; import { GitCommandOptions, Repository, RepositoryChangeEvent } from '../git/git'; import { Logger } from '../logger'; import { debug, log } from '../system'; @@ -43,12 +42,12 @@ export class VslsGuestService implements Disposable { @log() private onAvailabilityChanged(available: boolean) { if (available) { - void setEnabled(true); + void this.container.git.setEnabledContext(true); return; } - void setEnabled(false); + void this.container.git.setEnabledContext(false); void window.showWarningMessage( 'GitLens features will be unavailable. Unable to connect to the host GitLens service. The host may have disabled GitLens guest access or may not have GitLens installed.', );