Browse Source

Optimizes best remote detection (still wip)

Fixes leak with rich remote providers
Avoids needing repo remote subscriptions
main
Eric Amodio 1 year ago
parent
commit
ceb5739e5a
13 changed files with 232 additions and 268 deletions
  1. +4
    -1
      src/annotations/lineAnnotationController.ts
  2. +20
    -19
      src/container.ts
  3. +16
    -0
      src/env/node/git/localGitProvider.ts
  4. +84
    -169
      src/git/gitProviderService.ts
  5. +29
    -32
      src/git/models/repository.ts
  6. +4
    -0
      src/git/parsers/remoteParser.ts
  7. +11
    -0
      src/git/remotes/remoteProviderService.ts
  8. +1
    -0
      src/git/remotes/remoteProviders.ts
  9. +24
    -3
      src/git/remotes/richRemoteProvider.ts
  10. +30
    -30
      src/hovers/hovers.ts
  11. +3
    -8
      src/views/nodes/commitNode.ts
  12. +3
    -3
      src/views/nodes/fileRevisionAsCommitNode.ts
  13. +3
    -3
      src/views/nodes/rebaseStatusNode.ts

+ 4
- 1
src/annotations/lineAnnotationController.ts View File

@ -15,6 +15,7 @@ import { detailsMessage } from '../hovers/hovers';
import { configuration } from '../system/configuration'; import { configuration } from '../system/configuration';
import { debug, log } from '../system/decorators/log'; import { debug, log } from '../system/decorators/log';
import { once } from '../system/event'; import { once } from '../system/event';
import { debounce } from '../system/function';
import { count, every, filter } from '../system/iterable'; import { count, every, filter } from '../system/iterable';
import { Logger } from '../system/logger'; import { Logger } from '../system/logger';
import type { LogScope } from '../system/logger.scope'; import type { LogScope } from '../system/logger.scope';
@ -45,7 +46,9 @@ export class LineAnnotationController implements Disposable {
once(container.onReady)(this.onReady, this), once(container.onReady)(this.onReady, this),
configuration.onDidChange(this.onConfigurationChanged, this), configuration.onDidChange(this.onConfigurationChanged, this),
container.fileAnnotations.onDidToggleAnnotations(this.onFileAnnotationsToggled, this), container.fileAnnotations.onDidToggleAnnotations(this.onFileAnnotationsToggled, this),
container.richRemoteProviders.onDidChangeConnectionState(() => void this.refresh(window.activeTextEditor)),
container.richRemoteProviders.onAfterDidChangeConnectionState(
debounce(() => void this.refresh(window.activeTextEditor), 250),
),
); );
} }

+ 20
- 19
src/container.ts View File

@ -190,8 +190,6 @@ export class Container {
configuration.onDidChangeAny(this.onAnyConfigurationChanged, this), configuration.onDidChangeAny(this.onAnyConfigurationChanged, this),
]; ];
this._richRemoteProviders = new RichRemoteProviderService(this);
this._disposables.push((this._connection = new ServerConnection(this))); this._disposables.push((this._connection = new ServerConnection(this)));
this._disposables.push( this._disposables.push(
@ -533,14 +531,6 @@ export class Container {
return this._lineTracker; return this._lineTracker;
} }
private _repositoryPathMapping: RepositoryPathMappingProvider | undefined;
get repositoryPathMapping() {
if (this._repositoryPathMapping == null) {
this._disposables.push((this._repositoryPathMapping = getSupportedRepositoryPathMappingProvider(this)));
}
return this._repositoryPathMapping;
}
private readonly _prerelease; private readonly _prerelease;
get prerelease() { get prerelease() {
return this._prerelease; return this._prerelease;
@ -566,21 +556,27 @@ export class Container {
return this._repositoriesView; return this._repositoriesView;
} }
private readonly _searchAndCompareView: SearchAndCompareView;
get searchAndCompareView() {
return this._searchAndCompareView;
}
private _subscription: SubscriptionService;
get subscription() {
return this._subscription;
private _repositoryPathMapping: RepositoryPathMappingProvider | undefined;
get repositoryPathMapping() {
if (this._repositoryPathMapping == null) {
this._disposables.push((this._repositoryPathMapping = getSupportedRepositoryPathMappingProvider(this)));
}
return this._repositoryPathMapping;
} }
private readonly _richRemoteProviders: RichRemoteProviderService;
private _richRemoteProviders: RichRemoteProviderService | undefined;
get richRemoteProviders(): RichRemoteProviderService { get richRemoteProviders(): RichRemoteProviderService {
if (this._richRemoteProviders == null) {
this._richRemoteProviders = new RichRemoteProviderService(this);
}
return this._richRemoteProviders; return this._richRemoteProviders;
} }
private readonly _searchAndCompareView: SearchAndCompareView;
get searchAndCompareView() {
return this._searchAndCompareView;
}
private readonly _stashesView: StashesView; private readonly _stashesView: StashesView;
get stashesView() { get stashesView() {
return this._stashesView; return this._stashesView;
@ -596,6 +592,11 @@ export class Container {
return this._storage; return this._storage;
} }
private _subscription: SubscriptionService;
get subscription() {
return this._subscription;
}
private readonly _tagsView: TagsView; private readonly _tagsView: TagsView;
get tagsView() { get tagsView() {
return this._tagsView; return this._tagsView;

+ 16
- 0
src/env/node/git/localGitProvider.ts View File

@ -291,6 +291,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
} }
if (e.changed(RepositoryChange.Remotes, RepositoryChange.RemoteProviders, RepositoryChangeComparisonMode.Any)) { if (e.changed(RepositoryChange.Remotes, RepositoryChange.RemoteProviders, RepositoryChangeComparisonMode.Any)) {
const remotes = this._remotesCache.get(repo.path);
void disposeRemotes([remotes]);
this._remotesCache.delete(repo.path); this._remotesCache.delete(repo.path);
} }
@ -1091,6 +1093,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
} }
if (caches.length === 0 || caches.includes('remotes')) { if (caches.length === 0 || caches.includes('remotes')) {
const remotes = this._remotesCache.get(repoPath);
void disposeRemotes([remotes]);
this._remotesCache.delete(repoPath); this._remotesCache.delete(repoPath);
} }
@ -1124,6 +1128,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
} }
if (caches.length === 0 || caches.includes('remotes')) { if (caches.length === 0 || caches.includes('remotes')) {
void disposeRemotes([...this._remotesCache.values()]);
this._remotesCache.clear(); this._remotesCache.clear();
} }
@ -5288,3 +5293,14 @@ async function getEncoding(uri: Uri): Promise {
const encodingExists = (await import(/* webpackChunkName: "encoding" */ 'iconv-lite')).encodingExists; const encodingExists = (await import(/* webpackChunkName: "encoding" */ 'iconv-lite')).encodingExists;
return encodingExists(encoding) ? encoding : 'utf8'; return encodingExists(encoding) ? encoding : 'utf8';
} }
async function disposeRemotes(remotes: (Promise<GitRemote[]> | undefined)[]) {
const remotesResults = await Promise.allSettled(remotes);
for (const remotes of remotesResults) {
for (const remote of getSettledValue(remotes) ?? []) {
if (remote.hasRichProvider()) {
remote.provider?.dispose();
}
}
}
}

