Browse Source

Improves startup perf

Avoid view flashing on startup
main
Eric Amodio 3 years ago
parent
commit
0799cc1fc2
8 changed files with 261 additions and 246 deletions
  1. +2
    -0
      src/constants.ts
  2. +7
    -4
      src/container.ts
  3. +22
    -37
      src/extension.ts
  4. +17
    -41
      src/git/git.ts
  5. +0
    -8
      src/git/gitProvider.ts
  6. +164
    -117
      src/git/gitProviderService.ts
  7. +47
    -36
      src/git/providers/localGitProvider.ts
  8. +2
    -3
      src/vsls/guest.ts

+ 2
- 0
src/constants.ts View File

@ -255,6 +255,8 @@ export interface Usage {
} }
export enum WorkspaceState { export enum WorkspaceState {
AssumeRepositoriesOnStartup = 'gitlens:assumeRepositoriesOnStartup',
BranchComparisons = 'gitlens:branch:comparisons', BranchComparisons = 'gitlens:branch:comparisons',
ConnectedPrefix = 'gitlens:connected:', ConnectedPrefix = 'gitlens:connected:',
DefaultRemote = 'gitlens:remote:default', DefaultRemote = 'gitlens:remote:default',

+ 7
- 4
src/container.ts View File

@ -24,8 +24,7 @@ import {
FileAnnotationType, FileAnnotationType,
} from './configuration'; } from './configuration';
import { GitFileSystemProvider } from './git/fsProvider'; 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 { LocalGitProvider } from './git/providers/localGitProvider';
import { LineHoverController } from './hovers/lineHoverController'; import { LineHoverController } from './hovers/lineHoverController';
import { Keyboard } from './keyboard'; import { Keyboard } from './keyboard';
@ -147,14 +146,18 @@ export class Container {
if (this._ready) throw new Error('Container is already ready'); if (this._ready) throw new Error('Container is already ready');
this._ready = true; this._ready = true;
this.registerGitProviders();
this._onReady.fire();
queueMicrotask(() => {
this.registerGitProviders();
this._onReady.fire();
});
} }
private registerGitProviders() { private registerGitProviders() {
if (env.uiKind !== UIKind.Web) { if (env.uiKind !== UIKind.Web) {
this._context.subscriptions.push(this._git.register(GitProviderId.Git, new LocalGitProvider(this))); this._context.subscriptions.push(this._git.register(GitProviderId.Git, new LocalGitProvider(this)));
} }
this._git.registrationComplete();
} }
private onConfigurationChanging(e: ConfigurationWillChangeEvent) { private onConfigurationChanging(e: ConfigurationWillChangeEvent) {

+ 22
- 37
src/extension.ts View File

@ -31,9 +31,6 @@ export function activate(context: ExtensionContext): Promise
} }
} }
// Pretend we are enabled (until we know otherwise) and set the view contexts to reduce flashing on load
void setContext(ContextKeys.Enabled, true);
if (!workspace.isTrusted) { if (!workspace.isTrusted) {
void setContext(ContextKeys.Readonly, true); void setContext(ContextKeys.Readonly, true);
context.subscriptions.push( context.subscriptions.push(
@ -68,7 +65,7 @@ export function activate(context: ExtensionContext): Promise
context.globalState.get<string>(GlobalState.Version) ?? context.globalState.get<string>(GlobalState.Version) ??
context.globalState.get<string>(GlobalState.Deprecated_Version); context.globalState.get<string>(GlobalState.Deprecated_Version);
let previousVersion;
let previousVersion: string | undefined;
if (localVersion == null || syncedVersion == null) { if (localVersion == null || syncedVersion == null) {
previousVersion = syncedVersion ?? localVersion; previousVersion = syncedVersion ?? localVersion;
} else if (Versions.compare(syncedVersion, localVersion) === 1) { } else if (Versions.compare(syncedVersion, localVersion) === 1) {
@ -95,46 +92,38 @@ export function activate(context: ExtensionContext): Promise
); );
} }
const enabled = workspace.getConfiguration('git', null).get<boolean>('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); Configuration.configure(context);
const cfg = configuration.get(); const cfg = configuration.get();
// await migrateSettings(context, previousVersion); // await migrateSettings(context, previousVersion);
const container = Container.create(context, cfg); 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( Logger.log(
`GitLens (v${gitlensVersion}${cfg.mode.active ? `, mode: ${cfg.mode.active}` : ''}) activated ${ `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<void> {
await Promise.all([setContext(ContextKeys.Enabled, enabled), setContext(ContextKeys.Disabled, !enabled)]);
}
function setKeysForSync(context: ExtensionContext, ...keys: (SyncedState | string)[]) { function setKeysForSync(context: ExtensionContext, ...keys: (SyncedState | string)[]) {
return context.globalState?.setKeysForSync([...keys, SyncedState.Version, SyncedState.WelcomeViewVisible]); return context.globalState?.setKeysForSync([...keys, SyncedState.Version, SyncedState.WelcomeViewVisible]);
} }

+ 17
- 41
src/git/git.ts View File

@ -1,14 +1,12 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
'use strict'; 'use strict';
import * as paths from 'path'; import * as paths from 'path';
import * as iconv from 'iconv-lite';
import { Uri, window, workspace } from 'vscode'; import { Uri, window, workspace } from 'vscode';
import { GlyphChars } from '../constants'; import { GlyphChars } from '../constants';
import { Container } from '../container'; import { Container } from '../container';
import { Logger } from '../logger'; 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 { GitRevision } from './models/models';
import { GitBranchParser, GitLogParser, GitReflogParser, GitStashParser, GitTagParser } from './parsers/parsers'; import { GitBranchParser, GitLogParser, GitReflogParser, GitStashParser, GitTagParser } from './parsers/parsers';
import { fsExists, run, RunError, RunOptions } from './shell'; 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'); args.splice(0, 0, '-c', 'core.longpaths=true');
} }
promise = run<TOut>(Git.getGitPath(), args, encoding ?? 'utf8', runOpts);
promise = run<TOut>(await Git.path(), args, encoding ?? 'utf8', runOpts);
pendingCommands.set(command, promise); pendingCommands.set(command, promise);
} else { } else {
@ -208,43 +206,21 @@ function defaultExceptionHandler(ex: Error, cwd: string | undefined, start?: [nu
} }
export namespace Git { 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<GitLocation>;
export function setLocator(locator: () => Promise<GitLocation>): void {
gitLocator = locator;
} }
export function hasGitPath(): boolean {
return Boolean(gitInfo?.path);
export async function path(): Promise<string> {
return (await gitLocator()).path;
} }
export async function setOrFindGitPath(gitPath?: string | string[]): Promise<void> {
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<string> {
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<boolean> {
const [gitMajor, gitMinor] = (await version()).split('.');
return parseInt(gitMajor, 10) >= major && parseInt(gitMinor, 10) >= minor; return parseInt(gitMajor, 10) >= major && parseInt(gitMinor, 10) >= minor;
} }
@ -286,7 +262,7 @@ export namespace Git {
const index = params.indexOf('--ignore-revs-file'); const index = params.indexOf('--ignore-revs-file');
if (index !== -1) { if (index !== -1) {
// Ensure the version of Git supports the --ignore-revs-file flag, otherwise the blame will fail // 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) { if (supported) {
let ignoreRevsFile = params[index + 1]; let ignoreRevsFile = params[index + 1];
if (!paths.isAbsolute(ignoreRevsFile)) { if (!paths.isAbsolute(ignoreRevsFile)) {
@ -1493,7 +1469,7 @@ export namespace Git {
void (await git<string>({ cwd: repoPath }, ...params)); void (await git<string>({ cwd: repoPath }, ...params));
} }
export function status(
export async function status(
repoPath: string, repoPath: string,
porcelainVersion: number = 1, porcelainVersion: number = 1,
{ similarityThreshold }: { similarityThreshold?: number | null } = {}, { similarityThreshold }: { similarityThreshold?: number | null } = {},
@ -1504,7 +1480,7 @@ export namespace Git {
'--branch', '--branch',
'-u', '-u',
]; ];
if (Git.validateVersion(2, 18)) {
if (await Git.validateVersion(2, 18)) {
params.push(`--find-renames${similarityThreshold == null ? '' : `=${similarityThreshold}%`}`); 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, repoPath: string,
fileName: string, fileName: string,
porcelainVersion: number = 1, porcelainVersion: number = 1,
@ -1524,7 +1500,7 @@ export namespace Git {
const [file, root] = Paths.splitPath(fileName, repoPath); const [file, root] = Paths.splitPath(fileName, repoPath);
const params = ['status', porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain']; 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}%`}`); params.push(`--find-renames${similarityThreshold == null ? '' : `=${similarityThreshold}%`}`);
} }

+ 0
- 8
src/git/gitProvider.ts View File

@ -396,15 +396,7 @@ export interface GitProvider {
hasTrackingBranch(repoPath: string | undefined): Promise<boolean>; hasTrackingBranch(repoPath: string | undefined): Promise<boolean>;
isActiveRepoPath(repoPath: string | undefined, editor?: TextEditor): Promise<boolean>; isActiveRepoPath(repoPath: string | undefined, editor?: TextEditor): Promise<boolean>;
isTrackable(scheme: string): boolean;
isTrackable(uri: Uri): boolean; isTrackable(uri: Uri): boolean;
isTrackable(schemeOruri: string | Uri): boolean;
isTracked(
fileName: string,
repoPath?: string,
options?: { ref?: string | undefined; skipCacheUpdate?: boolean | undefined },
): Promise<boolean>;
isTracked(uri: GitUri): Promise<boolean>;
isTracked( isTracked(
fileNameOrUri: string | GitUri, fileNameOrUri: string | GitUri,
repoPath?: string, repoPath?: string,

+ 164
- 117
src/git/gitProviderService.ts View File

@ -1,4 +1,5 @@
'use strict'; 'use strict';
import { encodingExists } from 'iconv-lite';
import { import {
ConfigurationChangeEvent, ConfigurationChangeEvent,
Disposable, Disposable,
@ -16,9 +17,15 @@ import {
} from 'vscode'; } from 'vscode';
import { resetAvatarCache } from '../avatars'; import { resetAvatarCache } from '../avatars';
import { configuration } from '../configuration'; 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 { Container } from '../container';
import { setEnabled } from '../extension';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { Arrays, debug, gate, Iterables, log, Paths, Promises, Strings } from '../system'; import { Arrays, debug, gate, Iterables, log, Paths, Promises, Strings } from '../system';
import { vslsUriPrefixRegex } from '../vsls/vsls'; import { vslsUriPrefixRegex } from '../vsls/vsls';
@ -28,7 +35,6 @@ import {
BranchDateFormatting, BranchDateFormatting,
BranchSortOptions, BranchSortOptions,
CommitDateFormatting, CommitDateFormatting,
Git,
GitBlame, GitBlame,
GitBlameLine, GitBlameLine,
GitBlameLines, GitBlameLines,
@ -68,7 +74,7 @@ import { GitProvider, GitProviderDescriptor, GitProviderId, PagedResult, ScmRepo
import { GitUri } from './gitUri'; import { GitUri } from './gitUri';
import { RemoteProvider, RemoteProviders, RichRemoteProvider } from './remotes/factory'; import { RemoteProvider, RemoteProviders, RichRemoteProvider } from './remotes/factory';
export type { GitProviderDescriptor, GitProviderId };
export { type GitProviderDescriptor, GitProviderId };
const slash = '/'; const slash = '/';
@ -122,7 +128,7 @@ export class GitProviderService implements Disposable {
} }
this.resetCaches('providers'); this.resetCaches('providers');
void this.updateContext(this._repositories);
void this.updateContext();
}), }),
); );
@ -130,7 +136,7 @@ export class GitProviderService implements Disposable {
CommitDateFormatting.reset(); CommitDateFormatting.reset();
PullRequestDateFormatting.reset(); PullRequestDateFormatting.reset();
void this.updateContext(this._repositories);
void this.updateContext();
} }
dispose() { dispose() {
@ -192,7 +198,7 @@ export class GitProviderService implements Disposable {
} }
if (removed.length) { if (removed.length) {
void this.updateContext(this._repositories);
void this.updateContext();
// Defer the event trigger enough to let everything unwind // Defer the event trigger enough to let everything unwind
queueMicrotask(() => { queueMicrotask(() => {
@ -203,70 +209,6 @@ export class GitProviderService implements Disposable {
} }
} }
private async updateContext(repositories: Map<string, Repository>) {
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 { get hasProviders(): boolean {
return this._providers.size !== 0; 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`); if (this._providers.has(id)) throw new Error(`Provider '${id}' has already been registered`);
this._providers.set(id, provider); 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: [] }); 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 { return {
dispose: () => { dispose: () => {
@ -359,7 +323,7 @@ export class GitProviderService implements Disposable {
} }
} }
void this.updateContext(this._repositories);
void this.updateContext();
if (removed.length) { if (removed.length) {
// Defer the event trigger enough to let everything unwind // 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<boolean | 'subFolders' | 'openEditors'>(
BuiltInGitConfiguration.AutoRepositoryDetection,
) ?? true;
if (autoRepositoryDetection !== false) {
void this.discoverRepositories(workspaceFolders);
return;
}
}
void this.updateContext();
}
private _discoveredWorkspaceFolders = new Map<WorkspaceFolder, Promise<Repository[]>>(); private _discoveredWorkspaceFolders = new Map<WorkspaceFolder, Promise<Repository[]>>();
async discoverRepositories(folders: readonly WorkspaceFolder[], options?: { force?: boolean }): Promise<void> { async discoverRepositories(folders: readonly WorkspaceFolder[], options?: { force?: boolean }): Promise<void> {
@ -408,9 +393,10 @@ export class GitProviderService implements Disposable {
this._repositories.set(repository.path, repository); this._repositories.set(repository.path, repository);
} }
void this.updateContext();
if (added.length === 0) return; if (added.length === 0) return;
void this.updateContext(this._repositories);
// Defer the event trigger enough to let everything unwind // Defer the event trigger enough to let everything unwind
queueMicrotask(() => this._onDidChangeRepositories.fire({ added: added, removed: [] })); 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<void> {
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<boolean>(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 } { 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() @log()
addRemote(repoPath: string | Uri, name: string, url: string): Promise<void> { addRemote(repoPath: string | Uri, name: string, url: string): Promise<void> {
const { provider, path } = this.getProvider(repoPath); const { provider, path } = this.getProvider(repoPath);
@ -1396,7 +1463,7 @@ export class GitProviderService implements Disposable {
repo = provider.createRepository(folder, rp, false); repo = provider.createRepository(folder, rp, false);
this._repositories.set(rp, repo); this._repositories.set(rp, repo);
void this.updateContext(this._repositories);
void this.updateContext();
// Send a notification that the repositories changed // Send a notification that the repositories changed
queueMicrotask(() => this._onDidChangeRepositories.fire({ added: [repo!], removed: [] })); queueMicrotask(() => this._onDidChangeRepositories.fire({ added: [repo!], removed: [] }));
@ -1638,41 +1705,20 @@ export class GitProviderService implements Disposable {
return repoPath === doc?.uri.repoPath; 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<boolean>;
async isTracked(
private async isTracked(
fileName: string, fileName: string,
repoPath: string | Uri, repoPath: string | Uri,
options?: { ref?: string; skipCacheUpdate?: boolean }, options?: { ref?: string; skipCacheUpdate?: boolean },
): Promise<boolean>;
@log<GitProviderService['isTracked']>({
exit: tracked => `returned ${tracked}`,
singleLine: true,
})
async isTracked(
fileNameOrUri: string | GitUri,
repoPath?: string | Uri,
options?: { ref?: string; skipCacheUpdate?: boolean },
): Promise<boolean> { ): Promise<boolean> {
if (options?.ref === GitRevision.deletedOrMissing) return false; 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() @log()
@ -1814,7 +1860,8 @@ export class GitProviderService implements Disposable {
static getEncoding(uri: Uri): string; static getEncoding(uri: Uri): string;
static getEncoding(repoPathOrUri: string | Uri, fileName?: string): string { static getEncoding(repoPathOrUri: string | Uri, fileName?: string): string {
const uri = typeof repoPathOrUri === 'string' ? GitUri.resolveToUri(fileName!, repoPathOrUri) : repoPathOrUri; const uri = typeof repoPathOrUri === 'string' ? GitUri.resolveToUri(fileName!, repoPathOrUri) : repoPathOrUri;
return Git.getEncoding(configuration.getAny<string>('files.encoding', uri));
const encoding = configuration.getAny<string>('files.encoding', uri);
return encoding != null && encodingExists(encoding) ? encoding : 'utf8';
} }
} }

+ 47
- 36
src/git/providers/localGitProvider.ts View File

@ -17,7 +17,7 @@ import {
} from 'vscode'; } from 'vscode';
import type { API as BuiltInGitApi, Repository as BuiltInGitRepository, GitExtension } from '../../@types/vscode.git'; import type { API as BuiltInGitApi, Repository as BuiltInGitRepository, GitExtension } from '../../@types/vscode.git';
import { configuration } from '../../configuration'; import { configuration } from '../../configuration';
import { BuiltInGitConfiguration, DocumentSchemes } from '../../constants';
import { BuiltInGitConfiguration, DocumentSchemes, GlyphChars } from '../../constants';
import { Container } from '../../container'; import { Container } from '../../container';
import { LogCorrelationContext, Logger } from '../../logger'; import { LogCorrelationContext, Logger } from '../../logger';
import { Messages } from '../../messages'; import { Messages } from '../../messages';
@ -84,7 +84,7 @@ import {
import { GitProvider, GitProviderId, PagedResult, RepositoryInitWatcher, ScmRepository } from '../gitProvider'; import { GitProvider, GitProviderId, PagedResult, RepositoryInitWatcher, ScmRepository } from '../gitProvider';
import { GitProviderService } from '../gitProviderService'; import { GitProviderService } from '../gitProviderService';
import { GitUri } from '../gitUri'; import { GitUri } from '../gitUri';
import { InvalidGitConfigError, UnableToFindGitError } from '../locator';
import { findGitPath, GitLocation, InvalidGitConfigError, UnableToFindGitError } from '../locator';
import { GitReflogParser, GitShortLogParser } from '../parsers/parsers'; import { GitReflogParser, GitShortLogParser } from '../parsers/parsers';
import { RemoteProvider, RemoteProviderFactory, RemoteProviders, RichRemoteProvider } from '../remotes/factory'; import { RemoteProvider, RemoteProviderFactory, RemoteProviders, RichRemoteProvider } from '../remotes/factory';
import { fsExists, isWindows } from '../shell'; import { fsExists, isWindows } from '../shell';
@ -121,7 +121,9 @@ export class LocalGitProvider implements GitProvider, Disposable {
private readonly _trackedCache = new Map<string, boolean | Promise<boolean>>(); private readonly _trackedCache = new Map<string, boolean | Promise<boolean>>();
private readonly _userMapCache = new Map<string, GitUser | null>(); private readonly _userMapCache = new Map<string, GitUser | null>();
constructor(private readonly container: Container) {}
constructor(private readonly container: Container) {
Git.setLocator(this.ensureGit.bind(this));
}
dispose() {} dispose() {}
@ -167,16 +169,24 @@ export class LocalGitProvider implements GitProvider, Disposable {
this._onDidChangeRepository.fire(e); this._onDidChangeRepository.fire(e);
} }
private _initialized: Promise<void> | undefined;
private async ensureInitialized(): Promise<void> {
if (this._initialized == null) {
this._initialized = this.initializeCore();
private _gitLocator: Promise<GitLocation> | undefined;
private async ensureGit(): Promise<GitLocation> {
if (this._gitLocator == null) {
this._gitLocator = this.findGit();
} }
return this._initialized;
return this._gitLocator;
} }
private async initializeCore() {
@log()
private async findGit(): Promise<GitLocation> {
if (!configuration.getAny<boolean>('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 // Try to use the same git as the built-in vscode git extension
const gitApi = await this.getScmGitApi(); const gitApi = await this.getScmGitApi();
if (gitApi != null) { if (gitApi != null) {
@ -196,14 +206,23 @@ export class LocalGitProvider implements GitProvider, Disposable {
); );
} }
if (Git.hasGitPath()) return;
const gitPath = gitApi?.git.path ?? configuration.getAny<string | string[]>('git.path');
await Git.setOrFindGitPath(gitApi?.git.path ?? configuration.getAny<string | string[]>('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 // 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<Repository[]> { async discoverRepositories(uri: Uri): Promise<Repository[]> {
@ -216,7 +235,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
if (autoRepositoryDetection === false) return []; if (autoRepositoryDetection === false) return [];
try { try {
await this.ensureInitialized();
void (await this.ensureGit());
const repositories = await this.repositorySearch(workspace.getWorkspaceFolder(uri)!); const repositories = await this.repositorySearch(workspace.getWorkspaceFolder(uri)!);
if (autoRepositoryDetection === true || autoRepositoryDetection === 'subFolders') { if (autoRepositoryDetection === true || autoRepositoryDetection === 'subFolders') {
@ -3143,7 +3162,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
@log() @log()
async getStatusForFile(repoPath: string, fileName: string): Promise<GitStatusFile | undefined> { async getStatusForFile(repoPath: string, fileName: string): Promise<GitStatusFile | undefined> {
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, { const data = await Git.status__file(repoPath, fileName, porcelainVersion, {
similarityThreshold: this.container.config.advanced.similarityThreshold, similarityThreshold: this.container.config.advanced.similarityThreshold,
@ -3156,7 +3175,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
@log() @log()
async getStatusForFiles(repoPath: string, path: string): Promise<GitStatusFile[] | undefined> { async getStatusForFiles(repoPath: string, path: string): Promise<GitStatusFile[] | undefined> {
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, { const data = await Git.status__file(repoPath, path, porcelainVersion, {
similarityThreshold: this.container.config.advanced.similarityThreshold, similarityThreshold: this.container.config.advanced.similarityThreshold,
@ -3171,7 +3190,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
async getStatusForRepo(repoPath: string | undefined): Promise<GitStatus | undefined> { async getStatusForRepo(repoPath: string | undefined): Promise<GitStatus | undefined> {
if (repoPath == null) return undefined; 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, { const data = await Git.status(repoPath, porcelainVersion, {
similarityThreshold: this.container.config.advanced.similarityThreshold, similarityThreshold: this.container.config.advanced.similarityThreshold,
@ -3396,10 +3415,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
return repoPath === doc?.uri.repoPath; 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 ( return (
scheme === DocumentSchemes.File || scheme === DocumentSchemes.File ||
scheme === DocumentSchemes.Git || 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<boolean>;
async isTracked(uri: GitUri): Promise<boolean>;
@log<LocalGitProvider['isTracked']>({ @log<LocalGitProvider['isTracked']>({
exit: tracked => `returned ${tracked}`, exit: tracked => `returned ${tracked}`,
singleLine: true, singleLine: true,
@ -3687,7 +3698,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
} }
@log() @log()
stashSave(
async stashSave(
repoPath: string, repoPath: string,
message?: string, message?: string,
uris?: Uri[], uris?: Uri[],
@ -3695,7 +3706,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
): Promise<void> { ): Promise<void> {
if (uris == null) return Git.stash__push(repoPath, message, options); if (uris == null) return Git.stash__push(repoPath, message, options);
this.ensureGitVersion(
await this.ensureGitVersion(
'2.13.2', '2.13.2',
'Stashing individual files', 'Stashing individual files',
' Please retry by stashing everything or install a more recent version of Git.', ' 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 pathspecs = uris.map(u => `./${Paths.splitPath(u.fsPath, repoPath)[0]}`);
const stdinVersion = '2.30.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 we don't support stdin, then error out if we are over the maximum allowed git cli length
if (!stdin && Arrays.countStringLength(pathspecs) > maxGitCliLength) { if (!stdin && Arrays.countStringLength(pathspecs) > maxGitCliLength) {
this.ensureGitVersion(
await this.ensureGitVersion(
stdinVersion, stdinVersion,
`Stashing so many files (${pathspecs.length}) at once`, `Stashing so many files (${pathspecs.length}) at once`,
' Please retry by stashing fewer files or install a more recent version of Git.', ' 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<void> {
if ((await this.compareGitVersion(version)) === -1) {
throw new Error( 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}`,
); );
} }
} }

+ 2
- 3
src/vsls/guest.ts View File

@ -2,7 +2,6 @@
import { CancellationToken, Disposable, window, WorkspaceFolder } from 'vscode'; import { CancellationToken, Disposable, window, WorkspaceFolder } from 'vscode';
import type { LiveShare, SharedServiceProxy } from '../@types/vsls'; import type { LiveShare, SharedServiceProxy } from '../@types/vsls';
import { Container } from '../container'; import { Container } from '../container';
import { setEnabled } from '../extension';
import { GitCommandOptions, Repository, RepositoryChangeEvent } from '../git/git'; import { GitCommandOptions, Repository, RepositoryChangeEvent } from '../git/git';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { debug, log } from '../system'; import { debug, log } from '../system';
@ -43,12 +42,12 @@ export class VslsGuestService implements Disposable {
@log() @log()
private onAvailabilityChanged(available: boolean) { private onAvailabilityChanged(available: boolean) {
if (available) { if (available) {
void setEnabled(true);
void this.container.git.setEnabledContext(true);
return; return;
} }
void setEnabled(false);
void this.container.git.setEnabledContext(false);
void window.showWarningMessage( 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.', '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.',
); );

Loading…
Cancel
Save