From b062e960b6ee5ca7ac081dd84d9217bd4b2051e0 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Fri, 3 Dec 2021 18:29:38 -0500 Subject: [PATCH] Refactors core git service into a provider model This is to ultimately will support virtual repositories (without git) --- src/commands/closeUnchangedFiles.ts | 2 +- src/commands/git/branch.ts | 2 +- src/commands/git/cherry-pick.ts | 2 +- src/commands/git/coauthors.ts | 13 +- src/commands/git/fetch.ts | 2 +- src/commands/git/log.ts | 2 +- src/commands/git/merge.ts | 2 +- src/commands/git/pull.ts | 2 +- src/commands/git/push.ts | 2 +- src/commands/git/rebase.ts | 2 +- src/commands/git/reset.ts | 2 +- src/commands/git/revert.ts | 2 +- src/commands/git/search.ts | 2 +- src/commands/git/show.ts | 2 +- src/commands/git/stash.ts | 2 +- src/commands/git/status.ts | 2 +- src/commands/git/switch.ts | 2 +- src/commands/git/tag.ts | 2 +- src/commands/gitCommands.actions.ts | 2 +- src/commands/quickCommand.steps.ts | 3 +- src/commands/remoteProviders.ts | 4 +- src/commands/repositories.ts | 6 +- src/commands/showQuickBranchHistory.ts | 2 +- src/constants.ts | 7 + src/container.ts | 26 +- src/extension.ts | 38 +- src/git/errors.ts | 17 + src/git/git.ts | 24 +- src/git/gitProvider.ts | 486 +++++ src/git/gitProviderService.ts | 1878 ++++++++++++++++++++ src/git/models/repository.ts | 74 +- src/git/providers/localGitProvider.ts | 879 ++------- src/git/remotes/provider.ts | 21 +- src/quickpicks/repositoryPicker.ts | 2 +- src/system/function.ts | 20 +- src/system/iterable.ts | 9 +- src/system/path.ts | 11 + src/system/promise.ts | 4 +- src/terminal/linkProvider.ts | 2 +- src/trackers/documentTracker.ts | 86 +- src/trackers/lineTracker.ts | 4 +- src/trackers/trackedDocument.ts | 91 +- src/views/branchesView.ts | 38 +- src/views/commitsView.ts | 38 +- src/views/contributorsView.ts | 42 +- src/views/nodes/branchNode.ts | 27 +- src/views/nodes/branchTrackingStatusFilesNode.ts | 5 +- src/views/nodes/branchTrackingStatusNode.ts | 9 +- src/views/nodes/commitFileNode.ts | 7 +- src/views/nodes/commitNode.ts | 17 +- src/views/nodes/compareBranchNode.ts | 23 +- src/views/nodes/comparePickerNode.ts | 5 +- src/views/nodes/compareResultsNode.ts | 29 +- src/views/nodes/contributorNode.ts | 9 +- src/views/nodes/contributorsNode.ts | 9 +- src/views/nodes/fileHistoryNode.ts | 15 +- src/views/nodes/fileHistoryTrackerNode.ts | 13 +- src/views/nodes/fileRevisionAsCommitNode.ts | 29 +- src/views/nodes/lineHistoryNode.ts | 17 +- src/views/nodes/lineHistoryTrackerNode.ts | 17 +- src/views/nodes/mergeConflictCurrentChangesNode.ts | 7 +- .../nodes/mergeConflictIncomingChangesNode.ts | 7 +- src/views/nodes/rebaseStatusNode.ts | 5 +- src/views/nodes/reflogNode.ts | 7 +- src/views/nodes/reflogRecordNode.ts | 3 +- src/views/nodes/remoteNode.ts | 5 +- src/views/nodes/repositoriesNode.ts | 12 +- src/views/nodes/repositoryNode.ts | 11 +- src/views/nodes/resultsCommitsNode.ts | 5 +- src/views/nodes/resultsFileNode.ts | 5 +- src/views/nodes/resultsFilesNode.ts | 9 +- src/views/nodes/searchResultsNode.ts | 13 +- src/views/nodes/stashNode.ts | 7 +- src/views/nodes/stashesNode.ts | 5 +- src/views/nodes/statusFileNode.ts | 5 +- src/views/nodes/statusFilesNode.ts | 11 +- src/views/nodes/tagNode.ts | 7 +- src/views/nodes/viewNode.ts | 46 +- src/views/remotesView.ts | 38 +- src/views/repositoriesView.ts | 11 +- src/views/searchAndCompareView.ts | 2 +- src/views/stashesView.ts | 45 +- src/views/tagsView.ts | 38 +- src/views/viewBase.ts | 6 +- src/views/viewCommands.ts | 5 +- src/vsls/guest.ts | 23 +- src/vsls/host.ts | 3 +- src/vsls/vsls.ts | 2 +- webpack.config.js | 6 +- 89 files changed, 3117 insertions(+), 1324 deletions(-) create mode 100644 src/git/errors.ts create mode 100644 src/git/gitProvider.ts create mode 100644 src/git/gitProviderService.ts diff --git a/src/commands/closeUnchangedFiles.ts b/src/commands/closeUnchangedFiles.ts index 78baf28..e984ad1 100644 --- a/src/commands/closeUnchangedFiles.ts +++ b/src/commands/closeUnchangedFiles.ts @@ -130,7 +130,7 @@ export class CloseUnchangedFilesCommand extends Command { private waitForEditorChange(timeout: number = 500): Promise { return new Promise(resolve => { - let timer: NodeJS.Timer | undefined; + let timer: any | undefined; this._onEditorChangedFn = (editor: TextEditor | undefined) => { if (timer != null) { diff --git a/src/commands/git/branch.ts b/src/commands/git/branch.ts index a720ff6..2471238 100644 --- a/src/commands/git/branch.ts +++ b/src/commands/git/branch.ts @@ -148,7 +148,7 @@ export class BranchGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, showTags: false, title: this.title, }; diff --git a/src/commands/git/cherry-pick.ts b/src/commands/git/cherry-pick.ts index 54dfc23..7d392bc 100644 --- a/src/commands/git/cherry-pick.ts +++ b/src/commands/git/cherry-pick.ts @@ -80,7 +80,7 @@ export class CherryPickGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, cache: new Map>(), destination: undefined!, selectedBranchOrTag: undefined, diff --git a/src/commands/git/coauthors.ts b/src/commands/git/coauthors.ts index dfb8dbe..eb9f72e 100644 --- a/src/commands/git/coauthors.ts +++ b/src/commands/git/coauthors.ts @@ -2,7 +2,6 @@ import { commands } from 'vscode'; import { Container } from '../../container'; import { GitContributor, Repository } from '../../git/git'; -import { GitService } from '../../git/providers/localGitProvider'; import { Strings } from '../../system'; import { PartialStepState, @@ -60,7 +59,7 @@ export class CoAuthorsGitCommand extends QuickCommand { } async execute(state: CoAuthorStepState) { - const repo = await GitService.getOrOpenBuiltInGitRepository(state.repo.path); + const repo = await Container.instance.git.getOrOpenScmRepository(state.repo.path); if (repo == null) return; let message = repo.inputBox.value; @@ -93,23 +92,23 @@ export class CoAuthorsGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, activeRepo: undefined, title: this.title, }; - const gitApi = await GitService.getBuiltInGitApi(); - if (gitApi != null) { + const scmRepositories = await Container.instance.git.getOpenScmRepositories(); + if (scmRepositories.length) { // Filter out any repo's that are not known to the built-in git context.repos = context.repos.filter(repo => - gitApi.repositories.find(r => Strings.normalizePath(r.rootUri.fsPath) === repo.path), + scmRepositories.find(r => Strings.normalizePath(r.rootUri.fsPath) === repo.path), ); // Ensure that the active repo is known to the built-in git context.activeRepo = await Container.instance.git.getActiveRepository(); if ( context.activeRepo != null && - !gitApi.repositories.some(r => r.rootUri.fsPath === context.activeRepo!.path) + !scmRepositories.some(r => r.rootUri.fsPath === context.activeRepo!.path) ) { context.activeRepo = undefined; } diff --git a/src/commands/git/fetch.ts b/src/commands/git/fetch.ts index 910483a..3fc16a3 100644 --- a/src/commands/git/fetch.ts +++ b/src/commands/git/fetch.ts @@ -67,7 +67,7 @@ export class FetchGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, title: this.title, }; diff --git a/src/commands/git/log.ts b/src/commands/git/log.ts index 289795b..f709802 100644 --- a/src/commands/git/log.ts +++ b/src/commands/git/log.ts @@ -76,7 +76,7 @@ export class LogGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, cache: new Map>(), selectedBranchOrTag: undefined, title: this.title, diff --git a/src/commands/git/merge.ts b/src/commands/git/merge.ts index d12c080..8033654 100644 --- a/src/commands/git/merge.ts +++ b/src/commands/git/merge.ts @@ -77,7 +77,7 @@ export class MergeGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, cache: new Map>(), destination: undefined!, pickCommit: false, diff --git a/src/commands/git/pull.ts b/src/commands/git/pull.ts index 45316c1..4700b30 100644 --- a/src/commands/git/pull.ts +++ b/src/commands/git/pull.ts @@ -73,7 +73,7 @@ export class PullGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, title: this.title, }; diff --git a/src/commands/git/push.ts b/src/commands/git/push.ts index 01304b6..404d89d 100644 --- a/src/commands/git/push.ts +++ b/src/commands/git/push.ts @@ -79,7 +79,7 @@ export class PushGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, title: this.title, }; diff --git a/src/commands/git/rebase.ts b/src/commands/git/rebase.ts index 4e288d0..77ac61b 100644 --- a/src/commands/git/rebase.ts +++ b/src/commands/git/rebase.ts @@ -98,7 +98,7 @@ export class RebaseGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, cache: new Map>(), destination: undefined!, pickCommit: false, diff --git a/src/commands/git/reset.ts b/src/commands/git/reset.ts index 58a42f4..5a7c77f 100644 --- a/src/commands/git/reset.ts +++ b/src/commands/git/reset.ts @@ -71,7 +71,7 @@ export class ResetGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, cache: new Map>(), destination: undefined!, title: this.title, diff --git a/src/commands/git/revert.ts b/src/commands/git/revert.ts index e069c64..7e54438 100644 --- a/src/commands/git/revert.ts +++ b/src/commands/git/revert.ts @@ -73,7 +73,7 @@ export class RevertGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, cache: new Map>(), destination: undefined!, title: this.title, diff --git a/src/commands/git/search.ts b/src/commands/git/search.ts index 9419f46..730f5d2 100644 --- a/src/commands/git/search.ts +++ b/src/commands/git/search.ts @@ -91,7 +91,7 @@ export class SearchGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, commit: undefined, resultsKey: undefined, resultsPromise: undefined, diff --git a/src/commands/git/show.ts b/src/commands/git/show.ts index 39f3101..0f48fb8 100644 --- a/src/commands/git/show.ts +++ b/src/commands/git/show.ts @@ -76,7 +76,7 @@ export class ShowGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, title: this.title, }; diff --git a/src/commands/git/stash.ts b/src/commands/git/stash.ts index 6b4948c..7f6ceb6 100644 --- a/src/commands/git/stash.ts +++ b/src/commands/git/stash.ts @@ -144,7 +144,7 @@ export class StashGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, title: this.title, }; diff --git a/src/commands/git/status.ts b/src/commands/git/status.ts index e2c195a..60cab0a 100644 --- a/src/commands/git/status.ts +++ b/src/commands/git/status.ts @@ -55,7 +55,7 @@ export class StatusGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, status: undefined!, title: this.title, }; diff --git a/src/commands/git/switch.ts b/src/commands/git/switch.ts index 9c0c01b..295ce69 100644 --- a/src/commands/git/switch.ts +++ b/src/commands/git/switch.ts @@ -88,7 +88,7 @@ export class SwitchGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, showTags: false, title: this.title, }; diff --git a/src/commands/git/tag.ts b/src/commands/git/tag.ts index 7b7a26e..2544c57 100644 --- a/src/commands/git/tag.ts +++ b/src/commands/git/tag.ts @@ -127,7 +127,7 @@ export class TagGitCommand extends QuickCommand { protected async *steps(state: PartialStepState): StepGenerator { const context: Context = { - repos: [...(await Container.instance.git.getOrderedRepositories())], + repos: Container.instance.git.openRepositories, showTags: false, title: this.title, }; diff --git a/src/commands/gitCommands.actions.ts b/src/commands/gitCommands.actions.ts index 47b04aa..af7e11d 100644 --- a/src/commands/gitCommands.actions.ts +++ b/src/commands/gitCommands.actions.ts @@ -728,7 +728,7 @@ export namespace GitActions { export namespace Remote { export async function add(repo?: string | Repository) { if (repo == null) { - repo = Container.instance.git.getHighlanderRepoPath(); + repo = Container.instance.git.highlanderRepoPath; if (repo == null) { const pick = await RepositoryPicker.show(undefined, 'Choose a repository to add a remote to'); diff --git a/src/commands/quickCommand.steps.ts b/src/commands/quickCommand.steps.ts index 0900437..22213dd 100644 --- a/src/commands/quickCommand.steps.ts +++ b/src/commands/quickCommand.steps.ts @@ -25,7 +25,6 @@ import { SearchPattern, TagSortOptions, } from '../git/git'; -import { GitService } from '../git/gitService'; import { GitUri } from '../git/gitUri'; import { BranchQuickPickItem, @@ -1075,7 +1074,7 @@ export async function* pickContributorsStep< context: Context, placeholder: string = 'Choose contributors', ): AsyncStepResultGenerator { - const message = (await GitService.getOrOpenBuiltInGitRepository(state.repo.path))?.inputBox.value; + const message = (await Container.instance.git.getOrOpenScmRepository(state.repo.path))?.inputBox.value; const step = QuickCommand.createPickStep({ title: appendReposToTitle(context.title, state, context), diff --git a/src/commands/remoteProviders.ts b/src/commands/remoteProviders.ts index ec33fcd..4a94346 100644 --- a/src/commands/remoteProviders.ts +++ b/src/commands/remoteProviders.ts @@ -47,7 +47,7 @@ export class ConnectRemoteProviderCommand extends Command { if (args?.repoPath == null) { const repos = new Map>(); - for (const repo of await Container.instance.git.getOrderedRepositories()) { + for (const repo of Container.instance.git.openRepositories) { const remote = await repo.getRichRemote(); if (remote?.provider != null && !(await remote.provider.isConnected())) { repos.set(repo, remote); @@ -135,7 +135,7 @@ export class DisconnectRemoteProviderCommand extends Command { if (args?.repoPath == null) { const repos = new Map>(); - for (const repo of await Container.instance.git.getOrderedRepositories()) { + for (const repo of Container.instance.git.openRepositories) { const remote = await repo.getRichRemote(true); if (remote != null) { repos.set(repo, remote); diff --git a/src/commands/repositories.ts b/src/commands/repositories.ts index 2465879..88e4c02 100644 --- a/src/commands/repositories.ts +++ b/src/commands/repositories.ts @@ -12,7 +12,7 @@ export class FetchRepositoriesCommand extends Command { async execute() { return executeGitCommand({ command: 'fetch', - state: { repos: await Container.instance.git.getOrderedRepositories() }, + state: { repos: Container.instance.git.openRepositories }, }); } } @@ -26,7 +26,7 @@ export class PullRepositoriesCommand extends Command { async execute() { return executeGitCommand({ command: 'pull', - state: { repos: await Container.instance.git.getOrderedRepositories() }, + state: { repos: Container.instance.git.openRepositories }, }); } } @@ -40,7 +40,7 @@ export class PushRepositoriesCommand extends Command { async execute() { return executeGitCommand({ command: 'push', - state: { repos: await Container.instance.git.getOrderedRepositories() }, + state: { repos: Container.instance.git.openRepositories }, }); } } diff --git a/src/commands/showQuickBranchHistory.ts b/src/commands/showQuickBranchHistory.ts index c989ddf..19297b3 100644 --- a/src/commands/showQuickBranchHistory.ts +++ b/src/commands/showQuickBranchHistory.ts @@ -32,7 +32,7 @@ export class ShowQuickBranchHistoryCommand extends ActiveEditorCachedCommand { const gitUri = uri != null ? await GitUri.fromUri(uri) : undefined; - const repoPath = args?.repoPath ?? gitUri?.repoPath ?? Container.instance.git.getHighlanderRepoPath(); + const repoPath = args?.repoPath ?? gitUri?.repoPath ?? Container.instance.git.highlanderRepoPath; let ref: GitReference | 'HEAD' | undefined; if (repoPath != null) { if (args?.branch != null) { diff --git a/src/constants.ts b/src/constants.ts index 103c578..25bd454 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -95,6 +95,7 @@ export enum DocumentSchemes { Output = 'output', PRs = 'pr', Vsls = 'vsls', + VirtualFS = 'vscode-vfs', } export function getEditorIfActive(document: TextDocument): TextEditor | undefined { @@ -107,6 +108,12 @@ export function isActiveDocument(document: TextDocument): boolean { return editor != null && editor.document === document; } +export function isVisibleDocument(document: TextDocument): boolean { + if (window.visibleTextEditors.length === 0) return false; + + return window.visibleTextEditors.some(e => e.document === document); +} + export function isTextEditor(editor: TextEditor): boolean { const scheme = editor.document.uri.scheme; return scheme !== DocumentSchemes.Output && scheme !== DocumentSchemes.DebugConsole; diff --git a/src/container.ts b/src/container.ts index 33a5c34..7e3c4c1 100644 --- a/src/container.ts +++ b/src/container.ts @@ -1,5 +1,14 @@ 'use strict'; -import { commands, ConfigurationChangeEvent, ConfigurationScope, Event, EventEmitter, ExtensionContext } from 'vscode'; +import { + commands, + ConfigurationChangeEvent, + ConfigurationScope, + env, + Event, + EventEmitter, + ExtensionContext, + UIKind, +} from 'vscode'; import { Autolinks } from './annotations/autolinks'; import { FileAnnotationController } from './annotations/fileAnnotationController'; import { LineAnnotationController } from './annotations/lineAnnotationController'; @@ -15,7 +24,9 @@ import { FileAnnotationType, } from './configuration'; import { GitFileSystemProvider } from './git/fsProvider'; -import { GitService } from './git/providers/localGitProvider'; +import { GitProviderId } from './git/gitProvider'; +import { GitProviderService } from './git/gitProviderService'; +import { LocalGitProvider } from './git/providers/localGitProvider'; import { LineHoverController } from './hovers/lineHoverController'; import { Keyboard } from './keyboard'; import { Logger } from './logger'; @@ -84,7 +95,7 @@ export class Container { this._config = this.applyMode(config); context.subscriptions.push(configuration.onWillChange(this.onConfigurationChanging, this)); - context.subscriptions.push((this._git = new GitService(this))); + context.subscriptions.push((this._git = new GitProviderService(this))); context.subscriptions.push(new GitFileSystemProvider(this)); context.subscriptions.push((this._actionRunners = new ActionRunners(this))); @@ -136,9 +147,16 @@ export class Container { if (this._ready) throw new Error('Container is already ready'); this._ready = true; + this.registerGitProviders(); this._onReady.fire(); } + private registerGitProviders() { + if (env.uiKind !== UIKind.Web) { + this._context.subscriptions.push(this._git.register(GitProviderId.Git, new LocalGitProvider(this))); + } + } + private onConfigurationChanging(e: ConfigurationWillChangeEvent) { this._config = undefined; @@ -235,7 +253,7 @@ export class Container { return this._fileHistoryView; } - private _git: GitService; + private _git: GitProviderService; get git() { return this._git; } diff --git a/src/extension.ts b/src/extension.ts index 6bfdfcd..2a2b4d0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,17 +7,15 @@ import { CreatePullRequestOnRemoteCommandArgs } from './commands/createPullReque import { configuration, Configuration, TraceLevel } from './configuration'; import { ContextKeys, GlobalState, GlyphChars, setContext, SyncedState } from './constants'; import { Container } from './container'; -import { Git, GitBranch, GitCommit } from './git/git'; +import { GitBranch, GitCommit } from './git/git'; import { GitUri } from './git/gitUri'; -import { InvalidGitConfigError, UnableToFindGitError } from './git/locator'; -import { GitService } from './git/providers/localGitProvider'; import { Logger } from './logger'; import { Messages } from './messages'; import { registerPartnerActionRunners } from './partners'; import { Strings, Versions } from './system'; import { ViewNode } from './views/nodes'; -export async function activate(context: ExtensionContext): Promise { +export function activate(context: ExtensionContext): Promise | undefined { const start = process.hrtime(); if (context.extension.id === 'eamodio.gitlens-insiders') { @@ -111,28 +109,6 @@ export async function activate(context: ExtensionContext): Promise('git.path')); - } catch (ex) { - Logger.error(ex, `GitLens (v${gitlensVersion}) activate`); - void setEnabled(false); - - if (ex instanceof InvalidGitConfigError) { - void Messages.showGitInvalidConfigErrorMessage(); - } else if (ex instanceof UnableToFindGitError) { - void Messages.showGitMissingErrorMessage(); - } else { - const msg: string = ex?.message ?? ''; - if (msg) { - void window.showErrorMessage(`Unable to initialize Git; ${msg}`); - } - } - - return undefined; - } - const container = Container.create(context, cfg); // Signal that the container is now ready container.ready(); @@ -141,7 +117,6 @@ export async function activate(context: ExtensionContext): Promise('createPullRequest', { diff --git a/src/git/errors.ts b/src/git/errors.ts new file mode 100644 index 0000000..34b5561 --- /dev/null +++ b/src/git/errors.ts @@ -0,0 +1,17 @@ +import { Uri } from 'vscode'; +import { GitProviderId, GitProviderService } from './gitProviderService'; + +export class ProviderNotFoundError extends Error { + readonly id: GitProviderId; + + constructor(id: GitProviderId); + constructor(uri: Uri); + constructor(idOrUri: GitProviderId | Uri); + constructor(idOrUri: GitProviderId | Uri) { + const id = typeof idOrUri === 'string' ? idOrUri : GitProviderService.getProviderId(idOrUri); + super(`No provider registered with ${id}`); + + this.id = id; + Error.captureStackTrace?.(this, ProviderNotFoundError); + } +} diff --git a/src/git/git.ts b/src/git/git.ts index 5b70256..b899c3f 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -6,7 +6,8 @@ import { Uri, window, workspace } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; import { Logger } from '../logger'; -import { Paths, Strings } from '../system'; +import { Messages } from '../messages'; +import { Paths, Strings, Versions } from '../system'; import { findGitPath, GitLocation } from './locator'; import { GitRevision } from './models/models'; import { GitBranchParser, GitLogParser, GitReflogParser, GitStashParser, GitTagParser } from './parsers/parsers'; @@ -129,7 +130,7 @@ export async function git(options: GitCommandOptio args.splice(0, 0, '-c', 'core.longpaths=true'); } - promise = run(gitInfo.path, args, encoding ?? 'utf8', runOpts); + promise = run(Git.getGitPath(), args, encoding ?? 'utf8', runOpts); pendingCommands.set(command, promise); } else { @@ -206,19 +207,23 @@ function defaultExceptionHandler(ex: Error, cwd: string | undefined, start?: [nu throw ex; } -let gitInfo: GitLocation; - 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; + return gitInfo?.path ?? ''; } export function getGitVersion(): string { - return gitInfo.version; + return gitInfo?.version ?? ''; + } + + export function hasGitPath(): boolean { + return Boolean(gitInfo?.path); } export async function setOrFindGitPath(gitPath?: string | string[]): Promise { @@ -231,10 +236,15 @@ export namespace Git { 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 function validateVersion(major: number, minor: number): boolean { - const [gitMajor, gitMinor] = gitInfo.version.split('.'); + const [gitMajor, gitMinor] = getGitVersion().split('.'); return parseInt(gitMajor, 10) >= major && parseInt(gitMinor, 10) >= minor; } diff --git a/src/git/gitProvider.ts b/src/git/gitProvider.ts new file mode 100644 index 0000000..83e8b51 --- /dev/null +++ b/src/git/gitProvider.ts @@ -0,0 +1,486 @@ +import { Disposable, Event, Range, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import { Commit, InputBox } from '../@types/vscode.git'; +import { + BranchSortOptions, + GitBlame, + GitBlameLine, + GitBlameLines, + GitBranch, + GitBranchReference, + GitContributor, + GitDiff, + GitDiffFilter, + GitDiffHunkLine, + GitDiffShortStat, + GitFile, + GitLog, + GitLogCommit, + GitMergeStatus, + GitRebaseStatus, + GitReflog, + GitRemote, + GitStash, + GitStatus, + GitStatusFile, + GitTag, + GitTree, + GitUser, + RemoteProvider, + Repository, + RepositoryChangeEvent, + RichRemoteProvider, + TagSortOptions, +} from './git'; +import { GitUri } from './gitUri'; +import { RemoteProviders } from './remotes/factory'; +import { SearchPattern } from './search'; + +export enum GitProviderId { + Git = 'git', + GitHub = 'github', +} + +export interface GitProviderDescriptor { + readonly id: GitProviderId; + readonly name: string; +} + +export interface RepositoryInitWatcher extends Disposable { + readonly onDidCreate: Event; +} + +export interface ScmRepository { + readonly rootUri: Uri; + readonly inputBox: InputBox; + + getCommit(ref: string): Promise; + push(remoteName?: string, branchName?: string, setUpstream?: boolean): Promise; +} + +export interface GitProvider { + get onDidChangeRepository(): Event; + + readonly descriptor: GitProviderDescriptor; + // get readonly(): boolean; + // get useCaching(): boolean; + + discoverRepositories(uri: Uri): Promise; + createRepository( + folder: WorkspaceFolder, + path: string, + root: boolean, + suspended?: boolean, + closed?: boolean, + ): Repository; + createRepositoryInitWatcher?(): RepositoryInitWatcher; + getOpenScmRepositories(): Promise; + getOrOpenScmRepository(repoPath: string): Promise; + + addRemote(repoPath: string, name: string, url: string): Promise; + pruneRemote(repoPath: string, remoteName: string): Promise; + applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string): Promise; + branchContainsCommit(repoPath: string, name: string, ref: string): Promise; + checkout( + repoPath: string, + ref: string, + options?: { createBranch?: string | undefined } | { fileName?: string | undefined }, + ): Promise; + resetCaches( + ...cache: ('branches' | 'contributors' | 'providers' | 'remotes' | 'stashes' | 'status' | 'tags')[] + ): void; + excludeIgnoredUris(repoPath: string, uris: Uri[]): Promise; + fetch( + repoPath: string, + options?: { + all?: boolean | undefined; + branch?: GitBranchReference | undefined; + prune?: boolean | undefined; + pull?: boolean | undefined; + remote?: string | undefined; + }, + ): Promise; + // getActiveRepository(editor?: TextEditor): Promise; + // getActiveRepoPath(editor?: TextEditor): Promise; + // getHighlanderRepoPath(): string | undefined; + getBlameForFile(uri: GitUri): Promise; + getBlameForFileContents(uri: GitUri, contents: string): Promise; + getBlameForLine( + uri: GitUri, + editorLine: number, + options?: { skipCache?: boolean | undefined }, + ): Promise; + getBlameForLineContents( + uri: GitUri, + editorLine: number, + contents: string, + options?: { skipCache?: boolean | undefined }, + ): Promise; + getBlameForRange(uri: GitUri, range: Range): Promise; + getBlameForRangeContents(uri: GitUri, range: Range, contents: string): Promise; + getBlameForRangeSync(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined; + getBranch(repoPath: string): Promise; + // getBranchAheadRange(branch: GitBranch): Promise; + getBranches( + repoPath: string, + options?: { filter?: ((b: GitBranch) => boolean) | undefined; sort?: boolean | BranchSortOptions | undefined }, + ): Promise; + // getBranchesAndOrTags( + // repoPath: string | undefined, + // options?: { + // filter?: + // | { branches?: ((b: GitBranch) => boolean) | undefined; tags?: ((t: GitTag) => boolean) | undefined } + // | undefined; + // include?: 'branches' | 'tags' | 'all' | undefined; + // sort?: + // | boolean + // | { branches?: BranchSortOptions | undefined; tags?: TagSortOptions | undefined } + // | undefined; + // }, + // ): Promise; + // getBranchesAndTagsTipsFn( + // repoPath: string | undefined, + // currentName?: string, + // ): Promise< + // (sha: string, options?: { compact?: boolean | undefined; icons?: boolean | undefined }) => string | undefined + // >; + getChangedFilesCount(repoPath: string, ref?: string): Promise; + getCommit(repoPath: string, ref: string): Promise; + getCommitBranches( + repoPath: string, + ref: string, + options?: { mode?: 'contains' | 'pointsAt' | undefined; remotes?: boolean | undefined }, + ): Promise; + getAheadBehindCommitCount(repoPath: string, refs: string[]): Promise<{ ahead: number; behind: number } | undefined>; + getCommitCount(repoPath: string, ref: string): Promise; + getCommitForFile( + repoPath: string, + fileName: string, + options?: { + ref?: string | undefined; + firstIfNotFound?: boolean | undefined; + range?: Range | undefined; + reverse?: boolean | undefined; + }, + ): Promise; + getOldestUnpushedRefForFile(repoPath: string, fileName: string): Promise; + getConfig(key: string, repoPath?: string): Promise; + getContributors( + repoPath: string, + options?: { all?: boolean | undefined; ref?: string | undefined; stats?: boolean | undefined }, + ): Promise; + getCurrentUser(repoPath: string): Promise; + getDefaultBranchName(repoPath: string | undefined, remote?: string): Promise; + getDiffForFile( + uri: GitUri, + ref1: string | undefined, + ref2?: string, + originalFileName?: string, + ): Promise; + getDiffForFileContents( + uri: GitUri, + ref: string, + contents: string, + originalFileName?: string, + ): Promise; + getDiffForLine( + uri: GitUri, + editorLine: number, + ref1: string | undefined, + ref2?: string, + originalFileName?: string, + ): Promise; + getDiffStatus( + repoPath: string, + ref1?: string, + ref2?: string, + options?: { filters?: GitDiffFilter[] | undefined; similarityThreshold?: number | undefined }, + ): Promise; + getFileStatusForCommit(repoPath: string, fileName: string, ref: string): Promise; + getLog( + repoPath: string, + options?: { + all?: boolean | undefined; + authors?: string[] | undefined; + limit?: number | undefined; + merges?: boolean | undefined; + ordering?: string | null | undefined; + ref?: string | undefined; + reverse?: boolean | undefined; + since?: string | undefined; + }, + ): Promise; + getLogRefsOnly( + repoPath: string, + options?: { + authors?: string[] | undefined; + limit?: number | undefined; + merges?: boolean | undefined; + ordering?: string | null | undefined; + ref?: string | undefined; + reverse?: boolean | undefined; + since?: string | undefined; + }, + ): Promise | undefined>; + getLogForSearch( + repoPath: string, + search: SearchPattern, + options?: { limit?: number | undefined; ordering?: string | null | undefined; skip?: number | undefined }, + ): Promise; + getLogForFile( + repoPath: string, + fileName: string, + options?: { + all?: boolean | undefined; + limit?: number | undefined; + ordering?: string | null | undefined; + range?: Range | undefined; + ref?: string | undefined; + renames?: boolean | undefined; + reverse?: boolean | undefined; + since?: string | undefined; + skip?: number | undefined; + }, + ): Promise; + getMergeBase( + repoPath: string, + ref1: string, + ref2: string, + options?: { forkPoint?: boolean | undefined }, + ): Promise; + getMergeStatus(repoPath: string): Promise; + getRebaseStatus(repoPath: string): Promise; + getNextDiffUris( + repoPath: string, + uri: Uri, + ref: string | undefined, + skip?: number, + ): Promise<{ current: GitUri; next: GitUri | undefined; deleted?: boolean | undefined } | undefined>; + getNextUri(repoPath: string, uri: Uri, ref?: string, skip?: number): Promise; + getPreviousDiffUris( + repoPath: string, + uri: Uri, + ref: string | undefined, + skip?: number, + firstParent?: boolean, + ): Promise<{ current: GitUri; previous: GitUri | undefined } | undefined>; + getPreviousLineDiffUris( + repoPath: string, + uri: Uri, + editorLine: number, + ref: string | undefined, + skip?: number, + ): Promise<{ current: GitUri; previous: GitUri | undefined; line: number } | undefined>; + getPreviousUri( + repoPath: string, + uri: Uri, + ref?: string, + skip?: number, + editorLine?: number, + firstParent?: boolean, + ): Promise; + // getPullRequestForBranch( + // branch: string, + // remote: GitRemote, + // options?: { + // avatarSize?: number | undefined; + // include?: PullRequestState[] | undefined; + // limit?: number | undefined; + // timeout?: number | undefined; + // }, + // ): Promise; + // getPullRequestForBranch( + // branch: string, + // provider: RichRemoteProvider, + // options?: { + // avatarSize?: number | undefined; + // include?: PullRequestState[] | undefined; + // limit?: number | undefined; + // timeout?: number | undefined; + // }, + // ): Promise; + // getPullRequestForBranch( + // branch: string, + // remoteOrProvider: RichRemoteProvider | GitRemote, + // options?: { + // avatarSize?: number | undefined; + // include?: PullRequestState[] | undefined; + // limit?: number | undefined; + // timeout?: number | undefined; + // }, + // ): Promise; + // getPullRequestForCommit( + // ref: string, + // remote: GitRemote, + // options?: { timeout?: number | undefined }, + // ): Promise; + // getPullRequestForCommit( + // ref: string, + // provider: RichRemoteProvider, + // options?: { timeout?: number | undefined }, + // ): Promise; + // getPullRequestForCommit( + // ref: string, + // remoteOrProvider: RichRemoteProvider | GitRemote, + // { timeout }?: { timeout?: number | undefined }, + // ): Promise; + getIncomingActivity( + repoPath: string, + options?: { + all?: boolean | undefined; + branch?: string | undefined; + limit?: number | undefined; + ordering?: string | null | undefined; + skip?: number | undefined; + }, + ): Promise; + getRichRemoteProvider( + repoPath: string | undefined, + options?: { includeDisconnected?: boolean | undefined }, + ): Promise | undefined>; + getRichRemoteProvider( + remotes: GitRemote[], + options?: { includeDisconnected?: boolean | undefined }, + ): Promise | undefined>; + getRichRemoteProvider( + remotesOrRepoPath: string | GitRemote[] | undefined, + options?: { includeDisconnected?: boolean | undefined }, + ): Promise | undefined>; + getRemotes( + repoPath: string | undefined, + options?: { sort?: boolean | undefined }, + ): Promise[]>; + getRemotesCore( + repoPath: string | undefined, + providers?: RemoteProviders, + options?: { sort?: boolean | undefined }, + ): Promise[]>; + // getRepoPath(filePath: string, options?: { ref?: string | undefined }): Promise; + // getRepoPath(uri: Uri | undefined, options?: { ref?: string | undefined }): Promise; + // getRepoPath( + // filePathOrUri: string | Uri | undefined, + // options?: { ref?: string | undefined }, + // ): Promise; + + getRepoPath(filePath: string, isDirectory: boolean): Promise; + + // getRepoPathOrActive(uri: Uri | undefined, editor: TextEditor | undefined): Promise; + // getRepositories(predicate?: (repo: Repository) => boolean): Promise>; + // getOrderedRepositories(): Promise; + // getRepository( + // repoPath: string, + // options?: { ref?: string | undefined; skipCacheUpdate?: boolean | undefined }, + // ): Promise; + // getRepository( + // uri: Uri, + // options?: { ref?: string | undefined; skipCacheUpdate?: boolean | undefined }, + // ): Promise; + // getRepository( + // repoPathOrUri: string | Uri, + // options?: { ref?: string | undefined; skipCacheUpdate?: boolean | undefined }, + // ): Promise; + // getRepository( + // repoPathOrUri: string | Uri, + // options?: { ref?: string | undefined; skipCacheUpdate?: boolean | undefined }, + // ): Promise; + // getLocalInfoFromRemoteUri( + // uri: Uri, + // options?: { validate?: boolean | undefined }, + // ): Promise<{ uri: Uri; startLine?: number | undefined; endLine?: number | undefined } | undefined>; + // getRepositoryCount(): Promise; + getStash(repoPath: string | undefined): Promise; + getStatusForFile(repoPath: string, fileName: string): Promise; + getStatusForFiles(repoPath: string, pathOrGlob: string): Promise; + getStatusForRepo(repoPath: string | undefined): Promise; + getTags( + repoPath: string | undefined, + options?: { filter?: ((t: GitTag) => boolean) | undefined; sort?: boolean | TagSortOptions | undefined }, + ): Promise; + getTreeFileForRevision(repoPath: string, fileName: string, ref: string): Promise; + getTreeForRevision(repoPath: string, ref: string): Promise; + getVersionedFileBuffer(repoPath: string, fileName: string, ref: string): Promise; + getVersionedUri(repoPath: string | undefined, fileName: string, ref: string | undefined): Promise; + getWorkingUri(repoPath: string, uri: Uri): Promise; + + // hasBranchesAndOrTags( + // repoPath: string | undefined, + // options?: { + // filter?: + // | { branches?: ((b: GitBranch) => boolean) | undefined; tags?: ((t: GitTag) => boolean) | undefined } + // | undefined; + // }, + // ): Promise; + hasRemotes(repoPath: string | undefined): Promise; + 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, + options?: { ref?: string | undefined; skipCacheUpdate?: boolean | undefined }, + ): Promise; + + getDiffTool(repoPath?: string): Promise; + openDiffTool( + repoPath: string, + uri: Uri, + options?: { + ref1?: string | undefined; + ref2?: string | undefined; + staged?: boolean | undefined; + tool?: string | undefined; + }, + ): Promise; + openDirectoryCompare(repoPath: string, ref1: string, ref2?: string, tool?: string): Promise; + + resolveReference( + repoPath: string, + ref: string, + fileName?: string, + options?: { timeout?: number | undefined }, + ): Promise; + resolveReference( + repoPath: string, + ref: string, + uri?: Uri, + options?: { timeout?: number | undefined }, + ): Promise; + resolveReference( + repoPath: string, + ref: string, + fileNameOrUri?: string | Uri, + options?: { timeout?: number | undefined }, + ): Promise; + validateBranchOrTagName(repoPath: string, ref: string): Promise; + validateReference(repoPath: string, ref: string): Promise; + + stageFile(repoPath: string, fileName: string): Promise; + stageFile(repoPath: string, uri: Uri): Promise; + stageFile(repoPath: string, fileNameOrUri: string | Uri): Promise; + stageDirectory(repoPath: string, directory: string): Promise; + stageDirectory(repoPath: string, uri: Uri): Promise; + stageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise; + unStageFile(repoPath: string, fileName: string): Promise; + unStageFile(repoPath: string, uri: Uri): Promise; + unStageFile(repoPath: string, fileNameOrUri: string | Uri): Promise; + unStageDirectory(repoPath: string, directory: string): Promise; + unStageDirectory(repoPath: string, uri: Uri): Promise; + unStageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise; + + stashApply(repoPath: string, stashName: string, options?: { deleteAfter?: boolean | undefined }): Promise; + stashDelete(repoPath: string, stashName: string, ref?: string): Promise; + stashSave( + repoPath: string, + message?: string, + uris?: Uri[], + options?: { includeUntracked?: boolean | undefined; keepIndex?: boolean | undefined }, + ): Promise; +} diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts new file mode 100644 index 0000000..a149b87 --- /dev/null +++ b/src/git/gitProviderService.ts @@ -0,0 +1,1878 @@ +'use strict'; +import { + ConfigurationChangeEvent, + Disposable, + Event, + EventEmitter, + ProgressLocation, + Range, + TextEditor, + Uri, + window, + WindowState, + workspace, + WorkspaceFolder, + WorkspaceFoldersChangeEvent, +} from 'vscode'; +import { resetAvatarCache } from '../avatars'; +import { configuration } from '../configuration'; +import { BuiltInGitConfiguration, ContextKeys, DocumentSchemes, GlyphChars, setContext } 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'; +import { ProviderNotFoundError } from './errors'; +import { + Authentication, + BranchDateFormatting, + BranchSortOptions, + CommitDateFormatting, + Git, + GitBlame, + GitBlameLine, + GitBlameLines, + GitBranch, + GitBranchReference, + GitContributor, + GitDiff, + GitDiffFilter, + GitDiffHunkLine, + GitDiffShortStat, + GitFile, + GitLog, + GitLogCommit, + GitMergeStatus, + GitRebaseStatus, + GitReference, + GitReflog, + GitRemote, + GitRevision, + GitStash, + GitStatus, + GitStatusFile, + GitTag, + GitTree, + GitUser, + PullRequest, + PullRequestDateFormatting, + PullRequestState, + Repository, + RepositoryChange, + RepositoryChangeComparisonMode, + RepositoryChangeEvent, + SearchPattern, + TagSortOptions, +} from './git'; +import { GitProvider, GitProviderDescriptor, GitProviderId, ScmRepository } from './gitProvider'; +import { GitUri } from './gitUri'; +import { RemoteProvider, RemoteProviders, RichRemoteProvider } from './remotes/factory'; + +export type { GitProviderDescriptor, GitProviderId }; + +const slash = '/'; + +const maxDefaultBranchWeight = 100; +const weightedDefaultBranches = new Map([ + ['master', maxDefaultBranchWeight], + ['main', 15], + ['default', 10], + ['develop', 5], + ['development', 1], +]); + +export type GitProvidersChangedEvent = { + readonly added: readonly GitProvider[]; + readonly removed: readonly GitProvider[]; +}; + +export type RepositoriesChangedEvent = { + readonly added: readonly Repository[]; + readonly removed: readonly Repository[]; +}; + +export class GitProviderService implements Disposable { + private readonly _onDidChangeProviders = new EventEmitter(); + get onDidChangeProviders(): Event { + return this._onDidChangeProviders.event; + } + + private _onDidChangeRepositories = new EventEmitter(); + get onDidChangeRepositories(): Event { + return this._onDidChangeRepositories.event; + } + + private readonly _onDidChangeRepository = new EventEmitter(); + get onDidChangeRepository(): Event { + return this._onDidChangeRepository.event; + } + + private readonly _disposable: Disposable; + private readonly _providers = new Map(); + private readonly _repositories = new Map(); + + constructor(private readonly container: Container) { + this._disposable = Disposable.from( + window.onDidChangeWindowState(this.onWindowStateChanged, this), + workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this), + configuration.onDidChange(this.onConfigurationChanged, this), + Authentication.onDidChange(e => { + if (e.reason === 'connected') { + resetAvatarCache('failed'); + } + + this.resetCaches('providers'); + void this.updateContext(this._repositories); + }), + ); + + BranchDateFormatting.reset(); + CommitDateFormatting.reset(); + PullRequestDateFormatting.reset(); + + void this.updateContext(this._repositories); + } + + dispose() { + this._disposable.dispose(); + this._providers.clear(); + + this._repositories.forEach(r => r.dispose()); + this._repositories.clear(); + } + + private onConfigurationChanged(e?: ConfigurationChangeEvent) { + if ( + configuration.changed(e, 'defaultDateFormat') || + configuration.changed(e, 'defaultDateSource') || + configuration.changed(e, 'defaultDateStyle') + ) { + BranchDateFormatting.reset(); + CommitDateFormatting.reset(); + PullRequestDateFormatting.reset(); + } + + if (configuration.changed(e, 'views.contributors.showAllBranches')) { + this.resetCaches('contributors'); + } + } + + @debug() + private onWindowStateChanged(e: WindowState) { + if (e.focused) { + this._repositories.forEach(r => r.resume()); + } else { + this._repositories.forEach(r => r.suspend()); + } + } + + private onWorkspaceFoldersChanged(e: WorkspaceFoldersChangeEvent) { + if (e.added.length) { + const autoRepositoryDetection = + configuration.getAny( + BuiltInGitConfiguration.AutoRepositoryDetection, + ) ?? true; + if (autoRepositoryDetection !== false) { + void this.discoverRepositories(e.added); + } + } + + if (e.removed.length) { + const removed: Repository[] = []; + + for (const folder of e.removed) { + const key = asKey(folder.uri); + + for (const repository of this._repositories.values()) { + if (key === asKey(repository.folder.uri)) { + this._repositories.delete(repository.path); + removed.push(repository); + } + } + } + + if (removed.length) { + void this.updateContext(this._repositories); + + // Defer the event trigger enough to let everything unwind + queueMicrotask(() => { + this._onDidChangeRepositories.fire({ added: [], removed: removed }); + removed.forEach(r => r.dispose()); + }); + } + } + } + + 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; + } + + get registeredProviders(): GitProviderDescriptor[] { + return [...Iterables.map(this._providers.values(), p => ({ ...p.descriptor }))]; + } + + get openRepositories(): Repository[] { + const repositories = [...Iterables.filter(this.repositories, r => !r.closed)]; + if (repositories.length === 0) return repositories; + + return Repository.sort(repositories); + } + + get openRepositoryCount(): number { + return Iterables.count(this.repositories, r => !r.closed); + } + + get repositories(): Iterable { + return this._repositories.values(); + } + + get repositoryCount(): number { + return this._repositories.size; + } + + get highlander(): Repository | undefined { + if (this.repositoryCount === 1) { + return this._repositories.values().next().value; + } + return undefined; + } + + @log() + get highlanderRepoPath(): string | undefined { + return this.highlander?.path; + } + + // get readonly() { + // return true; + // // return this.container.vsls.readonly; + // } + + // get useCaching() { + // return this.container.config.advanced.caching.enabled; + // } + + getCachedRepository(repoPath: string): Repository | undefined { + return this._repositories.get(repoPath); + } + + /** + * Registers a {@link GitProvider} + * @param id A unique indentifier for the provider + * @param name A name for the provider + * @param provider A provider for handling git operations + * @returns A disposable to unregister the {@link GitProvider} + */ + register(id: GitProviderId, provider: GitProvider): 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] })); + } + + this._onDidChangeRepository.fire(e); + }); + + this._onDidChangeProviders.fire({ added: [provider], removed: [] }); + + void this.onWorkspaceFoldersChanged({ added: workspace.workspaceFolders ?? [], removed: [] }); + + return { + dispose: () => { + disposable.dispose(); + this._providers.delete(id); + + const removed: Repository[] = []; + + for (const [key, repository] of [...this._repositories]) { + if (repository?.provider.id === id) { + this._repositories.delete(key); + removed.push(repository); + } + } + + void this.updateContext(this._repositories); + + if (removed.length) { + // Defer the event trigger enough to let everything unwind + queueMicrotask(() => { + this._onDidChangeRepositories.fire({ added: [], removed: removed }); + removed.forEach(r => r.dispose()); + }); + } + + this._onDidChangeProviders.fire({ added: [], removed: [provider] }); + }, + }; + } + + private _discoveredWorkspaceFolders = new Map>(); + + async discoverRepositories(folders: readonly WorkspaceFolder[], options?: { force?: boolean }): Promise { + const promises = []; + + for (const folder of folders) { + if (!options?.force && this._discoveredWorkspaceFolders.has(folder)) continue; + + const promise = this.discoverRepositoriesCore(folder); + promises.push(promise); + this._discoveredWorkspaceFolders.set(folder, promise); + } + + if (promises.length === 0) return; + + const results = await Promise.allSettled(promises); + + const repositories = Iterables.flatMap, Repository>( + Iterables.filter, PromiseFulfilledResult>( + results, + (r): r is PromiseFulfilledResult => r.status === 'fulfilled', + ), + r => r.value, + ); + + const added: Repository[] = []; + + for (const repository of repositories) { + if (this._repositories.has(repository.path)) continue; + + added.push(repository); + this._repositories.set(repository.path, repository); + } + + 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: [] })); + } + + private async discoverRepositoriesCore(folder: WorkspaceFolder): Promise { + const { provider } = this.getProvider(folder.uri); + + try { + return await provider.discoverRepositories(folder.uri); + } catch (ex) { + this._discoveredWorkspaceFolders.delete(folder); + + Logger.error( + ex, + `${provider.descriptor.name} Provider(${ + provider.descriptor.id + }) failed discovering repositories in ${folder.uri.toString(true)}`, + ); + + return []; + } + } + + 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; + } + + private getProvider(repoPath: string | Uri): { provider: GitProvider; path: string } { + const id = GitProviderService.getProviderId(repoPath); + + const provider = this._providers.get(id); + if (provider == null) throw new ProviderNotFoundError(id); + + switch (id) { + case GitProviderId.Git: + return { + provider: provider, + path: typeof repoPath === 'string' ? repoPath : repoPath.fsPath, + }; + + default: + return { + provider: provider, + path: typeof repoPath === 'string' ? repoPath : repoPath.toString(), + }; + } + } + + @log() + addRemote(repoPath: string | Uri, name: string, url: string): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.addRemote(path, name, url); + } + + @log() + pruneRemote(repoPath: string | Uri, remoteName: string): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.pruneRemote(path, remoteName); + } + + @log() + async applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string): Promise { + const { provider } = this.getProvider(uri); + return provider.applyChangesToWorkingFile(uri, ref1, ref2); + } + + @log() + async branchContainsCommit(repoPath: string | Uri, name: string, ref: string): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.branchContainsCommit(path, name, ref); + } + + @log() + async checkout( + repoPath: string, + ref: string, + options?: { createBranch?: string } | { fileName?: string }, + ): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.checkout(path, ref, options); + } + + @log() + resetCaches( + ...cache: ('branches' | 'contributors' | 'providers' | 'remotes' | 'stashes' | 'status' | 'tags')[] + ): void { + const repoCache = cache.filter((c): c is 'branches' | 'remotes' => c === 'branches' || c === 'remotes'); + // Delegate to the repos, if we are clearing everything or one of the per-repo caches + if (cache.length === 0 || repoCache.length > 0) { + for (const repo of this.repositories) { + repo.resetCaches(...repoCache); + } + } + + void Promise.allSettled([...this._providers.values()].map(provider => provider.resetCaches(...cache))); + } + + @log({ + args: { + 0: (repoPath: string) => repoPath, + 1: (uris: Uri[]) => `${uris.length}`, + }, + }) + async excludeIgnoredUris(repoPath: string, uris: Uri[]): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.excludeIgnoredUris(path, uris); + } + + @gate() + @log() + async fetch( + repoPath: string, + options?: { all?: boolean; branch?: GitBranchReference; prune?: boolean; pull?: boolean; remote?: string }, + ): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.fetch(path, options); + } + + @gate( + (repos, opts) => `${repos == null ? '' : repos.map(r => r.id).join(',')}|${JSON.stringify(opts)}`, + ) + @log({ + args: { + 0: (repos?: Repository[]) => (repos == null ? false : repos.map(r => r.name).join(', ')), + }, + }) + async fetchAll(repositories?: Repository[], options?: { all?: boolean; prune?: boolean }) { + if (repositories == null) { + repositories = this.openRepositories; + } + if (repositories.length === 0) return; + + if (repositories.length === 1) { + await repositories[0].fetch(options); + + return; + } + + await window.withProgress( + { + location: ProgressLocation.Notification, + title: `Fetching ${repositories.length} repositories`, + }, + () => Promise.all(repositories!.map(r => r.fetch({ progress: false, ...options }))), + ); + } + + @gate( + (repos, opts) => `${repos == null ? '' : repos.map(r => r.id).join(',')}|${JSON.stringify(opts)}`, + ) + @log({ + args: { + 0: (repos?: Repository[]) => (repos == null ? false : repos.map(r => r.name).join(', ')), + }, + }) + async pullAll(repositories?: Repository[], options?: { rebase?: boolean }) { + if (repositories == null) { + repositories = this.openRepositories; + } + if (repositories.length === 0) return; + + if (repositories.length === 1) { + await repositories[0].pull(options); + + return; + } + + await window.withProgress( + { + location: ProgressLocation.Notification, + title: `Pulling ${repositories.length} repositories`, + }, + () => Promise.all(repositories!.map(r => r.pull({ progress: false, ...options }))), + ); + } + + @gate(repos => `${repos == null ? '' : repos.map(r => r.id).join(',')}`) + @log({ + args: { + 0: (repos?: Repository[]) => (repos == null ? false : repos.map(r => r.name).join(', ')), + }, + }) + async pushAll( + repositories?: Repository[], + options?: { + force?: boolean; + reference?: GitReference; + publish?: { + remote: string; + }; + }, + ) { + if (repositories == null) { + repositories = this.openRepositories; + } + if (repositories.length === 0) return; + + if (repositories.length === 1) { + await repositories[0].push(options); + + return; + } + + await window.withProgress( + { + location: ProgressLocation.Notification, + title: `Pushing ${repositories.length} repositories`, + }, + () => Promise.all(repositories!.map(r => r.push({ progress: false, ...options }))), + ); + } + + @log({ + args: { + 0: (editor: TextEditor) => + editor != null ? `TextEditor(${Logger.toLoggable(editor.document.uri)})` : 'undefined', + }, + }) + async getActiveRepository(editor?: TextEditor): Promise { + const repoPath = await this.getActiveRepoPath(editor); + if (repoPath == null) return undefined; + + return this.getRepository(repoPath); + } + + @log({ + args: { + 0: (editor: TextEditor) => + editor != null ? `TextEditor(${Logger.toLoggable(editor.document.uri)})` : 'undefined', + }, + }) + async getActiveRepoPath(editor?: TextEditor): Promise { + editor = editor ?? window.activeTextEditor; + + let repoPath; + if (editor != null) { + const doc = await this.container.tracker.getOrAdd(editor.document.uri); + if (doc != null) { + repoPath = doc.uri.repoPath; + } + } + + if (repoPath != null) return repoPath; + + return this.highlanderRepoPath; + } + + @log() + async getBlameForFile(uri: GitUri): Promise { + const { provider } = this.getProvider(uri); + return provider.getBlameForFile(uri); + } + + @log({ + args: { + 1: _contents => '', + }, + }) + async getBlameForFileContents(uri: GitUri, contents: string): Promise { + const { provider } = this.getProvider(uri); + return provider.getBlameForFileContents(uri, contents); + } + + @log() + async getBlameForLine( + uri: GitUri, + editorLine: number, // editor lines are 0-based + options?: { skipCache?: boolean }, + ): Promise { + const { provider } = this.getProvider(uri); + return provider.getBlameForLine(uri, editorLine, options); + } + + @log({ + args: { + 2: _contents => '', + }, + }) + async getBlameForLineContents( + uri: GitUri, + editorLine: number, // editor lines are 0-based + contents: string, + options?: { skipCache?: boolean }, + ): Promise { + const { provider } = this.getProvider(uri); + return provider.getBlameForLineContents(uri, editorLine, contents, options); + } + + @log() + async getBlameForRange(uri: GitUri, range: Range): Promise { + const { provider } = this.getProvider(uri); + return provider.getBlameForRange(uri, range); + } + + @log({ + args: { + 2: _contents => '', + }, + }) + async getBlameForRangeContents(uri: GitUri, range: Range, contents: string): Promise { + const { provider } = this.getProvider(uri); + return provider.getBlameForRangeContents(uri, range, contents); + } + + @log({ + args: { + 0: _blame => '', + }, + }) + getBlameForRangeSync(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined { + const { provider } = this.getProvider(uri); + return provider.getBlameForRangeSync(blame, uri, range); + } + + @log() + async getBranch(repoPath: string | Uri | undefined): Promise { + if (repoPath == null) return undefined; + + const { provider, path } = this.getProvider(repoPath); + return provider.getBranch(path); + } + + @log({ + args: { + 0: b => b.name, + }, + }) + async getBranchAheadRange(branch: GitBranch): Promise { + if (branch.state.ahead > 0) { + return GitRevision.createRange(branch.upstream?.name, branch.ref); + } + + if (branch.upstream == null) { + // If we have no upstream branch, try to find a best guess branch to use as the "base" + const branches = await this.getBranches(branch.repoPath, { + filter: b => weightedDefaultBranches.has(b.name), + }); + if (branches.length > 0) { + let weightedBranch: { weight: number; branch: GitBranch } | undefined; + for (const branch of branches) { + const weight = weightedDefaultBranches.get(branch.name)!; + if (weightedBranch == null || weightedBranch.weight < weight) { + weightedBranch = { weight: weight, branch: branch }; + } + + if (weightedBranch.weight === maxDefaultBranchWeight) break; + } + + const possibleBranch = weightedBranch!.branch.upstream?.name ?? weightedBranch!.branch.ref; + if (possibleBranch !== branch.ref) { + return GitRevision.createRange(possibleBranch, branch.ref); + } + } + } + + return undefined; + } + + @log({ + args: { + 1: () => false, + }, + }) + async getBranches( + repoPath: string | Uri | undefined, + options?: { + filter?: (b: GitBranch) => boolean; + sort?: boolean | BranchSortOptions; + }, + ): Promise { + if (repoPath == null) return []; + + const { provider, path } = this.getProvider(repoPath); + return provider.getBranches(path, options); + } + + @log({ + args: { + 1: () => false, + }, + }) + async getBranchesAndOrTags( + repoPath: string | Uri | undefined, + { + filter, + include, + sort, + ...options + }: { + filter?: { branches?: (b: GitBranch) => boolean; tags?: (t: GitTag) => boolean }; + include?: 'all' | 'branches' | 'tags'; + sort?: boolean | { branches?: BranchSortOptions; tags?: TagSortOptions }; + } = {}, + ): Promise<(GitBranch | GitTag)[] | GitBranch[] | GitTag[]> { + const [branches, tags] = await Promise.all([ + include == null || include === 'all' || include === 'branches' + ? this.getBranches(repoPath, { + ...options, + filter: filter?.branches, + sort: typeof sort === 'boolean' ? undefined : sort?.branches, + }) + : undefined, + include == null || include === 'all' || include === 'tags' + ? this.getTags(repoPath, { + ...options, + filter: filter?.tags, + sort: typeof sort === 'boolean' ? undefined : sort?.tags, + }) + : undefined, + ]); + + if (branches != null && tags != null) { + return [...branches.filter(b => !b.remote), ...tags, ...branches.filter(b => b.remote)]; + } + + return branches ?? tags ?? []; + } + + @log() + async getBranchesAndTagsTipsFn( + repoPath: string | Uri | undefined, + currentName?: string, + ): Promise< + (sha: string, options?: { compact?: boolean | undefined; icons?: boolean | undefined }) => string | undefined + > { + const [branches, tags] = await Promise.all([this.getBranches(repoPath), this.getTags(repoPath)]); + + const branchesAndTagsBySha = Arrays.groupByFilterMap( + (branches as (GitBranch | GitTag)[]).concat(tags as (GitBranch | GitTag)[]), + bt => bt.sha, + bt => { + if (currentName) { + if (bt.name === currentName) return undefined; + if (bt.refType === 'branch' && bt.getNameWithoutRemote() === currentName) { + return { name: bt.name, compactName: bt.getRemoteName(), type: bt.refType }; + } + } + + return { name: bt.name, compactName: undefined, type: bt.refType }; + }, + ); + + return (sha: string, options?: { compact?: boolean; icons?: boolean }): string | undefined => { + const branchesAndTags = branchesAndTagsBySha.get(sha); + if (branchesAndTags == null || branchesAndTags.length === 0) return undefined; + + if (!options?.compact) { + return branchesAndTags + .map( + bt => `${options?.icons ? `${bt.type === 'tag' ? '$(tag)' : '$(git-branch)'} ` : ''}${bt.name}`, + ) + .join(', '); + } + + if (branchesAndTags.length > 1) { + const [bt] = branchesAndTags; + return `${options?.icons ? `${bt.type === 'tag' ? '$(tag)' : '$(git-branch)'} ` : ''}${ + bt.compactName ?? bt.name + }, ${GlyphChars.Ellipsis}`; + } + + return branchesAndTags + .map( + bt => + `${options?.icons ? `${bt.type === 'tag' ? '$(tag)' : '$(git-branch)'} ` : ''}${ + bt.compactName ?? bt.name + }`, + ) + .join(', '); + }; + } + + @log() + async getChangedFilesCount(repoPath: string | Uri, ref?: string): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getChangedFilesCount(path, ref); + } + + @log() + async getCommit(repoPath: string | Uri, ref: string): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getCommit(path, ref); + } + + @log() + async getCommitBranches( + repoPath: string | Uri, + ref: string, + options?: { mode?: 'contains' | 'pointsAt'; remotes?: boolean }, + ): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getCommitBranches(path, ref, options); + } + + @log() + getAheadBehindCommitCount( + repoPath: string | Uri, + refs: string[], + ): Promise<{ ahead: number; behind: number } | undefined> { + const { provider, path } = this.getProvider(repoPath); + return provider.getAheadBehindCommitCount(path, refs); + } + + @log() + getCommitCount(repoPath: string | Uri, ref: string): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getCommitCount(path, ref); + } + + @log() + async getCommitForFile( + repoPath: string | Uri | undefined, + fileName: string, + options?: { ref?: string; firstIfNotFound?: boolean; range?: Range; reverse?: boolean }, + ): Promise { + if (repoPath == null) return undefined; + + const { provider, path } = this.getProvider(repoPath); + return provider.getCommitForFile(path, fileName, options); + } + + @log() + async getOldestUnpushedRefForFile(repoPath: string | Uri, fileName: string): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getOldestUnpushedRefForFile(path, fileName); + } + + // eslint-disable-next-line @typescript-eslint/require-await + @log() + async getConfig(_key: string, _repoPath?: string): Promise { + // return Git.config__get(key, repoPath); + return undefined; + } + + @log() + async getContributors( + repoPath: string | Uri, + options?: { all?: boolean; ref?: string; stats?: boolean }, + ): Promise { + if (repoPath == null) return []; + + const { provider, path } = this.getProvider(repoPath); + return provider.getContributors(path, options); + } + + @log() + @gate() + async getCurrentUser(repoPath: string | Uri): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getCurrentUser(path); + } + + @log() + async getDefaultBranchName(repoPath: string | Uri | undefined, remote?: string): Promise { + if (repoPath == null) return undefined; + + const { provider, path } = this.getProvider(repoPath); + return provider.getDefaultBranchName(path, remote); + } + + @log() + async getDiffForFile( + uri: GitUri, + ref1: string | undefined, + ref2?: string, + originalFileName?: string, + ): Promise { + const { provider } = this.getProvider(uri); + return provider.getDiffForFile(uri, ref1, ref2, originalFileName); + } + + @log({ + args: { + 1: _contents => '', + }, + }) + async getDiffForFileContents( + uri: GitUri, + ref: string, + contents: string, + originalFileName?: string, + ): Promise { + const { provider } = this.getProvider(uri); + return provider.getDiffForFile(uri, ref, contents, originalFileName); + } + + @log() + async getDiffForLine( + uri: GitUri, + editorLine: number, // editor lines are 0-based + ref1: string | undefined, + ref2?: string, + originalFileName?: string, + ): Promise { + const { provider } = this.getProvider(uri); + return provider.getDiffForLine(uri, editorLine, ref1, ref2, originalFileName); + } + + @log() + async getDiffStatus( + repoPath: string | Uri, + ref1?: string, + ref2?: string, + options?: { filters?: GitDiffFilter[]; similarityThreshold?: number }, + ): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getDiffStatus(path, ref1, ref2, options); + } + + @log() + async getFileStatusForCommit(repoPath: string | Uri, fileName: string, ref: string): Promise { + if (ref === GitRevision.deletedOrMissing || GitRevision.isUncommitted(ref)) return undefined; + + const { provider, path } = this.getProvider(repoPath); + return provider.getFileStatusForCommit(path, fileName, ref); + } + + @log() + async getLog( + repoPath: string | Uri, + options?: { + all?: boolean; + authors?: string[]; + limit?: number; + merges?: boolean; + ordering?: string | null; + ref?: string; + reverse?: boolean; + since?: string; + }, + ): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getLog(path, options); + } + + @log() + async getLogRefsOnly( + repoPath: string | Uri, + options?: { + authors?: string[]; + limit?: number; + merges?: boolean; + ordering?: string | null; + ref?: string; + reverse?: boolean; + since?: string; + }, + ): Promise | undefined> { + const { provider, path } = this.getProvider(repoPath); + return provider.getLogRefsOnly(path, options); + } + + @log() + async getLogForSearch( + repoPath: string | Uri, + search: SearchPattern, + options?: { limit?: number; ordering?: string | null; skip?: number }, + ): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getLogForSearch(path, search, options); + } + + @log() + async getLogForFile( + repoPath: string | Uri | undefined, + fileName: string, + options?: { + all?: boolean; + limit?: number; + ordering?: string | null; + range?: Range; + ref?: string; + renames?: boolean; + reverse?: boolean; + since?: string; + skip?: number; + }, + ): Promise { + if (repoPath == null) return undefined; + + const { provider, path } = this.getProvider(repoPath); + return provider.getLogForFile(path, fileName, options); + } + + @log() + async getMergeBase( + repoPath: string | Uri, + ref1: string, + ref2: string, + options?: { forkPoint?: boolean }, + ): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getMergeBase(path, ref1, ref2, options); + } + + @gate() + @log() + async getMergeStatus(repoPath: string | Uri): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getMergeStatus(path); + } + + @gate() + @log() + async getRebaseStatus(repoPath: string | Uri): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getRebaseStatus(path); + } + + @log() + async getNextDiffUris( + repoPath: string | Uri, + uri: Uri, + ref: string | undefined, + skip: number = 0, + ): Promise<{ current: GitUri; next: GitUri | undefined; deleted?: boolean } | undefined> { + // If we have no ref (or staged ref) there is no next commit + if (ref == null || ref.length === 0) return undefined; + + const { provider, path } = this.getProvider(repoPath); + return provider.getNextDiffUris(path, uri, ref, skip); + } + + @log() + async getNextUri( + repoPath: string | Uri, + uri: Uri, + ref?: string, + skip: number = 0, + // editorLine?: number + ): Promise { + // If we have no ref (or staged ref) there is no next commit + if (ref == null || ref.length === 0 || GitRevision.isUncommittedStaged(ref)) return undefined; + + const { provider, path } = this.getProvider(repoPath); + return provider.getNextUri(path, uri, ref, skip); + } + + @log() + async getPreviousDiffUris( + repoPath: string | Uri, + uri: Uri, + ref: string | undefined, + skip: number = 0, + firstParent: boolean = false, + ): Promise<{ current: GitUri; previous: GitUri | undefined } | undefined> { + if (ref === GitRevision.deletedOrMissing) return undefined; + + const { provider, path } = this.getProvider(repoPath); + return provider.getPreviousDiffUris(path, uri, ref, skip, firstParent); + } + + @log() + async getPreviousLineDiffUris( + repoPath: string | Uri, + uri: Uri, + editorLine: number, + ref: string | undefined, + skip: number = 0, + ): Promise<{ current: GitUri; previous: GitUri | undefined; line: number } | undefined> { + if (ref === GitRevision.deletedOrMissing) return undefined; + + const { provider, path } = this.getProvider(repoPath); + return provider.getPreviousLineDiffUris(path, uri, editorLine, ref, skip); + } + + @log() + async getPreviousUri( + repoPath: string | Uri, + uri: Uri, + ref?: string, + skip: number = 0, + editorLine?: number, + firstParent: boolean = false, + ): Promise { + if (ref === GitRevision.deletedOrMissing) return undefined; + + const { provider, path } = this.getProvider(repoPath); + return provider.getPreviousUri(path, uri, ref, skip, editorLine, firstParent); + } + + async getPullRequestForBranch( + branch: string, + remote: GitRemote, + options?: { avatarSize?: number; include?: PullRequestState[]; limit?: number; timeout?: number }, + ): Promise; + async getPullRequestForBranch( + branch: string, + provider: RichRemoteProvider, + options?: { avatarSize?: number; include?: PullRequestState[]; limit?: number; timeout?: number }, + ): Promise; + @gate((ref, remoteOrProvider, options) => { + const provider = GitRemote.is(remoteOrProvider) ? remoteOrProvider.provider : remoteOrProvider; + return `${ref}${provider != null ? `|${provider.id}:${provider.domain}/${provider.path}` : ''}${ + options != null ? `|${options.limit ?? -1}:${options.include?.join(',')}` : '' + }`; + }) + @debug({ + args: { + 1: (remoteOrProvider: GitRemote | RichRemoteProvider) => remoteOrProvider.name, + }, + }) + async getPullRequestForBranch( + branch: string, + remoteOrProvider: GitRemote | RichRemoteProvider, + { + timeout, + ...options + }: { avatarSize?: number; include?: PullRequestState[]; limit?: number; timeout?: number } = {}, + ): Promise { + let provider; + if (GitRemote.is(remoteOrProvider)) { + ({ provider } = remoteOrProvider); + if (!provider?.hasApi()) return undefined; + } else { + provider = remoteOrProvider; + } + + let promiseOrPR = provider.getPullRequestForBranch(branch, options); + if (promiseOrPR == null || !Promises.is(promiseOrPR)) { + return promiseOrPR; + } + + if (timeout != null && timeout > 0) { + promiseOrPR = Promises.cancellable(promiseOrPR, timeout); + } + + try { + return await promiseOrPR; + } catch (ex) { + if (ex instanceof Promises.CancellationError) { + throw ex; + } + + return undefined; + } + } + + async getPullRequestForCommit( + ref: string, + remote: GitRemote, + options?: { timeout?: number }, + ): Promise; + async getPullRequestForCommit( + ref: string, + provider: RichRemoteProvider, + options?: { timeout?: number }, + ): Promise; + @gate((ref, remoteOrProvider, options) => { + const provider = GitRemote.is(remoteOrProvider) ? remoteOrProvider.provider : remoteOrProvider; + return `${ref}${provider != null ? `|${provider.id}:${provider.domain}/${provider.path}` : ''}|${ + options?.timeout + }`; + }) + @debug({ + args: { + 1: (remoteOrProvider: GitRemote | RichRemoteProvider) => remoteOrProvider.name, + }, + }) + async getPullRequestForCommit( + ref: string, + remoteOrProvider: GitRemote | RichRemoteProvider, + { timeout }: { timeout?: number } = {}, + ): Promise { + if (GitRevision.isUncommitted(ref)) return undefined; + + let provider; + if (GitRemote.is(remoteOrProvider)) { + ({ provider } = remoteOrProvider); + if (!provider?.hasApi()) return undefined; + } else { + provider = remoteOrProvider; + } + + let promiseOrPR = provider.getPullRequestForCommit(ref); + if (promiseOrPR == null || !Promises.is(promiseOrPR)) { + return promiseOrPR; + } + + if (timeout != null && timeout > 0) { + promiseOrPR = Promises.cancellable(promiseOrPR, timeout); + } + + try { + return await promiseOrPR; + } catch (ex) { + if (ex instanceof Promises.CancellationError) { + throw ex; + } + + return undefined; + } + } + + @log() + async getIncomingActivity( + repoPath: string | Uri, + options?: { all?: boolean; branch?: string; limit?: number; ordering?: string | null; skip?: number }, + ): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getIncomingActivity(path, options); + } + + async getRichRemoteProvider( + repoPath: string | Uri | undefined, + options?: { includeDisconnected?: boolean }, + ): Promise | undefined>; + async getRichRemoteProvider( + remotes: GitRemote[], + options?: { includeDisconnected?: boolean }, + ): Promise | undefined>; + @gate( + (remotesOrRepoPath, options) => + `${typeof remotesOrRepoPath === 'string' ? remotesOrRepoPath : remotesOrRepoPath[0]?.repoPath}:${ + options?.includeDisconnected ?? false + }`, + ) + @log({ args: { 0: () => false } }) + async getRichRemoteProvider( + remotesOrRepoPath: GitRemote[] | string | Uri | undefined, + options?: { includeDisconnected?: boolean }, + ): Promise | undefined> { + if (remotesOrRepoPath == null) return undefined; + + if (Array.isArray(remotesOrRepoPath)) { + if (remotesOrRepoPath.length === 0) return undefined; + + const repoPath = remotesOrRepoPath[0].repoPath; + + const { provider } = this.getProvider(repoPath); + return provider.getRichRemoteProvider(remotesOrRepoPath, options); + } + + const { provider, path } = this.getProvider(remotesOrRepoPath); + return provider.getRichRemoteProvider(path, options); + } + + @log() + async getRemotes( + repoPath: string | Uri | undefined, + options?: { sort?: boolean }, + ): Promise[]> { + if (repoPath == null) return []; + + const { provider, path } = this.getProvider(repoPath); + return provider.getRemotes(path, options); + } + + async getRemotesCore( + repoPath: string | Uri | undefined, + providers?: RemoteProviders, + options?: { sort?: boolean }, + ): Promise { + if (repoPath == null) return []; + + const { provider, path } = this.getProvider(repoPath); + return provider.getRemotesCore(path, providers, options); + } + + async getRepoPath(filePath: string, options?: { ref?: string }): Promise; + async getRepoPath(uri: Uri | undefined, options?: { ref?: string }): Promise; + @log({ + exit: path => `returned ${path}`, + }) + async getRepoPath( + filePathOrUri: string | Uri | undefined, + options?: { ref?: string }, + ): Promise { + if (filePathOrUri == null) return this.highlanderRepoPath; + if (GitUri.is(filePathOrUri)) return filePathOrUri.repoPath; + + const cc = Logger.getCorrelationContext(); + + // Don't save the tracking info to the cache, because we could be looking in the wrong place (e.g. looking in the root when the file is in a submodule) + let repo = await this.getRepository(filePathOrUri, { ...options, skipCacheUpdate: true }); + if (repo != null) return repo.path; + + const { provider, path } = this.getProvider(filePathOrUri); + const rp = await provider.getRepoPath(path, false); + + // const rp = await this.getRepoPathCore( + // typeof filePathOrUri === 'string' ? filePathOrUri : filePathOrUri.fsPath, + // false, + // ); + if (rp == null) return undefined; + + // Recheck this._repositoryTree.get(rp) to make sure we haven't already tried adding this due to awaits + if (this._repositories.get(rp) != null) return rp; + + const isVslsScheme = + typeof filePathOrUri === 'string' ? undefined : filePathOrUri.scheme === DocumentSchemes.Vsls; + + // If this new repo is inside one of our known roots and we we don't already know about, add it + const root = this.findRepositoryForPath(rp, isVslsScheme); + + let folder; + if (root != null) { + // Not sure why I added this for vsls (I can't see a reason for it anymore), but if it is added it will break submodules + // rp = root.path; + folder = root.folder; + } else { + folder = workspace.getWorkspaceFolder(GitUri.file(rp, isVslsScheme)); + if (folder == null) { + const parts = rp.split(slash); + folder = { + uri: GitUri.file(rp, isVslsScheme), + name: parts[parts.length - 1], + index: this.container.git.repositoryCount, + }; + } + } + + Logger.log(cc, `Repository found in '${rp}'`); + repo = provider.createRepository(folder, rp, false); + this._repositories.set(rp, repo); + + void this.updateContext(this._repositories); + // Send a notification that the repositories changed + queueMicrotask(() => this._onDidChangeRepositories.fire({ added: [repo!], removed: [] })); + + return rp; + } + + @log() + async getRepoPathOrActive(uri: Uri | undefined, editor: TextEditor | undefined) { + const repoPath = await this.getRepoPath(uri); + if (repoPath) return repoPath; + + return this.getActiveRepoPath(editor); + } + + async getRepository( + repoPath: string, + options?: { ref?: string; skipCacheUpdate?: boolean }, + ): Promise; + async getRepository( + uri: Uri, + options?: { ref?: string; skipCacheUpdate?: boolean }, + ): Promise; + async getRepository( + repoPathOrUri: string | Uri, + options?: { ref?: string; skipCacheUpdate?: boolean }, + ): Promise; + @log({ + exit: repo => `returned ${repo != null ? `${repo.path}` : 'undefined'}`, + }) + async getRepository( + repoPathOrUri: string | Uri, + options: { ref?: string; skipCacheUpdate?: boolean } = {}, + ): Promise { + let isVslsScheme; + + let path: string; + if (typeof repoPathOrUri === 'string') { + const repo = this._repositories.get(repoPathOrUri); + if (repo != null) return repo; + + path = repoPathOrUri; + isVslsScheme = undefined; + } else { + if (GitUri.is(repoPathOrUri)) { + if (repoPathOrUri.repoPath) { + const repo = this._repositories.get(repoPathOrUri.repoPath); + if (repo != null) return repo; + } + + path = repoPathOrUri.fsPath; + } else { + path = repoPathOrUri.fsPath; + } + + isVslsScheme = repoPathOrUri.scheme === DocumentSchemes.Vsls; + } + + const repo = this.findRepositoryForPath(path, isVslsScheme); + if (repo == null) return undefined; + + // Make sure the file is tracked in this repo before returning -- it could be from a submodule + if (!(await this.isTracked(path, repo.path, options))) return undefined; + return repo; + } + + @debug() + private findRepositoryForPath(path: string, isVslsScheme: boolean | undefined): Repository | undefined { + if (this._repositories.size === 0) return undefined; + + function findBySubPath(repositories: Map, path: string) { + const repos = [...repositories.values()].sort((a, b) => a.path.length - b.path.length); + for (const repo of repos) { + if (Paths.isDescendent(path, repo.path)) return repo; + } + + return undefined; + } + + let repo = findBySubPath(this._repositories, path); + // If we can't find the repo and we are a guest, check if we are a "root" workspace + if (repo == null && isVslsScheme !== false && this.container.vsls.isMaybeGuest) { + if (!vslsUriPrefixRegex.test(path)) { + path = Strings.normalizePath(path); + const vslsPath = `/~0${path.startsWith(slash) ? path : `/${path}`}`; + repo = findBySubPath(this._repositories, vslsPath); + } + } + return repo; + } + + async getLocalInfoFromRemoteUri( + uri: Uri, + options?: { validate?: boolean }, + ): Promise<{ uri: Uri; startLine?: number; endLine?: number } | undefined> { + for (const repo of this.openRepositories) { + for (const remote of await repo.getRemotes()) { + const local = await remote?.provider?.getLocalInfoFromRemoteUri(repo, uri, options); + if (local != null) return local; + } + } + + return undefined; + } + + @gate() + @log() + async getStash(repoPath: string | Uri | undefined): Promise { + if (repoPath == null) return undefined; + + const { provider, path } = this.getProvider(repoPath); + return provider.getStash(path); + } + + @log() + async getStatusForFile(repoPath: string | Uri, fileName: string): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getStatusForFile(path, fileName); + } + + @log() + async getStatusForFiles(repoPath: string | Uri, pathOrGlob: string): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getStatusForFiles(path, pathOrGlob); + } + + @log() + async getStatusForRepo(repoPath: string | Uri | undefined): Promise { + if (repoPath == null) return undefined; + + const { provider, path } = this.getProvider(repoPath); + return provider.getStatusForRepo(path); + } + + @log({ + args: { + 1: () => false, + }, + }) + async getTags( + repoPath: string | Uri | undefined, + options?: { filter?: (t: GitTag) => boolean; sort?: boolean | TagSortOptions }, + ): Promise { + if (repoPath == null) return []; + + const { provider, path } = this.getProvider(repoPath); + return provider.getTags(path, options); + } + + @log() + async getTreeFileForRevision( + repoPath: string | Uri | undefined, + fileName: string, + ref: string, + ): Promise { + if (repoPath == null || fileName == null || fileName.length === 0) return undefined; + + const { provider, path } = this.getProvider(repoPath); + return provider.getTreeFileForRevision(path, fileName, ref); + } + + @log() + async getTreeForRevision(repoPath: string | Uri | undefined, ref: string): Promise { + if (repoPath == null) return []; + + const { provider, path } = this.getProvider(repoPath); + return provider.getTreeForRevision(path, ref); + } + + @log() + getVersionedFileBuffer(repoPath: string | Uri, fileName: string, ref: string) { + const { provider, path } = this.getProvider(repoPath); + return provider.getVersionedFileBuffer(path, fileName, ref); + } + + @log() + async getVersionedUri( + repoPath: string | Uri | undefined, + fileName: string, + ref: string | undefined, + ): Promise { + if (repoPath == null || ref === GitRevision.deletedOrMissing) return undefined; + + const { provider, path } = this.getProvider(repoPath); + return provider.getVersionedUri(path, fileName, ref); + } + + @log() + async getWorkingUri(repoPath: string | Uri, uri: Uri) { + const { provider, path } = this.getProvider(repoPath); + return provider.getWorkingUri(path, uri); + } + + @log() + async hasBranchesAndOrTags( + repoPath: string | Uri | undefined, + { + filter, + }: { + filter?: { branches?: (b: GitBranch) => boolean; tags?: (t: GitTag) => boolean }; + } = {}, + ): Promise { + const [branches, tags] = await Promise.all([ + this.getBranches(repoPath, { + filter: filter?.branches, + sort: false, + }), + this.getTags(repoPath, { + filter: filter?.tags, + sort: false, + }), + ]); + + return (branches != null && branches.length !== 0) || (tags != null && tags.length !== 0); + } + + @log() + async hasRemotes(repoPath: string | Uri | undefined): Promise { + if (repoPath == null) return false; + + const repository = await this.getRepository(repoPath); + if (repository == null) return false; + + return repository.hasRemotes(); + } + + @log() + async hasTrackingBranch(repoPath: string | undefined): Promise { + if (repoPath == null) return false; + + const repository = await this.getRepository(repoPath); + if (repository == null) return false; + + return repository.hasUpstreamBranch(); + } + + @log({ + args: { + 1: (editor: TextEditor) => + editor != null ? `TextEditor(${Logger.toLoggable(editor.document.uri)})` : 'undefined', + }, + }) + async isActiveRepoPath(repoPath: string | undefined, editor?: TextEditor): Promise { + if (repoPath == null) return false; + + editor = editor ?? window.activeTextEditor; + if (editor == null) return false; + + const doc = await this.container.tracker.getOrAdd(editor.document.uri); + 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 + ); + } + + async isTracked(uri: GitUri): Promise; + 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); + } + + @log() + async getDiffTool(repoPath?: string | Uri): Promise { + if (repoPath == null) return undefined; + + const { provider, path } = this.getProvider(repoPath); + return provider.getDiffTool(path); + } + + @log() + async openDiffTool( + repoPath: string | Uri, + uri: Uri, + options?: { ref1?: string; ref2?: string; staged?: boolean; tool?: string }, + ): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.openDiffTool(path, uri, options); + } + + @log() + async openDirectoryCompare(repoPath: string | Uri, ref1: string, ref2?: string, tool?: string): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.openDirectoryCompare(path, ref1, ref2, tool); + } + + async resolveReference( + repoPath: string, + ref: string, + fileName?: string, + options?: { timeout?: number }, + ): Promise; + async resolveReference(repoPath: string, ref: string, uri?: Uri, options?: { timeout?: number }): Promise; + @log() + async resolveReference( + repoPath: string | Uri, + ref: string, + fileNameOrUri?: string | Uri, + options?: { timeout?: number }, + ) { + if (ref == null || ref.length === 0 || ref === GitRevision.deletedOrMissing || GitRevision.isUncommitted(ref)) { + return ref; + } + + const { provider, path } = this.getProvider(repoPath); + return provider.resolveReference(path, ref, fileNameOrUri, options); + } + + @log() + validateBranchOrTagName(repoPath: string | Uri, ref: string): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.validateBranchOrTagName(path, ref); + } + + @log() + async validateReference(repoPath: string | Uri, ref: string) { + if (ref == null || ref.length === 0) return false; + if (ref === GitRevision.deletedOrMissing || GitRevision.isUncommitted(ref)) return true; + + const { provider, path } = this.getProvider(repoPath); + return provider.validateReference(path, ref); + } + + stageFile(repoPath: string | Uri, fileName: string): Promise; + stageFile(repoPath: string | Uri, uri: Uri): Promise; + @log() + stageFile(repoPath: string | Uri, fileNameOrUri: string | Uri): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.stageFile(path, fileNameOrUri); + } + + stageDirectory(repoPath: string | Uri, directory: string): Promise; + stageDirectory(repoPath: string | Uri, uri: Uri): Promise; + @log() + stageDirectory(repoPath: string | Uri, directoryOrUri: string | Uri): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.stageDirectory(path, directoryOrUri); + } + + unStageFile(repoPath: string | Uri, fileName: string): Promise; + unStageFile(repoPath: string | Uri, uri: Uri): Promise; + @log() + unStageFile(repoPath: string | Uri, fileNameOrUri: string | Uri): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.unStageFile(path, fileNameOrUri); + } + + unStageDirectory(repoPath: string | Uri, directory: string): Promise; + unStageDirectory(repoPath: string | Uri, uri: Uri): Promise; + @log() + unStageDirectory(repoPath: string | Uri, directoryOrUri: string | Uri): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.unStageDirectory(path, directoryOrUri); + } + + @log() + stashApply(repoPath: string | Uri, stashName: string, options?: { deleteAfter?: boolean }): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.stashApply(path, stashName, options); + } + + @log() + stashDelete(repoPath: string | Uri, stashName: string, ref?: string): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.stashDelete(path, stashName, ref); + } + + @log() + stashSave( + repoPath: string | Uri, + message?: string, + uris?: Uri[], + options?: { includeUntracked?: boolean; keepIndex?: boolean }, + ): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.stashSave(path, message, uris, options); + } + + @log() + async getOpenScmRepositories(): Promise { + const results = await Promise.allSettled([...this._providers.values()].map(p => p.getOpenScmRepositories())); + const repositories = Iterables.flatMap, ScmRepository>( + Iterables.filter, PromiseFulfilledResult>( + results, + (r): r is PromiseFulfilledResult => r.status === 'fulfilled', + ), + r => r.value, + ); + return [...repositories]; + } + + @log() + async getOrOpenScmRepository(repoPath: string): Promise { + const { provider, path } = this.getProvider(repoPath); + return provider.getOrOpenScmRepository(path); + } + + static getEncoding(repoPath: string, fileName: string): string; + 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)); + } +} + +export function asKey(uri: Uri): string; +export function asKey(uri: Uri | undefined): string | undefined; +export function asKey(uri: Uri | undefined): string | undefined { + if (uri === undefined) return undefined; + const hasTrailingSlash = uri.path.endsWith('/'); + if (!hasTrailingSlash && !uri.fragment) return uri.toString(); + + return uri.with({ path: hasTrailingSlash ? uri.path.slice(0, -1) : uri.path, fragment: '' }).toString(); +} diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index 4799e8e..6a03f78 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -32,8 +32,8 @@ import { GitTag, SearchPattern, } from '../git'; +import { GitProviderDescriptor } from '../gitProvider'; import { GitUri } from '../gitUri'; -import { GitService } from '../providers/localGitProvider'; import { RemoteProviderFactory, RemoteProviders, RichRemoteProvider } from '../remotes/factory'; import { BranchSortOptions, @@ -211,10 +211,12 @@ export class Repository implements Disposable { private _suspended: boolean; constructor( + private readonly container: Container, + private readonly onDidRepositoryChange: (repo: Repository, e: RepositoryChangeEvent) => void, + public readonly provider: GitProviderDescriptor, public readonly folder: WorkspaceFolder, public readonly path: string, public readonly root: boolean, - private readonly onAnyRepositoryChanged: (repo: Repository, e: RepositoryChangeEvent) => void, suspended: boolean, closed: boolean = false, ) { @@ -354,7 +356,7 @@ export class Repository implements Disposable { } if (uri.path.endsWith('.git/HEAD') || uri.path.endsWith('.git/ORIG_HEAD')) { - this.resetCaches('branch'); + this.resetCaches('branches'); this.fireChange(RepositoryChange.Heads); return; @@ -394,7 +396,7 @@ export class Repository implements Disposable { if (match != null) { switch (match[1]) { case 'heads': - this.resetCaches('branch'); + this.resetCaches('branches'); this.fireChange(RepositoryChange.Heads); return; @@ -528,7 +530,7 @@ export class Repository implements Disposable { options: { all?: boolean; branch?: GitBranchReference; prune?: boolean; pull?: boolean; remote?: string } = {}, ) { try { - void (await Container.instance.git.fetch(this.path, options)); + void (await this.container.git.fetch(this.path, options)); this.fireChange(RepositoryChange.Unknown); } catch (ex) { @@ -544,7 +546,7 @@ export class Repository implements Disposable { } if (this._branch == null || !this.supportsChangeEvents) { - this._branch = Container.instance.git.getBranch(this.path); + this._branch = this.container.git.getBranch(this.path); } return this._branch; } @@ -555,7 +557,7 @@ export class Repository implements Disposable { sort?: boolean | BranchSortOptions; } = {}, ): Promise { - return Container.instance.git.getBranches(this.path, options); + return this.container.git.getBranches(this.path, options); } getBranchesAndOrTags( @@ -565,19 +567,19 @@ export class Repository implements Disposable { sort?: boolean | { branches?: BranchSortOptions; tags?: TagSortOptions }; } = {}, ) { - return Container.instance.git.getBranchesAndOrTags(this.path, options); + return this.container.git.getBranchesAndOrTags(this.path, options); } getChangedFilesCount(sha?: string): Promise { - return Container.instance.git.getChangedFilesCount(this.path, sha); + return this.container.git.getChangedFilesCount(this.path, sha); } getCommit(ref: string): Promise { - return Container.instance.git.getCommit(this.path, ref); + return this.container.git.getCommit(this.path, ref); } getContributors(options?: { all?: boolean; ref?: string; stats?: boolean }): Promise { - return Container.instance.git.getContributors(this.path, options); + return this.container.git.getContributors(this.path, options); } private _lastFetched: number | undefined; @@ -585,7 +587,7 @@ export class Repository implements Disposable { async getLastFetched(): Promise { if (this._lastFetched == null) { const hasRemotes = await this.hasRemotes(); - if (!hasRemotes || Container.instance.vsls.isMaybeGuest) return 0; + if (!hasRemotes || this.container.vsls.isMaybeGuest) return 0; } try { @@ -602,11 +604,11 @@ export class Repository implements Disposable { } getMergeStatus(): Promise { - return Container.instance.git.getMergeStatus(this.path); + return this.container.git.getMergeStatus(this.path); } getRebaseStatus(): Promise { - return Container.instance.git.getRebaseStatus(this.path); + return this.container.git.getRebaseStatus(this.path); } async getRemote(remote: string): Promise { @@ -621,7 +623,7 @@ export class Repository implements Disposable { } // Since we are caching the results, always sort - this._remotes = Container.instance.git.getRemotesCore(this.path, this._providers, { sort: true }); + this._remotes = this.container.git.getRemotesCore(this.path, this._providers, { sort: true }); void this.subscribeToRemotes(this._remotes); } @@ -629,7 +631,7 @@ export class Repository implements Disposable { } async getRichRemote(connectedOnly: boolean = false): Promise | undefined> { - return Container.instance.git.getRichRemoteProvider(await this.getRemotes(), { + return this.container.git.getRichRemoteProvider(await this.getRemotes(), { includeDisconnected: !connectedOnly, }); } @@ -648,15 +650,15 @@ export class Repository implements Disposable { } getStash(): Promise { - return Container.instance.git.getStash(this.path); + return this.container.git.getStash(this.path); } getStatus(): Promise { - return Container.instance.git.getStatusForRepo(this.path); + return this.container.git.getStatusForRepo(this.path); } getTags(options?: { filter?: (t: GitTag) => boolean; sort?: boolean | TagSortOptions }): Promise { - return Container.instance.git.getTags(this.path, options); + return this.container.git.getTags(this.path, options); } async hasRemotes(): Promise { @@ -704,7 +706,7 @@ export class Repository implements Disposable { this.path, )); } else if (configuration.getAny(BuiltInGitConfiguration.FetchOnPull, Uri.file(this.path))) { - void (await Container.instance.git.fetch(this.path)); + void (await this.container.git.fetch(this.path)); } this.fireChange(RepositoryChange.Unknown); @@ -741,7 +743,7 @@ export class Repository implements Disposable { } private async showCreatePullRequestPrompt(remoteName: string, branch: GitBranchReference) { - if (!Container.instance.actionRunners.count('createPullRequest')) return; + if (!this.container.actionRunners.count('createPullRequest')) return; if (!(await Messages.showCreatePullRequestPrompt(branch.name))) return; const remote = await this.getRemote(remoteName); @@ -782,7 +784,7 @@ export class Repository implements Disposable { ) { try { if (GitReference.isBranch(options.reference)) { - const repo = await GitService.getOrOpenBuiltInGitRepository(this.path); + const repo = await this.container.git.getOrOpenScmRepository(this.path); if (repo == null) return; if (options.publish != null) { @@ -803,7 +805,7 @@ export class Repository implements Disposable { } } } else if (options.reference != null) { - const repo = await GitService.getOrOpenBuiltInGitRepository(this.path); + const repo = await this.container.git.getOrOpenScmRepository(this.path); if (repo == null) return; const branch = await this.getBranch(); @@ -839,8 +841,8 @@ export class Repository implements Disposable { this.runTerminalCommand('reset', ...args); } - resetCaches(...cache: ('branch' | 'remotes')[]) { - if (cache.length === 0 || cache.includes('branch')) { + resetCaches(...cache: ('branches' | 'remotes')[]) { + if (cache.length === 0 || cache.includes('branches')) { this._branch = undefined; } @@ -877,11 +879,11 @@ export class Repository implements Disposable { search: SearchPattern, options: { limit?: number; skip?: number } = {}, ): Promise { - return Container.instance.git.getLogForSearch(this.path, search, options); + return this.container.git.getLogForSearch(this.path, search, options); } get starred() { - const starred = Container.instance.context.workspaceState.get(WorkspaceState.StarredRepositories); + const starred = this.container.context.workspaceState.get(WorkspaceState.StarredRepositories); return starred != null && starred[this.id] === true; } @@ -892,7 +894,7 @@ export class Repository implements Disposable { @gate(() => '') @log() async stashApply(stashName: string, options: { deleteAfter?: boolean } = {}) { - void (await Container.instance.git.stashApply(this.path, stashName, options)); + void (await this.container.git.stashApply(this.path, stashName, options)); this.fireChange(RepositoryChange.Stash); } @@ -900,7 +902,7 @@ export class Repository implements Disposable { @gate(() => '') @log() async stashDelete(stashName: string, ref?: string) { - void (await Container.instance.git.stashDelete(this.path, stashName, ref)); + void (await this.container.git.stashDelete(this.path, stashName, ref)); this.fireChange(RepositoryChange.Stash); } @@ -908,7 +910,7 @@ export class Repository implements Disposable { @gate(() => '') @log() async stashSave(message?: string, uris?: Uri[], options: { includeUntracked?: boolean; keepIndex?: boolean } = {}) { - void (await Container.instance.git.stashSave(this.path, message, uris, options)); + void (await this.container.git.stashSave(this.path, message, uris, options)); this.fireChange(RepositoryChange.Stash); } @@ -931,7 +933,7 @@ export class Repository implements Disposable { private async switchCore(ref: string, options: { createBranch?: string } = {}) { try { - void (await Container.instance.git.checkout(this.path, ref, options)); + void (await this.container.git.checkout(this.path, ref, options)); this.fireChange(RepositoryChange.Unknown); } catch (ex) { @@ -960,7 +962,7 @@ export class Repository implements Disposable { } private async updateStarredCore(key: WorkspaceState, id: string, star: boolean) { - let starred = Container.instance.context.workspaceState.get(key); + let starred = this.container.context.workspaceState.get(key); if (starred === undefined) { starred = Object.create(null) as Starred; } @@ -971,7 +973,7 @@ export class Repository implements Disposable { const { [id]: _, ...rest } = starred; starred = rest; } - await Container.instance.context.workspaceState.update(key, starred); + await this.container.context.workspaceState.update(key, starred); this.fireChange(RepositoryChange.Starred); } @@ -1035,7 +1037,7 @@ export class Repository implements Disposable { this._pendingRepoChange = this._pendingRepoChange?.with(changes) ?? new RepositoryChangeEvent(this, changes); - this.onAnyRepositoryChanged(this, new RepositoryChangeEvent(this, changes)); + this.onDidRepositoryChange(this, new RepositoryChangeEvent(this, changes)); if (this._suspended) { Logger.debug(cc, `queueing suspended ${this._pendingRepoChange.toString(true)}`); @@ -1087,7 +1089,7 @@ export class Repository implements Disposable { this._pendingFileSystemChange = undefined; - const uris = await Container.instance.git.excludeIgnoredUris(this.path, e.uris); + const uris = await this.container.git.excludeIgnoredUris(this.path, e.uris); if (uris.length === 0) return; if (uris.length !== e.uris.length) { @@ -1109,7 +1111,7 @@ export class Repository implements Disposable { } private async tryWatchingForChangesViaBuiltInApi() { - const repo = await GitService.getOrOpenBuiltInGitRepository(this.path); + const repo = await this.container.git.getOrOpenScmRepository(this.path); if (repo != null) { const internalRepo = (repo as any)._repository; if (internalRepo != null && 'onDidChangeRepository' in internalRepo) { diff --git a/src/git/providers/localGitProvider.ts b/src/git/providers/localGitProvider.ts index 6e4f8d2..0181b69 100644 --- a/src/git/providers/localGitProvider.ts +++ b/src/git/providers/localGitProvider.ts @@ -3,43 +3,25 @@ import * as fs from 'fs'; import * as os from 'os'; import * as paths from 'path'; import { - ConfigurationChangeEvent, Disposable, env, Event, EventEmitter, extensions, - ProgressLocation, Range, TextEditor, Uri, window, - WindowState, workspace, WorkspaceFolder, - WorkspaceFoldersChangeEvent, } from 'vscode'; import type { API as BuiltInGitApi, Repository as BuiltInGitRepository, GitExtension } from '../../@types/vscode.git'; -import { resetAvatarCache } from '../../avatars'; import { configuration } from '../../configuration'; -import { BuiltInGitConfiguration, ContextKeys, DocumentSchemes, GlyphChars, setContext } from '../../constants'; +import { BuiltInGitConfiguration, DocumentSchemes, GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { setEnabled } from '../../extension'; import { LogCorrelationContext, Logger } from '../../logger'; import { Messages } from '../../messages'; -import { - Arrays, - debug, - Functions, - gate, - Iterables, - log, - Paths, - Promises, - Strings, - TernarySearchTree, - Versions, -} from '../../system'; +import { Arrays, debug, Functions, gate, Iterables, log, Paths, Promises, Strings, Versions } from '../../system'; import { CachedBlame, CachedDiff, @@ -47,12 +29,8 @@ import { GitDocumentState, TrackedDocument, } from '../../trackers/gitDocumentTracker'; -import { vslsUriPrefixRegex } from '../../vsls/vsls'; import { - Authentication, - BranchDateFormatting, BranchSortOptions, - CommitDateFormatting, Git, GitAuthor, GitBlame, @@ -95,7 +73,6 @@ import { isFolderGlob, maxGitCliLength, PullRequest, - PullRequestDateFormatting, PullRequestState, Repository, RepositoryChange, @@ -104,13 +81,15 @@ import { SearchPattern, TagSortOptions, } from '../git'; +import { GitProvider, GitProviderId, RepositoryInitWatcher, ScmRepository } from '../gitProvider'; +import { GitProviderService } from '../gitProviderService'; import { GitUri } from '../gitUri'; +import { InvalidGitConfigError, UnableToFindGitError } from '../locator'; import { GitReflogParser, GitShortLogParser } from '../parsers/parsers'; import { RemoteProvider, RemoteProviderFactory, RemoteProviders, RichRemoteProvider } from '../remotes/factory'; import { fsExists, isWindows } from '../shell'; const emptyStr = ''; -const slash = '/'; const RepoSearchWarnings = { doesNotExist: /no such file or directory/i, @@ -133,16 +112,13 @@ const weightedDefaultBranches = new Map([ ['development', 1], ]); -export class GitService implements Disposable { - private _onDidChangeRepositories = new EventEmitter(); - get onDidChangeRepositories(): Event { - return this._onDidChangeRepositories.event; - } +export class LocalGitProvider implements GitProvider, Disposable { + descriptor = { id: GitProviderId.Git, name: 'Git' }; - private readonly _disposable: Disposable; - private readonly _repositoryTree: TernarySearchTree; - private _repositoriesLoadingPromise: Promise | undefined; - private _repositoriesLoadingPromiseResolver: (() => void) | undefined; + private _onDidChangeRepository = new EventEmitter(); + get onDidChangeRepository(): Event { + return this._onDidChangeRepository.event; + } private readonly _branchesCache = new Map>(); private readonly _contributorsCache = new Map>(); @@ -154,47 +130,15 @@ export class GitService implements Disposable { private readonly _trackedCache = new Map>(); private readonly _userMapCache = new Map(); - constructor(private readonly container: Container) { - this._repositoryTree = TernarySearchTree.forPaths(); - - this._disposable = Disposable.from( - container.onReady(this.onReady, this), - window.onDidChangeWindowState(this.onWindowStateChanged, this), - workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this), - configuration.onDidChange(this.onConfigurationChanged, this), - Authentication.onDidChange(e => { - if (e.reason === 'connected') { - resetAvatarCache('failed'); - } - this._remotesWithApiProviderCache.clear(); - void this.updateContext(this._repositoryTree); - }), - ); - - this._repositoriesLoadingPromise = new Promise(resolve => (this._repositoriesLoadingPromiseResolver = resolve)); - - this.onConfigurationChanged(); - } - - dispose() { - this._repositoryTree.forEach(r => r.dispose()); - void this.resetCaches(); - this._disposable.dispose(); - } + constructor(private readonly container: Container) {} - private onReady() { - this.onWorkspaceFoldersChanged().finally(() => this._repositoriesLoadingPromiseResolver!()); - } - - get readonly() { - return this.container.vsls.readonly; - } + dispose() {} - get useCaching() { + private get useCaching() { return this.container.config.advanced.caching.enabled; } - private onAnyRepositoryChanged(repo: Repository, e: RepositoryChangeEvent) { + private onRepositoryChanged(repo: Repository, e: RepositoryChangeEvent) { if (e.changed(RepositoryChange.Config, RepositoryChangeComparisonMode.Any)) { this._userMapCache.delete(repo.path); } @@ -229,136 +173,117 @@ export class GitService implements Disposable { this._tagsCache.delete(repo.path); } - if (e.changed(RepositoryChange.Closed, RepositoryChangeComparisonMode.Any)) { - // Send a notification that the repositories changed - setImmediate(async () => { - await this.updateContext(this._repositoryTree); - - this.fireRepositoriesChanged(); - }); - } + this._onDidChangeRepository.fire(e); } - private onConfigurationChanged(e?: ConfigurationChangeEvent) { - if ( - configuration.changed(e, 'defaultDateFormat') || - configuration.changed(e, 'defaultDateSource') || - configuration.changed(e, 'defaultDateStyle') - ) { - BranchDateFormatting.reset(); - CommitDateFormatting.reset(); - PullRequestDateFormatting.reset(); + private _initialized: Promise | undefined; + private async ensureInitialized(): Promise { + if (this._initialized == null) { + this._initialized = this.initializeCore(); } - if (configuration.changed(e, 'views.contributors.showAllBranches')) { - this._contributorsCache.clear(); - } + return this._initialized; } - @debug() - private onWindowStateChanged(e: WindowState) { - if (e.focused) { - this._repositoryTree.forEach(r => r.resume()); - } else { - this._repositoryTree.forEach(r => r.suspend()); + private async initializeCore() { + // Try to use the same git as the built-in vscode git extension + const gitApi = await this.getScmGitApi(); + if (gitApi != null) { + this.container.context.subscriptions.push( + gitApi.onDidCloseRepository(e => { + const repository = this.container.git.getCachedRepository(Strings.normalizePath(e.rootUri.fsPath)); + if (repository != null) { + repository.closed = true; + } + }), + gitApi.onDidOpenRepository(e => { + const repository = this.container.git.getCachedRepository(Strings.normalizePath(e.rootUri.fsPath)); + if (repository != null) { + repository.closed = false; + } + }), + ); } - } - private async onWorkspaceFoldersChanged(e?: WorkspaceFoldersChangeEvent) { - let initializing = false; - if (e == null) { - initializing = true; - e = { - added: workspace.workspaceFolders ?? [], - removed: [], - }; + if (Git.hasGitPath()) return; + + await Git.setOrFindGitPath(gitApi?.git.path ?? configuration.getAny('git.path')); - Logger.log(`Starting repository search in ${e.added.length} folders`); + // Warn if git is less than v2.7.2 + if (this.compareGitVersion('2.7.2') === -1) { + void Messages.showGitVersionUnsupportedErrorMessage(Git.getGitVersion(), '2.7.2'); } + } + + async discoverRepositories(uri: Uri): Promise { + if (uri.scheme !== DocumentSchemes.File) return []; const autoRepositoryDetection = configuration.getAny( BuiltInGitConfiguration.AutoRepositoryDetection, ) ?? true; - if (autoRepositoryDetection === false) return; - - for (const f of e.added) { - const { scheme } = f.uri; - if (scheme !== DocumentSchemes.File && scheme !== DocumentSchemes.Vsls) continue; - - if (scheme === DocumentSchemes.Vsls) { - if (this.container.vsls.isMaybeGuest) { - const guest = await this.container.vsls.guest(); - if (guest != null) { - const repositories = await guest.getRepositoriesInFolder( - f, - this.onAnyRepositoryChanged.bind(this), - ); - for (const r of repositories) { - if (!this._repositoryTree.has(r.path)) { - this._repositoryTree.set(r.path, r); - } - } - } - } - } else { - // Search for and add all repositories (nested and/or submodules) - const repositories = await this.repositorySearch(f); - for (const r of repositories) { - if (!this._repositoryTree.has(r.path)) { - this._repositoryTree.set(r.path, r); - } + if (autoRepositoryDetection === false) return []; - if (autoRepositoryDetection === true || autoRepositoryDetection === 'subFolders') { - void GitService.openBuiltInGitRepository(r.path); - } + try { + await this.ensureInitialized(); + + const repositories = await this.repositorySearch(workspace.getWorkspaceFolder(uri)!); + if (autoRepositoryDetection === true || autoRepositoryDetection === 'subFolders') { + for (const repository of repositories) { + void this.openScmRepository(repository.path); } } - } - for (const f of e.removed) { - const { fsPath, scheme } = f.uri; - if (scheme !== DocumentSchemes.File && scheme !== DocumentSchemes.Vsls) continue; - - const repos = this._repositoryTree.findSuperstr(fsPath); - const reposToDelete = - repos != null - ? // Since the filtered tree will have keys that are relative to the fsPath, normalize to the full path - [...Iterables.map(repos, r => [r, r.path])] - : []; - - // const filteredTree = this._repositoryTree.findSuperstr(fsPath); - // const reposToDelete = - // filteredTree != null - // ? // Since the filtered tree will have keys that are relative to the fsPath, normalize to the full path - // [ - // ...Iterables.map<[Repository, string], [Repository, string]>( - // filteredTree.entries(), - // ([r, k]) => [r, path.join(fsPath, k)] - // ) - // ] - // : []; - - const repo = this._repositoryTree.get(fsPath); - if (repo != null) { - reposToDelete.push([repo, fsPath]); + if (repositories.length > 0) { + this._trackedCache.clear(); } - for (const [r, k] of reposToDelete) { - this._repositoryTree.delete(k); - r.dispose(); + return repositories; + } catch (ex) { + if (ex instanceof InvalidGitConfigError) { + void Messages.showGitInvalidConfigErrorMessage(); + } else if (ex instanceof UnableToFindGitError) { + void Messages.showGitMissingErrorMessage(); + } else { + const msg: string = ex?.message ?? ''; + if (msg) { + void window.showErrorMessage(`Unable to initialize Git; ${msg}`); + } } + + throw ex; } + } - await this.updateContext(this._repositoryTree); + createRepository( + folder: WorkspaceFolder, + path: string, + root: boolean, + suspended?: boolean, + closed?: boolean, + ): Repository { + void this.openScmRepository(path); + return new Repository( + this.container, + this.onRepositoryChanged.bind(this), + this.descriptor, + folder, + path, + root, + suspended ?? !window.state.focused, + closed, + ); + } - if (!initializing) { - // Defer the event trigger enough to let everything unwind - setImmediate(() => this.fireRepositoriesChanged()); - } + createRepositoryInitWatcher(): RepositoryInitWatcher { + const watcher = workspace.createFileSystemWatcher('**/.git', false, true, true); + return { + onDidCreate: watcher.onDidCreate, + dispose: () => watcher.dispose(), + }; } - @log({ + @log({ args: false, singleLine: true, prefix: (context, folder) => `${context.prefix}(${folder.uri.fsPath})`, @@ -375,12 +300,11 @@ export class GitService implements Disposable { Logger.log(cc, `searching (depth=${depth})...`); const repositories: Repository[] = []; - const anyRepoChangedFn = this.onAnyRepositoryChanged.bind(this); - const rootPath = await this.getRepoPathCore(uri.fsPath, true); + const rootPath = await this.getRepoPath(uri.fsPath, true); if (rootPath != null) { Logger.log(cc, `found root repository in '${rootPath}'`); - repositories.push(new Repository(folder, rootPath, true, anyRepoChangedFn, !window.state.focused)); + repositories.push(this.createRepository(folder, rootPath, true)); } if (depth <= 0) return repositories; @@ -426,11 +350,11 @@ export class GitService implements Disposable { Logger.log(cc, `searching in '${p}'...`); Logger.debug(cc, `normalizedRepoPath=${Strings.normalizePath(p)}, rootPath=${rootPath}`); - const rp = await this.getRepoPathCore(p, true); + const rp = await this.getRepoPath(p, true); if (rp == null) continue; Logger.log(cc, `found repository in '${rp}'`); - repositories.push(new Repository(folder, rp, false, anyRepoChangedFn, !window.state.focused)); + repositories.push(this.createRepository(folder, rp, false)); } return repositories; @@ -486,91 +410,14 @@ export class GitService implements Disposable { }); } - private async updateContext(repositoryTree: TernarySearchTree) { - const hasRepository = repositoryTree.any(); - await setEnabled(hasRepository); - - // 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 (hasRepository) { - for (const repo of repositoryTree.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 (!hasRepository) { - const watcher = workspace.createFileSystemWatcher('**/.git', false, true, true); - const disposable = Disposable.from( - watcher, - watcher.onDidCreate(async uri => { - const f = workspace.getWorkspaceFolder(uri); - if (f == null) return; - - // Search for and add all repositories (nested and/or submodules) - const repositories = await this.repositorySearch(f); - if (repositories.length === 0) return; - - disposable.dispose(); - - for (const r of repositories) { - if (!this._repositoryTree.has(r.path)) { - this._repositoryTree.set(r.path, r); - } - - void GitService.openBuiltInGitRepository(r.path); - } - - await this.updateContext(this._repositoryTree); - - // Defer the event trigger enough to let everything unwind - setImmediate(() => this.fireRepositoriesChanged()); - }, this), - ); - } - } - - private fireRepositoriesChanged() { - this._onDidChangeRepositories.fire(); - } - @log() - addRemote(repoPath: string, name: string, url: string) { - return Git.remote__add(repoPath, name, url); + async addRemote(repoPath: string, name: string, url: string): Promise { + await Git.remote__add(repoPath, name, url); } @log() - pruneRemote(repoPath: string, remoteName: string) { - return Git.remote__prune(repoPath, remoteName); + async pruneRemote(repoPath: string, remoteName: string): Promise { + await Git.remote__prune(repoPath, remoteName); } @log() @@ -625,38 +472,33 @@ export class GitService implements Disposable { } @log() - async checkout(repoPath: string, ref: string, options: { createBranch?: string } | { fileName?: string } = {}) { + async checkout( + repoPath: string, + ref: string, + options?: { createBranch?: string } | { fileName?: string }, + ): Promise { const cc = Logger.getCorrelationContext(); try { - return await Git.checkout(repoPath, ref, options); + await Git.checkout(repoPath, ref, options); } catch (ex) { const msg: string = ex?.toString() ?? emptyStr; if (/overwritten by checkout/i.test(msg)) { void Messages.showGenericErrorMessage( `Unable to checkout '${ref}'. Please commit or stash your changes before switching branches`, ); - return undefined; + return; } Logger.error(ex, cc); void void Messages.showGenericErrorMessage(`Unable to checkout '${ref}'`); - return undefined; } } @log() - async resetCaches( - ...cache: ('branches' | 'contributors' | 'providers' | 'remotes' | 'stashes' | 'status' | 'tags')[] - ) { + resetCaches(...cache: ('branches' | 'contributors' | 'providers' | 'remotes' | 'stashes' | 'status' | 'tags')[]) { if (cache.length === 0 || cache.includes('branches')) { this._branchesCache.clear(); - - if (cache.length !== 0) { - for (const repo of await this.getRepositories()) { - repo.resetCaches('branch'); - } - } } if (cache.length === 0 || cache.includes('contributors')) { @@ -667,12 +509,6 @@ export class GitService implements Disposable { this._remotesWithApiProviderCache.clear(); } - if (cache.includes('remotes')) { - for (const repo of await this.getRepositories()) { - repo.resetCaches('remotes'); - } - } - if (cache.length === 0 || cache.includes('stashes')) { this._stashesCache.clear(); } @@ -689,10 +525,6 @@ export class GitService implements Disposable { if (cache.length === 0) { this._trackedCache.clear(); this._userMapCache.clear(); - - for (const repo of await this.getRepositories()) { - repo.resetCaches(); - } } } @@ -726,7 +558,7 @@ export class GitService implements Disposable { ): Promise { const { branch: branchRef, ...opts } = options; if (GitReference.isBranch(branchRef)) { - const repo = await this.getRepository(repoPath); + const repo = await this.container.git.getRepository(repoPath); const branch = await repo?.getBranch(branchRef?.name); if (!branch?.remote && branch?.upstream == null) return undefined; @@ -741,144 +573,6 @@ export class GitService implements Disposable { return Git.fetch(repoPath, opts); } - @gate( - (repos, opts) => `${repos == null ? '' : repos.map(r => r.id).join(',')}|${JSON.stringify(opts)}`, - ) - @log({ - args: { - 0: (repos?: Repository[]) => (repos == null ? false : repos.map(r => r.name).join(', ')), - }, - }) - async fetchAll(repositories?: Repository[], options: { all?: boolean; prune?: boolean } = {}) { - if (repositories == null) { - repositories = await this.getOrderedRepositories(); - } - if (repositories.length === 0) return; - - if (repositories.length === 1) { - await repositories[0].fetch(options); - - return; - } - - await window.withProgress( - { - location: ProgressLocation.Notification, - title: `Fetching ${repositories.length} repositories`, - }, - () => Promise.all(repositories!.map(r => r.fetch({ progress: false, ...options }))), - ); - } - - @gate( - (repos, opts) => `${repos == null ? '' : repos.map(r => r.id).join(',')}|${JSON.stringify(opts)}`, - ) - @log({ - args: { - 0: (repos?: Repository[]) => (repos == null ? false : repos.map(r => r.name).join(', ')), - }, - }) - async pullAll(repositories?: Repository[], options: { rebase?: boolean } = {}) { - if (repositories == null) { - repositories = await this.getOrderedRepositories(); - } - if (repositories.length === 0) return; - - if (repositories.length === 1) { - await repositories[0].pull(options); - - return; - } - - await window.withProgress( - { - location: ProgressLocation.Notification, - title: `Pulling ${repositories.length} repositories`, - }, - () => Promise.all(repositories!.map(r => r.pull({ progress: false, ...options }))), - ); - } - - @gate(repos => `${repos == null ? '' : repos.map(r => r.id).join(',')}`) - @log({ - args: { - 0: (repos?: Repository[]) => (repos == null ? false : repos.map(r => r.name).join(', ')), - }, - }) - async pushAll( - repositories?: Repository[], - options: { - force?: boolean; - reference?: GitReference; - publish?: { - remote: string; - }; - } = {}, - ) { - if (repositories == null) { - repositories = await this.getOrderedRepositories(); - } - if (repositories.length === 0) return; - - if (repositories.length === 1) { - await repositories[0].push(options); - - return; - } - - await window.withProgress( - { - location: ProgressLocation.Notification, - title: `Pushing ${repositories.length} repositories`, - }, - () => Promise.all(repositories!.map(r => r.push({ progress: false, ...options }))), - ); - } - - @log({ - args: { - 0: (editor: TextEditor) => - editor != null ? `TextEditor(${Logger.toLoggable(editor.document.uri)})` : 'undefined', - }, - }) - async getActiveRepository(editor?: TextEditor): Promise { - const repoPath = await this.getActiveRepoPath(editor); - if (repoPath == null) return undefined; - - return this.getRepository(repoPath); - } - - @log({ - args: { - 0: (editor: TextEditor) => - editor != null ? `TextEditor(${Logger.toLoggable(editor.document.uri)})` : 'undefined', - }, - }) - async getActiveRepoPath(editor?: TextEditor): Promise { - editor = editor ?? window.activeTextEditor; - - let repoPath; - if (editor != null) { - const doc = await this.container.tracker.getOrAdd(editor.document.uri); - if (doc != null) { - repoPath = doc.uri.repoPath; - } - } - - if (repoPath != null) return repoPath; - - return this.getHighlanderRepoPath(); - } - - @log() - getHighlanderRepoPath(): string | undefined { - const entry = this._repositoryTree.highlander(); - if (entry == null) return undefined; - - const [, repo] = entry; - return repo.path; - } - @log() async getBlameForFile(uri: GitUri): Promise { const cc = Logger.getCorrelationContext(); @@ -1221,9 +915,7 @@ export class GitService implements Disposable { } @log() - async getBranch(repoPath: string | undefined): Promise { - if (repoPath == null) return undefined; - + async getBranch(repoPath: string): Promise { let [branch] = await this.getBranches(repoPath, { filter: b => b.current }); if (branch != null) return branch; @@ -1307,7 +999,7 @@ export class GitService implements Disposable { let branchesPromise = this.useCaching ? this._branchesCache.get(repoPath) : undefined; if (branchesPromise == null) { - async function load(this: GitService) { + async function load(this: LocalGitProvider) { try { const data = await Git.for_each_ref__branch(repoPath!, { all: true }); // If we don't get any data, assume the repo doesn't have any commits yet so check if we have a current branch @@ -1356,7 +1048,7 @@ export class GitService implements Disposable { if (this.useCaching) { this._branchesCache.set(repoPath, branchesPromise); - if (!(await this.getRepository(repoPath))?.supportsChangeEvents) { + if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { this._branchesCache.delete(repoPath); } } @@ -1561,7 +1253,7 @@ export class GitService implements Disposable { let contributors = this.useCaching ? this._contributorsCache.get(key) : undefined; if (contributors == null) { - async function load(this: GitService) { + async function load(this: LocalGitProvider) { try { const currentUser = await this.getCurrentUser(repoPath); @@ -1584,7 +1276,7 @@ export class GitService implements Disposable { if (this.useCaching) { this._contributorsCache.set(key, contributors); - if (!(await this.getRepository(repoPath))?.supportsChangeEvents) { + if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { this._contributorsCache.delete(key); } } @@ -1713,7 +1405,7 @@ export class GitService implements Disposable { ref1, ref2, originalFileName, - { encoding: GitService.getEncoding(uri) }, + { encoding: GitProviderService.getEncoding(uri) }, doc, key, cc, @@ -1819,7 +1511,7 @@ export class GitService implements Disposable { ref, contents, originalFileName, - { encoding: GitService.getEncoding(uri) }, + { encoding: GitProviderService.getEncoding(uri) }, doc, key, cc, @@ -2610,7 +2302,7 @@ export class GitService implements Disposable { if (this.useCaching) { this._mergeStatusCache.set(repoPath, status ?? null); - if (!(await this.getRepository(repoPath))?.supportsChangeEvents) { + if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { this._mergeStatusCache.delete(repoPath); } } @@ -2689,7 +2381,7 @@ export class GitService implements Disposable { if (this.useCaching) { this._rebaseStatusCache.set(repoPath, status ?? null); - if (!(await this.getRepository(repoPath))?.supportsChangeEvents) { + if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { this._rebaseStatusCache.delete(repoPath); } } @@ -3062,13 +2754,13 @@ export class GitService implements Disposable { provider: RichRemoteProvider, options?: { avatarSize?: number; include?: PullRequestState[]; limit?: number; timeout?: number }, ): Promise; - @gate((ref, remoteOrProvider, options) => { + @gate((ref, remoteOrProvider, options) => { const provider = GitRemote.is(remoteOrProvider) ? remoteOrProvider.provider : remoteOrProvider; return `${ref}${provider != null ? `|${provider.id}:${provider.domain}/${provider.path}` : ''}${ options != null ? `|${options.limit ?? -1}:${options.include?.join(',')}` : '' }`; }) - @debug({ + @debug({ args: { 1: (remoteOrProvider: GitRemote | RichRemoteProvider) => remoteOrProvider.name, }, @@ -3119,13 +2811,13 @@ export class GitService implements Disposable { provider: RichRemoteProvider, options?: { timeout?: number }, ): Promise; - @gate((ref, remoteOrProvider, options) => { + @gate((ref, remoteOrProvider, options) => { const provider = GitRemote.is(remoteOrProvider) ? remoteOrProvider.provider : remoteOrProvider; return `${ref}${provider != null ? `|${provider.id}:${provider.domain}/${provider.path}` : ''}|${ options?.timeout }`; }) - @debug({ + @debug({ args: { 1: (remoteOrProvider: GitRemote | RichRemoteProvider) => remoteOrProvider.name, }, @@ -3236,7 +2928,7 @@ export class GitService implements Disposable { remotes: GitRemote[], options?: { includeDisconnected?: boolean }, ): Promise | undefined>; - @gate( + @gate( (remotesOrRepoPath, options) => `${typeof remotesOrRepoPath === 'string' ? remotesOrRepoPath : remotesOrRepoPath[0]?.repoPath}:${ options?.includeDisconnected ?? false @@ -3329,7 +3021,7 @@ export class GitService implements Disposable { ): Promise[]> { if (repoPath == null) return []; - const repository = await this.getRepository(repoPath); + const repository = await this.container.git.getRepository(repoPath); const remotes = await (repository != null ? repository.getRemotes({ sort: options.sort }) : this.getRemotesCore(repoPath, undefined, { sort: options.sort })); @@ -3362,72 +3054,8 @@ export class GitService implements Disposable { } } - async getRepoPath(filePath: string, options?: { ref?: string }): Promise; - async getRepoPath(uri: Uri | undefined, options?: { ref?: string }): Promise; - @log({ - exit: path => `returned ${path}`, - }) - async getRepoPath( - filePathOrUri: string | Uri | undefined, - options: { ref?: string } = {}, - ): Promise { - if (filePathOrUri == null) return this.getHighlanderRepoPath(); - if (GitUri.is(filePathOrUri)) return filePathOrUri.repoPath; - - const cc = Logger.getCorrelationContext(); - - // Don't save the tracking info to the cache, because we could be looking in the wrong place (e.g. looking in the root when the file is in a submodule) - let repo = await this.getRepository(filePathOrUri, { ...options, skipCacheUpdate: true }); - if (repo != null) return repo.path; - - const rp = await this.getRepoPathCore( - typeof filePathOrUri === 'string' ? filePathOrUri : filePathOrUri.fsPath, - false, - ); - if (rp == null) return undefined; - - // Recheck this._repositoryTree.get(rp) to make sure we haven't already tried adding this due to awaits - if (this._repositoryTree.get(rp) != null) return rp; - - const isVslsScheme = - typeof filePathOrUri === 'string' ? undefined : filePathOrUri.scheme === DocumentSchemes.Vsls; - - // If this new repo is inside one of our known roots and we we don't already know about, add it - const root = this.findRepositoryForPath(this._repositoryTree, rp, isVslsScheme); - - let folder; - if (root != null) { - // Not sure why I added this for vsls (I can't see a reason for it anymore), but if it is added it will break submodules - // rp = root.path; - folder = root.folder; - } else { - folder = workspace.getWorkspaceFolder(GitUri.file(rp, isVslsScheme)); - if (folder == null) { - const parts = rp.split(slash); - folder = { - uri: GitUri.file(rp, isVslsScheme), - name: parts[parts.length - 1], - index: this._repositoryTree.count(), - }; - } - } - - Logger.log(cc, `Repository found in '${rp}'`); - repo = new Repository(folder, rp, false, this.onAnyRepositoryChanged.bind(this), !window.state.focused); - this._repositoryTree.set(rp, repo); - - // Send a notification that the repositories changed - setImmediate(async () => { - await this.updateContext(this._repositoryTree); - - this.fireRepositoriesChanged(); - }); - - return rp; - } - @debug() - private async getRepoPathCore(filePath: string, isDirectory: boolean): Promise { + async getRepoPath(filePath: string, isDirectory: boolean): Promise { const cc = Logger.getCorrelationContext(); let repoPath: string | undefined; @@ -3535,133 +3163,6 @@ export class GitService implements Disposable { } } - @log() - async getRepoPathOrActive(uri: Uri | undefined, editor: TextEditor | undefined) { - const repoPath = await this.getRepoPath(uri); - if (repoPath) return repoPath; - - return this.getActiveRepoPath(editor); - } - - @log() - async getRepositories(predicate?: (repo: Repository) => boolean): Promise> { - const repositoryTree = await this.getRepositoryTree(); - - const values = repositoryTree.values(); - return predicate != null ? Iterables.filter(values, predicate) : values; - } - - @log() - async getOrderedRepositories(): Promise { - const repositories = [...(await this.getRepositories())]; - if (repositories.length === 0) return repositories; - - return Repository.sort(repositories.filter(r => !r.closed)); - } - - private async getRepositoryTree(): Promise> { - if (this._repositoriesLoadingPromise != null) { - await this._repositoriesLoadingPromise; - this._repositoriesLoadingPromise = undefined; - } - - return this._repositoryTree; - } - - async getRepository( - repoPath: string, - options?: { ref?: string; skipCacheUpdate?: boolean }, - ): Promise; - async getRepository( - uri: Uri, - options?: { ref?: string; skipCacheUpdate?: boolean }, - ): Promise; - async getRepository( - repoPathOrUri: string | Uri, - options?: { ref?: string; skipCacheUpdate?: boolean }, - ): Promise; - @log({ - exit: repo => `returned ${repo != null ? `${repo.path}` : 'undefined'}`, - }) - async getRepository( - repoPathOrUri: string | Uri, - options: { ref?: string; skipCacheUpdate?: boolean } = {}, - ): Promise { - const repositoryTree = await this.getRepositoryTree(); - - let isVslsScheme; - - let path: string; - if (typeof repoPathOrUri === 'string') { - const repo = repositoryTree.get(repoPathOrUri); - if (repo != null) return repo; - - path = repoPathOrUri; - isVslsScheme = undefined; - } else { - if (GitUri.is(repoPathOrUri)) { - if (repoPathOrUri.repoPath) { - const repo = repositoryTree.get(repoPathOrUri.repoPath); - if (repo != null) return repo; - } - - path = repoPathOrUri.fsPath; - } else { - path = repoPathOrUri.fsPath; - } - - isVslsScheme = repoPathOrUri.scheme === DocumentSchemes.Vsls; - } - - const repo = this.findRepositoryForPath(repositoryTree, path, isVslsScheme); - if (repo == null) return undefined; - - // Make sure the file is tracked in this repo before returning -- it could be from a submodule - if (!(await this.isTracked(path, repo.path, options))) return undefined; - return repo; - } - - @debug({ - args: { - 0: (repositoryTree: TernarySearchTree) => `count=${repositoryTree.count()}`, - }, - }) - private findRepositoryForPath( - repositoryTree: TernarySearchTree, - path: string, - isVslsScheme: boolean | undefined, - ): Repository | undefined { - let repo = repositoryTree.findSubstr(path); - // If we can't find the repo and we are a guest, check if we are a "root" workspace - if (repo == null && isVslsScheme !== false && this.container.vsls.isMaybeGuest) { - if (!vslsUriPrefixRegex.test(path)) { - path = Strings.normalizePath(path); - const vslsPath = `/~0${path.startsWith(slash) ? path : `/${path}`}`; - repo = repositoryTree.findSubstr(vslsPath); - } - } - return repo; - } - - async getLocalInfoFromRemoteUri( - uri: Uri, - options?: { validate?: boolean }, - ): Promise<{ uri: Uri; startLine?: number; endLine?: number } | undefined> { - for (const repo of await this.getRepositories()) { - for (const remote of await repo.getRemotes()) { - const local = await remote?.provider?.getLocalInfoFromRemoteUri(repo, uri, options); - if (local != null) return local; - } - } - - return undefined; - } - - async getRepositoryCount(): Promise { - const repositoryTree = await this.getRepositoryTree(); - return repositoryTree.count(); - } - @gate() @log() async getStash(repoPath: string | undefined): Promise { @@ -3677,7 +3178,7 @@ export class GitService implements Disposable { if (this.useCaching) { this._stashesCache.set(repoPath, stash ?? null); - if (!(await this.getRepository(repoPath))?.supportsChangeEvents) { + if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { this._stashesCache.delete(repoPath); } } @@ -3753,7 +3254,7 @@ export class GitService implements Disposable { let tagsPromise = this.useCaching ? this._tagsCache.get(repoPath) : undefined; if (tagsPromise == null) { - async function load(this: GitService) { + async function load(this: LocalGitProvider) { try { const data = await Git.tag(repoPath!); return GitTagParser.parse(data, repoPath!) ?? []; @@ -3769,7 +3270,7 @@ export class GitService implements Disposable { if (this.useCaching) { this._tagsCache.set(repoPath, tagsPromise); - if (!(await this.getRepository(repoPath))?.supportsChangeEvents) { + if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) { this._tagsCache.delete(repoPath); } } @@ -3909,7 +3410,7 @@ export class GitService implements Disposable { async hasRemotes(repoPath: string | undefined): Promise { if (repoPath == null) return false; - const repository = await this.getRepository(repoPath); + const repository = await this.container.git.getRepository(repoPath); if (repository == null) return false; return repository.hasRemotes(); @@ -3919,7 +3420,7 @@ export class GitService implements Disposable { async hasTrackingBranch(repoPath: string | undefined): Promise { if (repoPath == null) return false; - const repository = await this.getRepository(repoPath); + const repository = await this.container.git.getRepository(repoPath); if (repository == null) return false; return repository.hasUpstreamBranch(); @@ -3960,7 +3461,7 @@ export class GitService implements Disposable { options?: { ref?: string; skipCacheUpdate?: boolean }, ): Promise; async isTracked(uri: GitUri): Promise; - @log({ + @log({ exit: tracked => `returned ${tracked}`, singleLine: true, }) @@ -4036,7 +3537,7 @@ export class GitService implements Disposable { } @log() - async getDiffTool(repoPath?: string) { + async getDiffTool(repoPath?: string): Promise { return ( (await Git.config__get('diff.guitool', repoPath, { local: true })) ?? Git.config__get('diff.tool', repoPath, { local: true }) @@ -4170,61 +3671,65 @@ export class GitService implements Disposable { } @log() - async validateReference(repoPath: string, ref: string) { + async validateReference(repoPath: string, ref: string): Promise { if (ref == null || ref.length === 0) return false; if (ref === GitRevision.deletedOrMissing || GitRevision.isUncommitted(ref)) return true; return (await Git.rev_parse__verify(repoPath, ref)) != null; } - stageFile(repoPath: string, fileName: string): Promise; - stageFile(repoPath: string, uri: Uri): Promise; + stageFile(repoPath: string, fileName: string): Promise; + stageFile(repoPath: string, uri: Uri): Promise; @log() - stageFile(repoPath: string, fileNameOrUri: string | Uri): Promise { - return Git.add( + async stageFile(repoPath: string, fileNameOrUri: string | Uri): Promise { + await Git.add( repoPath, typeof fileNameOrUri === 'string' ? fileNameOrUri : Paths.splitPath(fileNameOrUri.fsPath, repoPath)[0], ); } - stageDirectory(repoPath: string, directory: string): Promise; - stageDirectory(repoPath: string, uri: Uri): Promise; + stageDirectory(repoPath: string, directory: string): Promise; + stageDirectory(repoPath: string, uri: Uri): Promise; @log() - stageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise { - return Git.add( + async stageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise { + await Git.add( repoPath, typeof directoryOrUri === 'string' ? directoryOrUri : Paths.splitPath(directoryOrUri.fsPath, repoPath)[0], ); } - unStageFile(repoPath: string, fileName: string): Promise; - unStageFile(repoPath: string, uri: Uri): Promise; + unStageFile(repoPath: string, fileName: string): Promise; + unStageFile(repoPath: string, uri: Uri): Promise; @log() - unStageFile(repoPath: string, fileNameOrUri: string | Uri): Promise { - return Git.reset( + async unStageFile(repoPath: string, fileNameOrUri: string | Uri): Promise { + await Git.reset( repoPath, typeof fileNameOrUri === 'string' ? fileNameOrUri : Paths.splitPath(fileNameOrUri.fsPath, repoPath)[0], ); } - unStageDirectory(repoPath: string, directory: string): Promise; - unStageDirectory(repoPath: string, uri: Uri): Promise; + unStageDirectory(repoPath: string, directory: string): Promise; + unStageDirectory(repoPath: string, uri: Uri): Promise; @log() - unStageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise { - return Git.reset( + async unStageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise { + await Git.reset( repoPath, typeof directoryOrUri === 'string' ? directoryOrUri : Paths.splitPath(directoryOrUri.fsPath, repoPath)[0], ); } @log() - stashApply(repoPath: string, stashName: string, { deleteAfter }: { deleteAfter?: boolean } = {}) { - return Git.stash__apply(repoPath, stashName, Boolean(deleteAfter)); + async stashApply( + repoPath: string, + stashName: string, + { deleteAfter }: { deleteAfter?: boolean } = {}, + ): Promise { + await Git.stash__apply(repoPath, stashName, Boolean(deleteAfter)); } @log() - stashDelete(repoPath: string, stashName: string, ref?: string) { - return Git.stash__delete(repoPath, stashName, ref); + async stashDelete(repoPath: string, stashName: string, ref?: string): Promise { + await Git.stash__delete(repoPath, stashName, ref); } @log() @@ -4233,10 +3738,10 @@ export class GitService implements Disposable { message?: string, uris?: Uri[], options: { includeUntracked?: boolean; keepIndex?: boolean } = {}, - ) { + ): Promise { if (uris == null) return Git.stash__push(repoPath, message, options); - GitService.ensureGitVersion( + this.ensureGitVersion( '2.13.2', 'Stashing individual files', ' Please retry by stashing everything or install a more recent version of Git.', @@ -4245,10 +3750,10 @@ export class GitService implements Disposable { const pathspecs = uris.map(u => `./${Paths.splitPath(u.fsPath, repoPath)[0]}`); const stdinVersion = '2.30.0'; - const stdin = GitService.compareGitVersion(stdinVersion) !== -1; + const stdin = 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) { - GitService.ensureGitVersion( + 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.', @@ -4262,19 +3767,8 @@ export class GitService implements Disposable { }); } - static compareGitVersion(version: string) { - return Versions.compare(Versions.fromString(Git.getGitVersion()), Versions.fromString(version)); - } - static ensureGitVersion(version: string, prefix: string, suffix: string): void { - if (GitService.compareGitVersion(version) === -1) { - throw new Error( - `${prefix} requires a newer version of Git (>= ${version}) than is currently installed (${Git.getGitVersion()}).${suffix}`, - ); - } - } - @log() - static async getBuiltInGitApi(): Promise { + private async getScmGitApi(): Promise { try { const extension = extensions.getExtension('vscode.git'); if (extension != null) { @@ -4287,10 +3781,22 @@ export class GitService implements Disposable { } @log() - static async getOrOpenBuiltInGitRepository(repoPath: string): Promise { + async getOpenScmRepositories(): Promise { const cc = Logger.getCorrelationContext(); try { - const gitApi = await GitService.getBuiltInGitApi(); + const gitApi = await this.getScmGitApi(); + return gitApi?.repositories ?? []; + } catch (ex) { + Logger.error(ex, cc); + return []; + } + } + + @log() + async getOrOpenScmRepository(repoPath: string): Promise { + const cc = Logger.getCorrelationContext(); + try { + const gitApi = await this.getScmGitApi(); if (gitApi?.openRepository != null) { return (await gitApi?.openRepository?.(Uri.file(repoPath))) ?? undefined; } @@ -4303,10 +3809,10 @@ export class GitService implements Disposable { } @log() - static async openBuiltInGitRepository(repoPath: string): Promise { + private async openScmRepository(repoPath: string): Promise { const cc = Logger.getCorrelationContext(); try { - const gitApi = await GitService.getBuiltInGitApi(); + const gitApi = await this.getScmGitApi(); return (await gitApi?.openRepository?.(Uri.file(repoPath))) ?? undefined; } catch (ex) { Logger.error(ex, cc); @@ -4314,10 +3820,15 @@ export class GitService implements Disposable { } } - static getEncoding(repoPath: string, fileName: string): string; - 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)); + private compareGitVersion(version: string) { + return Versions.compare(Versions.fromString(Git.getGitVersion()), Versions.fromString(version)); + } + + private ensureGitVersion(version: string, prefix: string, suffix: string): void { + if (this.compareGitVersion(version) === -1) { + throw new Error( + `${prefix} requires a newer version of Git (>= ${version}) than is currently installed (${Git.getGitVersion()}).${suffix}`, + ); + } } } diff --git a/src/git/remotes/provider.ts b/src/git/remotes/provider.ts index b7c4d07..d099944 100644 --- a/src/git/remotes/provider.ts +++ b/src/git/remotes/provider.ts @@ -238,7 +238,20 @@ export abstract class RemoteProvider implements RemoteProviderReference { } } +const _connectedCache = new Set(); const _onDidChangeAuthentication = new EventEmitter<{ reason: 'connected' | 'disconnected'; key: string }>(); +function fireAuthenticationChanged(key: string, reason: 'connected' | 'disconnected') { + // Only fire events if the key is being connected for the first time (we could probably do the same for disconnected, but better safe on those imo) + if (_connectedCache.has(key)) { + if (reason === 'connected') return; + + _connectedCache.delete(key); + } else if (reason === 'connected') { + _connectedCache.add(key); + } + + _onDidChangeAuthentication.fire({ key: key, reason: reason }); +} export class Authentication { static get onDidChange(): Event<{ reason: 'connected' | 'disconnected'; key: string }> { @@ -350,7 +363,7 @@ export abstract class RichRemoteProvider extends RemoteProvider { this._onDidChange.fire(); if (!silent) { - _onDidChangeAuthentication.fire({ reason: 'disconnected', key: this.key }); + fireAuthenticationChanged(this.key, 'disconnected'); } } } @@ -603,8 +616,10 @@ export abstract class RichRemoteProvider extends RemoteProvider { if (session != null) { await Container.instance.context.workspaceState.update(this.connectedKey, true); - this._onDidChange.fire(); - _onDidChangeAuthentication.fire({ reason: 'connected', key: this.key }); + queueMicrotask(() => { + this._onDidChange.fire(); + fireAuthenticationChanged(this.key, 'connected'); + }); } return session ?? undefined; diff --git a/src/quickpicks/repositoryPicker.ts b/src/quickpicks/repositoryPicker.ts index a1319d3..65c6708 100644 --- a/src/quickpicks/repositoryPicker.ts +++ b/src/quickpicks/repositoryPicker.ts @@ -12,7 +12,7 @@ export namespace RepositoryPicker { repositories?: Repository[], ): Promise { const items: RepositoryQuickPickItem[] = await Promise.all([ - ...Iterables.map(repositories ?? (await Container.instance.git.getOrderedRepositories()), r => + ...Iterables.map(repositories ?? Container.instance.git.openRepositories, r => RepositoryQuickPickItem.create(r, undefined, { branch: true, status: true }), ), ]); diff --git a/src/system/function.ts b/src/system/function.ts index 9f4a42f..61d74da 100644 --- a/src/system/function.ts +++ b/src/system/function.ts @@ -152,7 +152,7 @@ export function propOf>(o: T, key: K) { } export function interval(fn: (...args: any[]) => void, ms: number): Disposable { - let timer: NodeJS.Timer | undefined; + let timer: any | undefined; const disposable = { dispose: () => { if (timer !== undefined) { @@ -161,15 +161,15 @@ export function interval(fn: (...args: any[]) => void, ms: number): Disposable { } }, }; - timer = global.setInterval(fn, ms); + timer = globalThis.setInterval(fn, ms); return disposable; } export function progress(promise: Promise, intervalMs: number, onProgress: () => boolean): Promise { return new Promise((resolve, reject) => { - let timer: NodeJS.Timer | undefined; - timer = global.setInterval(() => { + let timer: any | undefined; + timer = globalThis.setInterval(() => { if (onProgress()) { if (timer !== undefined) { clearInterval(timer); @@ -202,15 +202,3 @@ export function progress(promise: Promise, intervalMs: number, onProgress: export async function wait(ms: number) { await new Promise(resolve => setTimeout(resolve, ms)); } - -export async function waitUntil(fn: (...args: any[]) => boolean, timeout: number): Promise { - const max = Math.round(timeout / 100); - let counter = 0; - while (true) { - if (fn()) return true; - if (counter > max) return false; - - await wait(100); - counter++; - } -} diff --git a/src/system/iterable.ts b/src/system/iterable.ts index 21227c6..d0f5dfd 100644 --- a/src/system/iterable.ts +++ b/src/system/iterable.ts @@ -65,7 +65,14 @@ export function every(source: Iterable | IterableIterator, predicate: ( export function filter(source: Iterable | IterableIterator): Iterable; export function filter(source: Iterable | IterableIterator, predicate: (item: T) => boolean): Iterable; -export function* filter(source: Iterable | IterableIterator, predicate?: (item: T) => boolean): Iterable { +export function filter( + source: Iterable | IterableIterator, + predicate: (item: T) => item is U, +): Iterable; +export function* filter( + source: Iterable | IterableIterator, + predicate?: ((item: T) => item is U) | ((item: T) => boolean), +): Iterable { if (predicate === undefined) { for (const item of source) { if (item != null) yield item; diff --git a/src/system/path.ts b/src/system/path.ts index 2f7c4d3..96955f0 100644 --- a/src/system/path.ts +++ b/src/system/path.ts @@ -1,6 +1,7 @@ 'use strict'; import * as paths from 'path'; import { Uri } from 'vscode'; +import { Strings } from '../system'; import { normalizePath } from './string'; const slash = '/'; @@ -36,10 +37,20 @@ export function isDescendent(path: string, basePath: string): boolean; export function isDescendent(uriOrPath: Uri | string, baseUriOrPath: Uri | string): boolean; export function isDescendent(uriOrPath: Uri | string, baseUriOrPath: Uri | string): boolean { if (typeof baseUriOrPath === 'string') { + baseUriOrPath = Strings.normalizePath(baseUriOrPath); if (!baseUriOrPath.startsWith('/')) { baseUriOrPath = `/${baseUriOrPath}`; } + } + + if (typeof uriOrPath === 'string') { + uriOrPath = Strings.normalizePath(uriOrPath); + if (!uriOrPath.startsWith('/')) { + uriOrPath = `/${uriOrPath}`; + } + } + if (typeof baseUriOrPath === 'string') { return ( baseUriOrPath.length === 1 || (typeof uriOrPath === 'string' ? uriOrPath : uriOrPath.path).startsWith( diff --git a/src/system/promise.ts b/src/system/promise.ts index 7a4d8b9..664b863 100644 --- a/src/system/promise.ts +++ b/src/system/promise.ts @@ -26,9 +26,9 @@ export function cancellable( return new Promise((resolve, reject) => { let fulfilled = false; - let timer: NodeJS.Timer | undefined; + let timer: any | undefined; if (typeof timeoutOrToken === 'number') { - timer = global.setTimeout(() => { + timer = globalThis.setTimeout(() => { if (typeof options.onDidCancel === 'function') { options.onDidCancel(resolve, reject); } else { diff --git a/src/terminal/linkProvider.ts b/src/terminal/linkProvider.ts index 7c983ad..53f84f5 100644 --- a/src/terminal/linkProvider.ts +++ b/src/terminal/linkProvider.ts @@ -37,7 +37,7 @@ export class GitTerminalLinkProvider implements Disposable, TerminalLinkProvider async provideTerminalLinks(context: TerminalLinkContext): Promise { if (context.line.trim().length === 0) return []; - const repoPath = this.container.git.getHighlanderRepoPath(); + const repoPath = this.container.git.highlanderRepoPath; if (repoPath == null) return []; const links: GitTerminalLink[] = []; diff --git a/src/trackers/documentTracker.ts b/src/trackers/documentTracker.ts index 735e0b0..6fa7100 100644 --- a/src/trackers/documentTracker.ts +++ b/src/trackers/documentTracker.ts @@ -19,8 +19,10 @@ import { import { configuration } from '../configuration'; import { ContextKeys, DocumentSchemes, isActiveDocument, isTextEditor, setContext } from '../constants'; import { Container } from '../container'; +import { RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent } from '../git/git'; +import { RepositoriesChangedEvent } from '../git/gitProviderService'; import { GitUri } from '../git/gitUri'; -import { Functions } from '../system'; +import { Functions, Iterables } from '../system'; import { DocumentBlameStateChangeEvent, TrackedDocument } from './trackedDocument'; export * from './trackedDocument'; @@ -63,7 +65,7 @@ export class DocumentTracker implements Disposable { return this._onDidTriggerDirtyIdle.event; } - private _dirtyIdleTriggerDelay!: number; + private _dirtyIdleTriggerDelay: number; private readonly _disposable: Disposable; private readonly _documentMap = new Map>>(); @@ -76,7 +78,11 @@ export class DocumentTracker implements Disposable { workspace.onDidChangeTextDocument(Functions.debounce(this.onTextDocumentChanged, 50), this), workspace.onDidCloseTextDocument(this.onTextDocumentClosed, this), workspace.onDidSaveTextDocument(this.onTextDocumentSaved, this), + this.container.git.onDidChangeRepositories(Functions.debounce(this.onRepositoriesChanged, 250), this), + this.container.git.onDidChangeRepository(Functions.debounce(this.onRepositoryChanged, 250), this), ); + + this._dirtyIdleTriggerDelay = configuration.get('advanced.blame.delayAfterEdit'); } dispose() { @@ -86,29 +92,12 @@ export class DocumentTracker implements Disposable { } private onReady(): void { - void this.onConfigurationChanged(); - void this.onActiveTextEditorChanged(window.activeTextEditor); + this.onConfigurationChanged(); + this.onActiveTextEditorChanged(window.activeTextEditor); } - private async onConfigurationChanged(e?: ConfigurationChangeEvent) { - // Only rest the cached state if we aren't initializing - if ( - e != null && - (configuration.changed(e, 'blame.ignoreWhitespace') || configuration.changed(e, 'advanced.caching.enabled')) - ) { - for (const d of this._documentMap.values()) { - (await d).reset('config'); - } - } - - if (configuration.changed(e, 'advanced.blame.delayAfterEdit')) { - this._dirtyIdleTriggerDelay = configuration.get('advanced.blame.delayAfterEdit'); - this._dirtyIdleTriggeredDebounced = undefined; - } - } - - private _timer: NodeJS.Timer | undefined; - private async onActiveTextEditorChanged(editor: TextEditor | undefined) { + private _timer: any | undefined; + private onActiveTextEditorChanged(editor: TextEditor | undefined) { if (editor != null && !isTextEditor(editor)) return; if (this._timer != null) { @@ -128,7 +117,10 @@ export class DocumentTracker implements Disposable { const doc = this._documentMap.get(editor.document); if (doc != null) { - void (await doc).activate(); + void doc.then( + d => d.activate(), + () => {}, + ); return; } @@ -137,6 +129,41 @@ export class DocumentTracker implements Disposable { void this.addCore(editor.document); } + private onConfigurationChanged(e?: ConfigurationChangeEvent) { + // Only rest the cached state if we aren't initializing + if ( + e != null && + (configuration.changed(e, 'blame.ignoreWhitespace') || configuration.changed(e, 'advanced.caching.enabled')) + ) { + this.reset('config'); + } + + if (configuration.changed(e, 'advanced.blame.delayAfterEdit')) { + this._dirtyIdleTriggerDelay = configuration.get('advanced.blame.delayAfterEdit'); + this._dirtyIdleTriggeredDebounced = undefined; + } + } + + private onRepositoriesChanged(_e: RepositoriesChangedEvent) { + this.reset('repository'); + } + + private onRepositoryChanged(e: RepositoryChangeEvent) { + if ( + e.changed( + RepositoryChange.Index, + RepositoryChange.Heads, + RepositoryChange.Status, + RepositoryChange.Unknown, + RepositoryChangeComparisonMode.Any, + ) + ) { + void Promise.allSettled( + Iterables.map(this._documentMap.values(), async d => (await d).reset('repository')), + ); + } + } + private async onTextDocumentChanged(e: TextDocumentChangeEvent) { const { scheme } = e.document.uri; if (scheme !== DocumentSchemes.File && scheme !== DocumentSchemes.Git && scheme !== DocumentSchemes.Vsls) { @@ -321,7 +348,7 @@ export class DocumentTracker implements Disposable { | undefined; private fireDocumentDirtyStateChanged(e: DocumentDirtyStateChangeEvent) { if (e.dirty) { - setImmediate(() => { + queueMicrotask(() => { this._dirtyStateChangedDebounced?.cancel(); if (window.activeTextEditor !== e.editor) return; @@ -358,6 +385,15 @@ export class DocumentTracker implements Disposable { this._dirtyStateChangedDebounced(e); } + + private reset(reason: 'config' | 'dispose' | 'document' | 'repository') { + void Promise.allSettled( + Iterables.map( + Iterables.filter(this._documentMap, ([key]) => typeof key === 'string'), + async ([, d]) => (await d).reset(reason), + ), + ); + } } class EmptyTextDocument implements TextDocument { diff --git a/src/trackers/lineTracker.ts b/src/trackers/lineTracker.ts index 8f2fe50..90cd121 100644 --- a/src/trackers/lineTracker.ts +++ b/src/trackers/lineTracker.ts @@ -135,7 +135,7 @@ export class LineTracker implements Disposable { this.onStart?.() ?? { dispose: () => {} }, ); - setImmediate(() => this.onActiveTextEditorChanged(window.activeTextEditor)); + queueMicrotask(() => this.onActiveTextEditorChanged(window.activeTextEditor)); } return disposable; @@ -202,7 +202,7 @@ export class LineTracker implements Disposable { private onLinesChanged(e: LinesChangeEvent) { if (e.selections === undefined) { - setImmediate(() => { + queueMicrotask(() => { if (window.activeTextEditor !== e.editor) return; if (this._linesChangedDebounced !== undefined) { diff --git a/src/trackers/trackedDocument.ts b/src/trackers/trackedDocument.ts index 3529396..fccdde0 100644 --- a/src/trackers/trackedDocument.ts +++ b/src/trackers/trackedDocument.ts @@ -2,13 +2,7 @@ import { Disposable, Event, EventEmitter, TextDocument, TextEditor } from 'vscode'; import { ContextKeys, getEditorIfActive, isActiveDocument, setContext } from '../constants'; import { Container } from '../container'; -import { - GitRevision, - Repository, - RepositoryChange, - RepositoryChangeComparisonMode, - RepositoryChangeEvent, -} from '../git/git'; +import { GitRevision } from '../git/git'; import { GitUri } from '../git/gitUri'; import { Logger } from '../logger'; @@ -40,7 +34,6 @@ export class TrackedDocument implements Disposable { private _disposable: Disposable | undefined; private _disposed: boolean = false; - private _repo: Repository | undefined; private _uri!: GitUri; private constructor( @@ -58,41 +51,15 @@ export class TrackedDocument implements Disposable { } private initializing = true; - private async initialize(): Promise { - this._uri = await GitUri.fromUri(this._document.uri); - if (this._disposed) return undefined; + private async initialize(): Promise { + const uri = this._document.uri; - const repo = await this.container.git.getRepository(this._uri); - this._repo = repo; - if (this._disposed) return undefined; - - if (repo != null) { - this._disposable = repo.onDidChange(this.onRepositoryChanged, this); + this._uri = await GitUri.fromUri(uri); + if (!this._disposed) { + await this.update(); } - await this.update(); - this.initializing = false; - - return repo; - } - - private onRepositoryChanged(e: RepositoryChangeEvent) { - if ( - !e.changed( - RepositoryChange.Index, - RepositoryChange.Heads, - RepositoryChange.Status, - RepositoryChange.Unknown, - RepositoryChangeComparisonMode.Any, - ) - ) { - return; - } - - // Reset any cached state - this.reset('repository'); - void this.update(); } private _forceDirtyStateChangeOnNextDocumentChange: boolean = false; @@ -106,9 +73,7 @@ export class TrackedDocument implements Disposable { } get isBlameable() { - if (this._blameFailed) return false; - - return this._isTracked; + return this._blameFailed ? false : this._isTracked; } private _isDirtyIdle: boolean = false; @@ -136,7 +101,10 @@ export class TrackedDocument implements Disposable { return this._uri; } - activate() { + async activate(): Promise { + if (this._requiresUpdate) { + await this.update(); + } void setContext(ContextKeys.ActiveFileStatus, this.getStatus()); } @@ -145,18 +113,23 @@ export class TrackedDocument implements Disposable { } reset(reason: 'config' | 'dispose' | 'document' | 'repository') { + this._requiresUpdate = true; this._blameFailed = false; this._isDirtyIdle = false; - if (this.state == null) return; + if (this.state != null) { + // // Don't remove broken blame on change (since otherwise we'll have to run the broken blame again) + // if (!this.state.hasErrors) { - // // Don't remove broken blame on change (since otherwise we'll have to run the broken blame again) - // if (!this.state.hasErrors) { + this.state = undefined; + Logger.log(`Reset state for '${this.key}', reason=${reason}`); - this.state = undefined; - Logger.log(`Reset state for '${this.key}', reason=${reason}`); + // } + } - // } + if (reason === 'repository' && isActiveDocument(this._document)) { + void this.update(); + } } private _blameFailed: boolean = false; @@ -178,7 +151,10 @@ export class TrackedDocument implements Disposable { this._forceDirtyStateChangeOnNextDocumentChange = true; } + private _requiresUpdate: boolean = true; async update({ forceBlameChange }: { forceBlameChange?: boolean } = {}) { + this._requiresUpdate = false; + if (this._disposed || this._uri == null) { this._hasRemotes = false; this._isTracked = false; @@ -188,20 +164,17 @@ export class TrackedDocument implements Disposable { this._isDirtyIdle = false; + // Caches these before the awaits const active = getEditorIfActive(this._document); const wasBlameable = forceBlameChange ? undefined : this.isBlameable; - this._isTracked = await this.container.git.isTracked(this._uri); - - let repo = undefined; - if (this._isTracked) { - repo = this._repo; - } - - if (repo != null) { - this._hasRemotes = await repo.hasRemotes(); - } else { + const repo = await this.container.git.getRepository(this._uri); + if (repo == null) { + this._isTracked = false; this._hasRemotes = false; + } else { + this._isTracked = true; + this._hasRemotes = await repo.hasRemotes(); } if (active != null) { diff --git a/src/views/branchesView.ts b/src/views/branchesView.ts index 8385683..ebf5c3e 100644 --- a/src/views/branchesView.ts +++ b/src/views/branchesView.ts @@ -28,14 +28,14 @@ import { RepositoryChangeEvent, } from '../git/git'; import { GitUri } from '../git/gitUri'; -import { debug, gate, Strings } from '../system'; +import { gate, Strings } from '../system'; import { BranchesNode, BranchNode, BranchOrTagFolderNode, + RepositoriesSubscribeableNode, RepositoryFolderNode, RepositoryNode, - unknownGitUri, ViewNode, } from './nodes'; import { ViewBase } from './viewBase'; @@ -63,17 +63,10 @@ export class BranchesRepositoryNode extends RepositoryFolderNode { - protected override splatted = true; - private children: BranchesRepositoryNode[] | undefined; - - constructor(view: BranchesView) { - super(unknownGitUri, view); - } - +export class BranchesViewNode extends RepositoriesSubscribeableNode { async getChildren(): Promise { if (this.children == null) { - const repositories = await Container.instance.git.getOrderedRepositories(); + const repositories = this.view.container.git.openRepositories; if (repositories.length === 0) { this.view.message = 'No branches could be found.'; @@ -118,25 +111,6 @@ export class BranchesViewNode extends ViewNode { const item = new TreeItem('Branches', TreeItemCollapsibleState.Expanded); return item; } - - override async getSplattedChild() { - if (this.children == null) { - await this.getChildren(); - } - - return this.children?.length === 1 ? this.children[0] : undefined; - } - - @gate() - @debug() - override refresh(reset: boolean = false) { - if (reset && this.children != null) { - for (const child of this.children) { - child.dispose(); - } - this.children = undefined; - } - } } export class BranchesView extends ViewBase { @@ -161,8 +135,8 @@ export class BranchesView extends ViewBase ), commands.registerCommand( this.getQualifiedCommand('refresh'), - async () => { - await this.container.git.resetCaches('branches'); + () => { + this.container.git.resetCaches('branches'); return this.refresh(true); }, this, diff --git a/src/views/commitsView.ts b/src/views/commitsView.ts index 049bb27..b0101c0 100644 --- a/src/views/commitsView.ts +++ b/src/views/commitsView.ts @@ -26,9 +26,9 @@ import { debug, Functions, gate, Strings } from '../system'; import { BranchNode, BranchTrackingStatusNode, + RepositoriesSubscribeableNode, RepositoryFolderNode, RepositoryNode, - unknownGitUri, ViewNode, } from './nodes'; import { ViewBase } from './viewBase'; @@ -47,7 +47,7 @@ export class CommitsRepositoryNode extends RepositoryFolderNode$`]; } @@ -118,17 +118,10 @@ export class CommitsRepositoryNode extends RepositoryFolderNode { - protected override splatted = true; - private children: CommitsRepositoryNode[] | undefined; - - constructor(view: CommitsView) { - super(unknownGitUri, view); - } - +export class CommitsViewNode extends RepositoriesSubscribeableNode { async getChildren(): Promise { if (this.children == null) { - const repositories = await Container.instance.git.getOrderedRepositories(); + const repositories = this.view.container.git.openRepositories; if (repositories.length === 0) { this.view.message = 'No commits could be found.'; @@ -177,25 +170,6 @@ export class CommitsViewNode extends ViewNode { const item = new TreeItem('Commits', TreeItemCollapsibleState.Expanded); return item; } - - override async getSplattedChild() { - if (this.children == null) { - await this.getChildren(); - } - - return this.children?.length === 1 ? this.children[0] : undefined; - } - - @gate() - @debug() - override refresh(reset: boolean = false) { - if (reset && this.children != null) { - for (const child of this.children) { - child.dispose(); - } - this.children = undefined; - } - } } interface CommitsViewState { @@ -229,8 +203,8 @@ export class CommitsView extends ViewBase { ), commands.registerCommand( this.getQualifiedCommand('refresh'), - async () => { - await this.container.git.resetCaches('branches', 'status', 'tags'); + () => { + this.container.git.resetCaches('branches', 'status', 'tags'); return this.refresh(true); }, this, diff --git a/src/views/contributorsView.ts b/src/views/contributorsView.ts index 637bc60..a3eea70 100644 --- a/src/views/contributorsView.ts +++ b/src/views/contributorsView.ts @@ -6,8 +6,8 @@ import { GlyphChars } from '../constants'; import { Container } from '../container'; import { RepositoryChange, RepositoryChangeComparisonMode, RepositoryChangeEvent } from '../git/git'; import { GitUri } from '../git/gitUri'; -import { debug, gate, Strings } from '../system'; -import { ContributorsNode, RepositoryFolderNode, unknownGitUri, ViewNode } from './nodes'; +import { debug, Strings } from '../system'; +import { ContributorsNode, RepositoriesSubscribeableNode, RepositoryFolderNode, ViewNode } from './nodes'; import { ViewBase } from './viewBase'; export class ContributorsRepositoryNode extends RepositoryFolderNode { @@ -38,17 +38,10 @@ export class ContributorsRepositoryNode extends RepositoryFolderNode { - protected override splatted = true; - private children: ContributorsRepositoryNode[] | undefined; - - constructor(view: ContributorsView) { - super(unknownGitUri, view); - } - +export class ContributorsViewNode extends RepositoriesSubscribeableNode { async getChildren(): Promise { if (this.children == null) { - const repositories = await Container.instance.git.getOrderedRepositories(); + const repositories = this.view.container.git.openRepositories; if (repositories.length === 0) { this.view.message = 'No contributors could be found.'; @@ -72,13 +65,13 @@ export class ContributorsViewNode extends ViewNode { const children = await child.getChildren(); - // const all = Container.instance.config.views.contributors.showAllBranches; + // const all = this.view.container.config.views.contributors.showAllBranches; // let ref: string | undefined; // // If we aren't getting all branches, get the upstream of the current branch if there is one // if (!all) { // try { - // const branch = await Container.instance.git.getBranch(this.uri.repoPath); + // const branch = await this.view.container.git.getBranch(this.uri.repoPath); // if (branch?.upstream?.name != null && !branch.upstream.missing) { // ref = '@{u}'; // } @@ -108,25 +101,6 @@ export class ContributorsViewNode extends ViewNode { const item = new TreeItem('Contributors', TreeItemCollapsibleState.Expanded); return item; } - - override async getSplattedChild() { - if (this.children == null) { - await this.getChildren(); - } - - return this.children?.length === 1 ? this.children[0] : undefined; - } - - @gate() - @debug() - override refresh(reset: boolean = false) { - if (reset && this.children != null) { - for (const child of this.children) { - child.dispose(); - } - this.children = undefined; - } - } } export class ContributorsView extends ViewBase { @@ -151,8 +125,8 @@ export class ContributorsView extends ViewBase { - await this.container.git.resetCaches('contributors'); + () => { + this.container.git.resetCaches('contributors'); return this.refresh(true); }, this, diff --git a/src/views/nodes/branchNode.ts b/src/views/nodes/branchNode.ts index 45a8c98..cf3a522 100644 --- a/src/views/nodes/branchNode.ts +++ b/src/views/nodes/branchNode.ts @@ -2,7 +2,6 @@ import { MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri, window } from 'vscode'; import { ViewBranchesLayout, ViewShowBranchComparison } from '../../configuration'; import { Colors, GlyphChars } from '../../constants'; -import { Container } from '../../container'; import { BranchDateFormatting, GitBranch, @@ -131,18 +130,18 @@ export class BranchNode if (this._children == null) { const children = []; - const range = await Container.instance.git.getBranchAheadRange(this.branch); + const range = await this.view.container.git.getBranchAheadRange(this.branch); const [log, getBranchAndTagTips, status, mergeStatus, rebaseStatus, pr, unpublishedCommits] = await Promise.all([ this.getLog(), - Container.instance.git.getBranchesAndTagsTipsFn(this.uri.repoPath, this.branch.name), + this.view.container.git.getBranchesAndTagsTipsFn(this.uri.repoPath, this.branch.name), this.options.showStatus && this.branch.current - ? Container.instance.git.getStatusForRepo(this.uri.repoPath) + ? this.view.container.git.getStatusForRepo(this.uri.repoPath) : undefined, this.options.showStatus && this.branch.current - ? Container.instance.git.getMergeStatus(this.uri.repoPath!) + ? this.view.container.git.getMergeStatus(this.uri.repoPath!) : undefined, - this.options.showStatus ? Container.instance.git.getRebaseStatus(this.uri.repoPath!) : undefined, + this.options.showStatus ? this.view.container.git.getRebaseStatus(this.uri.repoPath!) : undefined, this.view.config.pullRequests.enabled && this.view.config.pullRequests.showForBranches && (this.branch.upstream != null || this.branch.remote) @@ -151,7 +150,7 @@ export class BranchNode ) : undefined, range && !this.branch.remote - ? Container.instance.git.getLogRefsOnly(this.uri.repoPath!, { + ? this.view.container.git.getLogRefsOnly(this.uri.repoPath!, { limit: 0, ref: range, }) @@ -183,7 +182,7 @@ export class BranchNode this, this.branch, mergeStatus, - status ?? (await Container.instance.git.getStatusForRepo(this.uri.repoPath)), + status ?? (await this.view.container.git.getStatusForRepo(this.uri.repoPath)), this.root, ), ); @@ -198,7 +197,7 @@ export class BranchNode this, this.branch, rebaseStatus, - status ?? (await Container.instance.git.getStatusForRepo(this.uri.repoPath)), + status ?? (await this.view.container.git.getStatusForRepo(this.uri.repoPath)), this.root, ), ); @@ -260,7 +259,7 @@ export class BranchNode if (log.hasMore) { children.push( new LoadMoreNode(this.view, this, children[children.length - 1], undefined, () => - Container.instance.git.getCommitCount(this.branch.repoPath, this.branch.name), + this.view.container.git.getCommitCount(this.branch.repoPath, this.branch.name), ), ); } @@ -370,7 +369,7 @@ export class BranchNode } } else { const providers = GitRemote.getHighlanderProviders( - await Container.instance.git.getRemotes(this.branch.repoPath), + await this.view.container.git.getRemotes(this.branch.repoPath), ); const providerName = providers?.length ? providers[0].name : undefined; @@ -406,8 +405,8 @@ export class BranchNode item.iconPath = this.options.showAsCommits ? new ThemeIcon('git-commit', color) : { - dark: Container.instance.context.asAbsolutePath(`images/dark/icon-branch${iconSuffix}.svg`), - light: Container.instance.context.asAbsolutePath(`images/light/icon-branch${iconSuffix}.svg`), + dark: this.view.container.context.asAbsolutePath(`images/dark/icon-branch${iconSuffix}.svg`), + light: this.view.container.context.asAbsolutePath(`images/light/icon-branch${iconSuffix}.svg`), }; item.tooltip = tooltip; item.resourceUri = Uri.parse( @@ -453,7 +452,7 @@ export class BranchNode limit = Math.min(this.branch.state.ahead + 1, limit * 2); } - this._log = await Container.instance.git.getLog(this.uri.repoPath!, { + this._log = await this.view.container.git.getLog(this.uri.repoPath!, { limit: limit, ref: this.ref.ref, authors: this.options?.authors, diff --git a/src/views/nodes/branchTrackingStatusFilesNode.ts b/src/views/nodes/branchTrackingStatusFilesNode.ts index 97eb79c..f105c65 100644 --- a/src/views/nodes/branchTrackingStatusFilesNode.ts +++ b/src/views/nodes/branchTrackingStatusFilesNode.ts @@ -2,7 +2,6 @@ import * as paths from 'path'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ViewFilesLayout } from '../../configuration'; -import { Container } from '../../container'; import { GitBranch, GitFileWithCommit, GitRevision } from '../../git/git'; import { GitUri } from '../../git/gitUri'; import { Arrays, Iterables, Strings } from '../../system'; @@ -45,7 +44,7 @@ export class BranchTrackingStatusFilesNode extends ViewNode { } async getChildren(): Promise { - const log = await Container.instance.git.getLog(this.repoPath, { + const log = await this.view.container.git.getLog(this.repoPath, { limit: 0, ref: GitRevision.createRange( this.status.upstream, @@ -103,7 +102,7 @@ export class BranchTrackingStatusFilesNode extends ViewNode { } async getTreeItem(): Promise { - const stats = await Container.instance.git.getChangedFilesCount( + const stats = await this.view.container.git.getChangedFilesCount( this.repoPath, `${this.status.upstream}${this.direction === 'behind' ? '..' : '...'}`, ); diff --git a/src/views/nodes/branchTrackingStatusNode.ts b/src/views/nodes/branchTrackingStatusNode.ts index 95eb190..125f574 100644 --- a/src/views/nodes/branchTrackingStatusNode.ts +++ b/src/views/nodes/branchTrackingStatusNode.ts @@ -1,7 +1,6 @@ 'use strict'; import { MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; import { Colors } from '../../constants'; -import { Container } from '../../container'; import { GitBranch, GitLog, GitRemote, GitRevision, GitTrackingState } from '../../git/git'; import { GitUri } from '../../git/gitUri'; import { Dates, debug, gate, Iterables, Strings } from '../../system'; @@ -79,7 +78,7 @@ export class BranchTrackingStatusNode extends ViewNode impleme commits = [...log.commits.values()]; const commit = commits[commits.length - 1]; if (commit.previousSha == null) { - const previousLog = await Container.instance.git.getLog(this.uri.repoPath!, { + const previousLog = await this.view.container.git.getLog(this.uri.repoPath!, { limit: 2, ref: commit.sha, }); @@ -151,7 +150,7 @@ export class BranchTrackingStatusNode extends ViewNode impleme let lastFetched = 0; if (this.upstreamType !== 'none') { - const repo = await Container.instance.git.getRepository(this.repoPath); + const repo = await this.view.container.git.getRepository(this.repoPath); lastFetched = (await repo?.getLastFetched()) ?? 0; } @@ -228,7 +227,7 @@ export class BranchTrackingStatusNode extends ViewNode impleme break; } case 'none': { - const remotes = await Container.instance.git.getRemotes(this.branch.repoPath); + const remotes = await this.view.container.git.getRemotes(this.branch.repoPath); const providers = GitRemote.getHighlanderProviders(remotes); const providerName = providers?.length ? providers[0].name : undefined; @@ -284,7 +283,7 @@ export class BranchTrackingStatusNode extends ViewNode impleme ? GitRevision.createRange(this.status.upstream, this.status.ref) : GitRevision.createRange(this.status.ref, this.status.upstream); - this._log = await Container.instance.git.getLog(this.uri.repoPath!, { + this._log = await this.view.container.git.getLog(this.uri.repoPath!, { limit: this.limit ?? this.view.config.defaultItemLimit, ref: range, }); diff --git a/src/views/nodes/commitFileNode.ts b/src/views/nodes/commitFileNode.ts index ea93ade..37738d9 100644 --- a/src/views/nodes/commitFileNode.ts +++ b/src/views/nodes/commitFileNode.ts @@ -2,7 +2,6 @@ import * as paths from 'path'; import { Command, Selection, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { Commands, DiffWithPreviousCommandArgs } from '../../commands'; -import { Container } from '../../container'; import { GitBranch, GitFile, GitLogCommit, GitRevisionReference, StatusFileFormatter } from '../../git/git'; import { GitUri } from '../../git/gitUri'; import { FileHistoryView } from '../fileHistoryView'; @@ -49,7 +48,7 @@ export class CommitFileNode { const label = CommitFormatter.fromTemplate(this.view.config.formats.commits.label, this.commit, { - dateFormat: Container.instance.config.defaultDateFormat, + dateFormat: this.view.container.config.defaultDateFormat, getBranchAndTagTips: (sha: string) => this.getBranchAndTagTips?.(sha, { compact: true }), messageTruncateAtNewLine: true, }); @@ -98,14 +97,14 @@ export class CommitNode extends ViewRefNode this.getBranchAndTagTips?.(sha, { compact: true }), messageTruncateAtNewLine: true, }); item.iconPath = this.unpublished ? new ThemeIcon('arrow-up', new ThemeColor(Colors.UnpublishedCommitIconColor)) : this.view.config.avatars - ? await this.commit.getAvatarUri({ defaultStyle: Container.instance.config.defaultGravatarsStyle }) + ? await this.commit.getAvatarUri({ defaultStyle: this.view.container.config.defaultGravatarsStyle }) : new ThemeIcon('git-commit'); // item.tooltip = this.tooltip; @@ -137,16 +136,16 @@ export class CommitNode extends ViewRefNode { - let files = await Container.instance.git.getDiffStatus( + let files = await this.view.container.git.getDiffStatus( this.repoPath, GitRevision.createRange(this._compareWith?.ref || 'HEAD', this.branch.ref || 'HEAD', '...'), ); if (this.compareWithWorkingTree) { - const workingFiles = await Container.instance.git.getDiffStatus(this.repoPath, 'HEAD'); + const workingFiles = await this.view.container.git.getDiffStatus(this.repoPath, 'HEAD'); if (workingFiles != null) { if (files != null) { for (const wf of workingFiles) { @@ -278,7 +277,7 @@ export class CompareBranchNode extends ViewNode { - const files = await Container.instance.git.getDiffStatus( + const files = await this.view.container.git.getDiffStatus( this.uri.repoPath!, GitRevision.createRange(this.branch.ref, this._compareWith?.ref || 'HEAD', '...'), ); @@ -292,7 +291,7 @@ export class CompareBranchNode extends ViewNode Promise { const repoPath = this.uri.repoPath!; return async (limit: number | undefined) => { - const log = await Container.instance.git.getLog(repoPath, { + const log = await this.view.container.git.getLog(repoPath, { limit: limit, ref: range, }); @@ -322,7 +321,7 @@ export class CompareBranchNode extends ViewNode( + const comparisons = this.view.container.context.workspaceState.get( WorkspaceState.BranchComparisons, ); @@ -351,7 +350,7 @@ export class CompareBranchNode extends ViewNode( + let comparisons = this.view.container.context.workspaceState.get( WorkspaceState.BranchComparisons, ); if (comparisons == null) { @@ -366,6 +365,6 @@ export class CompareBranchNode extends ViewNode { let description; if (repoPath !== undefined) { - if ((await Container.instance.git.getRepositoryCount()) > 1) { - const repo = await Container.instance.git.getRepository(repoPath); + if (this.view.container.git.repositoryCount > 1) { + const repo = await this.view.container.git.getRepository(repoPath); description = repo?.formattedName ?? repoPath; } } diff --git a/src/views/nodes/compareResultsNode.ts b/src/views/nodes/compareResultsNode.ts index b8603e7..b479102 100644 --- a/src/views/nodes/compareResultsNode.ts +++ b/src/views/nodes/compareResultsNode.ts @@ -1,7 +1,6 @@ 'use strict'; import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { NamedRef } from '../../constants'; -import { Container } from '../../container'; import { GitRevision } from '../../git/git'; import { GitUri } from '../../git/gitUri'; import { debug, gate, log, Strings } from '../../system'; @@ -74,13 +73,13 @@ export class CompareResultsNode extends ViewNode { const ahead = this.ahead; const behind = this.behind; - const aheadBehindCounts = await Container.instance.git.getAheadBehindCommitCount(this.repoPath, [ + const aheadBehindCounts = await this.view.container.git.getAheadBehindCommitCount(this.repoPath, [ GitRevision.createRange(behind.ref1 || 'HEAD', behind.ref2, '...'), ]); const mergeBase = - (await Container.instance.git.getMergeBase(this.repoPath, behind.ref1, behind.ref2, { + (await this.view.container.git.getMergeBase(this.repoPath, behind.ref1, behind.ref2, { forkPoint: true, - })) ?? (await Container.instance.git.getMergeBase(this.repoPath, behind.ref1, behind.ref2)); + })) ?? (await this.view.container.git.getMergeBase(this.repoPath, behind.ref1, behind.ref2)); this._children = [ new ResultsCommitsNode( @@ -144,8 +143,8 @@ export class CompareResultsNode extends ViewNode { async getTreeItem(): Promise { let description; - if ((await Container.instance.git.getRepositoryCount()) > 1) { - const repo = await Container.instance.git.getRepository(this.uri.repoPath!); + if (this.view.container.git.repositoryCount > 1) { + const repo = await this.view.container.git.getRepository(this.uri.repoPath!); description = repo?.formattedName ?? this.uri.repoPath; } @@ -181,7 +180,7 @@ export class CompareResultsNode extends ViewNode { this._pinned = Date.now(); await this.updatePinned(); - setImmediate(() => this.view.reveal(this, { focus: true, select: true })); + queueMicrotask(() => this.view.reveal(this, { focus: true, select: true })); } @gate() @@ -209,7 +208,7 @@ export class CompareResultsNode extends ViewNode { this._children = undefined; this.view.triggerNodeChange(this.parent); - setImmediate(() => this.view.reveal(this, { expand: true, focus: true, select: true })); + queueMicrotask(() => this.view.reveal(this, { expand: true, focus: true, select: true })); } @log() @@ -219,7 +218,7 @@ export class CompareResultsNode extends ViewNode { this._pinned = 0; await this.view.updatePinned(this.getPinnableId()); - setImmediate(() => this.view.reveal(this, { focus: true, select: true })); + queueMicrotask(() => this.view.reveal(this, { focus: true, select: true })); } private getPinnableId() { @@ -227,13 +226,13 @@ export class CompareResultsNode extends ViewNode { } private async getAheadFilesQuery(): Promise { - let files = await Container.instance.git.getDiffStatus( + let files = await this.view.container.git.getDiffStatus( this.repoPath, GitRevision.createRange(this._compareWith?.ref || 'HEAD', this._ref.ref || 'HEAD', '...'), ); if (this._ref.ref === '') { - const workingFiles = await Container.instance.git.getDiffStatus(this.repoPath, 'HEAD'); + const workingFiles = await this.view.container.git.getDiffStatus(this.repoPath, 'HEAD'); if (workingFiles != null) { if (files != null) { for (const wf of workingFiles) { @@ -257,13 +256,13 @@ export class CompareResultsNode extends ViewNode { } private async getBehindFilesQuery(): Promise { - let files = await Container.instance.git.getDiffStatus( + let files = await this.view.container.git.getDiffStatus( this.repoPath, GitRevision.createRange(this._ref.ref || 'HEAD', this._compareWith.ref || 'HEAD', '...'), ); if (this._compareWith.ref === '') { - const workingFiles = await Container.instance.git.getDiffStatus(this.repoPath, 'HEAD'); + const workingFiles = await this.view.container.git.getDiffStatus(this.repoPath, 'HEAD'); if (workingFiles != null) { if (files != null) { for (const wf of workingFiles) { @@ -289,7 +288,7 @@ export class CompareResultsNode extends ViewNode { private getCommitsQuery(range: string): (limit: number | undefined) => Promise { const repoPath = this.repoPath; return async (limit: number | undefined) => { - const log = await Container.instance.git.getLog(repoPath, { + const log = await this.view.container.git.getLog(repoPath, { limit: limit, ref: range, }); @@ -319,7 +318,7 @@ export class CompareResultsNode extends ViewNode { comparison = `${this._compareWith.ref}..${this._ref.ref}`; } - const files = await Container.instance.git.getDiffStatus(this.uri.repoPath!, comparison); + const files = await this.view.container.git.getDiffStatus(this.uri.repoPath!, comparison); return { label: `${Strings.pluralize('file', files?.length ?? 0, { zero: 'No' })} changed`, diff --git a/src/views/nodes/contributorNode.ts b/src/views/nodes/contributorNode.ts index 5617b50..0b2e8c3 100644 --- a/src/views/nodes/contributorNode.ts +++ b/src/views/nodes/contributorNode.ts @@ -2,7 +2,6 @@ import { MarkdownString, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; import { getPresenceDataUri } from '../../avatars'; import { GlyphChars } from '../../constants'; -import { Container } from '../../container'; import { GitContributor, GitLog } from '../../git/git'; import { GitUri } from '../../git/gitUri'; import { debug, gate, Iterables, Strings } from '../../system'; @@ -47,7 +46,7 @@ export class ContributorNode extends ViewNode { if (this._children == null) { - const all = Container.instance.config.views.contributors.showAllBranches; + const all = this.view.container.config.views.contributors.showAllBranches; let ref: string | undefined; // If we aren't getting all branches, get the upstream of the current branch if there is one if (!all) { try { - const branch = await Container.instance.git.getBranch(this.uri.repoPath); + const branch = await this.view.container.git.getBranch(this.uri.repoPath); if (branch?.upstream?.name != null && !branch.upstream.missing) { ref = '@{u}'; } } catch {} } - const stats = Container.instance.config.views.contributors.showStatistics; + const stats = this.view.container.config.views.contributors.showStatistics; const contributors = await this.repo.getContributors({ all: all, ref: ref, stats: stats }); if (contributors.length === 0) return [new MessageNode(this.view, this, 'No contributors could be found.')]; @@ -98,6 +97,6 @@ export class ContributorsNode extends ViewNode c.current)?.email; if (email == null) return undefined; - return Container.instance.vsls.getContactsPresence([email]); + return this.view.container.vsls.getContactsPresence([email]); } } diff --git a/src/views/nodes/fileHistoryNode.ts b/src/views/nodes/fileHistoryNode.ts index 8730aec..7477370 100644 --- a/src/views/nodes/fileHistoryNode.ts +++ b/src/views/nodes/fileHistoryNode.ts @@ -2,7 +2,6 @@ import * as paths from 'path'; import { Disposable, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; import { configuration } from '../../configuration'; -import { Container } from '../../container'; import { GitBranch, GitLog, @@ -58,18 +57,18 @@ export class FileHistoryNode extends SubscribeableViewNode impl const children: ViewNode[] = []; - const range = this.branch != null ? await Container.instance.git.getBranchAheadRange(this.branch) : undefined; + const range = this.branch != null ? await this.view.container.git.getBranchAheadRange(this.branch) : undefined; const [log, fileStatuses, currentUser, getBranchAndTagTips, unpublishedCommits] = await Promise.all([ this.getLog(), this.uri.sha == null - ? Container.instance.git.getStatusForFiles(this.uri.repoPath!, this.getPathOrGlob()) + ? this.view.container.git.getStatusForFiles(this.uri.repoPath!, this.getPathOrGlob()) : undefined, - this.uri.sha == null ? Container.instance.git.getCurrentUser(this.uri.repoPath!) : undefined, + this.uri.sha == null ? this.view.container.git.getCurrentUser(this.uri.repoPath!) : undefined, this.branch != null - ? Container.instance.git.getBranchesAndTagsTipsFn(this.uri.repoPath, this.branch.name) + ? this.view.container.git.getBranchesAndTagsTipsFn(this.uri.repoPath, this.branch.name) : undefined, range - ? Container.instance.git.getLogRefsOnly(this.uri.repoPath!, { + ? this.view.container.git.getLogRefsOnly(this.uri.repoPath!, { limit: 0, ref: range, }) @@ -169,7 +168,7 @@ export class FileHistoryNode extends SubscribeableViewNode impl @debug() protected async subscribe() { - const repo = await Container.instance.git.getRepository(this.uri); + const repo = await this.view.container.git.getRepository(this.uri); if (repo == null) return undefined; const subscription = Disposable.from( @@ -233,7 +232,7 @@ export class FileHistoryNode extends SubscribeableViewNode impl private _log: GitLog | undefined; private async getLog() { if (this._log == null) { - this._log = await Container.instance.git.getLogForFile(this.uri.repoPath, this.getPathOrGlob(), { + this._log = await this.view.container.git.getLogForFile(this.uri.repoPath, this.getPathOrGlob(), { limit: this.limit ?? this.view.config.pageItemLimit, ref: this.uri.sha, }); diff --git a/src/views/nodes/fileHistoryTrackerNode.ts b/src/views/nodes/fileHistoryTrackerNode.ts index 12d9e1f..17c2ea1 100644 --- a/src/views/nodes/fileHistoryTrackerNode.ts +++ b/src/views/nodes/fileHistoryTrackerNode.ts @@ -2,7 +2,6 @@ import { Disposable, FileType, TextEditor, TreeItem, TreeItemCollapsibleState, window, workspace } from 'vscode'; import { UriComparer } from '../../comparers'; import { ContextKeys, setContext } from '../../constants'; -import { Container } from '../../container'; import { GitReference, GitRevision } from '../../git/git'; import { GitCommitish, GitUri } from '../../git/gitUri'; import { Logger } from '../../logger'; @@ -65,9 +64,9 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode b.name === commitish.sha, }); } @@ -111,7 +110,7 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode e.document?.uri.path === this.uri.path)) ) { return true; @@ -167,7 +166,7 @@ export class FileHistoryTrackerNode extends SubscribeableViewNode this._options.getBranchAndTagTips?.(sha, { compact: true }), messageTruncateAtNewLine: true, }), @@ -111,7 +110,7 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode this._options.getBranchAndTagTips?.(sha, { compact: true }), messageTruncateAtNewLine: true, }); @@ -121,14 +120,14 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode { if (!this.commit.hasConflicts) return undefined; - const mergeBase = await Container.instance.git.getMergeBase(this.repoPath, 'MERGE_HEAD', 'HEAD'); + const mergeBase = await this.view.container.git.getMergeBase(this.repoPath, 'MERGE_HEAD', 'HEAD'); return GitUri.fromFile(this.file, this.repoPath, mergeBase ?? 'HEAD'); } private async getTooltip() { - const remotes = await Container.instance.git.getRemotes(this.commit.repoPath); - const remote = await Container.instance.git.getRichRemoteProvider(remotes); + const remotes = await this.view.container.git.getRemotes(this.commit.repoPath); + const remote = await this.view.container.git.getRichRemoteProvider(remotes); let autolinkedIssuesOrPullRequests; let pr; if (remote?.provider != null) { [autolinkedIssuesOrPullRequests, pr] = await Promise.all([ - Container.instance.autolinks.getIssueOrPullRequestLinks(this.commit.message, remote), - Container.instance.git.getPullRequestForCommit(this.commit.ref, remote.provider), + this.view.container.autolinks.getIssueOrPullRequestLinks(this.commit.message, remote), + this.view.container.git.getPullRequestForCommit(this.commit.ref, remote.provider), ]); } @@ -234,7 +233,7 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode b.name === commitish.sha, }); } @@ -123,7 +122,7 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode e.document?.uri.path === this.uri.path)) ) { return true; @@ -211,13 +210,13 @@ export class LineHistoryTrackerNode extends SubscribeableViewNode { + this.view.container.lineTracker.onDidChangeActiveLines((e: LinesChangeEvent) => { if (e.pending) return; onActiveLinesChanged(e); diff --git a/src/views/nodes/mergeConflictCurrentChangesNode.ts b/src/views/nodes/mergeConflictCurrentChangesNode.ts index 3f54cc9..834378a 100644 --- a/src/views/nodes/mergeConflictCurrentChangesNode.ts +++ b/src/views/nodes/mergeConflictCurrentChangesNode.ts @@ -2,7 +2,6 @@ import { Command, MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Commands, DiffWithCommandArgs } from '../../commands'; import { BuiltInCommands, GlyphChars } from '../../constants'; -import { Container } from '../../container'; import { CommitFormatter, GitFile, GitMergeStatus, GitRebaseStatus, GitReference } from '../../git/git'; import { GitUri } from '../../git/gitUri'; import { FileHistoryView } from '../fileHistoryView'; @@ -25,7 +24,7 @@ export class MergeConflictCurrentChangesNode extends ViewNode { - const commit = await Container.instance.git.getCommit(this.status.repoPath, 'HEAD'); + const commit = await this.view.container.git.getCommit(this.status.repoPath, 'HEAD'); const item = new TreeItem('Current changes', TreeItemCollapsibleState.None); item.contextValue = ContextValues.MergeConflictCurrentChanges; @@ -33,7 +32,7 @@ export class MergeConflictCurrentChangesNode extends ViewNode { - const commit = await Container.instance.git.getCommit( + const commit = await this.view.container.git.getCommit( this.status.repoPath, this.status.type === 'rebase' ? this.status.steps.current.commit.ref : this.status.HEAD.ref, ); @@ -38,7 +37,7 @@ export class MergeConflictIncomingChangesNode extends ViewNode { ); } - const commit = await Container.instance.git.getCommit( + const commit = await this.view.container.git.getCommit( this.rebaseStatus.repoPath, this.rebaseStatus.steps.current.commit.ref, ); @@ -168,7 +167,7 @@ export class RebaseCommitNode extends ViewRefNode implements PageableVi item.contextValue = ContextValues.Reflog; item.description = 'experimental'; item.iconPath = { - dark: Container.instance.context.asAbsolutePath('images/dark/icon-activity.svg'), - light: Container.instance.context.asAbsolutePath('images/light/icon-activity.svg'), + dark: this.view.container.context.asAbsolutePath('images/dark/icon-activity.svg'), + light: this.view.container.context.asAbsolutePath('images/light/icon-activity.svg'), }; return item; @@ -71,7 +70,7 @@ export class ReflogNode extends ViewNode implements PageableVi private _reflog: GitReflog | undefined; private async getReflog() { if (this._reflog === undefined) { - this._reflog = await Container.instance.git.getIncomingActivity(this.repo.path, { + this._reflog = await this.view.container.git.getIncomingActivity(this.repo.path, { all: true, limit: this.limit ?? this.view.config.defaultItemLimit, }); diff --git a/src/views/nodes/reflogRecordNode.ts b/src/views/nodes/reflogRecordNode.ts index 6862e59..e5f2028 100644 --- a/src/views/nodes/reflogRecordNode.ts +++ b/src/views/nodes/reflogRecordNode.ts @@ -1,7 +1,6 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState, window } from 'vscode'; import { GlyphChars } from '../../constants'; -import { Container } from '../../container'; import { GitLog, GitReflogRecord } from '../../git/git'; import { GitUri } from '../../git/gitUri'; import { debug, gate, Iterables } from '../../system'; @@ -90,7 +89,7 @@ export class ReflogRecordNode extends ViewNode implements Page private async getLog() { if (this._log === undefined) { const range = `${this.record.previousSha}..${this.record.sha}`; - this._log = await Container.instance.git.getLog(this.uri.repoPath!, { + this._log = await this.view.container.git.getLog(this.uri.repoPath!, { limit: this.limit ?? this.view.config.defaultItemLimit, ref: range, }); diff --git a/src/views/nodes/remoteNode.ts b/src/views/nodes/remoteNode.ts index 3aded68..9d43264 100644 --- a/src/views/nodes/remoteNode.ts +++ b/src/views/nodes/remoteNode.ts @@ -2,7 +2,6 @@ import { ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { ViewBranchesLayout } from '../../configuration'; import { GlyphChars } from '../../constants'; -import { Container } from '../../container'; import { GitRemote, GitRemoteType, Repository } from '../../git/git'; import { GitUri } from '../../git/gitUri'; import { Arrays, log } from '../../system'; @@ -117,8 +116,8 @@ export class RemoteNode extends ViewNode { provider.icon === 'remote' ? new ThemeIcon('cloud') : { - dark: Container.instance.context.asAbsolutePath(`images/dark/icon-${provider.icon}.svg`), - light: Container.instance.context.asAbsolutePath(`images/light/icon-${provider.icon}.svg`), + dark: this.view.container.context.asAbsolutePath(`images/dark/icon-${provider.icon}.svg`), + light: this.view.container.context.asAbsolutePath(`images/light/icon-${provider.icon}.svg`), }; if (provider.hasApi()) { diff --git a/src/views/nodes/repositoriesNode.ts b/src/views/nodes/repositoriesNode.ts index c246b6c..7190a59 100644 --- a/src/views/nodes/repositoriesNode.ts +++ b/src/views/nodes/repositoriesNode.ts @@ -1,6 +1,6 @@ 'use strict'; import { Disposable, TextEditor, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; -import { Container } from '../../container'; +import { RepositoriesChangedEvent } from '../../git/gitProviderService'; import { GitUri } from '../../git/gitUri'; import { Logger } from '../../logger'; import { debug, Functions, gate } from '../../system'; @@ -34,9 +34,9 @@ export class RepositoriesNode extends SubscribeableViewNode { this._children = undefined; } - async getChildren(): Promise { + getChildren(): ViewNode[] { if (this._children === undefined) { - const repositories = await Container.instance.git.getOrderedRepositories(); + const repositories = this.view.container.git.openRepositories; if (repositories.length === 0) return [new MessageNode(this.view, this, 'No repositories could be found.')]; this._children = repositories.map(r => new RepositoryNode(GitUri.fromRepoPath(r.path), this.view, this, r)); @@ -65,7 +65,7 @@ export class RepositoriesNode extends SubscribeableViewNode { return; } - const repositories = await Container.instance.git.getOrderedRepositories(); + const repositories = this.view.container.git.openRepositories; if (repositories.length === 0 && (this._children === undefined || this._children.length === 0)) return; if (repositories.length === 0) { @@ -98,7 +98,7 @@ export class RepositoriesNode extends SubscribeableViewNode { @debug() protected subscribe() { - const subscriptions = [Container.instance.git.onDidChangeRepositories(this.onRepositoriesChanged, this)]; + const subscriptions = [this.view.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this)]; if (this.view.config.autoReveal) { subscriptions.push( @@ -141,7 +141,7 @@ export class RepositoriesNode extends SubscribeableViewNode { } @debug() - private onRepositoriesChanged() { + private onRepositoriesChanged(_e: RepositoriesChangedEvent) { void this.triggerChange(); } } diff --git a/src/views/nodes/repositoryNode.ts b/src/views/nodes/repositoryNode.ts index c051f2c..c3bbd26 100644 --- a/src/views/nodes/repositoryNode.ts +++ b/src/views/nodes/repositoryNode.ts @@ -1,7 +1,6 @@ 'use strict'; import { Disposable, MarkdownString, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { GlyphChars } from '../../constants'; -import { Container } from '../../container'; import { GitBranch, GitRemote, @@ -87,8 +86,8 @@ export class RepositoryNode extends SubscribeableViewNode { } const [mergeStatus, rebaseStatus] = await Promise.all([ - Container.instance.git.getMergeStatus(status.repoPath), - Container.instance.git.getRebaseStatus(status.repoPath), + this.view.container.git.getMergeStatus(status.repoPath), + this.view.container.git.getRebaseStatus(status.repoPath), ]); if (mergeStatus != null) { @@ -212,7 +211,7 @@ export class RepositoryNode extends SubscribeableViewNode { let providerName; if (status.upstream != null) { const providers = GitRemote.getHighlanderProviders( - await Container.instance.git.getRemotes(status.repoPath), + await this.view.container.git.getRemotes(status.repoPath), ); providerName = providers?.length ? providers[0].name : undefined; } else { @@ -267,8 +266,8 @@ export class RepositoryNode extends SubscribeableViewNode { : '' }`; item.iconPath = { - dark: Container.instance.context.asAbsolutePath(`images/dark/icon-repo${iconSuffix}.svg`), - light: Container.instance.context.asAbsolutePath(`images/light/icon-repo${iconSuffix}.svg`), + dark: this.view.container.context.asAbsolutePath(`images/dark/icon-repo${iconSuffix}.svg`), + light: this.view.container.context.asAbsolutePath(`images/light/icon-repo${iconSuffix}.svg`), }; const markdown = new MarkdownString(tooltip, true); diff --git a/src/views/nodes/resultsCommitsNode.ts b/src/views/nodes/resultsCommitsNode.ts index 30fad93..c930727 100644 --- a/src/views/nodes/resultsCommitsNode.ts +++ b/src/views/nodes/resultsCommitsNode.ts @@ -1,6 +1,5 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { Container } from '../../container'; import { GitLog } from '../../git/git'; import { GitUri } from '../../git/gitUri'; import { debug, gate, Iterables, Promises } from '../../system'; @@ -69,7 +68,7 @@ export class ResultsCommitsNode { : TreeItemCollapsibleState.Collapsed; } catch (ex) { if (ex instanceof Promises.CancellationError) { - ex.promise.then(() => setTimeout(() => this.triggerChange(false), 0)); + ex.promise.then(() => queueMicrotask(() => this.triggerChange(false))); } label = 'files changed'; @@ -184,18 +183,18 @@ export class ResultsFilesNode extends ViewNode { const ref = this.filter === 'left' ? this.ref2 : this.ref1; - const mergeBase = await Container.instance.git.getMergeBase( + const mergeBase = await this.view.container.git.getMergeBase( this.repoPath, this.ref1 || 'HEAD', this.ref2 || 'HEAD', ); if (mergeBase != null) { - const files = await Container.instance.git.getDiffStatus(this.uri.repoPath!, `${mergeBase}..${ref}`); + const files = await this.view.container.git.getDiffStatus(this.uri.repoPath!, `${mergeBase}..${ref}`); if (files != null) { filterTo = new Set(files.map(f => f.fileName)); } } else { - const commit = await Container.instance.git.getCommit(this.uri.repoPath!, ref || 'HEAD'); + const commit = await this.view.container.git.getCommit(this.uri.repoPath!, ref || 'HEAD'); if (commit?.files != null) { filterTo = new Set(commit.files.map(f => f.fileName)); } diff --git a/src/views/nodes/searchResultsNode.ts b/src/views/nodes/searchResultsNode.ts index 5af0fd2..d72c0ce 100644 --- a/src/views/nodes/searchResultsNode.ts +++ b/src/views/nodes/searchResultsNode.ts @@ -1,7 +1,6 @@ 'use strict'; import { ThemeIcon, TreeItem } from 'vscode'; import { executeGitCommand } from '../../commands'; -import { Container } from '../../container'; import { GitLog, SearchPattern } from '../../git/git'; import { GitUri } from '../../git/gitUri'; import { debug, gate, log, Strings } from '../../system'; @@ -132,8 +131,8 @@ export class SearchResultsNode extends ViewNode implements const item = await this.ensureResults().getTreeItem(); item.id = this.id; item.contextValue = `${ContextValues.SearchResults}${this._pinned ? '+pinned' : ''}`; - if ((await Container.instance.git.getRepositoryCount()) > 1) { - const repo = await Container.instance.git.getRepository(this.repoPath); + if (this.view.container.git.repositoryCount > 1) { + const repo = await this.view.container.git.getRepository(this.repoPath); item.description = repo?.formattedName ?? this.repoPath; } if (this._pinned) { @@ -194,7 +193,7 @@ export class SearchResultsNode extends ViewNode implements } void this.triggerChange(false); - setImmediate(() => this.view.reveal(this, { expand: true, focus: true, select: true })); + queueMicrotask(() => this.view.reveal(this, { expand: true, focus: true, select: true })); } @gate() @@ -210,7 +209,7 @@ export class SearchResultsNode extends ViewNode implements this._pinned = Date.now(); await this.updatePinned(); - setImmediate(() => this.view.reveal(this, { focus: true, select: true })); + queueMicrotask(() => this.view.reveal(this, { focus: true, select: true })); } @log() @@ -220,7 +219,7 @@ export class SearchResultsNode extends ViewNode implements this._pinned = 0; await this.view.updatePinned(this.getPinnableId()); - setImmediate(() => this.view.reveal(this, { focus: true, select: true })); + queueMicrotask(() => this.view.reveal(this, { focus: true, select: true })); } private getPinnableId() { @@ -264,7 +263,7 @@ export class SearchResultsNode extends ViewNode implements let useCacheOnce = true; return async (limit: number | undefined) => { - log = await (log ?? Container.instance.git.getLogForSearch(this.repoPath, this.search)); + log = await (log ?? this.view.container.git.getLogForSearch(this.repoPath, this.search)); if (!useCacheOnce && log != null && log.query != null) { log = await log.query(limit); diff --git a/src/views/nodes/stashNode.ts b/src/views/nodes/stashNode.ts index 33cca4d..abd83d5 100644 --- a/src/views/nodes/stashNode.ts +++ b/src/views/nodes/stashNode.ts @@ -2,7 +2,6 @@ import * as paths from 'path'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ViewFilesLayout } from '../../config'; -import { Container } from '../../container'; import { CommitFormatter, GitStashCommit, GitStashReference } from '../../git/git'; import { Arrays, Strings } from '../../system'; import { ContextValues, FileNode, FolderNode, RepositoryNode, StashFileNode, ViewNode, ViewRefNode } from '../nodes'; @@ -61,18 +60,18 @@ export class StashNode extends ViewRefNode { item.contextValue = ContextValues.Stashes; item.iconPath = { - dark: Container.instance.context.asAbsolutePath('images/dark/icon-stash.svg'), - light: Container.instance.context.asAbsolutePath('images/light/icon-stash.svg'), + dark: this.view.container.context.asAbsolutePath('images/dark/icon-stash.svg'), + light: this.view.container.context.asAbsolutePath('images/light/icon-stash.svg'), }; return item; diff --git a/src/views/nodes/statusFileNode.ts b/src/views/nodes/statusFileNode.ts index febf10d..16a8c83 100644 --- a/src/views/nodes/statusFileNode.ts +++ b/src/views/nodes/statusFileNode.ts @@ -2,7 +2,6 @@ import * as paths from 'path'; import { Command, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Commands, DiffWithCommandArgs, DiffWithPreviousCommandArgs } from '../../commands'; -import { Container } from '../../container'; import { GitFile, GitLogCommit, StatusFileFormatter } from '../../git/git'; import { GitUri } from '../../git/gitUri'; import { Strings } from '../../system'; @@ -111,8 +110,8 @@ export class StatusFileNode extends ViewNode implements FileNo const icon = GitFile.getStatusIcon(this.file.status); item.iconPath = { - dark: Container.instance.context.asAbsolutePath(paths.join('images', 'dark', icon)), - light: Container.instance.context.asAbsolutePath(paths.join('images', 'light', icon)), + dark: this.view.container.context.asAbsolutePath(paths.join('images', 'dark', icon)), + light: this.view.container.context.asAbsolutePath(paths.join('images', 'light', icon)), }; } diff --git a/src/views/nodes/statusFilesNode.ts b/src/views/nodes/statusFilesNode.ts index 5bd9e93..16d3c90 100644 --- a/src/views/nodes/statusFilesNode.ts +++ b/src/views/nodes/statusFilesNode.ts @@ -2,7 +2,6 @@ import * as paths from 'path'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ViewFilesLayout } from '../../configuration'; -import { Container } from '../../container'; import { GitCommitType, GitFileWithCommit, @@ -57,7 +56,7 @@ export class StatusFilesNode extends ViewNode { let log: GitLog | undefined; if (this.range != null) { - log = await Container.instance.git.getLog(repoPath, { limit: 0, ref: this.range }); + log = await this.view.container.git.getLog(repoPath, { limit: 0, ref: this.range }); if (log != null) { files = [ ...Iterables.flatMap(log.commits.values(), c => @@ -135,7 +134,7 @@ export class StatusFilesNode extends ViewNode { if (this.range != null) { if (this.status.upstream != null && this.status.state.ahead > 0) { if (files > 0) { - const aheadFiles = await Container.instance.git.getDiffStatus( + const aheadFiles = await this.view.container.git.getDiffStatus( this.repoPath, `${this.status.upstream}...`, ); @@ -152,7 +151,7 @@ export class StatusFilesNode extends ViewNode { files = uniques.size; } } else { - const stats = await Container.instance.git.getChangedFilesCount( + const stats = await this.view.container.git.getChangedFilesCount( this.repoPath, `${this.status.upstream}...`, ); @@ -170,8 +169,8 @@ export class StatusFilesNode extends ViewNode { item.id = this.id; item.contextValue = ContextValues.StatusFiles; item.iconPath = { - dark: Container.instance.context.asAbsolutePath('images/dark/icon-diff.svg'), - light: Container.instance.context.asAbsolutePath('images/light/icon-diff.svg'), + dark: this.view.container.context.asAbsolutePath('images/dark/icon-diff.svg'), + light: this.view.container.context.asAbsolutePath('images/light/icon-diff.svg'), }; return item; diff --git a/src/views/nodes/tagNode.ts b/src/views/nodes/tagNode.ts index fb2b7b0..0ca7d7a 100644 --- a/src/views/nodes/tagNode.ts +++ b/src/views/nodes/tagNode.ts @@ -2,7 +2,6 @@ import { TreeItem, TreeItemCollapsibleState, window } from 'vscode'; import { ViewBranchesLayout } from '../../configuration'; import { GlyphChars } from '../../constants'; -import { Container } from '../../container'; import { emojify } from '../../emojis'; import { GitLog, GitRevision, GitTag, GitTagReference, TagDateFormatting } from '../../git/git'; import { GitUri } from '../../git/gitUri'; @@ -45,7 +44,7 @@ export class TagNode extends ViewRefNode - Container.instance.git.getCommitCount(this.tag.repoPath, this.tag.name), + this.view.container.git.getCommitCount(this.tag.repoPath, this.tag.name), ), ); } @@ -98,7 +97,7 @@ export class TagNode extends ViewRefNode extends SubscribeableViewNode { + protected override splatted = true; + protected children: TChild[] | undefined; + + constructor(view: TView) { + super(unknownGitUri, view); + } + + override async getSplattedChild() { + if (this.children == null) { + await this.getChildren(); + } + + return this.children?.length === 1 ? this.children[0] : undefined; + } + + @gate() + @debug() + override refresh(reset: boolean = false) { + if (reset && this.children != null) { + for (const child of this.children) { + child.dispose(); + } + this.children = undefined; + } + } + + @debug() + protected subscribe(): Disposable | Promise { + return this.view.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this); + } + + private onRepositoriesChanged(_e: RepositoriesChangedEvent) { + void this.triggerChange(true); + } +} + interface AutoRefreshableView { autoRefresh: boolean; onDidChangeAutoRefresh: Event; diff --git a/src/views/remotesView.ts b/src/views/remotesView.ts index 263f305..dbf3318 100644 --- a/src/views/remotesView.ts +++ b/src/views/remotesView.ts @@ -24,15 +24,15 @@ import { RepositoryChangeEvent, } from '../git/git'; import { GitUri } from '../git/gitUri'; -import { debug, gate, Strings } from '../system'; +import { gate, Strings } from '../system'; import { BranchNode, BranchOrTagFolderNode, RemoteNode, RemotesNode, + RepositoriesSubscribeableNode, RepositoryFolderNode, RepositoryNode, - unknownGitUri, ViewNode, } from './nodes'; import { ViewBase } from './viewBase'; @@ -57,17 +57,10 @@ export class RemotesRepositoryNode extends RepositoryFolderNode { - protected override splatted = true; - private children: RemotesRepositoryNode[] | undefined; - - constructor(view: RemotesView) { - super(unknownGitUri, view); - } - +export class RemotesViewNode extends RepositoriesSubscribeableNode { async getChildren(): Promise { if (this.children == null) { - const repositories = await Container.instance.git.getOrderedRepositories(); + const repositories = this.view.container.git.openRepositories; if (repositories.length === 0) { this.view.message = 'No remotes could be found.'; @@ -112,25 +105,6 @@ export class RemotesViewNode extends ViewNode { const item = new TreeItem('Remotes', TreeItemCollapsibleState.Expanded); return item; } - - override async getSplattedChild() { - if (this.children == null) { - await this.getChildren(); - } - - return this.children?.length === 1 ? this.children[0] : undefined; - } - - @gate() - @debug() - override refresh(reset: boolean = false) { - if (reset && this.children != null) { - for (const child of this.children) { - child.dispose(); - } - this.children = undefined; - } - } } export class RemotesView extends ViewBase { @@ -155,8 +129,8 @@ export class RemotesView extends ViewBase { ), commands.registerCommand( this.getQualifiedCommand('refresh'), - async () => { - await this.container.git.resetCaches('branches', 'remotes'); + () => { + this.container.git.resetCaches('branches', 'remotes'); return this.refresh(true); }, this, diff --git a/src/views/repositoriesView.ts b/src/views/repositoriesView.ts index a17dd95..69a4e10 100644 --- a/src/views/repositoriesView.ts +++ b/src/views/repositoriesView.ts @@ -73,15 +73,8 @@ export class RepositoriesView extends ViewBase { - await this.container.git.resetCaches( - 'branches', - 'contributors', - 'remotes', - 'stashes', - 'status', - 'tags', - ); + () => { + this.container.git.resetCaches('branches', 'contributors', 'remotes', 'stashes', 'status', 'tags'); return this.refresh(true); }, this, diff --git a/src/views/searchAndCompareView.ts b/src/views/searchAndCompareView.ts index a0357e3..12eaa8e 100644 --- a/src/views/searchAndCompareView.ts +++ b/src/views/searchAndCompareView.ts @@ -514,7 +514,7 @@ export class SearchAndCompareView extends ViewBase this.reveal(results, options)); + queueMicrotask(() => this.reveal(results, options)); } private setFilesLayout(layout: ViewFilesLayout) { diff --git a/src/views/stashesView.ts b/src/views/stashesView.ts index d5cc215..c950aa7 100644 --- a/src/views/stashesView.ts +++ b/src/views/stashesView.ts @@ -20,8 +20,15 @@ import { RepositoryChangeEvent, } from '../git/git'; import { GitUri } from '../git/gitUri'; -import { debug, gate, Strings } from '../system'; -import { RepositoryFolderNode, RepositoryNode, StashesNode, StashNode, unknownGitUri, ViewNode } from './nodes'; +import { gate, Strings } from '../system'; +import { + RepositoriesSubscribeableNode, + RepositoryFolderNode, + RepositoryNode, + StashesNode, + StashNode, + ViewNode, +} from './nodes'; import { ViewBase } from './viewBase'; export class StashesRepositoryNode extends RepositoryFolderNode { @@ -38,17 +45,10 @@ export class StashesRepositoryNode extends RepositoryFolderNode { - protected override splatted = true; - private children: StashesRepositoryNode[] | undefined; - - constructor(view: StashesView) { - super(unknownGitUri, view); - } - +export class StashesViewNode extends RepositoriesSubscribeableNode { async getChildren(): Promise { if (this.children == null) { - const repositories = await Container.instance.git.getOrderedRepositories(); + const repositories = this.view.container.git.openRepositories; if (repositories.length === 0) { this.view.message = 'No stashes could be found.'; @@ -93,25 +93,6 @@ export class StashesViewNode extends ViewNode { const item = new TreeItem('Stashes', TreeItemCollapsibleState.Expanded); return item; } - - override async getSplattedChild() { - if (this.children == null) { - await this.getChildren(); - } - - return this.children?.length === 1 ? this.children[0] : undefined; - } - - @gate() - @debug() - override refresh(reset: boolean = false) { - if (reset && this.children != null) { - for (const child of this.children) { - child.dispose(); - } - this.children = undefined; - } - } } export class StashesView extends ViewBase { @@ -136,8 +117,8 @@ export class StashesView extends ViewBase { ), commands.registerCommand( this.getQualifiedCommand('refresh'), - async () => { - await this.container.git.resetCaches('stashes'); + () => { + this.container.git.resetCaches('stashes'); return this.refresh(true); }, this, diff --git a/src/views/tagsView.ts b/src/views/tagsView.ts index ba830b6..51b29c1 100644 --- a/src/views/tagsView.ts +++ b/src/views/tagsView.ts @@ -20,13 +20,13 @@ import { RepositoryChangeEvent, } from '../git/git'; import { GitUri } from '../git/gitUri'; -import { debug, gate, Strings } from '../system'; +import { gate, Strings } from '../system'; import { BranchOrTagFolderNode, + RepositoriesSubscribeableNode, RepositoryFolderNode, RepositoryNode, TagsNode, - unknownGitUri, ViewNode, } from './nodes'; import { ViewBase } from './viewBase'; @@ -45,17 +45,10 @@ export class TagsRepositoryNode extends RepositoryFolderNode } } -export class TagsViewNode extends ViewNode { - protected override splatted = true; - private children: TagsRepositoryNode[] | undefined; - - constructor(view: TagsView) { - super(unknownGitUri, view); - } - +export class TagsViewNode extends RepositoriesSubscribeableNode { async getChildren(): Promise { if (this.children == null) { - const repositories = await Container.instance.git.getOrderedRepositories(); + const repositories = this.view.container.git.openRepositories; if (repositories.length === 0) { this.view.message = 'No tags could be found.'; @@ -100,25 +93,6 @@ export class TagsViewNode extends ViewNode { const item = new TreeItem('Tags', TreeItemCollapsibleState.Expanded); return item; } - - override async getSplattedChild() { - if (this.children == null) { - await this.getChildren(); - } - - return this.children?.length === 1 ? this.children[0] : undefined; - } - - @gate() - @debug() - override refresh(reset: boolean = false) { - if (reset && this.children != null) { - for (const child of this.children) { - child.dispose(); - } - this.children = undefined; - } - } } export class TagsView extends ViewBase { @@ -143,8 +117,8 @@ export class TagsView extends ViewBase { ), commands.registerCommand( this.getQualifiedCommand('refresh'), - async () => { - await this.container.git.resetCaches('tags'); + () => { + this.container.git.resetCaches('tags'); return this.refresh(true); }, this, diff --git a/src/views/viewBase.ts b/src/views/viewBase.ts index b887539..bb57145 100644 --- a/src/views/viewBase.ts +++ b/src/views/viewBase.ts @@ -114,7 +114,7 @@ export abstract class ViewBase< private readonly _lastKnownLimits = new Map(); - constructor(public readonly id: string, public readonly name: string, protected readonly container: Container) { + constructor(public readonly id: string, public readonly name: string, public readonly container: Container) { this.disposables.push(container.onReady(this.onReady, this)); if (Logger.isDebugging || this.container.config.debug) { @@ -174,7 +174,7 @@ export abstract class ViewBase< private onReady() { this.initialize({ showCollapseAll: this.showCollapseAll }); - setImmediate(() => this.onConfigurationChanged()); + queueMicrotask(() => this.onConfigurationChanged()); } protected get showCollapseAll(): boolean { @@ -374,7 +374,7 @@ export abstract class ViewBase< // If we have no root (e.g. never been initialized) force it so the tree will load properly await this.show({ preserveFocus: true }); // Since we have to show the view, let the callstack unwind before we try to find the node - return new Promise(resolve => setTimeout(() => resolve(find.call(this)), 0)); + return new Promise(resolve => queueMicrotask(() => resolve(find.call(this)))); } private async findNodeCoreBFS( diff --git a/src/views/viewCommands.ts b/src/views/viewCommands.ts index 9363d51..eda280a 100644 --- a/src/views/viewCommands.ts +++ b/src/views/viewCommands.ts @@ -17,7 +17,6 @@ import { BuiltInCommands, BuiltInGitCommands, ContextKeys, setContext } from '.. import { Container } from '../container'; import { GitReference, GitRevision } from '../git/git'; import { GitUri } from '../git/gitUri'; -import { GitService } from '../git/providers/localGitProvider'; import { debug } from '../system'; import { runGitCommandInTerminal } from '../terminal'; import { @@ -657,7 +656,7 @@ export class ViewCommands { @debug() private switch(node?: ViewRefNode | BranchesNode) { if (node == null) { - return GitActions.switchTo(this.container.git.getHighlanderRepoPath()); + return GitActions.switchTo(this.container.git.highlanderRepoPath); } if (!(node instanceof ViewRefNode) && !(node instanceof BranchesNode)) return Promise.resolve(); @@ -672,7 +671,7 @@ export class ViewCommands { private async undoCommit(node: CommitNode | FileRevisionAsCommitNode) { if (!(node instanceof CommitNode) && !(node instanceof FileRevisionAsCommitNode)) return; - const repo = await GitService.getOrOpenBuiltInGitRepository(node.repoPath); + const repo = await Container.instance.git.getOrOpenScmRepository(node.repoPath); const commit = await repo?.getCommit('HEAD'); if (commit?.hash !== node.ref.ref) { diff --git a/src/vsls/guest.ts b/src/vsls/guest.ts index 5885efe..f4accfc 100644 --- a/src/vsls/guest.ts +++ b/src/vsls/guest.ts @@ -1,6 +1,7 @@ 'use strict'; 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'; @@ -10,7 +11,7 @@ import { GitCommandRequestType, RepositoriesInFolderRequestType, RepositoryProxy export class VslsGuestService implements Disposable { @log() - static async connect(api: LiveShare) { + static async connect(api: LiveShare, container: Container) { const cc = Logger.getCorrelationContext(); try { @@ -19,14 +20,18 @@ export class VslsGuestService implements Disposable { throw new Error('Failed to connect to host service'); } - return new VslsGuestService(api, service); + return new VslsGuestService(api, service, container); } catch (ex) { Logger.error(ex, cc); return undefined; } } - constructor(private readonly _api: LiveShare, private readonly _service: SharedServiceProxy) { + constructor( + private readonly _api: LiveShare, + private readonly _service: SharedServiceProxy, + private readonly container: Container, + ) { _service.onDidChangeIsServiceAvailable(this.onAvailabilityChanged.bind(this)); this.onAvailabilityChanged(_service.isServiceAvailable); } @@ -70,7 +75,17 @@ export class VslsGuestService implements Disposable { return response.repositories.map( (r: RepositoryProxy) => - new Repository(folder, r.path, r.root, onAnyRepositoryChanged, !window.state.focused, r.closed), + new Repository( + this.container, + onAnyRepositoryChanged, + // TODO@eamodio add live share provider + undefined!, + folder, + r.path, + r.root, + !window.state.focused, + r.closed, + ), ); } diff --git a/src/vsls/host.ts b/src/vsls/host.ts index 3bcba14..222df07 100644 --- a/src/vsls/host.ts +++ b/src/vsls/host.ts @@ -205,6 +205,7 @@ export class VslsHostService implements Disposable { return { data: data.toString('binary'), isBuffer: true }; } + // eslint-disable-next-line @typescript-eslint/require-await @log() private async onRepositoriesInFolderRequest( request: RepositoriesInFolderRequest, @@ -214,7 +215,7 @@ export class VslsHostService implements Disposable { const normalized = Strings.normalizePath(uri.fsPath, { stripTrailingSlash: true }).toLowerCase(); const repos = [ - ...Iterables.filterMap(await this.container.git.getRepositories(), r => { + ...Iterables.filterMap(this.container.git.repositories, r => { if (!r.normalizedPath.startsWith(normalized)) return undefined; const vslsUri = this.convertLocalUriToShared(r.folder.uri); diff --git a/src/vsls/vsls.ts b/src/vsls/vsls.ts index 4453510..ae63c64 100644 --- a/src/vsls/vsls.ts +++ b/src/vsls/vsls.ts @@ -209,7 +209,7 @@ export class VslsController implements Disposable { case 2 /*Role.Guest*/: this.setReadonly(true); void setContext(ContextKeys.Vsls, 'guest'); - this._guest = await VslsGuestService.connect(api); + this._guest = await VslsGuestService.connect(api, this.container); break; default: diff --git a/webpack.config.js b/webpack.config.js index d9f08be..88b0222 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -127,13 +127,13 @@ function getExtensionConfig(target, mode, env) { treeShaking: true, // Keep the class names otherwise @log won't provide a useful name keepNames: true, - target: 'es2019', + target: 'es2020', }) : new TerserPlugin({ extractComments: false, parallel: true, terserOptions: { - ecma: 2019, + ecma: 2020, // Keep the class names otherwise @log won't provide a useful name keep_classnames: true, module: true, @@ -284,7 +284,7 @@ function getWebviewsConfig(mode, env) { eslint: { enabled: true, files: path.join(basePath, '**', '*.ts'), - options: { cache: true }, + // options: { cache: true }, }, formatter: 'basic', typescript: {