+ 84
- 169
src/git/gitProviderService.ts View File

@ -35,6 +35,7 @@ import { Logger } from '../system/logger';
import { getLogScope, setLogScopeExit } from '../system/logger.scope'; import { getLogScope, setLogScopeExit } from '../system/logger.scope';
import { getBestPath, getScheme, isAbsolute, maybeUri, normalizePath } from '../system/path'; import { getBestPath, getScheme, isAbsolute, maybeUri, normalizePath } from '../system/path';
import { asSettled, cancellable, defer, getSettledValue, isPromise, PromiseCancelledError } from '../system/promise'; import { asSettled, cancellable, defer, getSettledValue, isPromise, PromiseCancelledError } from '../system/promise';
import { sortCompare } from '../system/string';
import { VisitedPathsTrie } from '../system/trie'; import { VisitedPathsTrie } from '../system/trie';
import type { import type {
GitCaches, GitCaches,
@ -198,13 +199,14 @@ export class GitProviderService implements Disposable {
readonly supportedSchemes = new Set<string>(); readonly supportedSchemes = new Set<string>();
private readonly _bestRemotesCache = new Map<
RepoComparisonKey,
Promise<GitRemote<RemoteProvider | RichRemoteProvider>[]>
>();
private readonly _disposable: Disposable; private readonly _disposable: Disposable;
private readonly _pendingRepositories = new Map<RepoComparisonKey, Promise<Repository | undefined>>(); private readonly _pendingRepositories = new Map<RepoComparisonKey, Promise<Repository | undefined>>();
private readonly _providers = new Map<GitProviderId, GitProvider>(); private readonly _providers = new Map<GitProviderId, GitProvider>();
private readonly _repositories = new Repositories(); private readonly _repositories = new Repositories();
private readonly _bestRemotesCache: Map<RepoComparisonKey, GitRemote<RemoteProvider | RichRemoteProvider> | null> &
Map<`rich|${RepoComparisonKey}`, GitRemote<RichRemoteProvider> | null> &
Map<`rich+connected|${RepoComparisonKey}`, GitRemote<RichRemoteProvider> | null> = new Map();
private readonly _visitedPaths = new VisitedPathsTrie(); private readonly _visitedPaths = new VisitedPathsTrie();
constructor(private readonly container: Container) { constructor(private readonly container: Container) {
@ -213,7 +215,7 @@ export class GitProviderService implements Disposable {
window.onDidChangeWindowState(this.onWindowStateChanged, this), window.onDidChangeWindowState(this.onWindowStateChanged, this),
workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this), workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this),
configuration.onDidChange(this.onConfigurationChanged, this), configuration.onDidChange(this.onConfigurationChanged, this),
container.richRemoteProviders.onDidChangeConnectionState(e => {
container.richRemoteProviders.onAfterDidChangeConnectionState(e => {
if (e.reason === 'connected') { if (e.reason === 'connected') {
resetAvatarCache('failed'); resetAvatarCache('failed');
} }
@ -2113,189 +2115,108 @@ export class GitProviderService implements Disposable {
return provider.getIncomingActivity(path, options); return provider.getIncomingActivity(path, options);
} }
@log()
async getBestRemoteWithProvider( async getBestRemoteWithProvider(
repoPath: string | Uri | undefined,
): Promise<GitRemote<RemoteProvider | RichRemoteProvider> | undefined>;
async getBestRemoteWithProvider(
remotes: GitRemote[],
): Promise<GitRemote<RemoteProvider | RichRemoteProvider> | undefined>;
@gate<GitProviderService['getBestRemoteWithProvider']>(
remotesOrRepoPath =>
`${
remotesOrRepoPath == null || typeof remotesOrRepoPath === 'string'
? remotesOrRepoPath
: remotesOrRepoPath instanceof Uri
? remotesOrRepoPath.toString()
: `${remotesOrRepoPath.length}:${remotesOrRepoPath[0]?.repoPath ?? ''}`
}`,
)
@log<GitProviderService['getBestRemoteWithProvider']>({
args: {
0: remotesOrRepoPath =>
Array.isArray(remotesOrRepoPath) ? remotesOrRepoPath.map(r => r.name).join(',') : remotesOrRepoPath,
},
})
async getBestRemoteWithProvider(
remotesOrRepoPath: GitRemote[] | string | Uri | undefined,
repoPath: string | Uri,
): Promise<GitRemote<RemoteProvider | RichRemoteProvider> | undefined> { ): Promise<GitRemote<RemoteProvider | RichRemoteProvider> | undefined> {
if (remotesOrRepoPath == null) return undefined;
let remotes;
let repoPath;
if (Array.isArray(remotesOrRepoPath)) {
if (remotesOrRepoPath.length === 0) return undefined;
remotes = remotesOrRepoPath;
repoPath = remotesOrRepoPath[0].repoPath;
} else {
repoPath = remotesOrRepoPath;
}
const remotes = await this.getBestRemotesWithProviders(repoPath);
return remotes[0];
}
@log()
async getBestRemotesWithProviders(
repoPath: string | Uri,
): Promise<GitRemote<RemoteProvider | RichRemoteProvider>[]> {
if (repoPath == null) return [];
if (typeof repoPath === 'string') { if (typeof repoPath === 'string') {
repoPath = this.getAbsoluteUri(repoPath); repoPath = this.getAbsoluteUri(repoPath);
} }
const cacheKey = asRepoComparisonKey(repoPath); const cacheKey = asRepoComparisonKey(repoPath);
let remote = this._bestRemotesCache.get(cacheKey);
if (remote !== undefined) return remote ?? undefined;
remotes = (remotes ?? (await this.getRemotesWithProviders(repoPath))).filter(
(r: GitRemote): r is GitRemote<RemoteProvider | RichRemoteProvider> => r.provider != null,
);
if (remotes.length === 0) return undefined;
if (remotes.length === 1) {
remote = remotes[0];
} else {
const weightedRemotes = new Map<string, number>([
['upstream', 15],
['origin', 10],
]);
const branch = await this.getBranch(remotes[0].repoPath);
const branchRemote = branch?.getRemoteName();
if (branchRemote != null) {
weightedRemotes.set(branchRemote, 100);
}
let bestRemote;
let weight = 0;
for (const r of remotes) {
if (r.default) {
bestRemote = r;
break;
}
// Don't choose a remote unless its weighted above
let matchedWeight = weightedRemotes.get(r.name) ?? -1;
let remotes = this._bestRemotesCache.get(cacheKey);
if (remotes == null) {
async function getBest(this: GitProviderService) {
const remotes = await this.getRemotesWithProviders(repoPath, { sort: true });
if (remotes.length === 0) return [];
if (remotes.length === 1) return [...remotes];
const defaultRemote = remotes.find(r => r.default)?.name;
const currentBranchRemote = (await this.getBranch(remotes[0].repoPath))?.getRemoteName();
const weighted: [number, GitRemote<RemoteProvider | RichRemoteProvider>][] = [];
let originalFound = false;
for (const remote of remotes) {
let weight;
switch (remote.name) {
case defaultRemote:
weight = 1000;
break;
case currentBranchRemote:
weight = 6;
break;
case 'upstream':
weight = 5;
break;
case 'origin':
weight = 4;
break;
default:
weight = 0;
}
const p = r.provider;
if (p.hasRichIntegration() && p.maybeConnected) {
const m = await p.getRepositoryMetadata();
if (m?.isFork === false) {
matchedWeight += 101;
// Only check remotes that have extra weighting and less than the default
if (weight > 0 && weight < 1000 && !originalFound) {
const p = remote.provider;
if (
p.hasRichIntegration() &&
(p.maybeConnected ||
(p.maybeConnected === undefined && p.shouldConnect && (await p.isConnected())))
) {
const repo = await p.getRepositoryMetadata();
if (repo != null) {
weight += repo.isFork ? -3 : 3;
// Once we've found the "original" (not a fork) don't bother looking for more
originalFound = !repo.isFork;
}
}
} }
}
if (matchedWeight > weight) {
bestRemote = r;
weight = matchedWeight;
weighted.push([weight, remote]);
} }
// Sort by the weight, but if both are 0 (no weight) then sort by name
weighted.sort(([aw, ar], [bw, br]) => (bw === 0 && aw === 0 ? sortCompare(ar.name, br.name) : bw - aw));
return weighted.map(wr => wr[1]);
} }
remote = bestRemote ?? null;
remotes = getBest.call(this);
this._bestRemotesCache.set(cacheKey, remotes);
} }
this._bestRemotesCache.set(cacheKey, remote);
return remote ?? undefined;
return [...(await remotes)];
} }
@log()
async getBestRemoteWithRichProvider( async getBestRemoteWithRichProvider(
repoPath: string | Uri | undefined,
options?: { includeDisconnected?: boolean },
): Promise<GitRemote<RichRemoteProvider> | undefined>;
async getBestRemoteWithRichProvider(
remotes: GitRemote[],
options?: { includeDisconnected?: boolean },
): Promise<GitRemote<RichRemoteProvider> | undefined>;
@gate<GitProviderService['getBestRemoteWithRichProvider']>(
(remotesOrRepoPath, options) =>
`${
remotesOrRepoPath == null || typeof remotesOrRepoPath === 'string'
? remotesOrRepoPath
: remotesOrRepoPath instanceof Uri
? remotesOrRepoPath.toString()
: `${remotesOrRepoPath.length}:${remotesOrRepoPath[0]?.repoPath ?? ''}`
}|${options?.includeDisconnected ?? false}`,
)
@log<GitProviderService['getBestRemoteWithRichProvider']>({
args: {
0: remotesOrRepoPath =>
Array.isArray(remotesOrRepoPath) ? remotesOrRepoPath.map(r => r.name).join(',') : remotesOrRepoPath,
},
})
async getBestRemoteWithRichProvider(
remotesOrRepoPath: GitRemote[] | string | Uri | undefined,
repoPath: string | Uri,
options?: { includeDisconnected?: boolean }, options?: { includeDisconnected?: boolean },
): Promise<GitRemote<RichRemoteProvider> | undefined> { ): Promise<GitRemote<RichRemoteProvider> | undefined> {
if (remotesOrRepoPath == null) return undefined;
let remotes;
let repoPath;
if (Array.isArray(remotesOrRepoPath)) {
if (remotesOrRepoPath.length === 0) return undefined;
remotes = remotesOrRepoPath;
repoPath = remotesOrRepoPath[0].repoPath;
} else {
repoPath = remotesOrRepoPath;
}
if (typeof repoPath === 'string') {
repoPath = this.getAbsoluteUri(repoPath);
}
const cacheKey = asRepoComparisonKey(repoPath);
let richRemote = this._bestRemotesCache.get(`rich+connected|${cacheKey}`);
if (richRemote != null) return richRemote;
if (richRemote === null && !options?.includeDisconnected) return undefined;
if (options?.includeDisconnected) {
richRemote = this._bestRemotesCache.get(`rich|${cacheKey}`);
if (richRemote !== undefined) return richRemote ?? undefined;
}
const remote = await (remotes != null
? this.getBestRemoteWithProvider(remotes)
: this.getBestRemoteWithProvider(repoPath));
if (!remote?.hasRichProvider()) {
this._bestRemotesCache.set(`rich|${cacheKey}`, null);
this._bestRemotesCache.set(`rich+connected|${cacheKey}`, null);
return undefined;
}
const { provider } = remote;
const connected = provider.maybeConnected ?? (await provider.isConnected());
if (connected) {
this._bestRemotesCache.set(`rich|${cacheKey}`, remote);
this._bestRemotesCache.set(`rich+connected|${cacheKey}`, remote);
} else {
this._bestRemotesCache.set(`rich|${cacheKey}`, remote);
this._bestRemotesCache.set(`rich+connected|${cacheKey}`, null);
const remotes = await this.getBestRemotesWithProviders(repoPath);
if (!options?.includeDisconnected) return undefined;
const includeDisconnected = options?.includeDisconnected ?? false;
for (const r of remotes) {
if (r.hasRichProvider() && (includeDisconnected || r.provider.maybeConnected === true)) {
return r;
}
} }
return remote;
return undefined;
} }
@log({ args: { 1: false } })
async getRemotes(repoPath: string | Uri | undefined, options?: { sort?: boolean }): Promise<GitRemote[]> {
@log()
async getRemotes(repoPath: string | Uri, options?: { sort?: boolean }): Promise<GitRemote[]> {
if (repoPath == null) return []; if (repoPath == null) return [];
const { provider, path } = this.getProvider(repoPath); const { provider, path } = this.getProvider(repoPath);
@ -2304,16 +2225,10 @@ export class GitProviderService implements Disposable {
@log() @log()
async getRemotesWithProviders( async getRemotesWithProviders(
repoPath: string | Uri | undefined,
repoPath: string | Uri,
options?: { sort?: boolean }, options?: { sort?: boolean },
): Promise<GitRemote<RemoteProvider | RichRemoteProvider>[]> { ): Promise<GitRemote<RemoteProvider | RichRemoteProvider>[]> {
if (repoPath == null) return [];
const repository = this.container.git.getRepository(repoPath);
const remotes = await (repository != null
? repository.getRemotes(options)
: this.getRemotes(repoPath, options));
const remotes = await this.getRemotes(repoPath, options);
return remotes.filter( return remotes.filter(
(r: GitRemote): r is GitRemote<RemoteProvider | RichRemoteProvider> => r.provider != null, (r: GitRemote): r is GitRemote<RemoteProvider | RichRemoteProvider> => r.provider != null,
); );

+ 29
- 32
src/git/models/repository.ts View File

@ -9,7 +9,7 @@ import type { Container } from '../../container';
import type { FeatureAccess, Features, PlusFeatures } from '../../features'; import type { FeatureAccess, Features, PlusFeatures } from '../../features';
import { showCreatePullRequestPrompt, showGenericErrorMessage } from '../../messages'; import { showCreatePullRequestPrompt, showGenericErrorMessage } from '../../messages';
import { asRepoComparisonKey } from '../../repositories'; import { asRepoComparisonKey } from '../../repositories';
import { filterMap, groupByMap } from '../../system/array';
import { groupByMap } from '../../system/array';
import { executeActionCommand, executeCoreGitCommand } from '../../system/command'; import { executeActionCommand, executeCoreGitCommand } from '../../system/command';
import { configuration } from '../../system/configuration'; import { configuration } from '../../system/configuration';
import { formatDate, fromNow } from '../../system/date'; import { formatDate, fromNow } from '../../system/date';
@ -214,8 +214,6 @@ export class Repository implements Disposable {
private _fsWatcherDisposable: Disposable | undefined; private _fsWatcherDisposable: Disposable | undefined;
private _pendingFileSystemChange?: RepositoryFileSystemChangeEvent; private _pendingFileSystemChange?: RepositoryFileSystemChangeEvent;
private _pendingRepoChange?: RepositoryChangeEvent; private _pendingRepoChange?: RepositoryChangeEvent;
private _remotes: Promise<GitRemote[]> | undefined;
private _remotesDisposable: Disposable | undefined;
private _suspended: boolean; private _suspended: boolean;
constructor( constructor(
@ -256,6 +254,19 @@ export class Repository implements Disposable {
this._disposable = Disposable.from( this._disposable = Disposable.from(
this.setupRepoWatchers(), this.setupRepoWatchers(),
configuration.onDidChange(this.onConfigurationChanged, this), configuration.onDidChange(this.onConfigurationChanged, this),
// Sending this event in the `'git:cache:reset'` below to avoid unnecessary work. While we will refresh more than needed, this doesn't happen often
// container.richRemoteProviders.onAfterDidChangeConnectionState(async e => {
// const uniqueKeys = new Set<string>();
// for (const remote of await this.getRemotes()) {
// if (remote.provider?.hasRichIntegration()) {
// uniqueKeys.add(remote.provider.key);
// }
// }
// if (uniqueKeys.has(e.key)) {
// this.fireChange(RepositoryChange.RemoteProviders);
// }
// }),
); );
this.onConfigurationChanged(); this.onConfigurationChanged();
@ -281,6 +292,10 @@ export class Repository implements Disposable {
this.container.events.on('git:cache:reset', e => { this.container.events.on('git:cache:reset', e => {
if (!e.data.repoPath || e.data.repoPath === this.path) { if (!e.data.repoPath || e.data.repoPath === this.path) {
this.resetCaches(...(e.data.caches ?? emptyArray)); this.resetCaches(...(e.data.caches ?? emptyArray));
if (e.data.caches?.includes('providers')) {
this.fireChange(RepositoryChange.RemoteProviders);
}
} }
}), }),
); );
@ -323,7 +338,6 @@ export class Repository implements Disposable {
dispose() { dispose() {
this.stopWatchingFileSystem(); this.stopWatchingFileSystem();
this._remotesDisposable?.dispose();
this._disposable.dispose(); this._disposable.dispose();
} }
@ -685,32 +699,15 @@ export class Repository implements Disposable {
} }
async getRemotes(options?: { filter?: (remote: GitRemote) => boolean; sort?: boolean }): Promise<GitRemote[]> { async getRemotes(options?: { filter?: (remote: GitRemote) => boolean; sort?: boolean }): Promise<GitRemote[]> {
if (this._remotes == null) {
// Since we are caching the results, always sort
this._remotes = this.container.git.getRemotes(this.uri, { sort: true });
void this.subscribeToRemotes(this._remotes);
}
return options?.filter != null ? (await this._remotes).filter(options.filter) : this._remotes;
const remotes = await this.container.git.getRemotes(
this.uri,
options?.sort != null ? { sort: options.sort } : undefined,
);
return options?.filter != null ? remotes.filter(options.filter) : remotes;
} }
async getRichRemote(connectedOnly: boolean = false): Promise<GitRemote<RichRemoteProvider> | undefined> { async getRichRemote(connectedOnly: boolean = false): Promise<GitRemote<RichRemoteProvider> | undefined> {
return this.container.git.getBestRemoteWithRichProvider(await this.getRemotes(), {
includeDisconnected: !connectedOnly,
});
}
private async subscribeToRemotes(remotes: Promise<GitRemote[]>) {
this._remotesDisposable?.dispose();
this._remotesDisposable = undefined;
this._remotesDisposable = Disposable.from(
...filterMap(await remotes, r => {
if (!r.provider?.hasRichIntegration()) return undefined;
return r.provider.onDidChange(() => this.fireChange(RepositoryChange.RemoteProviders));
}),
);
return this.container.git.getBestRemoteWithRichProvider(this.uri, { includeDisconnected: !connectedOnly });
} }
getStash(): Promise<GitStash | undefined> { getStash(): Promise<GitStash | undefined> {
@ -955,11 +952,11 @@ export class Repository implements Disposable {
this._branch = undefined; this._branch = undefined;
} }
if (caches.length === 0 || caches.includes('remotes')) {
this._remotes = undefined;
this._remotesDisposable?.dispose();
this._remotesDisposable = undefined;
}
// if (caches.length === 0 || caches.includes('remotes')) {
// this._remotes = undefined;
// this._remotesDisposable?.dispose();
// this._remotesDisposable = undefined;
// }
} }
resume() { resume() {

+ 4
- 0
src/git/parsers/remoteParser.ts View File

@ -55,6 +55,10 @@ export class GitRemoteParser {
remote.urls.push({ url: url, type: type as GitRemoteType }); remote.urls.push({ url: url, type: type as GitRemoteType });
if (remote.provider != null && type !== 'push') continue; if (remote.provider != null && type !== 'push') continue;
if (remote.provider?.hasRichIntegration()) {
remote.provider.dispose();
}
const provider = remoteProviderMatcher(url, domain, path); const provider = remoteProviderMatcher(url, domain, path);
if (provider == null) continue; if (provider == null) continue;

+ 11
- 0
src/git/remotes/remoteProviderService.ts View File

@ -13,6 +13,11 @@ export class RichRemoteProviderService {
return this._onDidChangeConnectionState.event; return this._onDidChangeConnectionState.event;
} }
private readonly _onAfterDidChangeConnectionState = new EventEmitter<ConnectionStateChangeEvent>();
get onAfterDidChangeConnectionState(): Event<ConnectionStateChangeEvent> {
return this._onAfterDidChangeConnectionState.event;
}
private readonly _connectedCache = new Set<string>(); private readonly _connectedCache = new Set<string>();
constructor(private readonly container: Container) {} constructor(private readonly container: Container) {}
@ -25,6 +30,7 @@ export class RichRemoteProviderService {
this.container.telemetry.sendEvent('remoteProviders/connected', { 'remoteProviders.key': key }); this.container.telemetry.sendEvent('remoteProviders/connected', { 'remoteProviders.key': key });
this._onDidChangeConnectionState.fire({ key: key, reason: 'connected' }); this._onDidChangeConnectionState.fire({ key: key, reason: 'connected' });
setTimeout(() => this._onAfterDidChangeConnectionState.fire({ key: key, reason: 'connected' }), 250);
} }
disconnected(key: string): void { disconnected(key: string): void {
@ -34,5 +40,10 @@ export class RichRemoteProviderService {
this.container.telemetry.sendEvent('remoteProviders/disconnected', { 'remoteProviders.key': key }); this.container.telemetry.sendEvent('remoteProviders/disconnected', { 'remoteProviders.key': key });
this._onDidChangeConnectionState.fire({ key: key, reason: 'disconnected' }); this._onDidChangeConnectionState.fire({ key: key, reason: 'disconnected' });
setTimeout(() => this._onAfterDidChangeConnectionState.fire({ key: key, reason: 'disconnected' }), 250);
}
isConnected(key?: string): boolean {
return key == null ? this._connectedCache.size !== 0 : this._connectedCache.has(key);
} }
} }

+ 1
- 0
src/git/remotes/remoteProviders.ts View File

@ -171,6 +171,7 @@ function createBestRemoteProvider(
return undefined; return undefined;
} catch (ex) { } catch (ex) {
debugger;
Logger.error(ex, 'createRemoteProvider'); Logger.error(ex, 'createRemoteProvider');
return undefined; return undefined;
} }

+ 24
- 3
src/git/remotes/richRemoteProvider.ts View File

@ -1,5 +1,5 @@
import type { AuthenticationSession, AuthenticationSessionsChangeEvent, Event, MessageItem } from 'vscode'; import type { AuthenticationSession, AuthenticationSessionsChangeEvent, Event, MessageItem } from 'vscode';
import { authentication, EventEmitter, window } from 'vscode';
import { authentication, Disposable, EventEmitter, window } from 'vscode';
import { wrapForForcedInsecureSSL } from '@env/fetch'; import { wrapForForcedInsecureSSL } from '@env/fetch';
import { isWeb } from '@env/platform'; import { isWeb } from '@env/platform';
import type { Container } from '../../container'; import type { Container } from '../../container';
@ -22,7 +22,7 @@ import { RemoteProvider } from './remoteProvider';
// TODO@eamodio revisit how once authenticated, all remotes are always connected, even after a restart // TODO@eamodio revisit how once authenticated, all remotes are always connected, even after a restart
export abstract class RichRemoteProvider extends RemoteProvider {
export abstract class RichRemoteProvider extends RemoteProvider implements Disposable {
override readonly type: 'simple' | 'rich' = 'rich'; override readonly type: 'simple' | 'rich' = 'rich';
private readonly _onDidChange = new EventEmitter<void>(); private readonly _onDidChange = new EventEmitter<void>();
@ -30,6 +30,8 @@ export abstract class RichRemoteProvider extends RemoteProvider {
return this._onDidChange.event; return this._onDidChange.event;
} }
private readonly _disposable: Disposable;
constructor( constructor(
protected readonly container: Container, protected readonly container: Container,
domain: string, domain: string,
@ -40,7 +42,7 @@ export abstract class RichRemoteProvider extends RemoteProvider {
) { ) {
super(domain, path, protocol, name, custom); super(domain, path, protocol, name, custom);
container.context.subscriptions.push(
this._disposable = Disposable.from(
configuration.onDidChange(e => { configuration.onDidChange(e => {
if (configuration.changed(e, 'remotes')) { if (configuration.changed(e, 'remotes')) {
this._ignoreSSLErrors.clear(); this._ignoreSSLErrors.clear();
@ -58,6 +60,20 @@ export abstract class RichRemoteProvider extends RemoteProvider {
}), }),
authentication.onDidChangeSessions(this.onAuthenticationSessionsChanged, this), authentication.onDidChangeSessions(this.onAuthenticationSessionsChanged, this),
); );
container.context.subscriptions.push(this._disposable);
// If we think we should be connected, try to
if (this.shouldConnect) {
void this.isConnected();
}
}
disposed = false;
dispose() {
this._disposable.dispose();
this.disposed = true;
} }
abstract get apiBaseUrl(): string; abstract get apiBaseUrl(): string;
@ -78,6 +94,11 @@ export abstract class RichRemoteProvider extends RemoteProvider {
return this._session === undefined ? undefined : this._session !== null; return this._session === undefined ? undefined : this._session !== null;
} }
// This is a hack for now, since providers come and go with remotes
get shouldConnect(): boolean {
return this.container.richRemoteProviders.isConnected(this.key);
}
protected _session: AuthenticationSession | null | undefined; protected _session: AuthenticationSession | null | undefined;
protected session() { protected session() {
if (this._session === undefined) { if (this._session === undefined) {

+ 30
- 30
src/hovers/hovers.ts View File

@ -213,33 +213,35 @@ export async function detailsMessage(
if (options?.cancellationToken?.isCancellationRequested) return new MarkdownString(); if (options?.cancellationToken?.isCancellationRequested) return new MarkdownString();
} }
const remotes = await Container.instance.git.getRemotesWithProviders(commit.repoPath, { sort: true });
if (options?.cancellationToken?.isCancellationRequested) return new MarkdownString();
const [previousLineComparisonUrisResult, autolinkedIssuesOrPullRequestsResult, prResult, presenceResult] =
await Promise.allSettled([
commit.isUncommitted ? commit.getPreviousComparisonUrisForLine(editorLine, uri.sha) : undefined,
getAutoLinkedIssuesOrPullRequests(message, remotes),
options?.pullRequests?.pr ??
getPullRequestForCommitOrBestRemote(commit.ref, remotes, {
pullRequests:
options?.pullRequests?.enabled !== false &&
CommitFormatter.has(
options.format,
'pullRequest',
'pullRequestAgo',
'pullRequestAgoOrDate',
'pullRequestDate',
'pullRequestState',
),
}),
Container.instance.vsls.maybeGetPresence(commit.author.email),
]);
const [
remotesResult,
previousLineComparisonUrisResult,
autolinkedIssuesOrPullRequestsResult,
prResult,
presenceResult,
] = await Promise.allSettled([
Container.instance.git.getRemotesWithProviders(commit.repoPath, { sort: true }),
commit.isUncommitted ? commit.getPreviousComparisonUrisForLine(editorLine, uri.sha) : undefined,
getAutoLinkedIssuesOrPullRequests(message, commit.repoPath),
options?.pullRequests?.pr ??
getPullRequestForCommitOrBestRemote(commit.ref, commit.repoPath, {
pullRequests:
options?.pullRequests?.enabled !== false &&
CommitFormatter.has(
options.format,
'pullRequest',
'pullRequestAgo',
'pullRequestAgoOrDate',
'pullRequestDate',
'pullRequestState',
),
}),
Container.instance.vsls.maybeGetPresence(commit.author.email),
]);
if (options?.cancellationToken?.isCancellationRequested) return new MarkdownString(); if (options?.cancellationToken?.isCancellationRequested) return new MarkdownString();
const remotes = getSettledValue(remotesResult);
const previousLineComparisonUris = getSettledValue(previousLineComparisonUrisResult); const previousLineComparisonUris = getSettledValue(previousLineComparisonUrisResult);
const autolinkedIssuesOrPullRequests = getSettledValue(autolinkedIssuesOrPullRequestsResult); const autolinkedIssuesOrPullRequests = getSettledValue(autolinkedIssuesOrPullRequestsResult);
const pr = getSettledValue(prResult); const pr = getSettledValue(prResult);
@ -286,7 +288,7 @@ function getDiffFromHunkLine(hunkLine: GitDiffHunkLine, diffStyle?: 'line' | 'hu
}\n\`\`\``; }\n\`\`\``;
} }
async function getAutoLinkedIssuesOrPullRequests(message: string, remotes: GitRemote[]) {
async function getAutoLinkedIssuesOrPullRequests(message: string, repoPath: string) {
const scope = getNewLogScope('Hovers.getAutoLinkedIssuesOrPullRequests'); const scope = getNewLogScope('Hovers.getAutoLinkedIssuesOrPullRequests');
Logger.debug(scope, `${GlyphChars.Dash} message=<message>`); Logger.debug(scope, `${GlyphChars.Dash} message=<message>`);
@ -303,7 +305,7 @@ async function getAutoLinkedIssuesOrPullRequests(message: string, remotes: GitRe
return undefined; return undefined;
} }
const remote = await Container.instance.git.getBestRemoteWithRichProvider(remotes);
const remote = await Container.instance.git.getBestRemoteWithRichProvider(repoPath);
if (remote?.provider == null) { if (remote?.provider == null) {
Logger.debug(scope, `completed [${getDurationMilliseconds(start)}ms]`); Logger.debug(scope, `completed [${getDurationMilliseconds(start)}ms]`);
@ -362,7 +364,7 @@ async function getAutoLinkedIssuesOrPullRequests(message: string, remotes: GitRe
async function getPullRequestForCommitOrBestRemote( async function getPullRequestForCommitOrBestRemote(
ref: string, ref: string,
remotes: GitRemote[],
repoPath: string,
options?: { options?: {
pullRequests?: boolean; pullRequests?: boolean;
}, },
@ -378,9 +380,7 @@ async function getPullRequestForCommitOrBestRemote(
return undefined; return undefined;
} }
const remote = await Container.instance.git.getBestRemoteWithRichProvider(remotes, {
includeDisconnected: true,
});
const remote = await Container.instance.git.getBestRemoteWithRichProvider(repoPath, { includeDisconnected: true });
if (remote?.provider == null) { if (remote?.provider == null) {
Logger.debug(scope, `completed [${getDurationMilliseconds(start)}ms]`); Logger.debug(scope, `completed [${getDurationMilliseconds(start)}ms]`);

+ 3
- 8
src/views/nodes/commitNode.ts View File

@ -236,13 +236,8 @@ export class CommitNode extends ViewRefNode
} }
private async getTooltip() { private async getTooltip() {
const remotes = await this.view.container.git.getRemotesWithProviders(this.commit.repoPath, { sort: true });
const remote = await this.view.container.git.getBestRemoteWithRichProvider(remotes);
// If we have a "best" remote, move it to the front of the list
if (remote != null) {
remotes.sort((a, b) => (a === remote ? -1 : b === remote ? 1 : 0));
}
const remotes = await this.view.container.git.getBestRemotesWithProviders(this.commit.repoPath);
const [remote] = remotes;
if (this.commit.message == null) { if (this.commit.message == null) {
await this.commit.ensureFullDetails(); await this.commit.ensureFullDetails();
@ -251,7 +246,7 @@ export class CommitNode extends ViewRefNode
let autolinkedIssuesOrPullRequests; let autolinkedIssuesOrPullRequests;
let pr; let pr;
if (remote?.provider != null) {
if (remote?.hasRichProvider()) {
const [autolinkedIssuesOrPullRequestsResult, prResult] = await Promise.allSettled([ const [autolinkedIssuesOrPullRequestsResult, prResult] = await Promise.allSettled([
this.view.container.autolinks.getLinkedIssuesAndPullRequests( this.view.container.autolinks.getLinkedIssuesAndPullRequests(
this.commit.message ?? this.commit.summary, this.commit.message ?? this.commit.summary,

+ 3
- 3
src/views/nodes/fileRevisionAsCommitNode.ts View File

@ -203,8 +203,8 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
} }
private async getTooltip() { private async getTooltip() {
const remotes = await this.view.container.git.getRemotesWithProviders(this.commit.repoPath);
const remote = await this.view.container.git.getBestRemoteWithRichProvider(remotes);
const remotes = await this.view.container.git.getBestRemotesWithProviders(this.commit.repoPath);
const [remote] = remotes;
if (this.commit.message == null) { if (this.commit.message == null) {
await this.commit.ensureFullDetails(); await this.commit.ensureFullDetails();
@ -213,7 +213,7 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
let autolinkedIssuesOrPullRequests; let autolinkedIssuesOrPullRequests;
let pr; let pr;
if (remote?.provider != null) {
if (remote?.hasRichProvider()) {
const [autolinkedIssuesOrPullRequestsResult, prResult] = await Promise.allSettled([ const [autolinkedIssuesOrPullRequestsResult, prResult] = await Promise.allSettled([
this.view.container.autolinks.getLinkedIssuesAndPullRequests( this.view.container.autolinks.getLinkedIssuesAndPullRequests(
this.commit.message ?? this.commit.summary, this.commit.message ?? this.commit.summary,

+ 3
- 3
src/views/nodes/rebaseStatusNode.ts View File

@ -195,8 +195,8 @@ export class RebaseCommitNode extends ViewRefNode
} }
private async getTooltip() { private async getTooltip() {
const remotes = await this.view.container.git.getRemotesWithProviders(this.commit.repoPath);
const remote = await this.view.container.git.getBestRemoteWithRichProvider(remotes);
const remotes = await this.view.container.git.getBestRemotesWithProviders(this.commit.repoPath);
const [remote] = remotes;
if (this.commit.message == null) { if (this.commit.message == null) {
await this.commit.ensureFullDetails(); await this.commit.ensureFullDetails();
@ -205,7 +205,7 @@ export class RebaseCommitNode extends ViewRefNode
let autolinkedIssuesOrPullRequests; let autolinkedIssuesOrPullRequests;
let pr; let pr;
if (remote?.provider != null) {
if (remote?.hasRichProvider()) {
const [autolinkedIssuesOrPullRequestsResult, prResult] = await Promise.allSettled([ const [autolinkedIssuesOrPullRequestsResult, prResult] = await Promise.allSettled([
this.view.container.autolinks.getLinkedIssuesAndPullRequests( this.view.container.autolinks.getLinkedIssuesAndPullRequests(
this.commit.message ?? this.commit.summary, this.commit.message ?? this.commit.summary,

Loading…
Cancel
Save