瀏覽代碼

Adds new repositories trie for fast repo lookups

Adds new visited paths trie for fast lookup of seen paths (to avoid git calls)
Replaces the tracked cache with a paths trie
All tries are case aware based on os and scheme (if uri based)

Adds new detectNestedRepositories setting to improve performance
Automatically handles nested repos for open document and remaps paths
Reworks isTracked for perf & nested repo detection

Changes excludes to a Set for faster matching
Splits getRepository into get & create (also removes async for get)
Replaces repo path methods with repo versions (part of moving away from string paths)
Renames getRemotes to getRemotesWithProviders & getRemotesCore to getRemotes
main
Eric Amodio 2 年之前
父節點
當前提交
138c1bd554
共有 44 個檔案被更改,包括 819 行新增601 行删除
  1. +8
    -1
      package.json
  2. +1
    -1
      src/commands/addAuthors.ts
  3. +5
    -3
      src/commands/common.ts
  4. +1
    -1
      src/commands/copyMessageToClipboard.ts
  5. +1
    -1
      src/commands/copyShaToClipboard.ts
  6. +1
    -1
      src/commands/createPullRequestOnRemote.ts
  7. +2
    -2
      src/commands/externalDiff.ts
  8. +1
    -1
      src/commands/git/coauthors.ts
  9. +6
    -4
      src/commands/gitCommands.actions.ts
  10. +1
    -1
      src/commands/openFileOnRemote.ts
  11. +2
    -1
      src/commands/openOnRemote.ts
  12. +10
    -7
      src/commands/quickCommand.steps.ts
  13. +6
    -3
      src/commands/remoteProviders.ts
  14. +2
    -2
      src/commands/stashSave.ts
  15. +1
    -0
      src/config.ts
  16. +6
    -6
      src/env/node/git/git.ts
  17. +288
    -239
      src/env/node/git/localGitProvider.ts
  18. +7
    -10
      src/git/gitProvider.ts
  19. +133
    -221
      src/git/gitProviderService.ts
  20. +39
    -18
      src/git/gitUri.ts
  21. +6
    -6
      src/git/models/branch.ts
  22. +1
    -1
      src/git/models/repository.ts
  23. +1
    -1
      src/git/models/status.ts
  24. +1
    -1
      src/hovers/hovers.ts
  25. +9
    -12
      src/quickpicks/referencePicker.ts
  26. +59
    -0
      src/repositories.ts
  27. +2
    -2
      src/system/iterable.ts
  28. +42
    -11
      src/system/path.ts
  29. +0
    -15
      src/system/string.ts
  30. +155
    -0
      src/test/suite/system/path.test.ts
  31. +1
    -1
      src/trackers/trackedDocument.ts
  32. +1
    -1
      src/views/nodes/branchNode.ts
  33. +2
    -2
      src/views/nodes/branchTrackingStatusNode.ts
  34. +1
    -1
      src/views/nodes/commitNode.ts
  35. +2
    -2
      src/views/nodes/comparePickerNode.ts
  36. +2
    -2
      src/views/nodes/compareResultsNode.ts
  37. +2
    -2
      src/views/nodes/fileHistoryNode.ts
  38. +1
    -1
      src/views/nodes/fileRevisionAsCommitNode.ts
  39. +2
    -2
      src/views/nodes/lineHistoryNode.ts
  40. +1
    -1
      src/views/nodes/repositoryNode.ts
  41. +1
    -1
      src/views/nodes/searchResultsNode.ts
  42. +3
    -10
      src/views/nodes/viewNode.ts
  43. +2
    -2
      src/views/viewCommands.ts
  44. +1
    -1
      src/webviews/rebaseEditor.ts

+ 8
- 1
package.json 查看文件

