2782 lines
86 KiB

import type {
CancellationToken,
ConfigurationChangeEvent,
Event,
Range,
TextDocument,
TextEditor,
WindowState,
WorkspaceFolder,
WorkspaceFoldersChangeEvent,
} from 'vscode';
import { Disposable, EventEmitter, FileType, ProgressLocation, Uri, window, workspace } from 'vscode';
import { isWeb } from '@env/platform';
import { resetAvatarCache } from '../avatars';
import type { CoreGitConfiguration } from '../constants';
import { GlyphChars, Schemes } from '../constants';
import type { Container } from '../container';
import { AccessDeniedError, CancellationError, ProviderNotFoundError } from '../errors';
import type { FeatureAccess, Features, PlusFeatures, RepoFeatureAccess } from '../features';
import type { SubscriptionChangeEvent } from '../plus/subscription/subscriptionService';
import type { RepoComparisonKey } from '../repositories';
import { asRepoComparisonKey, Repositories } from '../repositories';
import type { Subscription } from '../subscription';
import { isSubscriptionPaidPlan, SubscriptionPlanId } from '../subscription';
import { groupByFilterMap, groupByMap, joinUnique } from '../system/array';
import { registerCommand } from '../system/command';
import { configuration } from '../system/configuration';
import { setContext } from '../system/context';
import { gate } from '../system/decorators/gate';
import { debug, log } from '../system/decorators/log';
import type { Deferrable } from '../system/function';
import { debounce } from '../system/function';
import { count, filter, first, flatMap, join, map, some } from '../system/iterable';
import { Logger } from '../system/logger';
import { getLogScope, setLogScopeExit } from '../system/logger.scope';
import { getBestPath, getScheme, isAbsolute, maybeUri, normalizePath } from '../system/path';
import { asSettled, cancellable, defer, getSettledValue, isPromise, PromiseCancelledError } from '../system/promise';
import { sortCompare } from '../system/string';
import { VisitedPathsTrie } from '../system/trie';
import type {
GitCaches,
GitDir,
GitProvider,
GitProviderDescriptor,
GitProviderId,
NextComparisonUrisResult,
PagedResult,
PreviousComparisonUrisResult,
PreviousLineComparisonUrisResult,
RepositoryVisibility,
RepositoryVisibilityInfo,
ScmRepository,
} from './gitProvider';
import type { GitUri } from './gitUri';
import type { GitBlame, GitBlameLine, GitBlameLines } from './models/blame';
import type { BranchSortOptions, GitBranch } from './models/branch';
import { GitCommit, GitCommitIdentity } from './models/commit';
import { deletedOrMissing, uncommitted, uncommittedStaged } from './models/constants';
import type { GitContributor } from './models/contributor';
import type { GitDiff, GitDiffFile, GitDiffFilter, GitDiffHunkLine, GitDiffShortStat } from './models/diff';
import type { GitFile } from './models/file';
import type { GitGraph } from './models/graph';
import type { SearchedIssue } from './models/issue';
import type { GitLog } from './models/log';
import type { GitMergeStatus } from './models/merge';
import type { SearchedPullRequest } from './models/pullRequest';
import type { GitRebaseStatus } from './models/rebase';
import type { GitBranchReference, GitReference } from './models/reference';
import { createRevisionRange, isSha, isUncommitted, isUncommittedParent } from './models/reference';
import type { GitReflog } from './models/reflog';
import { getVisibilityCacheKey, GitRemote } from './models/remote';
import type { RepositoryChangeEvent } from './models/repository';
import { Repository, RepositoryChange, RepositoryChangeComparisonMode } from './models/repository';
import type { GitStash } from './models/stash';
import type { GitStatus, GitStatusFile } from './models/status';
import type { GitTag, TagSortOptions } from './models/tag';
import type { GitTreeEntry } from './models/tree';
import type { GitUser } from './models/user';
import type { GitWorktree } from './models/worktree';
import type { RemoteProvider } from './remotes/remoteProvider';
import type { RichRemoteProvider } from './remotes/richRemoteProvider';
import type { GitSearch, SearchQuery } from './search';
const emptyArray = Object.freeze([]) as unknown as any[];
const emptyDisposable = Object.freeze({
dispose: () => {
/* noop */
},
});
const maxDefaultBranchWeight = 100;
const weightedDefaultBranches = new Map<string, number>([
['master', maxDefaultBranchWeight],
['main', 15],
['default', 10],
['develop', 5],
['development', 1],
]);
export type GitProvidersChangeEvent = {
readonly added: readonly GitProvider[];
readonly removed: readonly GitProvider[];
readonly etag: number;
};
export type RepositoriesChangeEvent = {
readonly added: readonly Repository[];
readonly removed: readonly Repository[];
readonly etag: number;
};
export interface GitProviderResult {
provider: GitProvider;
path: string;
}
export type RepositoriesVisibility = RepositoryVisibility | 'mixed';
export class GitProviderService implements Disposable {
private readonly _onDidChangeProviders = new EventEmitter<GitProvidersChangeEvent>();
get onDidChangeProviders(): Event<GitProvidersChangeEvent> {
return this._onDidChangeProviders.event;
}
private fireProvidersChanged(added?: GitProvider[], removed?: GitProvider[]) {
if (this.container.telemetry.enabled) {
this.container.telemetry.setGlobalAttributes({
'providers.count': this._providers.size,
'providers.ids': join(this._providers.keys(), ','),
});
this.container.telemetry.sendEvent('providers/changed', {
'providers.added': added?.length ?? 0,
'providers.removed': removed?.length ?? 0,
});
}
this._etag = Date.now();
this._onDidChangeProviders.fire({ added: added ?? [], removed: removed ?? [], etag: this._etag });
}
private _onDidChangeRepositories = new EventEmitter<RepositoriesChangeEvent>();
get onDidChangeRepositories(): Event<RepositoriesChangeEvent> {
return this._onDidChangeRepositories.event;
}
private fireRepositoriesChanged(added?: Repository[], removed?: Repository[]) {
const openSchemes = this.openRepositories.map(r => r.uri.scheme);
if (this.container.telemetry.enabled) {
this.container.telemetry.setGlobalAttributes({
'repositories.count': openSchemes.length,
'repositories.schemes': joinUnique(openSchemes, ','),
});
this.container.telemetry.sendEvent('repositories/changed', {
'repositories.added': added?.length ?? 0,
'repositories.removed': removed?.length ?? 0,
});
}
this._etag = Date.now();
this._accessCache.clear();
this._reposVisibilityCache = undefined;
this._onDidChangeRepositories.fire({ added: added ?? [], removed: removed ?? [], etag: this._etag });
if (added?.length && this.container.telemetry.enabled) {
queueMicrotask(async () => {
for (const repo of added) {
const remoteProviders = new Set<string>();
const remotes = await repo.getRemotes();
for (const remote of remotes) {
remoteProviders.add(remote.provider?.id ?? 'unknown');
}
this.container.telemetry.sendEvent('repository/opened', {
'repository.id': repo.idHash,
'repository.scheme': repo.uri.scheme,
'repository.closed': repo.closed,
'repository.folder.scheme': repo.folder?.uri.scheme,
'repository.provider.id': repo.provider.id,
'repository.remoteProviders': join(remoteProviders, ','),
});
}
});
}
}
private readonly _onDidChangeRepository = new EventEmitter<RepositoryChangeEvent>();
get onDidChangeRepository(): Event<RepositoryChangeEvent> {
return this._onDidChangeRepository.event;
}
readonly supportedSchemes = new Set<string>();
private readonly _bestRemotesCache = new Map<
RepoComparisonKey,
Promise<GitRemote<RemoteProvider | RichRemoteProvider>[]>
>();
private readonly _disposable: Disposable;
private readonly _pendingRepositories = new Map<RepoComparisonKey, Promise<Repository | undefined>>();
private readonly _providers = new Map<GitProviderId, GitProvider>();
private readonly _repositories = new Repositories();
private readonly _visitedPaths = new VisitedPathsTrie();
constructor(private readonly container: Container) {
this._disposable = Disposable.from(
container.subscription.onDidChange(this.onSubscriptionChanged, this),
window.onDidChangeWindowState(this.onWindowStateChanged, this),
workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this),
configuration.onDidChange(this.onConfigurationChanged, this),
container.richRemoteProviders.onAfterDidChangeConnectionState(e => {
if (e.reason === 'connected') {
resetAvatarCache('failed');
}
this.resetCaches('providers');
this.updateContext();
}),
!workspace.isTrusted
? workspace.onDidGrantWorkspaceTrust(() => {
if (workspace.isTrusted && workspace.workspaceFolders?.length) {
void this.discoverRepositories(workspace.workspaceFolders, { force: true });
}
})
: emptyDisposable,
...this.registerCommands(),
);
this.container.BranchDateFormatting.reset();
this.container.CommitDateFormatting.reset();
this.container.CommitShaFormatting.reset();
this.container.PullRequestDateFormatting.reset();
this.updateContext();
}
dispose() {
this._disposable.dispose();
this._providers.clear();
this._repositories.forEach(r => r.dispose());
this._repositories.clear();
}
private _etag: number = 0;
get etag(): number {
return this._etag;
}
private onConfigurationChanged(e?: ConfigurationChangeEvent) {
if (
configuration.changed(e, 'defaultDateFormat') ||
configuration.changed(e, 'defaultDateSource') ||
configuration.changed(e, 'defaultDateStyle')
) {
this.container.BranchDateFormatting.reset();
this.container.CommitDateFormatting.reset();
this.container.PullRequestDateFormatting.reset();
}
if (configuration.changed(e, 'advanced.abbreviatedShaLength')) {
this.container.CommitShaFormatting.reset();
}
if (configuration.changed(e, 'views.contributors.showAllBranches')) {
this.resetCaches('contributors');
}
if (e != null && configuration.changed(e, 'integrations.enabled')) {
this.updateContext();
}
}
private registerCommands(): Disposable[] {
return [
registerCommand('gitlens.plus.resetRepositoryAccess', () => this.clearAllRepoVisibilityCaches()),
registerCommand('gitlens.plus.refreshRepositoryAccess', () => this.clearAllOpenRepoVisibilityCaches()),
];
}
@debug()
onSubscriptionChanged(e: SubscriptionChangeEvent) {
this._accessCache.clear();
this._subscription = e.current;
}
@debug<GitProviderService['onWindowStateChanged']>({ args: { 0: e => `focused=${e.focused}` } })
private onWindowStateChanged(e: WindowState) {
if (e.focused) {
this._repositories.forEach(r => r.resume());
} else {
this._repositories.forEach(r => r.suspend());
}
}
@debug<GitProviderService['onWorkspaceFoldersChanged']>({
args: { 0: e => `added=${e.added.length}, removed=${e.removed.length}` },
singleLine: true,
})
private onWorkspaceFoldersChanged(e: WorkspaceFoldersChangeEvent) {
if (this.container.telemetry.enabled) {
const schemes = workspace.workspaceFolders?.map(f => f.uri.scheme);
this.container.telemetry.setGlobalAttributes({
'folders.count': schemes?.length ?? 0,
'folders.schemes': schemes != null ? joinUnique(schemes, ', ') : '',
});
}
if (e.added.length) {
void this.discoverRepositories(e.added);
}
if (e.removed.length) {
const removed: Repository[] = [];
for (const folder of e.removed) {
const repository = this._repositories.getClosest(folder.uri);
if (repository != null) {
this._repositories.remove(repository.uri, false);
removed.push(repository);
}
}
if (removed.length) {
this.updateContext();
// Defer the event trigger enough to let everything unwind
queueMicrotask(() => {
this.fireRepositoriesChanged([], removed);
removed.forEach(r => r.dispose());
});
}
}
}
get hasProviders(): boolean {
return this._providers.size !== 0;
}
get registeredProviders(): GitProviderDescriptor[] {
return [...map(this._providers.values(), p => ({ ...p.descriptor }))];
}
get openRepositories(): Repository[] {
if (this.repositoryCount === 0) return emptyArray as Repository[];
const repositories = [...filter(this.repositories, r => !r.closed)];
if (repositories.length === 0) return repositories;
return Repository.sort(repositories);
}
get openRepositoryCount(): number {
return this.repositoryCount === 0 ? 0 : count(this.repositories, r => !r.closed);
}
get repositories(): IterableIterator<Repository> {
return this._repositories.values();
}
get repositoryCount(): number {
return this._repositories.count;
}
get highlander(): Repository | undefined {
return this.repositoryCount === 1 ? first(this._repositories.values()) : undefined;
}
// get readonly() {
// return true;
// // return this.container.vsls.readonly;
// }
// get useCaching() {
// return configuration.get('advanced.caching.enabled');
// }
/**
* 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}
*/
@log({ args: { 1: false }, singleLine: true })
register(id: GitProviderId, provider: GitProvider): Disposable {
if (id !== provider.descriptor.id) {
throw new Error(`Id '${id}' must match provider id '${provider.descriptor.id}'`);
}
if (this._providers.has(id)) throw new Error(`Provider '${id}' has already been registered`);
this._providers.set(id, provider);
for (const scheme of provider.supportedSchemes) {
this.supportedSchemes.add(scheme);
}
const disposables = [];
const watcher = provider.openRepositoryInitWatcher?.();
if (watcher != null) {
disposables.push(
watcher,
watcher.onDidCreate(uri => {
const f = workspace.getWorkspaceFolder(uri);
if (f == null) return;
void this.discoverRepositories([f], { force: true });
}),
);
}
const disposable = Disposable.from(
provider,
...disposables,
provider.onDidChange(() => {
const { workspaceFolders } = workspace;
if (workspaceFolders?.length) {
void this.discoverRepositories(workspaceFolders, { force: true });
}
}),
provider.onDidChangeRepository(async e => {
if (
e.changed(
RepositoryChange.Remotes,
RepositoryChange.RemoteProviders,
RepositoryChangeComparisonMode.Any,
)
) {
this._bestRemotesCache.clear();
}
if (e.changed(RepositoryChange.Closed, RepositoryChangeComparisonMode.Any)) {
this.updateContext();
// Send a notification that the repositories changed
queueMicrotask(() => this.fireRepositoriesChanged([], [e.repository]));
} else if (e.changed(RepositoryChange.Opened, RepositoryChangeComparisonMode.Any)) {
this.updateContext();
// Send a notification that the repositories changed
queueMicrotask(() => this.fireRepositoriesChanged([e.repository], []));
}
if (e.changed(RepositoryChange.Remotes, RepositoryChangeComparisonMode.Any)) {
const remotes = await provider.getRemotes(e.repository.path);
const visibilityInfo = this.getVisibilityInfoFromCache(e.repository.path);
if (visibilityInfo != null) {
this.checkVisibilityCachedRemotes(e.repository.path, visibilityInfo, remotes);
}
}
this._onDidChangeRepository.fire(e);
}),
provider.onDidCloseRepository(e => {
const repository = this._repositories.get(e.uri);
if (repository != null) {
repository.closed = true;
}
}),
provider.onDidOpenRepository(e => {
const repository = this._repositories.get(e.uri);
if (repository != null) {
repository.closed = false;
} else {
void this.getOrOpenRepository(e.uri);
}
}),
);
this.fireProvidersChanged([provider]);
// Don't kick off the discovery if we're still initializing (we'll do it at the end for all "known" providers)
if (!this._initializing) {
this.onWorkspaceFoldersChanged({ added: workspace.workspaceFolders ?? [], removed: [] });
}
return {
dispose: () => {
disposable.dispose();
this._providers.delete(id);
const removed: Repository[] = [];
for (const repository of [...this._repositories.values()]) {
if (repository?.provider.id === id) {
this._repositories.remove(repository.uri, false);
removed.push(repository);
}
}
const { deactivating } = this.container;
if (!deactivating) {
this.updateContext();
}
if (removed.length) {
// Defer the event trigger enough to let everything unwind
queueMicrotask(() => {
if (!deactivating) {
this.fireRepositoriesChanged([], removed);
}
removed.forEach(r => r.dispose());
});
}
if (!deactivating) {
this.fireProvidersChanged([], [provider]);
}
},
};
}
private _initializing: boolean = true;
@log({ singleLine: true })
async registrationComplete() {
const scope = getLogScope();
this._initializing = false;
let { workspaceFolders } = workspace;
if (workspaceFolders?.length) {
await this.discoverRepositories(workspaceFolders);
// This is a hack to work around some issue with remote repositories on the web not being discovered on the initial load
if (this.repositoryCount === 0 && isWeb) {
setTimeout(() => {
({ workspaceFolders } = workspace);
if (workspaceFolders?.length) {
void this.discoverRepositories(workspaceFolders, { force: true });
}
}, 1000);
}
} else {
this.updateContext();
}
const autoRepositoryDetection = configuration.getAny<
CoreGitConfiguration,
boolean | 'subFolders' | 'openEditors'
>('git.autoRepositoryDetection');
if (this.container.telemetry.enabled) {
queueMicrotask(() =>
this.container.telemetry.sendEvent('providers/registrationComplete', {
'config.git.autoRepositoryDetection': autoRepositoryDetection,
}),
);
}
setLogScopeExit(
scope,
` ${GlyphChars.Dot} workspaceFolders=${workspaceFolders?.length}, git.autoRepositoryDetection=${autoRepositoryDetection}`,
);
}
getOpenProviders(): GitProvider[] {
const map = this.getOpenRepositoriesByProvider();
return [...map.keys()].map(id => this._providers.get(id)!);
}
getOpenRepositories(id: GitProviderId): Iterable<Repository> {
return filter(this.repositories, r => !r.closed && (id == null || id === r.provider.id));
}
getOpenRepositoriesByProvider(): Map<GitProviderId, Repository[]> {
const repositories = [...filter(this.repositories, r => !r.closed)];
if (repositories.length === 0) return new Map();
return groupByMap(repositories, r => r.provider.id);
}
hasOpenRepositories(id: GitProviderId): boolean {
return some(this.repositories, r => !r.closed && (id == null || id === r.provider.id));
}
private _discoveredWorkspaceFolders = new Map<WorkspaceFolder, Promise<Repository[]>>();
private _isDiscoveringRepositories: Promise<void> | undefined;
get isDiscoveringRepositories(): Promise<void> | undefined {
return this._isDiscoveringRepositories;
}
@log<GitProviderService['discoverRepositories']>({ args: { 0: folders => folders.length } })
async discoverRepositories(folders: readonly WorkspaceFolder[], options?: { force?: boolean }): Promise<void> {
if (this._isDiscoveringRepositories != null) {
await this._isDiscoveringRepositories;
this._isDiscoveringRepositories = undefined;
}
const deferred = defer<void>();
this._isDiscoveringRepositories = deferred.promise;
try {
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 = flatMap<PromiseFulfilledResult<Repository[]>, Repository>(
filter<PromiseSettledResult<Repository[]>, PromiseFulfilledResult<Repository[]>>(
results,
(r): r is PromiseFulfilledResult<Repository[]> => r.status === 'fulfilled',
),
r => r.value,
);
const added: Repository[] = [];
for (const repository of repositories) {
this._repositories.add(repository);
if (!repository.closed) {
added.push(repository);
}
}
this.updateContext();
if (added.length) {
// Defer the event trigger enough to let everything unwind
queueMicrotask(() => this.fireRepositoriesChanged(added));
}
} finally {
deferred.fulfill();
}
}
@debug({ exit: true })
private async discoverRepositoriesCore(folder: WorkspaceFolder): Promise<Repository[]> {
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 [];
}
}
@log()
async findRepositories(
uri: Uri,
options?: { cancellation?: CancellationToken; depth?: number; silent?: boolean },
): Promise<Repository[]> {
const { provider } = this.getProvider(uri);
return provider.discoverRepositories(uri, options);
}
private _subscription: Subscription | undefined;
private async getSubscription(): Promise<Subscription> {
return this._subscription ?? (this._subscription = await this.container.subscription.getSubscription());
}
private _accessCache: Map<string, Promise<RepoFeatureAccess>> &
Map<undefined, Promise<FeatureAccess | RepoFeatureAccess>> = new Map();
async access(feature: PlusFeatures | undefined, repoPath: string | Uri): Promise<RepoFeatureAccess>;
async access(feature?: PlusFeatures, repoPath?: string | Uri): Promise<FeatureAccess | RepoFeatureAccess>;
@debug({ exit: true })
async access(feature?: PlusFeatures, repoPath?: string | Uri): Promise<FeatureAccess | RepoFeatureAccess> {
if (repoPath == null) {
let access = this._accessCache.get(undefined);
if (access == null) {
access = this.accessCore(feature, repoPath);
this._accessCache.set(undefined, access);
}
return access;
}
const { path } = this.getProvider(repoPath);
const cacheKey = path;
let access = this._accessCache.get(cacheKey);
if (access == null) {
access = this.accessCore(feature, repoPath);
this._accessCache.set(cacheKey, access);
}
return access;
}
private async accessCore(feature: PlusFeatures | undefined, repoPath: string | Uri): Promise<RepoFeatureAccess>;
private async accessCore(
feature?: PlusFeatures,
repoPath?: string | Uri,
): Promise<FeatureAccess | RepoFeatureAccess>;
@debug({ exit: true })
private async accessCore(
_feature?: PlusFeatures,
repoPath?: string | Uri,
): Promise<FeatureAccess | RepoFeatureAccess> {
const subscription = await this.getSubscription();
if (this.container.telemetry.enabled) {
queueMicrotask(() => void this.visibility());
}
const plan = subscription.plan.effective.id;
if (isSubscriptionPaidPlan(plan)) {
return { allowed: subscription.account?.verified !== false, subscription: { current: subscription } };
}
function getRepoAccess(
this: GitProviderService,
repoPath: string | Uri,
force: boolean = false,
): Promise<RepoFeatureAccess> {
const { path: cacheKey } = this.getProvider(repoPath);
let access = force ? undefined : this._accessCache.get(cacheKey);
if (access == null) {
access = this.visibility(repoPath).then(
visibility => {
if (visibility === 'private') {
return {
allowed: false,
subscription: { current: subscription, required: SubscriptionPlanId.Pro },
visibility: visibility,
};
}
return {
allowed: true,
subscription: { current: subscription },
visibility: visibility,
};
},
// If there is a failure assume access is allowed
() => ({ allowed: true, subscription: { current: subscription } }),
);
this._accessCache.set(cacheKey, access);
}
return access;
}
if (repoPath == null) {
const repositories = this.openRepositories;
if (repositories.length === 0) {
return { allowed: false, subscription: { current: subscription } };
}
if (repositories.length === 1) {
return getRepoAccess.call(this, repositories[0].path);
}
const visibility = await this.visibility();
switch (visibility) {
case 'private':
return {
allowed: false,
subscription: { current: subscription, required: SubscriptionPlanId.Pro },
visibility: 'private',
};
case 'mixed':
return {
allowed: 'mixed',
subscription: { current: subscription, required: SubscriptionPlanId.Pro },
};
default:
return {
allowed: true,
subscription: { current: subscription },
visibility: 'public',
};
}
}
// Pass force = true to bypass the cache and avoid a promise loop (where we used the cached promise we just created to try to resolve itself 🤦)
return getRepoAccess.call(this, repoPath, true);
}
async ensureAccess(feature: PlusFeatures, repoPath?: string): Promise<void> {
const { allowed, subscription } = await this.access(feature, repoPath);
if (allowed === false) throw new AccessDeniedError(subscription.current, subscription.required);
}
@debug({ exit: true })
supports(repoPath: string | Uri, feature: Features): Promise<boolean> {
const { provider } = this.getProvider(repoPath);
return provider.supports(feature);
}
private _reposVisibilityCache: RepositoriesVisibility | undefined;
private _repoVisibilityCache: Map<string, RepositoryVisibilityInfo> | undefined;
private ensureRepoVisibilityCache(): void {
if (this._repoVisibilityCache == null) {
const repoVisibility: [string, RepositoryVisibilityInfo][] | undefined = this.container.storage
.get('repoVisibility')
?.map<[string, RepositoryVisibilityInfo]>(([key, visibilityInfo]) => [
key,
{
visibility: visibilityInfo.visibility as RepositoryVisibility,
timestamp: visibilityInfo.timestamp,
remotesHash: visibilityInfo.remotesHash,
},
]);
this._repoVisibilityCache = new Map(repoVisibility);
}
}
private clearRepoVisibilityCache(keys?: string[]): void {
if (keys == null) {
this._repoVisibilityCache = undefined;
void this.container.storage.delete('repoVisibility');
} else {
keys?.forEach(key => this._repoVisibilityCache?.delete(key));
const repoVisibility = Array.from(this._repoVisibilityCache?.entries() ?? []);
if (repoVisibility.length === 0) {
void this.container.storage.delete('repoVisibility');
} else {
void this.container.storage.store('repoVisibility', repoVisibility);
}
}
}
@debug<GitProviderService['getVisibilityInfoFromCache']>({ exit: r => `returned ${r?.visibility}` })
private getVisibilityInfoFromCache(key: string): RepositoryVisibilityInfo | undefined {
this.ensureRepoVisibilityCache();
const visibilityInfo = this._repoVisibilityCache?.get(key);
if (visibilityInfo == null) return undefined;
const now = Date.now();
if (now - visibilityInfo.timestamp > 1000 * 60 * 60 * 24 * 30 /* TTL is 30 days */) {
this.clearRepoVisibilityCache([key]);
return undefined;
}
return visibilityInfo;
}
private checkVisibilityCachedRemotes(
key: string,
visibilityInfo: RepositoryVisibilityInfo | undefined,
remotes: GitRemote[],
): boolean {
if (visibilityInfo == null) return true;
if (visibilityInfo.visibility === 'public') {
if (remotes.length == 0 || !remotes.some(r => r.remoteKey === visibilityInfo.remotesHash)) {
this.clearRepoVisibilityCache([key]);
return false;
}
} else if (visibilityInfo.visibility === 'private') {
const remotesHash = getVisibilityCacheKey(remotes);
if (remotesHash !== visibilityInfo.remotesHash) {
this.clearRepoVisibilityCache([key]);
return false;
}
}
return true;
}
private updateVisibilityCache(key: string, visibilityInfo: RepositoryVisibilityInfo): void {
this.ensureRepoVisibilityCache();
this._repoVisibilityCache?.set(key, visibilityInfo);
void this.container.storage.store('repoVisibility', Array.from(this._repoVisibilityCache!.entries()));
}
@debug()
clearAllRepoVisibilityCaches(): void {
this.clearRepoVisibilityCache();
}
@debug()
clearAllOpenRepoVisibilityCaches(): void {
const openRepoProviderPaths = this.openRepositories.map(r => this.getProvider(r.path).path);
this.clearRepoVisibilityCache(openRepoProviderPaths);
}
visibility(): Promise<RepositoriesVisibility>;
visibility(repoPath: string | Uri): Promise<RepositoryVisibility>;
@debug({ exit: true })
async visibility(repoPath?: string | Uri): Promise<RepositoriesVisibility | RepositoryVisibility> {
if (repoPath == null) {
let visibility = this._reposVisibilityCache;
if (visibility == null) {
visibility = await this.visibilityCore();
if (this.container.telemetry.enabled) {
this.container.telemetry.setGlobalAttribute('repositories.visibility', visibility);
this.container.telemetry.sendEvent('repositories/visibility');
}
this._reposVisibilityCache = visibility;
}
return visibility;
}
const { path: cacheKey } = this.getProvider(repoPath);
let visibility = this.getVisibilityInfoFromCache(cacheKey)?.visibility;
if (visibility == null) {
visibility = await this.visibilityCore(repoPath);
if (this.container.telemetry.enabled) {
queueMicrotask(() => {
const repo = this.getRepository(repoPath);
this.container.telemetry.sendEvent('repository/visibility', {
'repository.visibility': visibility,
'repository.id': repo?.idHash,
'repository.scheme': repo?.uri.scheme,
'repository.closed': repo?.closed,
'repository.folder.scheme': repo?.folder?.uri.scheme,
'repository.provider.id': repo?.provider.id,
});
});
}
}
return visibility;
}
private visibilityCore(): Promise<RepositoriesVisibility>;
private visibilityCore(repoPath: string | Uri): Promise<RepositoryVisibility>;
@debug({ exit: true })
private async visibilityCore(repoPath?: string | Uri): Promise<RepositoriesVisibility | RepositoryVisibility> {
async function getRepoVisibility(
this: GitProviderService,
repoPath: string | Uri,
): Promise<RepositoryVisibility> {
const { provider, path } = this.getProvider(repoPath);
const remotes = await provider.getRemotes(path, { sort: true });
const visibilityInfo = this.getVisibilityInfoFromCache(path);
if (visibilityInfo == null || !this.checkVisibilityCachedRemotes(path, visibilityInfo, remotes)) {
const [visibility, remotesHash] = await provider.visibility(path);
if (visibility !== 'local') {
this.updateVisibilityCache(path, {
visibility: visibility,
timestamp: Date.now(),
remotesHash: remotesHash,
});
}
return visibility;
}
return visibilityInfo.visibility;
}
if (repoPath == null) {
const repositories = this.openRepositories;
if (repositories.length === 0) return 'private';
if (repositories.length === 1) {
return getRepoVisibility.call(this, repositories[0].path);
}
let isPublic = false;
let isPrivate = false;
let isLocal = false;
for await (const result of asSettled(repositories.map(r => getRepoVisibility.call(this, r.path)))) {
if (result.status !== 'fulfilled') continue;
if (result.value === 'public') {
if (isLocal || isPrivate) return 'mixed';
isPublic = true;
} else if (result.value === 'local') {
if (isPublic || isPrivate) return 'mixed';
isLocal = true;
} else if (result.value === 'private') {
if (isPublic || isLocal) return 'mixed';
isPrivate = true;
}
}
if (isPublic) return 'public';
if (isLocal) return 'local';
return 'private';
}
return getRepoVisibility.call(this, repoPath);
}
private _context: { enabled: boolean; disabled: boolean } = { enabled: false, disabled: false };
@debug()
async setEnabledContext(enabled: boolean): Promise<void> {
let disabled = !enabled;
// If we think we should be disabled during startup, check if we have a saved value from the last time this repo was loaded
if (!enabled && this._initializing) {
disabled = !(this.container.storage.getWorkspace('assumeRepositoriesOnStartup') ?? false);
}
this.container.telemetry.setGlobalAttribute('enabled', enabled);
if (this._context.enabled === enabled && this._context.disabled === disabled) return;
const promises = [];
if (this._context.enabled !== enabled) {
this._context.enabled = enabled;
promises.push(setContext('gitlens:enabled', enabled));
}
if (this._context.disabled !== disabled) {
this._context.disabled = disabled;
promises.push(setContext('gitlens:disabled', disabled));
}
await Promise.allSettled(promises);
if (!this._initializing) {
void this.container.storage.storeWorkspace('assumeRepositoriesOnStartup', enabled).catch();
}
}
private _sendProviderContextTelemetryDebounced: Deferrable<() => void> | undefined;
private updateContext() {
if (this.container.deactivating) return;
const openRepositoryCount = this.openRepositoryCount;
const hasRepositories = openRepositoryCount !== 0;
void this.setEnabledContext(hasRepositories);
// Don't bother trying to set the values if we're still starting up
if (this._initializing) return;
this.container.telemetry.setGlobalAttributes({
enabled: hasRepositories,
'repositories.count': openRepositoryCount,
});
if (!hasRepositories) return;
// Don't block for the remote context updates (because it can block other downstream requests during initialization)
async function updateRemoteContext(this: GitProviderService) {
const integrations = configuration.get('integrations.enabled');
const telemetryEnabled = this.container.telemetry.enabled;
const remoteProviders = new Set<string>();
let hasRemotes = false;
let hasRichRemotes = false;
let hasConnectedRemotes = false;
if (hasRepositories) {
for (const repo of this._repositories.values()) {
if (telemetryEnabled) {
const remotes = await repo.getRemotes();
for (const remote of remotes) {
remoteProviders.add(remote.provider?.id ?? 'unknown');
}
}
if (!hasConnectedRemotes && integrations) {
hasConnectedRemotes = await repo.hasRichRemote(true);
if (hasConnectedRemotes) {
hasRichRemotes = true;
hasRemotes = true;
}
}
if (!hasRichRemotes && integrations) {
hasRichRemotes = await repo.hasRichRemote();
if (hasRichRemotes) {
hasRemotes = true;
}
}
if (!hasRemotes) {
hasRemotes = await repo.hasRemotes();
}
if (hasRemotes && ((hasRichRemotes && hasConnectedRemotes) || !integrations)) break;
}
}
if (telemetryEnabled) {
this.container.telemetry.setGlobalAttributes({
'repositories.hasRemotes': hasRemotes,
'repositories.hasRichRemotes': hasRichRemotes,
'repositories.hasConnectedRemotes': hasConnectedRemotes,
'repositories.remoteProviders': join(remoteProviders, ','),
});
if (this._sendProviderContextTelemetryDebounced == null) {
this._sendProviderContextTelemetryDebounced = debounce(
() => this.container.telemetry.sendEvent('providers/context'),
2500,
);
}
this._sendProviderContextTelemetryDebounced();
}
await Promise.allSettled([
setContext('gitlens:hasRemotes', hasRemotes),
setContext('gitlens:hasRichRemotes', hasRichRemotes),
setContext('gitlens:hasConnectedRemotes', hasConnectedRemotes),
]);
}
void updateRemoteContext.call(this);
this._providers.forEach(p => p.updateContext?.());
}
private getProvider(repoPath: string | Uri): GitProviderResult {
if (repoPath == null || (typeof repoPath !== 'string' && !this.supportedSchemes.has(repoPath.scheme))) {
debugger;
throw new ProviderNotFoundError(repoPath);
}
let scheme;
if (typeof repoPath === 'string') {
scheme = getScheme(repoPath) ?? Schemes.File;
} else {
({ scheme } = repoPath);
}
const possibleResults = new Set<GitProviderResult>();
for (const provider of this._providers.values()) {
const path = provider.canHandlePathOrUri(scheme, repoPath);
if (path == null) continue;
possibleResults.add({ provider: provider, path: path });
}
if (possibleResults.size === 0) {
debugger;
throw new ProviderNotFoundError(repoPath);
}
// Prefer the provider with an open repository
if (possibleResults.size > 1) {
for (const result of possibleResults) {
if (this.hasOpenRepositories(result.provider.descriptor.id)) {
return result;
}
}
}
return first(possibleResults)!;
}
getAbsoluteUri(pathOrUri: string | Uri, base?: string | Uri): Uri {
if (base == null) {
if (typeof pathOrUri === 'string') {
if (maybeUri(pathOrUri)) return Uri.parse(pathOrUri, true);
// I think it is safe to assume this should be file://
return Uri.file(pathOrUri);
}
return pathOrUri;
}
// Short-circuit if the base is already a Uri and the path is relative
if (typeof base !== 'string' && typeof pathOrUri === 'string') {
const normalized = normalizePath(pathOrUri);
if (!isAbsolute(normalized)) return Uri.joinPath(base, normalized);
}
const { provider } = this.getProvider(base);
return provider.getAbsoluteUri(pathOrUri, base);
}
@log()
async getBestRevisionUri(
repoPath: string | Uri | undefined,
path: string,
ref: string | undefined,
): Promise<Uri | undefined> {
if (repoPath == null || ref === deletedOrMissing) return undefined;
const { provider, path: rp } = this.getProvider(repoPath);
return provider.getBestRevisionUri(rp, provider.getRelativePath(path, rp), ref);
}
getRelativePath(pathOrUri: string | Uri, base: string | Uri): string {
const { provider } = this.getProvider(pathOrUri instanceof Uri ? pathOrUri : base);
return provider.getRelativePath(pathOrUri, base);
}
getRevisionUri(uri: GitUri): Uri;
getRevisionUri(ref: string, path: string, repoPath: string | Uri): Uri;
getRevisionUri(ref: string, file: GitFile, repoPath: string | Uri): Uri;
@log()
getRevisionUri(refOrUri: string | GitUri, pathOrFile?: string | GitFile, repoPath?: string | Uri): Uri {
let path: string;
let ref: string | undefined;
if (typeof refOrUri === 'string') {
ref = refOrUri;
if (typeof pathOrFile === 'string') {
path = pathOrFile;
} else {
path = pathOrFile?.originalPath ?? pathOrFile?.path ?? '';
}
} else {
ref = refOrUri.sha;
repoPath = refOrUri.repoPath!;
path = getBestPath(refOrUri);
}
const { provider, path: rp } = this.getProvider(repoPath!);
return provider.getRevisionUri(rp, provider.getRelativePath(path, rp), ref!);
}
@log()
getWorkingUri(repoPath: string | Uri, uri: Uri) {
const { provider, path } = this.getProvider(repoPath);
return provider.getWorkingUri(path, uri);
}
@log()
addRemote(repoPath: string | Uri, name: string, url: string, options?: { fetch?: boolean }): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.addRemote(path, name, url, options);
}
@log()
pruneRemote(repoPath: string | Uri, name: string): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.pruneRemote(path, name);
}
@log()
removeRemote(repoPath: string | Uri, name: string): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.removeRemote(path, name);
}
@log()
applyChangesToWorkingFile(uri: GitUri, ref1?: string, ref2?: string): Promise<void> {
const { provider } = this.getProvider(uri);
return provider.applyChangesToWorkingFile(uri, ref1, ref2);
}
@log()
checkout(
repoPath: string | Uri,
ref: string,
options?: { createBranch?: string } | { path?: string },
): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.checkout(path, ref, options);
}
@log()
async clone(url: string, parentPath: string): Promise<string | undefined> {
const { provider } = this.getProvider(parentPath);
return provider.clone?.(url, parentPath);
}
@log({ singleLine: true })
resetCaches(...caches: GitCaches[]): void {
if (caches.length === 0 || caches.includes('providers')) {
this._bestRemotesCache.clear();
}
this.container.events.fire('git:cache:reset', { caches: caches });
}
@log<GitProviderService['excludeIgnoredUris']>({ args: { 1: uris => uris.length } })
excludeIgnoredUris(repoPath: string | Uri, uris: Uri[]): Promise<Uri[]> {
const { provider, path } = this.getProvider(repoPath);
return provider.excludeIgnoredUris(path, uris);
}
@gate()
@log()
fetch(
repoPath: string | Uri,
options?: { all?: boolean; branch?: GitBranchReference; prune?: boolean; pull?: boolean; remote?: string },
): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.fetch(path, options);
}
@gate<GitProviderService['fetchAll']>(
(repos, opts) => `${repos == null ? '' : repos.map(r => r.id).join(',')}|${JSON.stringify(opts)}`,
)
@log<GitProviderService['fetchAll']>({ args: { 0: repos => 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()
@log()
pull(
repoPath: string | Uri,
options?: { branch?: GitBranchReference; rebase?: boolean; tags?: boolean },
): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.pull(path, options);
}
@gate<GitProviderService['pullAll']>(
(repos, opts) => `${repos == null ? '' : repos.map(r => r.id).join(',')}|${JSON.stringify(opts)}`,
)
@log<GitProviderService['pullAll']>({ args: { 0: repos => 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()
@log()
push(
repoPath: string | Uri,
options?: { branch?: GitBranchReference; force?: boolean; publish?: { remote: string } },
): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.push(path, options);
}
@gate<GitProviderService['pushAll']>(repos => `${repos == null ? '' : repos.map(r => r.id).join(',')}`)
@log<GitProviderService['pushAll']>({ args: { 0: repos => 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<GitProviderService['getAheadBehindCommitCount']>({ args: { 1: refs => refs.join(',') } })
getAheadBehindCommitCount(
repoPath: string | Uri,
refs: string[],
): Promise<{ ahead: number; behind: number } | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getAheadBehindCommitCount(path, refs);
}
@log<GitProviderService['getBlame']>({ args: { 1: d => d?.isDirty } })
/**
* Returns the blame of a file
* @param uri Uri of the file to blame
* @param document Optional TextDocument to blame the contents of if dirty
*/
async getBlame(uri: GitUri, document?: TextDocument | undefined): Promise<GitBlame | undefined> {
const { provider } = this.getProvider(uri);
return provider.getBlame(uri, document);
}
@log<GitProviderService['getBlameContents']>({ args: { 1: '<contents>' } })
/**
* Returns the blame of a file, using the editor contents (for dirty editors)
* @param uri Uri of the file to blame
* @param contents Contents from the editor to use
*/
async getBlameContents(uri: GitUri, contents: string): Promise<GitBlame | undefined> {
const { provider } = this.getProvider(uri);
return provider.getBlameContents(uri, contents);
}
@log<GitProviderService['getBlameForLine']>({ args: { 2: d => d?.isDirty } })
/**
* Returns the blame of a single line
* @param uri Uri of the file to blame
* @param editorLine Editor line number (0-based) to blame (Git is 1-based)
* @param document Optional TextDocument to blame the contents of if dirty
* @param options.forceSingleLine Forces blame to be for the single line (rather than the whole file)
*/
async getBlameForLine(
uri: GitUri,
editorLine: number,
document?: TextDocument | undefined,
options?: { forceSingleLine?: boolean },
): Promise<GitBlameLine | undefined> {
const { provider } = this.getProvider(uri);
return provider.getBlameForLine(uri, editorLine, document, options);
}
@log<GitProviderService['getBlameForLineContents']>({ args: { 2: '<contents>' } })
/**
* Returns the blame of a single line, using the editor contents (for dirty editors)
* @param uri Uri of the file to blame
* @param editorLine Editor line number (0-based) to blame (Git is 1-based)
* @param contents Contents from the editor to use
* @param options.forceSingleLine Forces blame to be for the single line (rather than the whole file)
*/
async getBlameForLineContents(
uri: GitUri,
editorLine: number,
contents: string,
options?: { forceSingleLine?: boolean },
): Promise<GitBlameLine | undefined> {
const { provider } = this.getProvider(uri);
return provider.getBlameForLineContents(uri, editorLine, contents, options);
}
@log()
async getBlameForRange(uri: GitUri, range: Range): Promise<GitBlameLines | undefined> {
const { provider } = this.getProvider(uri);
return provider.getBlameForRange(uri, range);
}
@log<GitProviderService['getBlameForRangeContents']>({ args: { 2: '<contents>' } })
async getBlameForRangeContents(uri: GitUri, range: Range, contents: string): Promise<GitBlameLines | undefined> {
const { provider } = this.getProvider(uri);
return provider.getBlameForRangeContents(uri, range, contents);
}
@log<GitProviderService['getBlameRange']>({ args: { 0: '<blame>' } })
getBlameRange(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined {
const { provider } = this.getProvider(uri);
return provider.getBlameRange(blame, uri, range);
}
@log()
async getBranch(repoPath: string | Uri | undefined): Promise<GitBranch | undefined> {
if (repoPath == null) return undefined;
const { provider, path } = this.getProvider(repoPath);
return provider.getBranch(path);
}
@log<GitProviderService['getBranchAheadRange']>({ args: { 0: b => b.name } })
async getBranchAheadRange(branch: GitBranch): Promise<string | undefined> {
if (branch.state.ahead > 0) {
return createRevisionRange(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 { values: 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 createRevisionRange(possibleBranch, branch.ref);
}
}
}
return undefined;
}
@log({ args: { 1: false } })
async getBranches(
repoPath: string | Uri | undefined,
options?: {
filter?: (b: GitBranch) => boolean;
sort?: boolean | BranchSortOptions;
},
): Promise<PagedResult<GitBranch>> {
if (repoPath == null) return { values: [] };
const { provider, path } = this.getProvider(repoPath);
return provider.getBranches(path, options);
}
@log()
async getBranchesAndTagsTipsFn(
repoPath: string | Uri | undefined,
currentName?: string,
): Promise<
(sha: string, options?: { compact?: boolean | undefined; icons?: boolean | undefined }) => string | undefined
> {
const [branchesResult, tagsResult] = await Promise.allSettled([
this.getBranches(repoPath),
this.getTags(repoPath),
]);
const branches = getSettledValue(branchesResult)?.values ?? [];
const tags = getSettledValue(tagsResult)?.values ?? [];
const branchesAndTagsBySha = 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()
getChangedFilesCount(repoPath: string | Uri, ref?: string): Promise<GitDiffShortStat | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getChangedFilesCount(path, ref);
}
@log()
async getCommit(repoPath: string | Uri, ref: string): Promise<GitCommit | undefined> {
const { provider, path } = this.getProvider(repoPath);
if (ref === uncommitted || ref === uncommittedStaged) {
const now = new Date();
const user = await this.getCurrentUser(repoPath);
return new GitCommit(
this.container,
path,
ref,
new GitCommitIdentity('You', user?.email ?? undefined, now),
new GitCommitIdentity('You', user?.email ?? undefined, now),
'Uncommitted changes',
[],
'Uncommitted changes',
undefined,
undefined,
[],
);
}
return provider.getCommit(path, ref);
}
@log()
getCommitBranches(
repoPath: string | Uri,
ref: string,
branch?: string | undefined,
options?:
| { all?: boolean; commitDate?: Date; mode?: 'contains' | 'pointsAt' }
| { commitDate?: Date; mode?: 'contains' | 'pointsAt'; remotes?: boolean },
): Promise<string[]> {
const { provider, path } = this.getProvider(repoPath);
return provider.getCommitBranches(path, ref, branch, options);
}
@log()
getCommitCount(repoPath: string | Uri, ref: string): Promise<number | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getCommitCount(path, ref);
}
@log()
async getCommitForFile(
repoPath: string | Uri | undefined,
uri: Uri,
options?: { ref?: string; firstIfNotFound?: boolean; range?: Range },
): Promise<GitCommit | undefined> {
if (repoPath == null) return undefined;
const { provider, path } = this.getProvider(repoPath);
return provider.getCommitForFile(path, uri, options);
}
@log()
getCommitsForGraph(
repoPath: string | Uri,
asWebviewUri: (uri: Uri) => Uri,
options?: {
branch?: string;
include?: { stats?: boolean };
limit?: number;
ref?: string;
},
): Promise<GitGraph> {
const { provider, path } = this.getProvider(repoPath);
return provider.getCommitsForGraph(path, asWebviewUri, options);
}
@log()
getCommitTags(
repoPath: string | Uri,
ref: string,
options?: { commitDate?: Date; mode?: 'contains' | 'pointsAt' },
): Promise<string[]> {
const { provider, path } = this.getProvider(repoPath);
return provider.getCommitTags(path, ref, options);
}
@log()
async getConfig(repoPath: string | Uri, key: string): Promise<string | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getConfig?.(path, key);
}
@log()
async setConfig(repoPath: string | Uri, key: string, value: string | undefined): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.setConfig?.(path, key, value);
}
@log()
async getContributors(
repoPath: string | Uri,
options?: { all?: boolean; ref?: string; stats?: boolean },
): Promise<GitContributor[]> {
if (repoPath == null) return [];
const { provider, path } = this.getProvider(repoPath);
return provider.getContributors(path, options);
}
@gate()
@log()
getCurrentUser(repoPath: string | Uri): Promise<GitUser | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getCurrentUser(path);
}
@log()
async getDefaultBranchName(repoPath: string | Uri | undefined, remote?: string): Promise<string | undefined> {
if (repoPath == null) return undefined;
const { provider, path } = this.getProvider(repoPath);
return provider.getDefaultBranchName(path, remote);
}
@log()
async getDiff(
repoPath: string | Uri,
ref1: string,
ref2?: string,
options?: { context?: number },
): Promise<GitDiff | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getDiff?.(path, ref1, ref2, options);
}
@log()
/**
* Returns a file diff between two commits
* @param uri Uri of the file to diff
* @param ref1 Commit to diff from
* @param ref2 Commit to diff to
*/
getDiffForFile(uri: GitUri, ref1: string | undefined, ref2?: string): Promise<GitDiffFile | undefined> {
const { provider } = this.getProvider(uri);
return provider.getDiffForFile(uri, ref1, ref2);
}
@log<GitProviderService['getDiffForFileContents']>({ args: { 1: '<contents>' } })
/**
* Returns a file diff between a commit and the specified contents
* @param uri Uri of the file to diff
* @param ref Commit to diff from
* @param contents Contents to use for the diff
*/
getDiffForFileContents(uri: GitUri, ref: string, contents: string): Promise<GitDiffFile | undefined> {
const { provider } = this.getProvider(uri);
return provider.getDiffForFileContents(uri, ref, contents);
}
@log()
/**
* Returns a line diff between two commits
* @param uri Uri of the file to diff
* @param editorLine Editor line number (0-based) to blame (Git is 1-based)
* @param ref1 Commit to diff from
* @param ref2 Commit to diff to
*/
getDiffForLine(
uri: GitUri,
editorLine: number,
ref1: string | undefined,
ref2?: string,
): Promise<GitDiffHunkLine | undefined> {
const { provider } = this.getProvider(uri);
return provider.getDiffForLine(uri, editorLine, ref1, ref2);
}
@log()
getDiffStatus(
repoPath: string | Uri,
ref1?: string,
ref2?: string,
options?: { filters?: GitDiffFilter[]; similarityThreshold?: number },
): Promise<GitFile[] | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getDiffStatus(path, ref1, ref2, options);
}
@log()
async getFileStatusForCommit(repoPath: string | Uri, uri: Uri, ref: string): Promise<GitFile | undefined> {
if (ref === deletedOrMissing || isUncommitted(ref)) return undefined;
const { provider, path } = this.getProvider(repoPath);
return provider.getFileStatusForCommit(path, uri, ref);
}
@debug()
getGitDir(repoPath: string | Uri): Promise<GitDir | undefined> {
const { provider, path } = this.getProvider(repoPath);
return Promise.resolve(provider.getGitDir?.(path));
}
@debug()
getLastFetchedTimestamp(repoPath: string | Uri): Promise<number | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getLastFetchedTimestamp(path);
}
@log()
async getLog(
repoPath: string | Uri,
options?: {
all?: boolean;
authors?: GitUser[];
limit?: number;
merges?: boolean;
ordering?: 'date' | 'author-date' | 'topo' | null;
ref?: string;
since?: string;
},
): Promise<GitLog | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getLog(path, options);
}
@log()
async getLogRefsOnly(
repoPath: string | Uri,
options?: {
authors?: GitUser[];
limit?: number;
merges?: boolean;
ordering?: 'date' | 'author-date' | 'topo' | null;
ref?: string;
since?: string;
},
): Promise<Set<string> | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getLogRefsOnly(path, options);
}
@log()
async getLogForFile(
repoPath: string | Uri | undefined,
pathOrUri: string | Uri,
options?: {
all?: boolean;
force?: boolean;
limit?: number;
ordering?: 'date' | 'author-date' | 'topo' | null;
range?: Range;
ref?: string;
renames?: boolean;
reverse?: boolean;
since?: string;
skip?: number;
},
): Promise<GitLog | undefined> {
if (repoPath == null) return undefined;
const { provider, path } = this.getProvider(repoPath);
return provider.getLogForFile(path, pathOrUri, options);
}
@log()
async getMergeBase(
repoPath: string | Uri,
ref1: string,
ref2: string,
options?: { forkPoint?: boolean },
): Promise<string | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getMergeBase(path, ref1, ref2, options);
}
@gate()
@log()
async getMergeStatus(repoPath: string | Uri): Promise<GitMergeStatus | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getMergeStatus(path);
}
@gate()
@log()
async getRebaseStatus(repoPath: string | Uri): Promise<GitRebaseStatus | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getRebaseStatus(path);
}
@log()
getNextComparisonUris(
repoPath: string | Uri,
uri: Uri,
ref: string | undefined,
skip: number = 0,
): Promise<NextComparisonUrisResult | undefined> {
if (!ref) return Promise.resolve(undefined);
const { provider, path } = this.getProvider(repoPath);
return provider.getNextComparisonUris(path, uri, ref, skip);
}
@log()
async getOldestUnpushedRefForFile(repoPath: string | Uri, uri: Uri): Promise<string | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getOldestUnpushedRefForFile(path, uri);
}
@log()
getPreviousComparisonUris(
repoPath: string | Uri,
uri: Uri,
ref: string | undefined,
skip: number = 0,
): Promise<PreviousComparisonUrisResult | undefined> {
if (ref === deletedOrMissing) return Promise.resolve(undefined);
const { provider, path } = this.getProvider(repoPath);
return provider.getPreviousComparisonUris(path, uri, ref, skip);
}
@log()
getPreviousComparisonUrisForLine(
repoPath: string | Uri,
uri: Uri,
editorLine: number,
ref: string | undefined,
skip: number = 0,
): Promise<PreviousLineComparisonUrisResult | undefined> {
if (ref === deletedOrMissing) return Promise.resolve(undefined);
const { provider, path } = this.getProvider(repoPath);
return provider.getPreviousComparisonUrisForLine(path, uri, editorLine, ref, skip);
}
@debug<GitProviderService['getMyPullRequests']>({ args: { 0: remoteOrProvider => remoteOrProvider.name } })
async getMyPullRequests(
remoteOrProvider: GitRemote | RichRemoteProvider,
options?: { timeout?: number },
): Promise<SearchedPullRequest[] | undefined> {
let provider;
if (GitRemote.is(remoteOrProvider)) {
({ provider } = remoteOrProvider);
if (!provider?.hasRichIntegration()) return undefined;
} else {
provider = remoteOrProvider;
}
let timeout;
if (options != null) {
({ timeout, ...options } = options);
}
let promiseOrPRs = provider.searchMyPullRequests();
if (promiseOrPRs == null || !isPromise(promiseOrPRs)) {
return promiseOrPRs;
}
if (timeout != null && timeout > 0) {
promiseOrPRs = cancellable(promiseOrPRs, timeout);
}
try {
return await promiseOrPRs;
} catch (ex) {
if (ex instanceof PromiseCancelledError) throw ex;
return undefined;
}
}
@debug<GitProviderService['getMyIssues']>({ args: { 0: remoteOrProvider => remoteOrProvider.name } })
async getMyIssues(
remoteOrProvider: GitRemote | RichRemoteProvider,
options?: { timeout?: number },
): Promise<SearchedIssue[] | undefined> {
let provider;
if (GitRemote.is(remoteOrProvider)) {
({ provider } = remoteOrProvider);
if (!provider?.hasRichIntegration()) return undefined;
} else {
provider = remoteOrProvider;
}
let timeout;
if (options != null) {
({ timeout, ...options } = options);
}
let promiseOrPRs = provider.searchMyIssues();
if (promiseOrPRs == null || !isPromise(promiseOrPRs)) {
return promiseOrPRs;
}
if (timeout != null && timeout > 0) {
promiseOrPRs = cancellable(promiseOrPRs, timeout);
}
try {
return await promiseOrPRs;
} catch (ex) {
if (ex instanceof PromiseCancelledError) throw ex;
return undefined;
}
}
@log()
async getIncomingActivity(
repoPath: string | Uri,
options?: {
all?: boolean;
branch?: string;
limit?: number;
ordering?: 'date' | 'author-date' | 'topo' | null;
skip?: number;
},
): Promise<GitReflog | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getIncomingActivity(path, options);
}
@log()
async getBestRemoteWithProvider(
repoPath: string | Uri,
cancellation?: CancellationToken,
): Promise<GitRemote<RemoteProvider> | undefined> {
const remotes = await this.getBestRemotesWithProviders(repoPath, cancellation);
return remotes[0];
}
@log()
async getBestRemotesWithProviders(
repoPath: string | Uri,
cancellation?: CancellationToken,
): Promise<GitRemote<RemoteProvider>[]> {
if (repoPath == null) return [];
if (typeof repoPath === 'string') {
repoPath = this.getAbsoluteUri(repoPath);
}
const cacheKey = asRepoComparisonKey(repoPath);
let remotes = this._bestRemotesCache.get(cacheKey);
if (remotes == null) {
async function getBest(this: GitProviderService) {
const remotes = await this.getRemotesWithProviders(repoPath, { sort: true }, cancellation);
if (remotes.length === 0) return [];
if (remotes.length === 1) return [...remotes];
if (cancellation?.isCancellationRequested) throw new CancellationError();
const defaultRemote = remotes.find(r => r.default)?.name;
const currentBranchRemote = (await this.getBranch(remotes[0].repoPath))?.getRemoteName();
const weighted: [number, GitRemote<RemoteProvider>][] = [];
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;
}
// 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())))
) {
if (cancellation?.isCancellationRequested) throw new CancellationError();
const repo = await p.getRepositoryMetadata(cancellation);
if (cancellation?.isCancellationRequested) throw new CancellationError();
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;
}
}
}
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]);
}
remotes = getBest.call(this);
this._bestRemotesCache.set(cacheKey, remotes);
}
return [...(await remotes)];
}
@log()
async getBestRemoteWithRichProvider(
repoPath: string | Uri,
options?: { includeDisconnected?: boolean },
cancellation?: CancellationToken,
): Promise<GitRemote<RichRemoteProvider> | undefined> {
const remotes = await this.getBestRemotesWithProviders(repoPath, cancellation);
const includeDisconnected = options?.includeDisconnected ?? false;
for (const r of remotes) {
if (r.hasRichIntegration()) {
if (includeDisconnected || r.provider.maybeConnected === true) return r;
if (r.provider.maybeConnected === undefined && r.default) {
if (await r.provider.isConnected()) return r;
}
}
}
return undefined;
}
@log()
async getRemotes(
repoPath: string | Uri,
options?: { sort?: boolean },
_cancellation?: CancellationToken,
): Promise<GitRemote[]> {
if (repoPath == null) return [];
const { provider, path } = this.getProvider(repoPath);
return provider.getRemotes(path, options);
}
@log()
async getRemotesWithProviders(
repoPath: string | Uri,
options?: { sort?: boolean },
cancellation?: CancellationToken,
): Promise<GitRemote<RemoteProvider>[]> {
const remotes = await this.getRemotes(repoPath, options, cancellation);
return remotes.filter((r: GitRemote): r is GitRemote<RemoteProvider> => r.provider != null);
}
@log()
async getRemotesWithRichProviders(
repoPath: string | Uri,
options?: { sort?: boolean },
cancellation?: CancellationToken,
): Promise<GitRemote<RichRemoteProvider>[]> {
const remotes = await this.getRemotes(repoPath, options, cancellation);
return remotes.filter((r: GitRemote): r is GitRemote<RichRemoteProvider> => r.hasRichIntegration());
}
getBestRepository(): Repository | undefined;
// eslint-disable-next-line @typescript-eslint/unified-signatures
getBestRepository(uri?: Uri, editor?: TextEditor): Repository | undefined;
// eslint-disable-next-line @typescript-eslint/unified-signatures
getBestRepository(editor?: TextEditor): Repository | undefined;
@log({ exit: true })
getBestRepository(editorOrUri?: TextEditor | Uri, editor?: TextEditor): Repository | undefined {
const count = this.repositoryCount;
if (count === 0) return undefined;
if (count === 1) return this.highlander;
if (editorOrUri != null && editorOrUri instanceof Uri) {
const repo = this.getRepository(editorOrUri);
if (repo != null) return repo;
editorOrUri = undefined;
}
editor = editorOrUri ?? editor ?? window.activeTextEditor;
return (editor != null ? this.getRepository(editor.document.uri) : undefined) ?? this.highlander;
}
getBestRepositoryOrFirst(): Repository | undefined;
// eslint-disable-next-line @typescript-eslint/unified-signatures
getBestRepositoryOrFirst(uri?: Uri, editor?: TextEditor): Repository | undefined;
// eslint-disable-next-line @typescript-eslint/unified-signatures
getBestRepositoryOrFirst(editor?: TextEditor): Repository | undefined;
@log({ exit: true })
getBestRepositoryOrFirst(editorOrUri?: TextEditor | Uri, editor?: TextEditor): Repository | undefined {
const count = this.repositoryCount;
if (count === 0) return undefined;
if (count === 1) return first(this._repositories.values());
if (editorOrUri != null && editorOrUri instanceof Uri) {
const repo = this.getRepository(editorOrUri);
if (repo != null) return repo;
editorOrUri = undefined;
}
editor = editorOrUri ?? editor ?? window.activeTextEditor;
return (
(editor != null ? this.getRepository(editor.document.uri) : undefined) ?? first(this._repositories.values())
);
}
getOrOpenRepository(
uri: Uri,
options?: { closeOnOpen?: boolean; detectNested?: boolean; force?: boolean },
): Promise<Repository | undefined>;
getOrOpenRepository(
path: string,
options?: { closeOnOpen?: boolean; detectNested?: boolean; force?: boolean },
): Promise<Repository | undefined>;
getOrOpenRepository(
pathOrUri: string | Uri,
options?: { closeOnOpen?: boolean; detectNested?: boolean; force?: boolean },
): Promise<Repository | undefined>;
@log({ exit: true })
async getOrOpenRepository(
pathOrUri?: string | Uri,
options?: { closeOnOpen?: boolean; detectNested?: boolean; force?: boolean },
): Promise<Repository | undefined> {
if (pathOrUri == null) return undefined;
const scope = getLogScope();
let uri: Uri;
if (typeof pathOrUri === 'string') {
if (!pathOrUri) return undefined;
uri = this.getAbsoluteUri(pathOrUri);
} else {
uri = pathOrUri;
}
const path = getBestPath(uri);
let repository: Repository | undefined;
repository = this.getRepository(uri);
if (repository == null && this._isDiscoveringRepositories != null) {
await this._isDiscoveringRepositories;
repository = this.getRepository(uri);
}
let isDirectory: boolean | undefined;
const detectNested = options?.detectNested ?? configuration.get('detectNestedRepositories', uri);
if (!detectNested) {
if (repository != null) return repository;
} else if (!options?.force && this._visitedPaths.has(path)) {
return repository;
} else {
const stats = await workspace.fs.stat(uri);
// If the uri isn't a directory, go up one level
if ((stats.type & FileType.Directory) !== FileType.Directory) {
uri = Uri.joinPath(uri, '..');
if (!options?.force && this._visitedPaths.has(getBestPath(uri))) return repository;
}
isDirectory = true;
}
const key = asRepoComparisonKey(uri);
let promise = this._pendingRepositories.get(key);
if (promise == null) {
async function findRepository(this: GitProviderService): Promise<Repository | undefined> {
const { provider } = this.getProvider(uri);
const repoUri = await provider.findRepositoryUri(uri, isDirectory);
this._visitedPaths.set(path);
if (repoUri == null) return undefined;
let root: Repository | undefined;
if (this._repositories.count !== 0) {
repository = this._repositories.get(repoUri);
if (repository != null) return repository;
// If this new repo is inside one of our known roots and we we don't already know about, add it
root = this._repositories.getClosest(provider.getAbsoluteUri(uri, repoUri));
}
const autoRepositoryDetection =
configuration.getAny<CoreGitConfiguration, boolean | 'subFolders' | 'openEditors'>(
'git.autoRepositoryDetection',
) ?? true;
const closed =
options?.closeOnOpen ??
(autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors');
Logger.log(scope, `Repository found in '${repoUri.toString(true)}'`);
const repositories = provider.openRepository(root?.folder, repoUri, false, undefined, closed);
const added: Repository[] = [];
for (const repository of repositories) {
this._repositories.add(repository);
if (!repository.closed) {
added.push(repository);
}
}
this._pendingRepositories.delete(key);
this.updateContext();
if (added.length) {
// Send a notification that the repositories changed
queueMicrotask(() => this.fireRepositoriesChanged(added));
}
repository = repositories.length === 1 ? repositories[0] : this.getRepository(uri);
return repository;
}
promise = findRepository.call(this);
this._pendingRepositories.set(key, promise);
}
return promise;
}
@log<GitProviderService['getOrOpenRepositoryForEditor']>({
args: { 0: e => (e != null ? `TextEditor(${Logger.toLoggable(e.document.uri)})` : undefined) },
})
async getOrOpenRepositoryForEditor(editor?: TextEditor): Promise<Repository | undefined> {
editor = editor ?? window.activeTextEditor;
if (editor == null) return this.highlander;
return this.getOrOpenRepository(editor.document.uri);
}
getRepository(uri: Uri): Repository | undefined;
getRepository(path: string): Repository | undefined;
getRepository(pathOrUri: string | Uri): Repository | undefined;
@log({ exit: true })
getRepository(pathOrUri?: string | Uri): Repository | undefined {
if (this.repositoryCount === 0) return undefined;
if (pathOrUri == null) return undefined;
if (typeof pathOrUri === 'string') {
if (!pathOrUri) return undefined;
return this._repositories.getClosest(this.getAbsoluteUri(pathOrUri));
}
return this._repositories.getClosest(pathOrUri);
}
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<GitStash | undefined> {
if (repoPath == null) return undefined;
const { provider, path } = this.getProvider(repoPath);
return provider.getStash(path);
}
@log()
async getStatusForFile(repoPath: string | Uri, uri: Uri): Promise<GitStatusFile | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getStatusForFile(path, uri);
}
@log()
async getStatusForFiles(repoPath: string | Uri, pathOrGlob: Uri): Promise<GitStatusFile[] | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getStatusForFiles(path, pathOrGlob);
}
@log()
async getStatusForRepo(repoPath: string | Uri | undefined): Promise<GitStatus | undefined> {
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?: { cursor?: string; filter?: (t: GitTag) => boolean; sort?: boolean | TagSortOptions },
): Promise<PagedResult<GitTag>> {
if (repoPath == null) return { values: [] };
const { provider, path } = this.getProvider(repoPath);
return provider.getTags(path, options);
}
@log()
async getTreeEntryForRevision(
repoPath: string | Uri | undefined,
path: string,
ref: string,
): Promise<GitTreeEntry | undefined> {
if (repoPath == null || !path) return undefined;
const { provider, path: rp } = this.getProvider(repoPath);
return provider.getTreeEntryForRevision(rp, provider.getRelativePath(path, rp), ref);
}
@log()
async getTreeForRevision(repoPath: string | Uri | undefined, ref: string): Promise<GitTreeEntry[]> {
if (repoPath == null) return [];
const { provider, path } = this.getProvider(repoPath);
return provider.getTreeForRevision(path, ref);
}
@gate()
@log()
getRevisionContent(repoPath: string | Uri, path: string, ref: string): Promise<Uint8Array | undefined> {
const { provider, path: rp } = this.getProvider(repoPath);
return provider.getRevisionContent(rp, path, ref);
}
@log()
async getFirstCommitSha(repoPath: string | Uri): Promise<string | undefined> {
const { provider, path } = this.getProvider(repoPath);
try {
return await provider.getFirstCommitSha?.(path);
} catch {
return undefined;
}
}
@log()
getUniqueRepositoryId(repoPath: string | Uri): Promise<string | undefined> {
return this.getFirstCommitSha(repoPath);
}
@log({ args: { 1: false } })
async hasBranchOrTag(
repoPath: string | Uri | undefined,
options?: {
filter?: { branches?: (b: GitBranch) => boolean; tags?: (t: GitTag) => boolean };
},
): Promise<boolean> {
if (repoPath == null) return false;
const { provider, path } = this.getProvider(repoPath);
return provider.hasBranchOrTag(path, options);
}
@log({ args: { 1: false } })
async hasCommitBeenPushed(repoPath: string | Uri, ref: string): Promise<boolean> {
if (repoPath == null) return false;
const { provider, path } = this.getProvider(repoPath);
return provider.hasCommitBeenPushed(path, ref);
}
@log()
async hasRemotes(repoPath: string | Uri | undefined): Promise<boolean> {
if (repoPath == null) return false;
const repository = this.getRepository(repoPath);
if (repository == null) return false;
return repository.hasRemotes();
}
@log()
async hasTrackingBranch(repoPath: string | undefined): Promise<boolean> {
if (repoPath == null) return false;
const repository = this.getRepository(repoPath);
if (repository == null) return false;
return repository.hasUpstreamBranch();
}
@log()
hasUnsafeRepositories(): boolean {
for (const provider of this._providers.values()) {
if (provider.hasUnsafeRepositories?.()) return true;
}
return false;
}
@log<GitProviderService['isRepositoryForEditor']>({
args: {
0: r => r.uri.toString(true),
1: e => (e != null ? `TextEditor(${Logger.toLoggable(e.document.uri)})` : undefined),
},
})
isRepositoryForEditor(repository: Repository, editor?: TextEditor): boolean {
editor = editor ?? window.activeTextEditor;
if (editor == null) return false;
return repository === this.getRepository(editor.document.uri);
}
isTrackable(uri: Uri): boolean {
if (!this.supportedSchemes.has(uri.scheme)) return false;
const { provider } = this.getProvider(uri);
return provider.isTrackable(uri);
}
async isTracked(uri: Uri): Promise<boolean> {
if (!this.supportedSchemes.has(uri.scheme)) return false;
const { provider } = this.getProvider(uri);
return provider.isTracked(uri);
}
@log()
async getDiffTool(repoPath?: string | Uri): Promise<string | undefined> {
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<void> {
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<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.openDirectoryCompare(path, ref1, ref2, tool);
}
async resolveReference(
repoPath: string | Uri,
ref: string,
path?: string,
options?: { force?: boolean; timeout?: number },
): Promise<string>;
async resolveReference(
repoPath: string | Uri,
ref: string,
uri?: Uri,
options?: { force?: boolean; timeout?: number },
): Promise<string>;
@gate()
@log()
async resolveReference(
repoPath: string | Uri,
ref: string,
pathOrUri?: string | Uri,
options?: { timeout?: number },
) {
if (pathOrUri != null && isUncommittedParent(ref)) {
ref = 'HEAD';
}
if (
!ref ||
ref === deletedOrMissing ||
(pathOrUri == null && isSha(ref)) ||
(pathOrUri != null && isUncommitted(ref))
) {
return ref;
}
const { provider, path } = this.getProvider(repoPath);
return provider.resolveReference(path, ref, pathOrUri, options);
}
@log<GitProviderService['richSearchCommits']>({
args: {
1: s =>
`[${s.matchAll ? 'A' : ''}${s.matchCase ? 'C' : ''}${s.matchRegex ? 'R' : ''}]: ${
s.query.length > 500 ? `${s.query.substring(0, 500)}...` : s.query
}`,
},
})
async richSearchCommits(
repoPath: string | Uri,
search: SearchQuery,
options?: { limit?: number; ordering?: 'date' | 'author-date' | 'topo' | null; skip?: number },
): Promise<GitLog | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.richSearchCommits(path, search, options);
}
@log()
searchCommits(
repoPath: string | Uri,
search: SearchQuery,
options?: {
cancellation?: CancellationToken;
limit?: number;
ordering?: 'date' | 'author-date' | 'topo';
},
): Promise<GitSearch> {
const { provider, path } = this.getProvider(repoPath);
return provider.searchCommits(path, search, options);
}
@log({ args: false })
async runGitCommandViaTerminal(
repoPath: string | Uri,
command: string,
args: string[],
options?: { execute?: boolean },
): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.runGitCommandViaTerminal?.(path, command, args, options);
}
@log()
validateBranchOrTagName(repoPath: string | Uri, ref: string): Promise<boolean> {
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 === deletedOrMissing || isUncommitted(ref)) return true;
const { provider, path } = this.getProvider(repoPath);
return provider.validateReference(path, ref);
}
stageFile(repoPath: string | Uri, path: string): Promise<void>;
stageFile(repoPath: string | Uri, uri: Uri): Promise<void>;
@log()
stageFile(repoPath: string | Uri, pathOrUri: string | Uri): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.stageFile(path, pathOrUri);
}
stageDirectory(repoPath: string | Uri, directory: string): Promise<void>;
stageDirectory(repoPath: string | Uri, uri: Uri): Promise<void>;
@log()
stageDirectory(repoPath: string | Uri, directoryOrUri: string | Uri): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.stageDirectory(path, directoryOrUri);
}
unstageFile(repoPath: string | Uri, path: string): Promise<void>;
unstageFile(repoPath: string | Uri, uri: Uri): Promise<void>;
@log()
unstageFile(repoPath: string | Uri, pathOrUri: string | Uri): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.unstageFile(path, pathOrUri);
}
unstageDirectory(repoPath: string | Uri, directory: string): Promise<void>;
unstageDirectory(repoPath: string | Uri, uri: Uri): Promise<void>;
@log()
unstageDirectory(repoPath: string | Uri, directoryOrUri: string | Uri): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.unstageDirectory(path, directoryOrUri);
}
@log()
async stashApply(repoPath: string | Uri, stashName: string, options?: { deleteAfter?: boolean }): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.stashApply?.(path, stashName, options);
}
@log()
async stashDelete(repoPath: string | Uri, stashName: string, ref?: string): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.stashDelete?.(path, stashName, ref);
}
@log()
async stashRename(
repoPath: string | Uri,
stashName: string,
ref: string,
message: string,
stashOnRef?: string,
): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.stashRename?.(path, stashName, ref, message, stashOnRef);
}
@log<GitProviderService['stashSave']>({ args: { 2: uris => uris?.length } })
async stashSave(
repoPath: string | Uri,
message?: string,
uris?: Uri[],
options?: { includeUntracked?: boolean; keepIndex?: boolean; onlyStaged?: boolean },
): Promise<void> {
const { provider, path } = this.getProvider(repoPath);
return provider.stashSave?.(path, message, uris, options);
}
@log()
createWorktree(
repoPath: string | Uri,
path: string,
options?: { commitish?: string; createBranch?: string; detach?: boolean; force?: boolean },
): Promise<void> {
const { provider, path: rp } = this.getProvider(repoPath);
return Promise.resolve(provider.createWorktree?.(rp, path, options));
}
@log()
async getWorktree(
repoPath: string | Uri,
predicate: (w: GitWorktree) => boolean,
): Promise<GitWorktree | undefined> {
const { provider, path } = this.getProvider(repoPath);
return ((await provider.getWorktrees?.(path)) ?? []).find(predicate);
}
@log()
async getWorktrees(repoPath: string | Uri): Promise<GitWorktree[]> {
const { provider, path } = this.getProvider(repoPath);
return (await provider.getWorktrees?.(path)) ?? [];
}
@log()
async getWorktreesDefaultUri(path: string | Uri): Promise<Uri | undefined> {
const { provider, path: rp } = this.getProvider(path);
let defaultUri = await provider.getWorktreesDefaultUri?.(rp);
if (defaultUri != null) return defaultUri;
// If we don't have a default set, default it to the parent folder of the repo folder
defaultUri = this.getRepository(rp)?.uri;
if (defaultUri != null) {
defaultUri = Uri.joinPath(defaultUri, '..');
}
return defaultUri;
}
@log()
deleteWorktree(repoPath: string | Uri, path: string, options?: { force?: boolean }): Promise<void> {
const { provider, path: rp } = this.getProvider(repoPath);
return Promise.resolve(provider.deleteWorktree?.(rp, path, options));
}
@log()
async getOpenScmRepositories(): Promise<ScmRepository[]> {
const results = await Promise.allSettled([...this._providers.values()].map(p => p.getOpenScmRepositories()));
const repositories = flatMap<PromiseFulfilledResult<ScmRepository[]>, ScmRepository>(
filter<PromiseSettledResult<ScmRepository[]>, PromiseFulfilledResult<ScmRepository[]>>(
results,
(r): r is PromiseFulfilledResult<ScmRepository[]> => r.status === 'fulfilled',
),
r => r.value,
);
return [...repositories];
}
@log()
getScmRepository(repoPath: string | Uri): Promise<ScmRepository | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getScmRepository(path);
}
@log()
getOrOpenScmRepository(repoPath: string | Uri): Promise<ScmRepository | undefined> {
const { provider, path } = this.getProvider(repoPath);
return provider.getOrOpenScmRepository(path);
}
}