|
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);
|
|
}
|
|
}
|