@ -2797,6 +2797,13 @@
"title": "Advanced",
"order": 1000,
"properties": {
"gitlens.detectNestedRepositories": {
"type": "boolean",
"default": false,
"markdownDescription": "Specifies whether to attempt to detect nested repositories when opening files",
"scope": "resource",
"order": 0
},
"gitlens.advanced.messages": {
"type": "object",
"default": {
@ -2878,7 +2885,7 @@
"additionalProperties": false,
"markdownDescription": "Specifies which messages should be suppressed",
"scope": "window",
"order": 0
"order": 1
},
"gitlens.advanced.repositorySearchDepth": {
"type": "number",

+ 1
- 1
src/commands/addAuthors.ts 查看文件

@ -13,7 +13,7 @@ export class AddAuthorsCommand extends Command {
async execute(sourceControl: SourceControl) {
let repo;
if (sourceControl?.rootUri != null) {
repo = await this.container.git.getRepository(sourceControl.rootUri);
repo = this.container.git.getRepository(sourceControl.rootUri);
}
return executeGitCommand({

+ 5
- 3
src/commands/common.ts 查看文件

@ -238,8 +238,8 @@ export function getCommandUri(uri?: Uri, editor?: TextEditor): Uri | undefined {
}
export async function getRepoPathOrActiveOrPrompt(uri: Uri | undefined, editor: TextEditor | undefined, title: string) {
const repoPath = await Container.instance.git.getRepoPathOrActive(uri, editor);
if (repoPath) return repoPath;
const repository = Container.instance.git.getBestRepository(uri, editor);
if (repository != null) return repository.path;
const pick = await RepositoryPicker.show(title);
if (pick instanceof CommandQuickPickItem) {
@ -251,7 +251,9 @@ export async function getRepoPathOrActiveOrPrompt(uri: Uri | undefined, editor:
}
export async function getRepoPathOrPrompt(title: string, uri?: Uri) {
const repoPath = await Container.instance.git.getRepoPath(uri);
if (uri == null) return Container.instance.git.highlander?.path;
const repoPath = (await Container.instance.git.getOrCreateRepository(uri))?.path;
if (repoPath) return repoPath;
const pick = await RepositoryPicker.show(title);

+ 1
- 1
src/commands/copyMessageToClipboard.ts 查看文件

@ -53,7 +53,7 @@ export class CopyMessageToClipboardCommand extends ActiveEditorCommand {
let repoPath;
// If we don't have an editor then get the message of the last commit to the branch
if (uri == null) {
repoPath = await this.container.git.getActiveRepoPath(editor);
repoPath = this.container.git.getBestRepository(editor)?.path;
if (!repoPath) return;
const log = await this.container.git.getLog(repoPath, { limit: 1 });

+ 1
- 1
src/commands/copyShaToClipboard.ts 查看文件

@ -53,7 +53,7 @@ export class CopyShaToClipboardCommand extends ActiveEditorCommand {
try {
// If we don't have an editor then get the sha of the last commit to the branch
if (uri == null) {
const repoPath = await this.container.git.getActiveRepoPath(editor);
const repoPath = this.container.git.getBestRepository(editor)?.path;
if (!repoPath) return;
const log = await this.container.git.getLog(repoPath, { limit: 1 });

+ 1
- 1
src/commands/createPullRequestOnRemote.ts 查看文件

@ -23,7 +23,7 @@ export class CreatePullRequestOnRemoteCommand extends Command {
async execute(args?: CreatePullRequestOnRemoteCommandArgs) {
if (args?.repoPath == null) return;
const repo = await this.container.git.getRepository(args.repoPath);
const repo = this.container.git.getRepository(args.repoPath);
if (repo == null) return;
const compareRemote = await repo.getRemote(args.remote);

+ 2
- 2
src/commands/externalDiff.ts 查看文件

@ -130,7 +130,7 @@ export class ExternalDiffCommand extends Command {
const editor = window.activeTextEditor;
if (editor == null) return;
repoPath = await this.container.git.getRepoPathOrActive(undefined, editor);
repoPath = this.container.git.getBestRepository(editor)?.path;
if (!repoPath) return;
const uri = editor.document.uri;
@ -150,7 +150,7 @@ export class ExternalDiffCommand extends Command {
args.files.push({ uri: status.uri, staged: false });
}
} else {
repoPath = await this.container.git.getRepoPath(args.files[0].uri.fsPath);
repoPath = (await this.container.git.getOrCreateRepository(args.files[0].uri))?.path;
if (!repoPath) return;
}

+ 1
- 1
src/commands/git/coauthors.ts 查看文件

@ -110,7 +110,7 @@ export class CoAuthorsGitCommand extends QuickCommand {
);
// Ensure that the active repo is known to the built-in git
context.activeRepo = await this.container.git.getActiveRepository();
context.activeRepo = await this.container.git.getOrCreateRepositoryForEditor();
if (
context.activeRepo != null &&
!scmRepositories.some(r => r.rootUri.fsPath === context.activeRepo!.path)

+ 6
- 4
src/commands/gitCommands.actions.ts 查看文件

@ -35,8 +35,10 @@ export async function executeGitCommand(args: GitCommandsCommandArgs): Promise
void (await executeCommand<GitCommandsCommandArgs>(Commands.GitCommands, args));
}
async function ensureRepo(repo: string | Repository): Promise<Repository> {
return typeof repo === 'string' ? (await Container.instance.git.getRepository(repo))! : repo;
function ensureRepo(repo: string | Repository): Repository {
const repository = typeof repo === 'string' ? Container.instance.git.getRepository(repo) : repo;
if (repository == null) throw new Error('Repository not found');
return repository;
}
export namespace GitActions {
@ -711,7 +713,7 @@ export namespace GitActions {
});
if (url == null || url.length === 0) return undefined;
repo = await ensureRepo(repo);
repo = ensureRepo(repo);
void (await Container.instance.git.addRemote(repo.path, name, url));
void (await repo.fetch({ remote: name }));
@ -720,7 +722,7 @@ export namespace GitActions {
export async function fetch(repo: string | Repository, remote: string) {
if (typeof repo === 'string') {
const r = await Container.instance.git.getRepository(repo);
const r = Container.instance.git.getRepository(repo);
if (r == null) return;
repo = r;

+ 1
- 1
src/commands/openFileOnRemote.ts 查看文件

@ -116,7 +116,7 @@ export class OpenFileOnRemoteCommand extends ActiveEditorCommand {
args = { range: true, ...args };
try {
let remotes = await this.container.git.getRemotes(gitUri.repoPath);
let remotes = await this.container.git.getRemotesWithProviders(gitUri.repoPath);
const range =
args.range && editor != null && UriComparer.equals(editor.document.uri, uri)
? new Range(

+ 2
- 1
src/commands/openOnRemote.ts 查看文件

@ -34,7 +34,8 @@ export class OpenOnRemoteCommand extends Command {
async execute(args?: OpenOnRemoteCommandArgs) {
if (args?.resource == null) return;
let remotes = 'remotes' in args ? args.remotes : await this.container.git.getRemotes(args.repoPath);
let remotes =
'remotes' in args ? args.remotes : await this.container.git.getRemotesWithProviders(args.repoPath);
if (args.remote != null) {
const filtered = remotes.filter(r => r.name === args.remote);

+ 10
- 7
src/commands/quickCommand.steps.ts 查看文件

@ -142,7 +142,7 @@ export async function getTags(
}
export async function getBranchesAndOrTags(
repos: Repository | Repository[],
repos: Repository | Repository[] | undefined,
include: ('tags' | 'branches')[],
{
buttons,
@ -156,6 +156,8 @@ export async function getBranchesAndOrTags(
sort?: boolean | { branches?: BranchSortOptions; tags?: TagSortOptions };
} = {},
): Promise<(BranchQuickPickItem | TagQuickPickItem)[]> {
if (repos == null) return [];
let branches: GitBranch[] | undefined;
let tags: GitTag[] | undefined;
@ -305,7 +307,7 @@ export async function getBranchesAndOrTags(
}
export function getValidateGitReferenceFn(
repos: Repository | Repository[],
repos: Repository | Repository[] | undefined,
options?: { buttons?: QuickInputButton[]; ranges?: boolean },
) {
return async (quickpick: QuickPick<any>, value: string) => {
@ -315,6 +317,7 @@ export function getValidateGitReferenceFn(
value = value.substring(1);
}
if (repos == null) return false;
if (Array.isArray(repos)) {
if (repos.length !== 1) return false;
@ -1125,10 +1128,10 @@ export async function* pickRepositoryStep<
Context extends { repos: Repository[]; title: string; associatedView: ViewsWithRepositoryFolders },
>(state: State, context: Context, placeholder: string = 'Choose a repository'): AsyncStepResultGenerator<Repository> {
if (typeof state.repo === 'string') {
state.repo = await Container.instance.git.getRepository(state.repo);
state.repo = Container.instance.git.getRepository(state.repo);
if (state.repo != null) return state.repo;
}
const active = state.repo ?? (await Container.instance.git.getActiveRepository());
const active = state.repo ?? (await Container.instance.git.getOrCreateRepositoryForEditor());
const step = QuickCommand.createPickStep<RepositoryQuickPickItem>({
title: context.title,
@ -1191,7 +1194,7 @@ export async function* pickRepositoriesStep<
actives = state.repos;
}
} else {
const active = await Container.instance.git.getActiveRepository();
const active = await Container.instance.git.getOrCreateRepositoryForEditor();
actives = active != null ? [active] : [];
}
@ -1466,7 +1469,7 @@ async function getShowCommitOrStashStepItems<
}),
);
} else {
remotes = await Container.instance.git.getRemotes(state.repo.path, { sort: true });
remotes = await Container.instance.git.getRemotesWithProviders(state.repo.path, { sort: true });
items.push(
new RevealInSideBarQuickPickItem(state.reference),
@ -1781,7 +1784,7 @@ async function getShowCommitOrStashFileStepItems<
items.push(new RevealInSideBarQuickPickItem(state.reference));
} else {
remotes = await Container.instance.git.getRemotes(state.repo.path, { sort: true });
remotes = await Container.instance.git.getRemotesWithProviders(state.repo.path, { sort: true });
items.push(
new RevealInSideBarQuickPickItem(state.reference),

+ 6
- 3
src/commands/remoteProviders.ts 查看文件

@ -79,13 +79,16 @@ export class ConnectRemoteProviderCommand extends Command {
} else {
repoPath = args.repoPath;
remotes = await this.container.git.getRemotes(repoPath);
remotes = await this.container.git.getRemotesWithProviders(repoPath);
remote = remotes.find(r => r.id === args.remote) as GitRemote<RichRemoteProvider> | undefined;
if (!remote?.hasRichProvider()) return false;
}
const connected = await remote.provider.connect();
if (connected && !(remotes ?? (await this.container.git.getRemotes(repoPath))).some(r => r.default)) {
if (
connected &&
!(remotes ?? (await this.container.git.getRemotesWithProviders(repoPath))).some(r => r.default)
) {
await remote.setAsDefault(true);
}
return connected;
@ -167,7 +170,7 @@ export class DisconnectRemoteProviderCommand extends Command {
} else {
repoPath = args.repoPath;
remote = (await this.container.git.getRemotes(repoPath)).find(r => r.id === args.remote) as
remote = (await this.container.git.getRemotesWithProviders(repoPath)).find(r => r.id === args.remote) as
| GitRemote<RichRemoteProvider>
| undefined;
if (!remote?.hasRichProvider()) return undefined;

+ 2
- 2
src/commands/stashSave.ts 查看文件

@ -42,7 +42,7 @@ export class StashSaveCommand extends Command {
} else if (context.type === 'scm-states') {
args = { ...args };
args.uris = context.scmResourceStates.map(s => s.resourceUri);
args.repoPath = await this.container.git.getRepoPath(args.uris[0].fsPath);
args.repoPath = (await this.container.git.getOrCreateRepository(args.uris[0]))?.path;
const status = await this.container.git.getStatusForRepo(args.repoPath);
if (status?.computeWorkingTreeStatus().staged) {
@ -60,7 +60,7 @@ export class StashSaveCommand extends Command {
(a, b) => a.concat(b.resourceStates.map(s => s.resourceUri)),
[],
);
args.repoPath = await this.container.git.getRepoPath(args.uris[0].fsPath);
args.repoPath = (await this.container.git.getOrCreateRepository(args.uris[0]))?.path;
const status = await this.container.git.getStatusForRepo(args.repoPath);
if (status?.computeWorkingTreeStatus().staged) {

+ 1
- 0
src/config.ts 查看文件

@ -48,6 +48,7 @@ export interface Config {
defaultDateStyle: DateStyle;
defaultGravatarsStyle: GravatarDefaultStyle;
defaultTimeFormat: DateTimeFormat | string | null;
detectNestedRepositories: boolean;
fileAnnotations: {
command: string | null;
};

+ 6
- 6
src/env/node/git/git.ts 查看文件

@ -234,7 +234,7 @@ export namespace Git {
ref?: string,
options: { args?: string[] | null; ignoreWhitespace?: boolean; startLine?: number; endLine?: number } = {},
) {
const [file, root] = splitPath(fileName, repoPath);
const [file, root] = splitPath(fileName, repoPath, true);
const params = ['blame', '--root', '--incremental'];
@ -306,7 +306,7 @@ export namespace Git {
endLine?: number;
} = {},
) {
const [file, root] = splitPath(fileName, repoPath);
const [file, root] = splitPath(fileName, repoPath, true);
const params = ['blame', '--root', '--incremental'];
@ -404,7 +404,7 @@ export namespace Git {
params.push(ref, '--');
if (fileName) {
[fileName, repoPath] = splitPath(fileName, repoPath);
[fileName, repoPath] = splitPath(fileName, repoPath, true);
params.push(fileName);
}
@ -807,7 +807,7 @@ export namespace Git {
endLine?: number;
} = {},
) {
const [file, root] = splitPath(fileName, repoPath);
const [file, root] = splitPath(fileName, repoPath, true);
const params = [
'log',
@ -1303,7 +1303,7 @@ export namespace Git {
encoding?: 'binary' | 'ascii' | 'utf8' | 'utf16le' | 'ucs2' | 'base64' | 'latin1' | 'hex' | 'buffer';
} = {},
): Promise<TOut | undefined> {
const [file, root] = splitPath(fileName, repoPath);
const [file, root] = splitPath(fileName, repoPath, true);
if (GitRevision.isUncommittedStaged(ref)) {
ref = ':';
@ -1487,7 +1487,7 @@ export namespace Git {
porcelainVersion: number = 1,
{ similarityThreshold }: { similarityThreshold?: number | null } = {},
): Promise<string> {
const [file, root] = splitPath(fileName, repoPath);
const [file, root] = splitPath(fileName, repoPath, true);
const params = ['status', porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain'];
if (await Git.isAtLeastVersion('2.18')) {

+ 288
- 239
src/env/node/git/localGitProvider.ts 查看文件

@ -1,5 +1,5 @@
'use strict';
import { readdir, realpath, stat, Stats } from 'fs';
import { readdir, realpath } from 'fs';
import { hostname, userInfo } from 'os';
import { dirname, relative, resolve as resolvePath } from 'path';
import {
@ -8,6 +8,7 @@ import {
Event,
EventEmitter,
extensions,
FileType,
Range,
TextEditor,
Uri,
@ -16,7 +17,7 @@ import {
WorkspaceFolder,
} from 'vscode';
import { hrtime } from '@env/hrtime';
import { isWindows } from '@env/platform';
import { isLinux, isWindows } from '@env/platform';
import type {
API as BuiltInGitApi,
Repository as BuiltInGitRepository,
@ -93,6 +94,8 @@ import { Messages } from '../../../messages';
import { Arrays, debug, Functions, gate, Iterables, log, Strings, Versions } from '../../../system';
import { isFolderGlob, normalizePath, splitPath } from '../../../system/path';
import { any, PromiseOrValue } from '../../../system/promise';
import { equalsIgnoreCase } from '../../../system/string';
import { PathTrie } from '../../../system/trie';
import {
CachedBlame,
CachedDiff,
@ -140,7 +143,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
private readonly _remotesWithApiProviderCache = new Map<string, GitRemote<RichRemoteProvider> | null>();
private readonly _stashesCache = new Map<string, GitStash | null>();
private readonly _tagsCache = new Map<string, Promise<PagedResult<GitTag>>>();
private readonly _trackedCache = new Map<string, PromiseOrValue<boolean>>();
private readonly _trackedPaths = new PathTrie<PromiseOrValue<[string, string] | undefined>>();
private readonly _userMapCache = new Map<string, GitUser | null>();
constructor(private readonly container: Container) {
@ -169,7 +172,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
if (e.changed(RepositoryChange.Index, RepositoryChange.Unknown, RepositoryChangeComparisonMode.Any)) {
this._trackedCache.clear();
this._trackedPaths.clear();
}
if (e.changed(RepositoryChange.Merge, RepositoryChangeComparisonMode.Any)) {
@ -303,12 +306,12 @@ export class LocalGitProvider implements GitProvider, Disposable {
const repositories = await this.repositorySearch(workspace.getWorkspaceFolder(uri)!);
if (autoRepositoryDetection === true || autoRepositoryDetection === 'subFolders') {
for (const repository of repositories) {
void this.openScmRepository(repository.path);
void this.openScmRepository(repository.uri);
}
}
if (repositories.length > 0) {
this._trackedCache.clear();
this._trackedPaths.clear();
}
return repositories;
@ -330,18 +333,18 @@ export class LocalGitProvider implements GitProvider, Disposable {
createRepository(
folder: WorkspaceFolder,
path: string,
uri: Uri,
root: boolean,
suspended?: boolean,
closed?: boolean,
): Repository {
void this.openScmRepository(path);
void this.openScmRepository(uri);
return new Repository(
this.container,
this.onRepositoryChanged.bind(this),
this.descriptor,
folder,
path,
uripan>.fsPath,
root,
suspended ?? !window.state.focused,
closed,
@ -367,43 +370,42 @@ export class LocalGitProvider implements GitProvider, Disposable {
})
private async repositorySearch(folder: WorkspaceFolder): Promise<Repository[]> {
const cc = Logger.getCorrelationContext();
const { uri } = folder;
const depth = configuration.get('advanced.repositorySearchDepth', uri);
const depth = configuration.get('advanced.repositorySearchDepth', folder.uri);
Logger.log(cc, `searching (depth=${depth})...`);
const repositories: Repository[] = [];
const rootPath = await this.getRepoPath(uri.fsPath, true);
if (rootPath != null) {
Logger.log(cc, `found root repository in '${rootPath}'`);
repositories.push(this.createRepository(folder, rootPath, true));
const uri = await this.findRepositoryUri(folder.uri, true);
if (uri != null) {
Logger.log(cc, `found root repository in '${uri.fsPath}'`);
repositories.push(this.createRepository(folder, uri, true));
}
if (depth <= 0) return repositories;
// Get any specified excludes -- this is a total hack, but works for some simple cases and something is better than nothing :)
let excludes = {
...configuration.getAny<Record<string, boolean>>('files.exclude', uri, {}),
...configuration.getAny<Record<string, boolean>>('search.exclude', uri, {}),
const excludedConfig = {
...configuration.getAny<Record<string, boolean>>('files.exclude', folder.uri, {}),
...configuration.getAny<Record<string, boolean>>('search.exclude', folder.uri, {}),
};
const excludedPaths = [
...Iterables.filterMap(Object.entries(excludes), ([key, value]) => {
...Iterables.filterMap(Object.entries(excludedConfig), ([key, value]) => {
if (!value) return undefined;
if (key.startsWith('**/')) return key.substring(3);
return key;
}),
];
excludes = excludedPaths.reduce((accumulator, current) => {
accumulator[current] = true;
const excludes = excludedPaths.reduce((accumulator, current) => {
accumulator.add(current);
return accumulator;
}, Object.create(null) as Record<string, boolean>);
}, new Set<string>());
let repoPaths;
try {
repoPaths = await this.repositorySearchCore(uri.fsPath, depth, excludes);
repoPaths = await this.repositorySearchCore(folder.uri.fsPath, depth, excludes);
} catch (ex) {
const msg: string = ex?.toString() ?? '';
if (RepoSearchWarnings.doesNotExist.test(msg)) {
@ -415,18 +417,25 @@ export class LocalGitProvider implements GitProvider, Disposable {
return repositories;
}
const rootPath = uri != null ? normalizePath(uri.fsPath) : undefined;
for (let p of repoPaths) {
p = dirname(p);
const normalized = normalizePath(p);
// If we are the same as the root, skip it
if (normalizePath(p) === rootPath) continue;
if (isLinux) {
if (normalized === rootPath) continue;
} else if (equalsIgnoreCase(normalized, rootPath)) {
continue;
}
Logger.log(cc, `searching in '${p}'...`);
Logger.debug(cc, `normalizedRepoPath=${normalizePath(p)}, rootPath=${rootPath}`);
Logger.debug(cc, `normalizedRepoPath=${normalized}, rootPath=${rootPath}`);
const rp = await this.getRepoPath(p, true);
const rp = await this.findRepositoryUri(Uri.file(p), true);
if (rp == null) continue;
Logger.log(cc, `found repository in '${rp}'`);
Logger.log(cc, `found repository in '${rp.fsPath}'`);
repositories.push(this.createRepository(folder, rp, false));
}
@ -437,7 +446,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
private repositorySearchCore(
root: string,
depth: number,
excludes: Record<string, boolean>,
excludes: Set<string>,
repositories: string[] = [],
): Promise<string[]> {
const cc = Logger.getCorrelationContext();
@ -460,7 +469,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
for (f of files) {
if (f.name === '.git') {
repositories.push(resolvePath(root, f.name));
} else if (depth >= 0 && f.isDirectory() && excludes[f.name] !== true) {
} else if (depth >= 0 && f.isDirectory() && !excludes.has(f.name)) {
try {
await this.repositorySearchCore(resolvePath(root, f.name), depth, excludes, repositories);
} catch (ex) {
@ -587,7 +596,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
if (cache.length === 0) {
this._trackedCache.clear();
this._trackedPaths.clear();
this._userMapCache.clear();
}
}
@ -617,7 +626,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
): Promise<void> {
const { branch: branchRef, ...opts } = options ?? {};
if (GitReference.isBranch(branchRef)) {
const repo = await this.container.git.getRepository(repoPath);
const repo = this.container.git.getRepository(repoPath);
const branch = await repo?.getBranch(branchRef?.name);
if (!branch?.remote && branch?.upstream == null) return undefined;
@ -632,6 +641,119 @@ export class LocalGitProvider implements GitProvider, Disposable {
return Git.fetch(repoPath, opts);
}
@gate()
@debug()
async findRepositoryUri(uri: Uri, isDirectory?: boolean): Promise<Uri | undefined> {
const cc = Logger.getCorrelationContext();
let repoPath: string | undefined;
try {
if (!isDirectory) {
const stats = await workspace.fs.stat(uri);
uri = stats?.type === FileType.Directory ? uri : Uri.file(dirname(uri.fsPath));
}
repoPath = await Git.rev_parse__show_toplevel(uri.fsPath);
if (!repoPath) return undefined;
if (isWindows) {
// On Git 2.25+ if you call `rev-parse --show-toplevel` on a mapped drive, instead of getting the mapped drive path back, you get the UNC path for the mapped drive.
// So try to normalize it back to the mapped drive path, if possible
const repoUri = Uri.file(repoPath);
if (repoUri.authority.length !== 0 && uri.authority.length === 0) {
const match = driveLetterRegex.exec(uri.path);
if (match != null) {
const [, letter] = match;
try {
const networkPath = await new Promise<string | undefined>(resolve =>
realpath.native(`${letter}:\\`, { encoding: 'utf8' }, (err, resolvedPath) =>
resolve(err != null ? undefined : resolvedPath),
),
);
if (networkPath != null) {
repoPath = normalizePath(
repoUri.fsPath.replace(
networkPath,
`${letter.toLowerCase()}:${networkPath.endsWith('\\') ? '\\' : ''}`,
),
);
return Uri.file(repoPath);
}
} catch {}
}
return Uri.file(normalizePath(uri.fsPath));
}
return repoUri;
}
// If we are not on Windows (symlinks don't seem to have the same issue on Windows), check if we are a symlink and if so, use the symlink path (not its resolved path)
// This is because VS Code will provide document Uris using the symlinked path
repoPath = await new Promise<string | undefined>(resolve => {
realpath(uri.fsPath, { encoding: 'utf8' }, (err, resolvedPath) => {
if (err != null) {
Logger.debug(cc, `fs.realpath failed; repoPath=${repoPath}`);
resolve(repoPath);
return;
}
if (Strings.equalsIgnoreCase(uri.fsPath, resolvedPath)) {
Logger.debug(cc, `No symlink detected; repoPath=${repoPath}`);
resolve(repoPath);
return;
}
const linkPath = normalizePath(resolvedPath);
repoPath = repoPath!.replace(linkPath, uri.fsPath);
Logger.debug(
cc,
`Symlink detected; repoPath=${repoPath}, path=${uri.fsPath}, resolvedPath=${resolvedPath}`,
);
resolve(repoPath);
});
});
return repoPath ? Uri.file(repoPath) : undefined;
} catch (ex) {
Logger.error(ex, cc);
repoPath = undefined;
return repoPath;
} finally {
if (repoPath) {
void this.ensureProperWorkspaceCasing(repoPath, uri);
}
}
}
@gate(() => '')
private async ensureProperWorkspaceCasing(repoPath: string, uri: Uri) {
if (this.container.config.advanced.messages.suppressImproperWorkspaceCasingWarning) return;
const path = uri.fsPath.replace(/\\/g, '/');
let regexPath;
let testPath;
if (path > repoPath) {
regexPath = path;
testPath = repoPath;
} else {
testPath = path;
regexPath = repoPath;
}
let pathRegex = new RegExp(`^${regexPath}`);
if (!pathRegex.test(testPath)) {
pathRegex = new RegExp(pathRegex, 'i');
if (pathRegex.test(testPath)) {
await Messages.showIncorrectWorkspaceCasingWarningMessage();
}
}
}
@log<LocalGitProvider['getAheadBehindCommitCount']>({ args: { 1: refs => refs.join(',') } })
getAheadBehindCommitCount(
repoPath: string,
@ -687,12 +809,13 @@ export class LocalGitProvider implements GitProvider, Disposable {
key: string,
cc: LogCorrelationContext | undefined,
): Promise<GitBlame | undefined> {
if (!(await this.isTracked(uri))) {
const paths = await this.isTracked(uri);
if (paths == null) {
Logger.log(cc, `Skipping blame; '${uri.fsPath}' is not tracked`);
return emptyPromise as Promise<GitBlame>;
}
const [file, root] = splitPath(<span class="nx">uri.fsPath, uri.repoPath, false);
const [file, root] = paths;
try {
const data = await Git.blame(root, file, uri.sha, {
@ -766,12 +889,13 @@ export class LocalGitProvider implements GitProvider, Disposable {
key: string,
cc: LogCorrelationContext | undefined,
): Promise<GitBlame | undefined> {
if (!(await this.isTracked(uri))) {
const paths = await this.isTracked(uri);
if (paths == null) {
Logger.log(cc, `Skipping blame; '${uri.fsPath}' is not tracked`);
return emptyPromise as Promise<GitBlame>;
}
const [file, root] = splitPath(<span class="nx">uri.fsPath, uri.repoPath, false);
const [file, root] = paths;
try {
const data = await Git.blame__contents(root, file, contents, {
@ -1102,8 +1226,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
this._branchesCache.set(repoPath, resultsPromise);
}
queueMicrotask(async () => {
if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) {
queueMicrotask(() => {
if (!this.container.git.getRepository(repoPath)?.supportsChangeEvents) {
this._branchesCache.delete(repoPath);
}
});
@ -1293,8 +1417,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
if (this.useCaching) {
this._contributorsCache.set(key, contributors);
queueMicrotask(async () => {
if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) {
queueMicrotask(() => {
if (!this.container.git.getRepository(repoPath)?.supportsChangeEvents) {
this._contributorsCache.delete(key);
}
});
@ -1446,7 +1570,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
key: string,
cc: LogCorrelationContext | undefined,
): Promise<GitDiff | undefined> {
const [file, root] = splitPath(fileName, repoPath, false);
const [file, root] = splitPath(fileName, repoPath);
try {
const data = await Git.diff(root, file, ref1, ref2, {
@ -1535,7 +1659,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
key: string,
cc: LogCorrelationContext | undefined,
): Promise<GitDiff | undefined> {
const [file, root] = splitPath(fileName, repoPath, false);
const [file, root] = splitPath(fileName, repoPath);
try {
const data = await Git.diff__contents(root, file, ref, contents, {
@ -2115,12 +2239,13 @@ export class LocalGitProvider implements GitProvider, Disposable {
key: string,
cc: LogCorrelationContext | undefined,
): Promise<GitLog | undefined> {
if (!(await this.isTracked(fileName, repoPath, ref))) {
Logger.log(cc, `Skipping log; '${fileName}' is not tracked`);
const paths = await this.isTracked(fileName, repoPath, ref);
if (paths == null) {
Logger.log(cc, `Skipping blame; '${fileName}' is not tracked`);
return emptyPromise as Promise<GitLog>;
}
const [file, root] = splitPath<span class="p">(fileName, repoPath, false);
const [file, root] = paths;
try {
if (range != null && range.start.line > range.end.line) {
@ -2299,8 +2424,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
if (this.useCaching) {
this._mergeStatusCache.set(repoPath, status ?? null);
queueMicrotask(async () => {
if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) {
queueMicrotask(() => {
if (!this.container.git.getRepository(repoPath)?.supportsChangeEvents) {
this._mergeStatusCache.delete(repoPath);
}
});
@ -2380,8 +2505,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
if (this.useCaching) {
this._rebaseStatusCache.set(repoPath, status ?? null);
queueMicrotask(async () => {
if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) {
queueMicrotask(() => {
if (!this.container.git.getRepository(repoPath)?.supportsChangeEvents) {
this._rebaseStatusCache.delete(repoPath);
}
});
@ -2828,7 +2953,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
if (richRemote !== undefined) return richRemote ?? undefined;
}
remotes = (remotes ?? (await this.getRemotes(repoPath))).filter(
remotes = (remotes ?? (await this.getRemotesWithProviders(repoPath))).filter(
(
r: GitRemote<RemoteProvider | RichRemoteProvider | undefined>,
): r is GitRemote<RemoteProvider | RichRemoteProvider> => r.provider != null,
@ -2890,27 +3015,14 @@ export class LocalGitProvider implements GitProvider, Disposable {
return remote;
}
@log()
async getRemotes(repoPath: string | undefined, options?: { sort?: boolean }): Promise<GitRemote<RemoteProvider>[]> {
if (repoPath == null) return [];
const repository = await this.container.git.getRepository(repoPath);
const remotes = await (repository != null
? repository.getRemotes(options)
: this.getRemotesCore(repoPath, undefined, options));
return remotes.filter(r => r.provider != null) as GitRemote<RemoteProvider>[];
}
@debug({ args: { 1: false } })
async getRemotesCore(
@log({ args: { 1: false } })
async getRemotes(
repoPath: string | undefined,
providers?: RemoteProviders,
options?: { sort?: boolean },
): Promise<GitRemote[]> {
options?: { providers?: RemoteProviders; sort?: boolean },
): Promise<GitRemote<RemoteProvider | RichRemoteProvider | undefined>[]> {
if (repoPath == null) return [];
providers = providers ?? RemoteProviderFactory.loadProviders(configuration.get('remotes', null));
const providers = options?.providers ?? RemoteProviderFactory.loadProviders(configuration.get('remotes', null));
try {
const data = await Git.remote(repoPath);
@ -2928,122 +3040,19 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
}
@gate()
@debug()
async getRepoPath(filePath: string, isDirectory?: boolean): Promise<string | undefined> {
const cc = Logger.getCorrelationContext();
let repoPath: string | undefined;
try {
let path: string;
if (isDirectory) {
path = filePath;
} else {
const stats = await new Promise<Stats | undefined>(resolve =>
stat(filePath, (err, stats) => resolve(err == null ? stats : undefined)),
);
path = stats?.isDirectory() ? filePath : dirname(filePath);
}
repoPath = await Git.rev_parse__show_toplevel(path);
if (repoPath == null) return repoPath;
if (isWindows) {
// On Git 2.25+ if you call `rev-parse --show-toplevel` on a mapped drive, instead of getting the mapped drive path back, you get the UNC path for the mapped drive.
// So try to normalize it back to the mapped drive path, if possible
const repoUri = Uri.file(repoPath);
const pathUri = Uri.file(path);
if (repoUri.authority.length !== 0 && pathUri.authority.length === 0) {
const match = driveLetterRegex.exec(pathUri.path);
if (match != null) {
const [, letter] = match;
try {
const networkPath = await new Promise<string | undefined>(resolve =>
realpath.native(`${letter}:\\`, { encoding: 'utf8' }, (err, resolvedPath) =>
resolve(err != null ? undefined : resolvedPath),
),
);
if (networkPath != null) {
repoPath = normalizePath(
repoUri.fsPath.replace(
networkPath,
`${letter.toLowerCase()}:${networkPath.endsWith('\\') ? '\\' : ''}`,
),
);
return repoPath;
}
} catch {}
}
repoPath = normalizePath(pathUri.fsPath);
}
return repoPath;
}
// If we are not on Windows (symlinks don't seem to have the same issue on Windows), check if we are a symlink and if so, use the symlink path (not its resolved path)
// This is because VS Code will provide document Uris using the symlinked path
repoPath = await new Promise<string | undefined>(resolve => {
realpath(path, { encoding: 'utf8' }, (err, resolvedPath) => {
if (err != null) {
Logger.debug(cc, `fs.realpath failed; repoPath=${repoPath}`);
resolve(repoPath);
return;
}
if (Strings.equalsIgnoreCase(path, resolvedPath)) {
Logger.debug(cc, `No symlink detected; repoPath=${repoPath}`);
resolve(repoPath);
return;
}
const linkPath = normalizePath(resolvedPath);
repoPath = repoPath!.replace(linkPath, path);
Logger.debug(
cc,
`Symlink detected; repoPath=${repoPath}, path=${path}, resolvedPath=${resolvedPath}`,
);
resolve(repoPath);
});
});
return repoPath;
} catch (ex) {
Logger.error(ex, cc);
repoPath = undefined;
return repoPath;
} finally {
if (repoPath) {
void this.ensureProperWorkspaceCasing(repoPath, filePath);
}
}
}
@gate(() => '')
private async ensureProperWorkspaceCasing(repoPath: string, filePath: string) {
if (this.container.config.advanced.messages.suppressImproperWorkspaceCasingWarning) return;
filePath = filePath.replace(/\\/g, '/');
@log()
async getRemotesWithProviders(
repoPath: string | undefined,
options?: { sort?: boolean },
): Promise<GitRemote<RemoteProvider | RichRemoteProvider>[]> {
if (repoPath == null) return [];
let regexPath;
let testPath;
if (filePath > repoPath) {
regexPath = filePath;
testPath = repoPath;
} else {
testPath = filePath;
regexPath = repoPath;
}
const repository = this.container.git.getRepository(repoPath);
const remotes = await (repository != null
? repository.getRemotes(options)
: this.getRemotes(repoPath, options));
let pathRegex = new RegExp(`^${regexPath}`);
if (!pathRegex.test(testPath)) {
pathRegex = new RegExp(pathRegex, 'i');
if (pathRegex.test(testPath)) {
await Messages.showIncorrectWorkspaceCasingWarningMessage();
}
}
return remotes.filter(r => r.provider != null) as GitRemote<RemoteProvider | RichRemoteProvider>[];
}
@gate()
@ -3067,8 +3076,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
if (this.useCaching) {
this._stashesCache.set(repoPath, stash ?? null);
queueMicrotask(async () => {
if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) {
queueMicrotask(() => {
if (!this.container.git.getRepository(repoPath)?.supportsChangeEvents) {
this._stashesCache.delete(repoPath);
}
});
@ -3157,8 +3166,8 @@ export class LocalGitProvider implements GitProvider, Disposable {
if (this.useCaching) {
this._tagsCache.set(repoPath, resultsPromise);
queueMicrotask(async () => {
if (!(await this.container.git.getRepository(repoPath))?.supportsChangeEvents) {
queueMicrotask(() => {
if (!this.container.git.getRepository(repoPath)?.supportsChangeEvents) {
this._tagsCache.delete(repoPath);
}
});
@ -3287,26 +3296,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
return branches.length !== 0 || tags.length !== 0;
}
@log()
async hasRemotes(repoPath: string | undefined): Promise<boolean> {
if (repoPath == null) return false;
const repository = await this.container.git.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 = await this.container.git.getRepository(repoPath);
if (repository == null) return false;
return repository.hasUpstreamBranch();
}
@log<LocalGitProvider['isActiveRepoPath']>({
args: { 1: e => (e != null ? `TextEditor(${Logger.toLoggable(e.document.uri)})` : undefined) },
})
@ -3324,61 +3313,121 @@ export class LocalGitProvider implements GitProvider, Disposable {
return this.supportedSchemes.includes(uri.scheme);
}
private async isTracked(filePath: string, repoPath?: string, ref?: string): Promise<boolean>;
private async isTracked(uri: GitUri): Promise<boolean>;
@log<LocalGitProvider['isTracked']>({ exit: tracked => `returned ${tracked}` /*, singleLine: true }*/ })
private async isTracked(filePathOrUri: string | GitUri, repoPath?: string, ref?: string): Promise<boolean> {
let cacheKey: string;
let relativeFilePath: string;
private async isTracked(uri: GitUri): Promise<[string, string] | undefined>;
private async isTracked(path: string, repoPath?: string, ref?: string): Promise<[string, string] | undefined>;
@log<LocalGitProvider['isTracked']>({ exit: tracked => `returned ${Boolean(tracked)}` })
private async isTracked(
pathOrUri: string | GitUri,
repoPath?: string,
ref?: string,
): Promise<[string, string] | undefined> {
let relativePath: string;
let repository: Repository | undefined;
if (typeof pathOrUri === 'string') {
if (ref === GitRevision.deletedOrMissing) return undefined;
if (typeof filePathOrUri === 'string') {
if (ref === GitRevision.deletedOrMissing) return false;
repository = this.container.git.getRepository(Uri.file(pathOrUri));
repoPath = repoPath || repository?.path;
cacheKey = ref ? `${ref}:${normalizePath(filePathOrUri)}` : normalizePath(filePathOrUri);
[relativeFilePath, repoPath] = splitPath(filePathOrUri, repoPath);
[relativePath, repoPath] = splitPath(pathOrUri, repoPath);
} else {
if (!this.isTrackable(filePathOrUri)) return false;
if (!this.isTrackable(pathOrUri)) return undefined;
// Always use the ref of the GitUri
ref = filePathOrUri.sha;
cacheKey = ref ? `${ref}:${normalizePath(filePathOrUri.fsPath)}` : normalizePath(filePathOrUri.fsPath);
relativeFilePath = filePathOrUri.fsPath;
repoPath = filePathOrUri.repoPath;
}
ref = pathOrUri.sha;
if (ref === GitRevision.deletedOrMissing) return undefined;
if (ref != null) {
cacheKey = `${ref}:${cacheKey}`;
repository = this.container.git.getRepository(pathOrUri);
repoPath = repoPath || repository?.path;
[relativePath, repoPath] = splitPath(pathOrUri.fsPath, repoPath);
}
let tracked = this._trackedCache.get(cacheKey);
const path = repoPath ? `${repoPath}/${relativePath}` : relativePath;
let key = path;
key = `${ref ?? ''}:${key[0] === '/' ? key : `/${key}`}`;
let tracked = this._trackedPaths.get(key);
if (tracked != null) return tracked;
tracked = this.isTrackedCore(relativeFilePath, repoPath ?? '', ref);
this._trackedCache.set(cacheKey, tracked);
tracked = this.isTrackedCore(path, relativePath, repoPath ?? '', ref, repository);
this._trackedPaths.set(key, tracked);
tracked = await tracked;
this._trackedCache.set(cacheKey, tracked);
this._trackedPaths.set(key, tracked);
return tracked;
}
@debug()
private async isTrackedCore(fileName: string, repoPath: string, ref?: string) {
if (ref === GitRevision.deletedOrMissing) return false;
private async isTrackedCore(
path: string,
relativePath: string,
repoPath: string,
ref: string | undefined,
repository: Repository | undefined,
): Promise<[string, string] | undefined> {
if (ref === GitRevision.deletedOrMissing) return undefined;
try {
// Even if we have a ref, check first to see if the file exists (that way the cache will be better reused)
let tracked = Boolean(await Git.ls_files(repoPath, fileName));
if (!tracked && ref != null && !GitRevision.isUncommitted(ref)) {
tracked = Boolean(await Git.ls_files(repoPath, fileName, { ref: ref }));
// If we still haven't found this file, make sure it wasn't deleted in that ref (i.e. check the previous)
while (true) {
if (!repoPath) {
[relativePath, repoPath] = splitPath(path, '', true);
}
// Even if we have a ref, check first to see if the file exists (that way the cache will be better reused)
let tracked = Boolean(await Git.ls_files(repoPath, relativePath));
if (tracked) return [relativePath, repoPath];
if (repoPath) {
const [newRelativePath, newRepoPath] = splitPath(path, '', true);
if (newRelativePath !== relativePath) {
// If we didn't find it, check it as close to the file as possible (will find nested repos)
tracked = Boolean(await Git.ls_files(newRepoPath, newRelativePath));
if (tracked) {
repository = await this.container.git.getOrCreateRepository(Uri.file(path), true);
if (repository != null) {
return splitPath(path, repository.path);
}
return [newRelativePath, newRepoPath];
}
}
}
if (!tracked && ref && !GitRevision.isUncommitted(ref)) {
tracked = Boolean(await Git.ls_files(repoPath, relativePath, { ref: ref }));
// If we still haven't found this file, make sure it wasn't deleted in that ref (i.e. check the previous)
if (!tracked) {
tracked = Boolean(await Git.ls_files(repoPath, relativePath, { ref: `${ref}^` }));
}
}
// Since the file isn't tracked, make sure it isn't part of a nested repository we don't know about yet
if (!tracked) {
tracked = Boolean(await Git.ls_files(repoPath, fileName, { ref: `${ref}^` }));
if (repository != null) {
// Don't look for a nested repository if the file isn't at least one folder deep
const index = relativePath.indexOf('/');
if (index < 0 || index === relativePath.length - 1) return undefined;
const nested = await this.container.git.getOrCreateRepository(Uri.file(path), true);
if (nested != null && nested !== repository) {
[relativePath, repoPath] = splitPath(path, repository.path);
repository = undefined;
continue;
}
}
return undefined;
}
return [relativePath, repoPath];
}
return tracked;
} catch (ex) {
Logger.error(ex);
return false;
return undefined;
}
}
@ -3673,11 +3722,11 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
@log()
private async openScmRepository(repoPath: string): Promise<BuiltInGitRepository | undefined> {
private async openScmRepository(uri: Uri): Promise<BuiltInGitRepository | undefined> {
const cc = Logger.getCorrelationContext();
try {
const gitApi = await this.getScmGitApi();
return (await gitApi?.openRepository?.(Uri.file(repoPath))) ?? undefined;
return (await gitApi?.openRepository?.(uri)) ?? undefined;
} catch (ex) {
Logger.error(ex, cc);
return undefined;

+ 7
- 10
src/git/gitProvider.ts 查看文件

@ -74,7 +74,7 @@ export interface GitProvider {
updateContext?(): void;
createRepository(
folder: WorkspaceFolder,
path: string,
uri: Uri,
root: boolean,
suspended?: boolean,
closed?: boolean,
@ -106,6 +106,7 @@ export interface GitProvider {
remote?: string | undefined;
},
): Promise<void>;
findRepositoryUri(uri: Uri, isDirectory?: boolean): Promise<Uri | undefined>;
getAheadBehindCommitCount(repoPath: string, refs: string[]): Promise<{ ahead: number; behind: number } | undefined>;
/**
* Returns the blame of a file
@ -314,14 +315,12 @@ export interface GitProvider {
): Promise<GitRemote<RichRemoteProvider> | undefined>;
getRemotes(
repoPath: string | undefined,
options?: { sort?: boolean | undefined },
): Promise<GitRemote<RemoteProvider>[]>;
getRemotesCore(
repoPath: string | undefined,
providers?: RemoteProviders,
options?: { sort?: boolean | undefined },
options?: { providers?: RemoteProviders; sort?: boolean },
): Promise<GitRemote<RemoteProvider | RichRemoteProvider | undefined>[]>;
getRepoPath(filePath: string, isDirectory?: boolean): Promise<string | undefined>;
getRemotesWithProviders(
repoPath: string | undefined,
options?: { force?: boolean; providers?: RemoteProviders; sort?: boolean | undefined },
): Promise<GitRemote<RemoteProvider | RichRemoteProvider>[]>;
getRevisionContent(repoPath: string, path: string, ref: string): Promise<Uint8Array | undefined>;
getStash(repoPath: string | undefined): Promise<GitStash | undefined>;
getStatusForFile(repoPath: string, fileName: string): Promise<GitStatusFile | undefined>;
@ -344,8 +343,6 @@ export interface GitProvider {
| undefined;
},
): Promise<boolean>;
hasRemotes(repoPath: string | undefined): Promise<boolean>;
hasTrackingBranch(repoPath: string | undefined): Promise<boolean>;
isActiveRepoPath(repoPath: string | undefined, editor?: TextEditor): Promise<boolean>;
isTrackable(uri: Uri): boolean;

+ 133
- 221
src/git/gitProviderService.ts 查看文件

@ -29,13 +29,15 @@ import {
import type { Container } from '../container';
import { ProviderNotFoundError } from '../errors';
import { Logger } from '../logger';
import { Repositories } from '../repositories';
import { groupByFilterMap, groupByMap } from '../system/array';
import { gate } from '../system/decorators/gate';
import { debug, log } from '../system/decorators/log';
import { count, filter, flatMap, map } from '../system/iterable';
import { isDescendent, normalizePath } from '../system/path';
import { cancellable, isPromise, PromiseCancelledError, PromiseOrValue } from '../system/promise';
import { count, filter, first, flatMap, map } from '../system/iterable';
import { basename, dirname, normalizePath } from '../system/path';
import { cancellable, isPromise, PromiseCancelledError } from '../system/promise';
import { CharCode } from '../system/string';
import { VisitedPathsTrie } from '../system/trie';
import { vslsUriPrefixRegex } from '../vsls/vsls';
import { GitProvider, GitProviderDescriptor, GitProviderId, PagedResult, ScmRepository } from './gitProvider';
import { GitUri } from './gitUri';
@ -113,20 +115,6 @@ export class GitProviderService implements Disposable {
private fireProvidersChanged(added?: GitProvider[], removed?: GitProvider[]) {
this._etag = Date.now();
if (this._pathToRepoPathCache.size !== 0) {
if (removed?.length) {
// If a repository was removed, clear the cache for all paths
this._pathToRepoPathCache.clear();
} else if (added?.length) {
// If a provider was added, only preserve paths with a resolved repoPath
for (const [key, value] of this._pathToRepoPathCache) {
if (value === null || isPromise(value)) {
this._pathToRepoPathCache.delete(key);
}
}
}
}
this._onDidChangeProviders.fire({ added: added ?? [], removed: removed ?? [] });
}
@ -137,20 +125,6 @@ export class GitProviderService implements Disposable {
private fireRepositoriesChanged(added?: Repository[], removed?: Repository[]) {
this._etag = Date.now();
if (this._pathToRepoPathCache.size !== 0) {
if (removed?.length) {
// If a repository was removed, clear the cache for all paths
this._pathToRepoPathCache.clear();
} else if (added?.length) {
// If a repository was added, only preserve paths with a resolved repoPath
for (const [key, value] of this._pathToRepoPathCache) {
if (value === null || isPromise(value)) {
this._pathToRepoPathCache.delete(key);
}
}
}
}
this._onDidChangeRepositories.fire({ added: added ?? [], removed: removed ?? [] });
}
@ -161,8 +135,9 @@ export class GitProviderService implements Disposable {
private readonly _disposable: Disposable;
private readonly _providers = new Map<GitProviderId, GitProvider>();
private readonly _repositories = new Map<string, Repository>();
private readonly _repositories = new Repositories();
private readonly _supportedSchemes = new Set<string>();
private _visitedPaths = new VisitedPathsTrie();
constructor(private readonly container: Container) {
this._disposable = Disposable.from(
@ -239,13 +214,10 @@ export class GitProviderService implements Disposable {
const removed: Repository[] = [];
for (const folder of e.removed) {
const key = asKey(folder.uri);
for (const repository of this._repositories.values()) {
if (key === asKey(repository.folder.uri)) {
this._repositories.delete(repository.path);
removed.push(repository);
}
const repository = this._repositories.getClosest(folder.uri);
if (repository != null) {
this._repositories.remove(repository.uri);
removed.push(repository);
}
}
@ -280,19 +252,16 @@ export class GitProviderService implements Disposable {
return count(this.repositories, r => !r.closed);
}
get repositories(): Iterable<Repository> {
get repositories(): IterableIterator<Repository> {
return this._repositories.values();
}
get repositoryCount(): number {
return this._repositories.size;
return this._repositories.count;
}
get highlander(): Repository | undefined {
if (this.repositoryCount === 1) {
return this._repositories.values().next().value;
}
return undefined;
return this.repositoryCount === 1 ? first(this._repositories.values()) : undefined;
}
@log()
@ -310,7 +279,9 @@ export class GitProviderService implements Disposable {
// }
getCachedRepository(repoPath: string): Repository | undefined {
return repoPath && this._repositories.size !== 0 ? this._repositories.get(repoPath) : undefined;
return repoPath && this._repositories.count !== 0
? this._repositories.getClosest(Uri.file(repoPath))
: undefined;
}
/**
@ -375,9 +346,9 @@ export class GitProviderService implements Disposable {
const removed: Repository[] = [];
for (const [key, repository] of [...this._repositories]) {
for (const repository of [...this._repositories.values()]) {
if (repository?.provider.id === id) {
this._repositories.delete(key);
this._repositories.remove(repository.uri);
removed.push(repository);
}
}
@ -465,10 +436,9 @@ export class GitProviderService implements Disposable {
const added: Repository[] = [];
for (const repository of repositories) {
if (this._repositories.has(repository.path)) continue;
added.push(repository);
this._repositories.set(repository.path, repository);
if (this._repositories.add(repository)) {
added.push(repository);
}
}
this.updateContext();
@ -762,35 +732,6 @@ export class GitProviderService implements Disposable {
);
}
@log<GitProviderService['getActiveRepository']>({
args: { 0: e => (e != null ? `TextEditor(${Logger.toLoggable(e.document.uri)})` : undefined) },
})
async getActiveRepository(editor?: TextEditor): Promise<Repository | undefined> {
const repoPath = await this.getActiveRepoPath(editor);
if (repoPath == null) return undefined;
return this.getRepository(repoPath);
}
@log<GitProviderService['getActiveRepoPath']>({
args: { 0: e => (e != null ? `TextEditor(${Logger.toLoggable(e.document.uri)})` : undefined) },
})
async getActiveRepoPath(editor?: TextEditor): Promise<string | undefined> {
editor = editor ?? window.activeTextEditor;
let repoPath;
if (editor != null) {
const doc = await this.container.tracker.getOrAdd(editor.document.uri);
if (doc != null) {
repoPath = doc.uri.repoPath;
}
}
if (repoPath != null) return repoPath;
return this.highlanderRepoPath;
}
@log()
/**
* Returns the blame of a file
@ -1328,9 +1269,7 @@ export class GitProviderService implements Disposable {
try {
return await promiseOrPR;
} catch (ex) {
if (ex instanceof PromiseCancelledError) {
throw ex;
}
if (ex instanceof PromiseCancelledError) throw ex;
return undefined;
}
@ -1380,9 +1319,7 @@ export class GitProviderService implements Disposable {
try {
return await promiseOrPR;
} catch (ex) {
if (ex instanceof PromiseCancelledError) {
throw ex;
}
if (ex instanceof PromiseCancelledError) throw ex;
return undefined;
}
@ -1439,176 +1376,151 @@ export class GitProviderService implements Disposable {
return provider.getRichRemoteProvider(path, remotes, options);
}
@log()
@log({ args: { 1: false } })
async getRemotes(
repoPath: string | Uri | undefined,
options?: { sort?: boolean },
): Promise<GitRemote<RemoteProvider>[]> {
options?: { providers?: RemoteProviders; sort?: boolean },
): Promise<GitRemote<RemoteProvider | RichRemoteProvider | undefined>[]> {
if (repoPath == null) return [];
const { provider, path } = this.getProvider(repoPath);
return provider.getRemotes(path, options);
}
async getRemotesCore(
@log()
async getRemotesWithProviders(
repoPath: string | Uri | undefined,
providers?: RemoteProviders,
options?: { sort?: boolean },
): Promise<GitRemote[]> {
): Promise<GitRemote<RemoteProvider | RichRemoteProvider>[]> {
if (repoPath == null) return [];
const { provider, path } = this.getProvider(repoPath);
return provider.getRemotesCore(path, providers, options);
return provider.getRemotesWithProviders(path, options);
}
async getRepoPath(filePath: string): Promise<string | undefined>;
async getRepoPath(uri: Uri | undefined): Promise<string | undefined>;
@log<GitProviderService['getRepoPath']>({ exit: path => `returned ${path}` })
async getRepoPath(filePathOrUri: string | Uri | undefined): Promise<string | undefined> {
if (filePathOrUri == null) return this.highlanderRepoPath;
if (GitUri.is(filePathOrUri)) return filePathOrUri.repoPath;
// const autoRepositoryDetection =
// configuration.getAny<boolean | 'subFolders' | 'openEditors'>(
// BuiltInGitConfiguration.AutoRepositoryDetection,
// ) ?? true;
getBestRepository(): Repository | undefined;
getBestRepository(uri?: Uri): Repository | undefined;
getBestRepository(editor?: TextEditor): Repository | undefined;
getBestRepository(uri?: TextEditor | Uri, editor?: TextEditor): Repository | undefined;
// const repo = await this.getRepository(
// filePathOrUri,
// autoRepositoryDetection === true || autoRepositoryDetection === 'openEditors',
// );
@log<GitProviderService['getBestRepository']>({ exit: r => `returned ${r?.path}` })
getBestRepository(editorOrUri?: TextEditor | Uri, editor?: TextEditor): Repository | undefined {
if (this.repositoryCount === 0) return undefined;
const repo = await this.getRepository(filePathOrUri, true);
return repo?.path;
}
if (editorOrUri != null && editorOrUri instanceof Uri) {
const repo = this.getRepository(editorOrUri);
if (repo != null) return repo;
@log<GitProviderService['getRepoPathOrActive']>({
args: { 1: e => (e != null ? `TextEditor(${Logger.toLoggable(e.document.uri)})` : undefined) },
})
async getRepoPathOrActive(uri: Uri | undefined, editor: TextEditor | undefined) {
const repoPath = await this.getRepoPath(uri);
if (repoPath) return repoPath;
editorOrUri = undefined;
}
return this.getActiveRepoPath(editor);
editor = editorOrUri ?? editor ?? window.activeTextEditor;
return (editor != null ? this.getRepository(editor.document.uri) : undefined) ?? this.highlander;
}
private _pathToRepoPathCache = new Map<string, PromiseOrValue<string | null>>();
async getRepository(repoPath: string, createIfNeeded?: boolean): Promise<Repository | undefined>;
async getRepository(uri: Uri, createIfNeeded?: boolean): Promise<Repository | undefined>;
async getRepository(repoPathOrUri: string | Uri, createIfNeeded?: boolean): Promise<Repository | undefined>;
@log<GitProviderService['getRepository']>({ exit: repo => `returned ${repo?.path ?? 'undefined'}` })
async getRepository(repoPathOrUri: string | Uri, createIfNeeded: boolean = false): Promise<Repository | undefined> {
if (!createIfNeeded && this.repositoryCount === 0) return undefined;
private _pendingRepositories = new Map<string, Promise<Repository | undefined>>();
@log<GitProviderService['getRepository']>({ exit: r => `returned ${r?.path}` })
async getOrCreateRepository(uri: Uri, detectNested?: boolean): Promise<Repository | undefined> {
const cc = Logger.getCorrelationContext();
let isVslsScheme: boolean | undefined;
let repo: Repository | undefined;
let rp: string | null;
let filePath: string;
if (typeof repoPathOrUri === 'string') {
filePath = normalizePath(repoPathOrUri);
} else {
if (GitUri.is(repoPathOrUri) && repoPathOrUri.repoPath) {
repo = this.getCachedRepository(repoPathOrUri.repoPath);
if (repo != null) return repo;
}
const folderPath = dirname(uri.fsPath);
const repository = this.getRepository(uri);
filePath = normalizePath(repoPathOrUri.fsPath);
isVslsScheme = repoPathOrUri.scheme === DocumentSchemes.Vsls;
detectNested = detectNested ?? configuration.get('detectNestedRepositories');
if (!detectNested) {
if (repository != null) return repository;
} else if (this._visitedPaths.has(folderPath)) {
return repository;
}
repo = this.getCachedRepository(filePath);
if (repo != null) return repo;
let repoPathOrPromise = this._pathToRepoPathCache.get(filePath);
if (repoPathOrPromise !== undefined) {
rp = isPromise(repoPathOrPromise) ? await repoPathOrPromise : repoPathOrPromise;
// If the repoPath is explicitly null, then we know no repo exists
if (rp === null) return undefined;
repo = this.getCachedRepository(rp);
// If the repo exists or if we aren't creating it, just return what we found cached
if (!createIfNeeded || repo != null) return repo;
}
async function findRepoPath(this: GitProviderService): Promise<string | null> {
const { provider, path } = this.getProvider(repoPathOrUri);
rp = (await provider.getRepoPath(path)) ?? null;
// Store the found repoPath for this filePath, so we can avoid future lookups for the filePath
this._pathToRepoPathCache.set(filePath, rp);
const key = asKey(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);
this._visitedPaths.set(folderPath);
if (repoUri == null) return undefined;
let 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
let root = this._repositories.getClosest(uri);
// If we can't find the repo and we are a guest, check if we are a "root" workspace
if (root == null && (uri.scheme === DocumentSchemes.Vsls || this.container.vsls.isMaybeGuest)) {
// TODO@eamodio verify this works for live share
let path = uri.fsPath;
if (!vslsUriPrefixRegex.test(path)) {
path = normalizePath(path);
const vslsPath = `/~0${path.charCodeAt(0) === CharCode.Slash ? path : `/${path}`}`;
root = this._repositories.getClosest(Uri.file(vslsPath).with({ scheme: DocumentSchemes.Vsls }));
}
}
if (rp == null) return null;
let folder: WorkspaceFolder | undefined;
if (root != null) {
folder = root.folder;
} else {
folder = workspace.getWorkspaceFolder(repoUri);
if (folder == null) {
folder = {
uri: uri,
name: basename(normalizePath(repoUri.path)),
index: this.repositoryCount,
};
}
}
// Store the found repoPath for itself, so we can avoid future lookups for the repoPath
this._pathToRepoPathCache.set(rp, rp);
Logger.log(cc, `Repository found in '${repoUri.toString(false)}'`);
repository = provider.createRepository(folder, repoUri, false);
this._repositories.add(repository);
if (!createIfNeeded || this._repositories.has(rp)) return rp;
this._pendingRepositories.delete(key);
// If this new repo is inside one of our known roots and we we don't already know about, add it
const root = this.findRepositoryForPath(rp, isVslsScheme);
this.updateContext();
// Send a notification that the repositories changed
queueMicrotask(() => this.fireRepositoriesChanged([repository!]));
let folder;
if (root != null) {
// Not sure why I added this for vsls (I can't see a reason for it anymore), but if it is added it will break submodules
// rp = root.path;
folder = root.folder;
} else {
const uri = GitUri.file(rp, isVslsScheme);
folder = workspace.getWorkspaceFolder(uri);
if (folder == null) {
const parts = rp.split('/');
folder = {
uri: uri,
name: parts[parts.length - 1],
index: this.repositoryCount,
};
}
return repository;
}
Logger.log(cc, `Repository found in '${rp}'`);
repo = provider.createRepository(folder, rp, false);
this._repositories.set(rp, repo);
promise = findRepository.call(this);
this._pendingRepositories.set(key, promise);
}
this.updateContext();
// Send a notification that the repositories changed
queueMicrotask(() => this.fireRepositoriesChanged([repo!]));
return promise;
}
return rp;
}
@log<GitProviderService['getOrCreateRepositoryForEditor']>({
args: { 0: e => (e != null ? `TextEditor(${Logger.toLoggable(e.document.uri)})` : undefined) },
})
async getOrCreateRepositoryForEditor(editor?: TextEditor): Promise<Repository | undefined> {
editor = editor ?? window.activeTextEditor;
repoPathOrPromise = findRepoPath.call(this);
this._pathToRepoPathCache.set(filePath, repoPathOrPromise);
if (editor == null) return this.highlander;
rp = await repoPathOrPromise;
return rp != null ? this.getCachedRepository(rp) : undefined;
return this.getOrCreateRepository(editor.document.uri);
}
@debug()
private findRepositoryForPath(path: string, isVslsScheme: boolean | undefined): Repository | undefined {
getRepository(uri: Uri): Repository | undefined;
getRepository(path: string): Repository | undefined;
getRepository(pathOrUri: string | Uri): Repository | undefined;
@log<GitProviderService['getRepository']>({ exit: r => `returned ${r?.path}` })
getRepository(pathOrUri?: string | Uri): Repository | undefined {
if (this.repositoryCount === 0) return undefined;
if (pathOrUri == null) return undefined;
function findBySubPath(repositories: Map<string, Repository>, path: string) {
const repos = [...repositories.values()].sort((a, b) => a.path.length - b.path.length);
for (const repo of repos) {
if (isDescendent(path, repo.path)) return repo;
}
if (typeof pathOrUri === 'string') {
if (!pathOrUri) return undefined;
return undefined;
return this._repositories.getClosest(Uri.file(normalizePath(pathOrUri)));
}
let repo = findBySubPath(this._repositories, path);
// If we can't find the repo and we are a guest, check if we are a "root" workspace
if (repo == null && isVslsScheme !== false && this.container.vsls.isMaybeGuest) {
if (!vslsUriPrefixRegex.test(path)) {
path = normalizePath(path);
const vslsPath = `/~0${path.charCodeAt(0) === CharCode.Slash ? path : `/${path}`}`;
repo = findBySubPath(this._repositories, vslsPath);
}
}
return repo;
return this._repositories.getClosest(pathOrUri);
}
async getLocalInfoFromRemoteUri(
@ -1726,7 +1638,7 @@ export class GitProviderService implements Disposable {
async hasRemotes(repoPath: string | Uri | undefined): Promise<boolean> {
if (repoPath == null) return false;
const repository = await this.getRepository(repoPath);
const repository = this.getRepository(repoPath);
if (repository == null) return false;
return repository.hasRemotes();
@ -1736,23 +1648,23 @@ export class GitProviderService implements Disposable {
async hasTrackingBranch(repoPath: string | undefined): Promise<boolean> {
if (repoPath == null) return false;
const repository = await this.getRepository(repoPath);
const repository = this.getRepository(repoPath);
if (repository == null) return false;
return repository.hasUpstreamBranch();
}
@log<GitProviderService['isActiveRepoPath']>({
args: { 1: e => (e != null ? `TextEditor(${Logger.toLoggable(e.document.uri)})` : undefined) },
@log<GitProviderService['isRepositoryForEditor']>({
args: {
0: r => r.uri.toString(false),
1: e => (e != null ? `TextEditor(${Logger.toLoggable(e.document.uri)})` : undefined),
},
})
async isActiveRepoPath(repoPath: string | undefined, editor?: TextEditor): Promise<boolean> {
if (repoPath == null) return false;
isRepositoryForEditor(repository: Repository, editor?: TextEditor): boolean {
editor = editor ?? window.activeTextEditor;
if (editor == null) return false;
const doc = await this.container.tracker.getOrAdd(editor.document.uri);
return repoPath === doc?.uri.repoPath;
return repository === this.getRepository(editor.document.uri);
}
isTrackable(uri: Uri): boolean {

+ 39
- 18
src/git/gitUri.ts 查看文件

@ -251,17 +251,22 @@ export class GitUri extends (Uri as any as UriEx) {
})
static async fromUri(uri: Uri): Promise<GitUri> {
if (GitUri.is(uri)) return uri;
if (!Container.instance.git.isTrackable(uri)) return new GitUri(uri);
if (uri.scheme === DocumentSchemes.GitLens) return new GitUri(uri);
// If this is a git uri, find its repoPath
if (uri.scheme === DocumentSchemes.Git) {
let data: { path: string; ref: string } | undefined;
try {
const data: { path: string; ref: string } = JSON.parse(uri.query);
data = JSON.parse(uri.query);
} catch {}
const repoPath = await Container.instance.git.getRepoPath(data.path);
if (data?.path) {
const repository = await Container.instance.git.getOrCreateRepository(Uri.file(data.path));
if (repository == null) {
debugger;
throw new Error(`Unable to find repository for uri=${uri.toString(false)}`);
}
let ref;
switch (data.ref) {
@ -281,30 +286,45 @@ export class GitUri extends (Uri as any as UriEx) {
const commitish: GitCommitish = {
fileName: data.path,
repoPath: repoPath!,
repoPath: repository?.path,
sha: ref,
};
return new GitUri(uri, commitish);
} catch {}
}
}
if (uri.scheme === DocumentSchemes.PRs) {
let data:
| {
baseCommit: string;
headCommit: string;
isBase: boolean;
fileName: string;
prNumber: number;
status: number;
remoteName: string;
}
| undefined;
try {
const data: {
baseCommit: string;
headCommit: string;
isBase: boolean;
fileName: string;
prNumber: number;
status: number;
remoteName: string;
} = JSON.parse(uri.query);
data = JSON.parse(uri.query);
} catch {}
if (data?.fileName) {
const repository = await Container.instance.git.getOrCreateRepository(Uri.file(data.fileName));
if (repository == null) {
debugger;
throw new Error(`Unable to find repository for uri=${uri.toString(false)}`);
}
let repoPath = normalizePath(uri.fsPath);
if (repoPath.endsWith(data.fileName)) {
repoPath = repoPath.substr(0, repoPath.length - data.fileName.length - 1);
} else {
repoPath = (await Container.instance.git.getRepoPath(uri.fsPath))!;
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
repoPath = (await Container.instance.git.getOrCreateRepository(uri))?.path!;
if (!repoPath) {
debugger;
}
}
const commitish: GitCommitish = {
@ -313,10 +333,11 @@ export class GitUri extends (Uri as any as UriEx) {
sha: data.isBase ? data.baseCommit : data.headCommit,
};
return new GitUri(uri, commitish);
} catch {}
}
}
return new GitUri(uri, await Container.instance.git.getRepoPath(uri));
const repository = await Container.instance.git.getOrCreateRepository(uri);
return new GitUri(uri, repository?.path);
}
static getDirectory(fileName: string, relativeTo?: string): string {

+ 6
- 6
src/git/models/branch.ts 查看文件

@ -206,7 +206,7 @@ export class GitBranch implements GitBranchReference {
const remoteName = this.getRemoteName();
if (remoteName == null) return undefined;
const remotes = await Container.instance.git.getRemotes(this.repoPath);
const remotes = await Container.instance.git.getRemotesWithProviders(this.repoPath);
if (remotes.length === 0) return undefined;
return remotes.find(r => r.name === remoteName);
@ -232,7 +232,7 @@ export class GitBranch implements GitBranchReference {
return GitBranchStatus.UpToDate;
}
const remotes = await Container.instance.git.getRemotes(this.repoPath);
const remotes = await Container.instance.git.getRemotesWithProviders(this.repoPath);
if (remotes.length > 0) return GitBranchStatus.Unpublished;
return GitBranchStatus.Local;
@ -255,12 +255,12 @@ export class GitBranch implements GitBranchReference {
return starred !== undefined && starred[this.id] === true;
}
async star() {
await (await Container.instance.git.getRepository(this.repoPath))?.star(this);
star() {
return Container.instance.git.getRepository(this.repoPath)?.star(this);
}
async unstar() {
await (await Container.instance.git.getRepository(this.repoPath))?.unstar(this);
unstar() {
return Container.instance.git.getRepository(this.repoPath)?.unstar(this);
}
static formatDetached(sha: string): string {

+ 1
- 1
src/git/models/repository.ts 查看文件

@ -599,7 +599,7 @@ export class Repository implements Disposable {
}
// Since we are caching the results, always sort
this._remotes = this.container.git.getRemotesCore(this.path, this._providers, { sort: true });
this._remotes = this.container.git.getRemotes(this.path, { providers: this._providers, sort: true });
void this.subscribeToRemotes(this._remotes);
}

+ 1
- 1
src/git/models/status.ts 查看文件

@ -238,7 +238,7 @@ export class GitStatus {
async getRemote(): Promise<GitRemote | undefined> {
if (this.upstream == null) return undefined;
const remotes = await Container.instance.git.getRemotes(this.repoPath);
const remotes = await Container.instance.git.getRemotesWithProviders(this.repoPath);
if (remotes.length === 0) return undefined;
const remoteName = GitBranch.getRemote(this.upstream);

+ 1
- 1
src/hovers/hovers.ts 查看文件

@ -203,7 +203,7 @@ export namespace Hovers {
dateFormat = 'MMMM Do, YYYY h:mma';
}
const remotes = await Container.instance.git.getRemotes(commit.repoPath, { sort: true });
const remotes = await Container.instance.git.getRemotesWithProviders(commit.repoPath, { sort: true });
if (options?.cancellationToken?.isCancellationRequested) return new MarkdownString();

+ 9
- 12
src/quickpicks/referencePicker.ts 查看文件

@ -88,17 +88,14 @@ export namespace ReferencePicker {
quickpick.show();
const getValidateGitReference = getValidateGitReferenceFn(
(await Container.instance.git.getRepository(repoPath))!,
{
buttons: [QuickCommandButtons.RevealInSideBar],
ranges:
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
options?.allowEnteringRefs && typeof options.allowEnteringRefs !== 'boolean'
? options.allowEnteringRefs.ranges
: undefined,
},
);
const getValidateGitReference = getValidateGitReferenceFn(Container.instance.git.getRepository(repoPath), {
buttons: [QuickCommandButtons.RevealInSideBar],
ranges:
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
options?.allowEnteringRefs && typeof options.allowEnteringRefs !== 'boolean'
? options.allowEnteringRefs.ranges
: undefined,
});
quickpick.items = await items;
@ -164,7 +161,7 @@ export namespace ReferencePicker {
include = include ?? ReferencesQuickPickIncludes.BranchesAndTags;
const items: ReferencesQuickPickItem[] = await getBranchesAndOrTags(
(await Container.instance.git.getRepository(repoPath))!,
Container.instance.git.getRepository(repoPath),
include && ReferencesQuickPickIncludes.BranchesAndTags
? ['branches', 'tags']
: include && ReferencesQuickPickIncludes.Branches

+ 59
- 0
src/repositories.ts 查看文件

@ -0,0 +1,59 @@
import { Uri } from 'vscode';
import { Repository } from './git/models/repository';
import { UriTrie } from './system/trie';
export class Repositories {
private readonly _trie: UriTrie<Repository>;
private _count: number = 0;
constructor() {
this._trie = new UriTrie<Repository>();
}
get count(): number {
return this._count;
}
add(repository: Repository): boolean {
const added = this._trie.set(repository.uri, repository);
if (added) {
this._count++;
}
return added;
}
clear(): void {
this._count = 0;
this._trie.clear();
}
forEach(fn: (repository: Repository) => void, thisArg?: unknown): void {
for (const value of this._trie.getDescendants()) {
fn.call(thisArg, value);
}
}
get(uri: Uri): Repository | undefined {
return this._trie.get(uri);
}
getClosest(uri: Uri): Repository | undefined {
return this._trie.getClosest(uri);
}
has(uri: Uri): boolean {
return this._trie.has(uri);
}
remove(uri: Uri): boolean {
const deleted = this._trie.delete(uri);
if (deleted) {
this._count--;
}
return deleted;
}
values(): IterableIterator<Repository> {
return this._trie.getDescendants();
}
}

+ 2
- 2
src/system/iterable.ts 查看文件

@ -109,7 +109,7 @@ export function find(source: Iterable | IterableIterator, predicate: (i
return null;
}
export function first<T>(source: Iterable<T>): T {
export function first<T>(source: Iterable<T> | IterableIterator<T>): T {
return source[Symbol.iterator]().next().value;
}
@ -163,7 +163,7 @@ export function last(source: Iterable): T | undefined {
export function* map<T, TMapped>(
source: Iterable<T> | IterableIterator<T>,
mapper: (item: T) => TMapped,
): Iterable<TMapped> {
): IterableIterator<TMapped> {
for (const item of source) {
yield mapper(item);
}

+ 42
- 11
src/system/path.ts 查看文件

@ -1,7 +1,7 @@
'use strict';
import { basename, dirname } from 'path';
import { Uri } from 'vscode';
import { isWindows } from '@env/platform';
import { isLinux, isWindows } from '@env/platform';
// TODO@eamodio don't import from string here since it will break the tests because of ESM dependencies
// import { CharCode } from './string';
@ -11,6 +11,33 @@ const driveLetterNormalizeRegex = /(?<=^\/?)([A-Z])(?=:\/)/;
const pathNormalizeRegex = /\\/g;
const slash = 47; //slash;
export function commonBase(s1: string, s2: string, delimiter: string, ignoreCase?: boolean): string | undefined {
const index = commonBaseIndex(s1, s2, delimiter, ignoreCase);
return index > 0 ? s1.substring(0, index + 1) : undefined;
}
export function commonBaseIndex(s1: string, s2: string, delimiter: string, ignoreCase?: boolean): number {
if (s1.length === 0 || s2.length === 0) return 0;
if (ignoreCase ?? !isLinux) {
s1 = s1.toLowerCase();
s2 = s2.toLowerCase();
}
let char;
let index = 0;
for (let i = 0; i < s1.length; i++) {
char = s1[i];
if (char !== s2[i]) break;
if (char === delimiter) {
index = i;
}
}
return index;
}
export function isChild(uri: Uri, baseUri: Uri): boolean;
export function isChild(uri: Uri, basePath: string): boolean;
export function isChild(path: string, basePath: string): boolean;
@ -99,21 +126,25 @@ export function normalizePath(path: string): string {
return path;
}
export function splitPath(filePath: string, repoPath: string | undefined, extract: boolean = true): [string, string] {
export function splitPath(
path: string,
repoPath: string | undefined,
splitOnBaseIfMissing: boolean = false,
ignoreCase?: boolean,
): [string, string] {
if (repoPath) {
filePath = normalizePath(filePath);
path = normalizePath(path);
repoPath = normalizePath(repoPath);
const normalizedRepoPath = (
repoPath.charCodeAt(repoPath.length - 1) === slash ? repoPath : `${repoPath}/`
).toLowerCase();
if (filePath.toLowerCase().startsWith(normalizedRepoPath)) {
filePath = filePath.substring(normalizedRepoPath.length);
const index = commonBaseIndex(`${repoPath}/`, path, '/', ignoreCase);
if (index > 0) {
repoPath = path.substring(0, index);
path = path.substring(index + 1);
}
} else {
repoPath = normalizePath(extract ? dirname(filePath) : repoPath!);
filePath = normalizePath(extract ? basename(filePath) : filePath);
repoPath = normalizePath(splitOnBaseIfMissing ? dirname(path) : repoPath ?? '');
path = normalizePath(splitOnBaseIfMissing ? basename(path) : path);
}
return [filePath, repoPath];
return [path, repoPath];
}

+ 0
- 15
src/system/string.ts 查看文件

@ -133,21 +133,6 @@ export function escapeRegex(s: string) {
return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}
export function getCommonBase(s1: string, s2: string, delimiter: string) {
let char;
let index = 0;
for (let i = 0; i < s1.length; i++) {
char = s1[i];
if (char !== s2[i]) break;
if (char === delimiter) {
index = i;
}
}
return index > 0 ? s1.substring(0, index + 1) : undefined;
}
export function getDurationMilliseconds(start: [number, number]) {
const [secs, nanosecs] = hrtime(start);
return secs * 1000 + Math.floor(nanosecs / 1000000);

+ 155
- 0
src/test/suite/system/path.test.ts 查看文件

@ -0,0 +1,155 @@
import * as assert from 'assert';
import { splitPath } from '../../../system/path';
describe('Path Test Suite', () => {
function assertSplitPath(actual: [string, string], expected: [string, string]) {
assert.strictEqual(actual[0], expected[0]);
assert.strictEqual(actual[1], expected[1]);
}
it('splitPath: no repoPath', () => {
assertSplitPath(splitPath('C:\\User\\Name\\code\\gitkraken\\vscode-gitlens', ''), [
'c:/User/Name/code/gitkraken/vscode-gitlens',
'',
]);
assertSplitPath(splitPath('C:\\User\\Name\\code\\gitkraken\\vscode-gitlens\\', ''), [
'c:/User/Name/code/gitkraken/vscode-gitlens',
'',
]);
assertSplitPath(splitPath('C:/User/Name/code/gitkraken/vscode-gitlens', ''), [
'c:/User/Name/code/gitkraken/vscode-gitlens',
'',
]);
assertSplitPath(splitPath('C:/User/Name/code/gitkraken/vscode-gitlens/', ''), [
'c:/User/Name/code/gitkraken/vscode-gitlens',
'',
]);
});
it('splitPath: no repoPath (split base)', () => {
assertSplitPath(splitPath('C:\\User\\Name\\code\\gitkraken\\vscode-gitlens', '', true), [
'vscode-gitlens',
'c:/User/Name/code/gitkraken',
]);
assertSplitPath(splitPath('C:\\User\\Name\\code\\gitkraken\\vscode-gitlens\\', '', true), [
'vscode-gitlens',
'c:/User/Name/code/gitkraken',
]);
assertSplitPath(splitPath('C:/User/Name/code/gitkraken/vscode-gitlens', '', true), [
'vscode-gitlens',
'c:/User/Name/code/gitkraken',
]);
assertSplitPath(splitPath('C:/User/Name/code/gitkraken/vscode-gitlens/', '', true), [
'vscode-gitlens',
'c:/User/Name/code/gitkraken',
]);
});
it('splitPath: match', () => {
assertSplitPath(
splitPath(
'C:\\User\\Name\\code\\gitkraken\\vscode-gitlens\\foo\\bar\\baz.ts',
'C:\\User\\Name\\code\\gitkraken\\vscode-gitlens',
),
['foo/bar/baz.ts', 'c:/User/Name/code/gitkraken/vscode-gitlens'],
);
assertSplitPath(
splitPath(
'C:\\User\\Name\\code\\gitkraken\\vscode-gitlens\\foo\\bar\\baz.ts',
'C:\\User\\Name\\code\\gitkraken\\vscode-gitlens\\',
),
['foo/bar/baz.ts', 'c:/User/Name/code/gitkraken/vscode-gitlens'],
);
assertSplitPath(
splitPath(
'C:\\User\\Name\\code\\gitkraken\\vscode-gitlens\\foo\\bar\\baz.ts',
'c:/User/Name/code/gitkraken/vscode-gitlens',
),
['foo/bar/baz.ts', 'c:/User/Name/code/gitkraken/vscode-gitlens'],
);
assertSplitPath(
splitPath(
'C:\\User\\Name\\code\\gitkraken\\vscode-gitlens\\foo\\bar\\baz.ts',
'c:/User/Name/code/gitkraken/vscode-gitlens/',
),
['foo/bar/baz.ts', 'c:/User/Name/code/gitkraken/vscode-gitlens'],
);
assertSplitPath(
splitPath(
'C:/User/Name/code/gitkraken/vscode-gitlens/foo/bar/baz.ts',
'c:/User/Name/code/gitkraken/vscode-gitlens',
),
['foo/bar/baz.ts', 'c:/User/Name/code/gitkraken/vscode-gitlens'],
);
assertSplitPath(
splitPath(
'C:/User/Name/code/gitkraken/vscode-gitlens/foo/bar/baz.ts',
'c:/User/Name/code/gitkraken/vscode-gitlens/',
),
['foo/bar/baz.ts', 'c:/User/Name/code/gitkraken/vscode-gitlens'],
);
});
it('splitPath: match (casing)', () => {
assertSplitPath(
splitPath(
'C:/USER/NAME/CODE/GITKRAKEN/VSCODE-GITLENS/FOO/BAR/BAZ.TS',
'c:/User/Name/code/gitkraken/vscode-gitlens/',
undefined,
true,
),
['FOO/BAR/BAZ.TS', 'c:/USER/NAME/CODE/GITKRAKEN/VSCODE-GITLENS'],
);
assertSplitPath(
splitPath(
'C:/USER/NAME/CODE/GITKRAKEN/VSCODE-GITLENS/FOO/BAR/BAZ.TS',
'c:/User/Name/code/gitkraken/vscode-gitlens/',
undefined,
false,
),
['USER/NAME/CODE/GITKRAKEN/VSCODE-GITLENS/FOO/BAR/BAZ.TS', 'c:'],
);
assertSplitPath(
splitPath(
'/USER/NAME/CODE/GITKRAKEN/VSCODE-GITLENS/FOO/BAR/BAZ.TS',
'/User/Name/code/gitkraken/vscode-gitlens/',
undefined,
true,
),
['FOO/BAR/BAZ.TS', '/USER/NAME/CODE/GITKRAKEN/VSCODE-GITLENS'],
);
assertSplitPath(
splitPath(
'/USER/NAME/CODE/GITKRAKEN/VSCODE-GITLENS/FOO/BAR/BAZ.TS',
'/User/Name/code/gitkraken/vscode-gitlens/',
undefined,
false,
),
['/USER/NAME/CODE/GITKRAKEN/VSCODE-GITLENS/FOO/BAR/BAZ.TS', '/User/Name/code/gitkraken/vscode-gitlens'],
);
});
it('splitPath: no match', () => {
assertSplitPath(
splitPath(
'/foo/User/Name/code/gitkraken/vscode-gitlens/foo/bar/baz.ts',
'/User/Name/code/gitkraken/vscode-gitlens',
),
['/foo/User/Name/code/gitkraken/vscode-gitlens/foo/bar/baz.ts', '/User/Name/code/gitkraken/vscode-gitlens'],
);
});
});

+ 1
- 1
src/trackers/trackedDocument.ts 查看文件

@ -173,7 +173,7 @@ export class TrackedDocument implements Disposable {
const active = getEditorIfActive(this.document);
const wasBlameable = forceBlameChange ? undefined : this.isBlameable;
const repo = await this.container.git.getRepository(this._uri);
const repo = this.container.git.getRepository(this._uri);
if (repo == null) {
this._isTracked = false;
this._hasRemotes = false;

+ 1
- 1
src/views/nodes/branchNode.ts 查看文件

@ -369,7 +369,7 @@ export class BranchNode
}
} else {
const providers = GitRemote.getHighlanderProviders(
await this.view.container.git.getRemotes(this.branch.repoPath),
await this.view.container.git.getRemotesWithProviders(this.branch.repoPath),
);
const providerName = providers?.length ? providers[0].name : undefined;

+ 2
- 2
src/views/nodes/branchTrackingStatusNode.ts 查看文件

@ -150,7 +150,7 @@ export class BranchTrackingStatusNode extends ViewNode impleme
let lastFetched = 0;
if (this.upstreamType !== 'none') {
const repo = await this.view.container.git.getRepository(this.repoPath);
const repo = this.view.container.git.getRepository(this.repoPath);
lastFetched = (await repo?.getLastFetched()) ?? 0;
}
@ -227,7 +227,7 @@ export class BranchTrackingStatusNode extends ViewNode impleme
break;
}
case 'none': {
const remotes = await this.view.container.git.getRemotes(this.branch.repoPath);
const remotes = await this.view.container.git.getRemotesWithProviders(this.branch.repoPath);
const providers = GitRemote.getHighlanderProviders(remotes);
const providerName = providers?.length ? providers[0].name : undefined;

+ 1
- 1
src/views/nodes/commitNode.ts 查看文件

@ -135,7 +135,7 @@ export class CommitNode extends ViewRefNode
}
private async getTooltip() {
const remotes = await this.view.container.git.getRemotes(this.commit.repoPath);
const remotes = await this.view.container.git.getRemotesWithProviders(this.commit.repoPath);
const remote = await this.view.container.git.getRichRemoteProvider(remotes);
let autolinkedIssuesOrPullRequests;

+ 2
- 2
src/views/nodes/comparePickerNode.ts 查看文件

@ -27,14 +27,14 @@ export class ComparePickerNode extends ViewNode {
return [];
}
async getTreeItem(): Promise<TreeItem> {
getTreeItem(): TreeItem {
const selectedRef = this.selectedRef;
const repoPath = selectedRef?.repoPath;
let description;
if (repoPath !== undefined) {
if (this.view.container.git.repositoryCount > 1) {
const repo = await this.view.container.git.getRepository(repoPath);
const repo = this.view.container.git.getRepository(repoPath);
description = repo?.formattedName ?? repoPath;
}
}

+ 2
- 2
src/views/nodes/compareResultsNode.ts 查看文件

@ -141,10 +141,10 @@ export class CompareResultsNode extends ViewNode {
return this._children;
}
async getTreeItem(): Promise<TreeItem> {
getTreeItem(): TreeItem {
let description;
if (this.view.container.git.repositoryCount > 1) {
const repo = await this.view.container.git.getRepository(this.uri.repoPath!);
const repo = this.uri.repoPath ? this.view.container.git.getRepository(this.uri.repoPath) : undefined;
description = repo?.formattedName ?? this.uri.repoPath;
}

+ 2
- 2
src/views/nodes/fileHistoryNode.ts 查看文件

@ -166,8 +166,8 @@ export class FileHistoryNode extends SubscribeableViewNode impl
}
@debug()
protected async subscribe() {
const repo = await this.view.container.git.getRepository(this.uri);
protected subscribe() {
const repo = this.view.container.git.getRepository(this.uri);
if (repo == null) return undefined;
const subscription = Disposable.from(

+ 1
- 1
src/views/nodes/fileRevisionAsCommitNode.ts 查看文件

@ -208,7 +208,7 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode
}
private async getTooltip() {
const remotes = await this.view.container.git.getRemotes(this.commit.repoPath);
const remotes = await this.view.container.git.getRemotesWithProviders(this.commit.repoPath);
const remote = await this.view.container.git.getRichRemoteProvider(remotes);
let autolinkedIssuesOrPullRequests;

+ 2
- 2
src/views/nodes/lineHistoryNode.ts 查看文件

@ -261,8 +261,8 @@ export class LineHistoryNode
}
@debug()
protected async subscribe() {
const repo = await this.view.container.git.getRepository(this.uri);
protected subscribe() {
const repo = this.view.container.git.getRepository(this.uri);
if (repo == null) return undefined;
const subscription = Disposable.from(

+ 1
- 1
src/views/nodes/repositoryNode.ts 查看文件

@ -211,7 +211,7 @@ export class RepositoryNode extends SubscribeableViewNode {
let providerName;
if (status.upstream != null) {
const providers = GitRemote.getHighlanderProviders(
await this.view.container.git.getRemotes(status.repoPath),
await this.view.container.git.getRemotesWithProviders(status.repoPath),
);
providerName = providers?.length ? providers[0].name : undefined;
} else {

+ 1
- 1
src/views/nodes/searchResultsNode.ts 查看文件

@ -133,7 +133,7 @@ export class SearchResultsNode extends ViewNode implements
item.id = this.id;
item.contextValue = `${ContextValues.SearchResults}${this._pinned ? '+pinned' : ''}`;
if (this.view.container.git.repositoryCount > 1) {
const repo = await this.view.container.git.getRepository(this.repoPath);
const repo = this.view.container.git.getRepository(this.repoPath);
item.description = repo?.formattedName ?? this.repoPath;
}
if (this._pinned) {

+ 3
- 10
src/views/nodes/viewNode.ts 查看文件

@ -373,18 +373,11 @@ export abstract class RepositoryFolderNode<
async getTreeItem(): Promise<TreeItem> {
this.splatted = false;
let expand = this.repo.starred;
const [active, branch] = await Promise.all([
expand ? undefined : this.view.container.git.isActiveRepoPath(this.uri.repoPath),
this.repo.getBranch(),
]);
const branch = await this.repo.getBranch();
const ahead = (branch?.state.ahead ?? 0) > 0;
const behind = (branch?.state.behind ?? 0) > 0;
if (!expand && (active || ahead || behind)) {
expand = true;
}
const expand = ahead || behind || this.repo.starred || this.view.container.git.isRepositoryForEditor(this.repo);
const item = new TreeItem(
this.repo.formattedName ?? this.uri.repoPath ?? '',
@ -413,7 +406,7 @@ export abstract class RepositoryFolderNode<
let providerName;
if (branch.upstream != null) {
const providers = GitRemote.getHighlanderProviders(
await this.view.container.git.getRemotes(branch.repoPath),
await this.view.container.git.getRemotesWithProviders(branch.repoPath),
);
providerName = providers?.length ? providers[0].name : undefined;
} else {

+ 2
- 2
src/views/viewCommands.ts 查看文件

@ -293,7 +293,7 @@ export class ViewCommands {
: undefined;
if (from == null) {
const branch = await this.container.git.getBranch(
node?.repoPath ?? (await this.container.git.getActiveRepoPath()),
node?.repoPath ?? this.container.git.getBestRepository()?.uri,
);
from = branch;
}
@ -343,7 +343,7 @@ export class ViewCommands {
: undefined;
if (from == null) {
const branch = await this.container.git.getBranch(
node?.repoPath ?? (await this.container.git.getActiveRepoPath()),
node?.repoPath ?? this.container.git.getBestRepository()?.uri,
);
from = branch;
}

+ 1
- 1
src/webviews/rebaseEditor.ts 查看文件

@ -166,7 +166,7 @@ export class RebaseEditorProvider implements CustomTextEditorProvider, Disposabl
@debug({ args: false })
async resolveCustomTextEditor(document: TextDocument, panel: WebviewPanel, _token: CancellationToken) {
const repoPath = normalizePath(Uri.joinPath(document.uri, '..', '..', '..').fsPath);
const repo = await this.container.git.getRepository(repoPath);
const repo = this.container.git.getRepository(repoPath);
const subscriptions: Disposable[] = [];
const context: RebaseEditorContext = {

Loading…
取消
儲存