From e1303d4b46279ff578ce456e0f59fc6001242874 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Fri, 28 Jan 2022 03:01:35 -0500 Subject: [PATCH] Refactors vsls support into its own provider Refactors Git static class into an instance (so it could be re-used for vsls) Reworks vsls guest for better stability --- src/codelens/codeLensProvider.ts | 1 + src/constants.ts | 1 + src/container.ts | 2 +- src/env/browser/git.ts | 17 - src/env/browser/providers.ts | 17 + src/env/node/git.ts | 12 - src/env/node/git/git.ts | 555 +++++++++++++++----------------- src/env/node/git/localGitProvider.ts | 338 ++++++++++--------- src/env/node/git/vslsGitProvider.ts | 95 ++++++ src/env/node/providers.ts | 34 ++ src/git/gitProvider.ts | 5 +- src/git/gitProviderService.ts | 41 +-- src/git/gitUri.ts | 9 - src/git/models/repository.ts | 3 +- src/premium/github/githubGitProvider.ts | 25 +- src/repositories.ts | 15 +- src/system/event.ts | 21 ++ src/system/function.ts | 50 --- src/system/path.ts | 98 +++++- src/system/promise.ts | 52 +++ src/trackers/documentTracker.ts | 6 +- src/vsls/guest.ts | 29 +- src/vsls/host.ts | 88 ++--- src/vsls/protocol.ts | 13 +- src/vsls/vsls.ts | 135 ++++---- 25 files changed, 915 insertions(+), 747 deletions(-) delete mode 100644 src/env/browser/git.ts create mode 100644 src/env/browser/providers.ts delete mode 100644 src/env/node/git.ts create mode 100644 src/env/node/git/vslsGitProvider.ts create mode 100644 src/env/node/providers.ts diff --git a/src/codelens/codeLensProvider.ts b/src/codelens/codeLensProvider.ts index bf49f7b..964e560 100644 --- a/src/codelens/codeLensProvider.ts +++ b/src/codelens/codeLensProvider.ts @@ -91,6 +91,7 @@ export class GitCodeLensProvider implements CodeLensProvider { { scheme: DocumentSchemes.GitLens }, { scheme: DocumentSchemes.PRs }, { scheme: DocumentSchemes.Vsls }, + { scheme: DocumentSchemes.VslsScc }, { scheme: DocumentSchemes.Virtual }, { scheme: DocumentSchemes.GitHub }, ]; diff --git a/src/constants.ts b/src/constants.ts index 0bda641..93de039 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -97,6 +97,7 @@ export const enum DocumentSchemes { Output = 'output', PRs = 'pr', Vsls = 'vsls', + VslsScc = 'vsls-scc', Virtual = 'vscode-vfs', } diff --git a/src/container.ts b/src/container.ts index da4eb9d..c9561a0 100644 --- a/src/container.ts +++ b/src/container.ts @@ -1,5 +1,5 @@ import { commands, ConfigurationChangeEvent, ConfigurationScope, Event, EventEmitter, ExtensionContext } from 'vscode'; -import { getSupportedGitProviders } from '@env/git'; +import { getSupportedGitProviders } from '@env/providers'; import { Autolinks } from './annotations/autolinks'; import { FileAnnotationController } from './annotations/fileAnnotationController'; import { LineAnnotationController } from './annotations/lineAnnotationController'; diff --git a/src/env/browser/git.ts b/src/env/browser/git.ts deleted file mode 100644 index c0d2508..0000000 --- a/src/env/browser/git.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Container } from '../../container'; -import { GitCommandOptions } from '../../git/commandOptions'; -import { GitHubGitProvider } from '../../premium/github/githubGitProvider'; -import { GitProvider } from '../../git/gitProvider'; -// Force import of GitHub since dynamic imports are not supported in the WebWorker ExtensionHost -import * as GitHub from '../../premium/github/github'; - -export function git(_options: GitCommandOptions, ..._args: any[]): Promise { - return Promise.resolve(''); -} - -export function getSupportedGitProviders(container: Container): GitProvider[] { - if (!container.config.experimental.virtualRepositories.enabled) return []; - - GitHub.GitHubApi; - return [new GitHubGitProvider(container)]; -} diff --git a/src/env/browser/providers.ts b/src/env/browser/providers.ts new file mode 100644 index 0000000..c0d2508 --- /dev/null +++ b/src/env/browser/providers.ts @@ -0,0 +1,17 @@ +import { Container } from '../../container'; +import { GitCommandOptions } from '../../git/commandOptions'; +import { GitHubGitProvider } from '../../premium/github/githubGitProvider'; +import { GitProvider } from '../../git/gitProvider'; +// Force import of GitHub since dynamic imports are not supported in the WebWorker ExtensionHost +import * as GitHub from '../../premium/github/github'; + +export function git(_options: GitCommandOptions, ..._args: any[]): Promise { + return Promise.resolve(''); +} + +export function getSupportedGitProviders(container: Container): GitProvider[] { + if (!container.config.experimental.virtualRepositories.enabled) return []; + + GitHub.GitHubApi; + return [new GitHubGitProvider(container)]; +} diff --git a/src/env/node/git.ts b/src/env/node/git.ts deleted file mode 100644 index d60c09e..0000000 --- a/src/env/node/git.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Container } from '../../container'; -import { GitProvider } from '../../git/gitProvider'; -import { GitHubGitProvider } from '../../premium/github/githubGitProvider'; -import { LocalGitProvider } from './git/localGitProvider'; - -export { git } from './git/git'; - -export function getSupportedGitProviders(container: Container): GitProvider[] { - return container.config.experimental.virtualRepositories.enabled - ? [new LocalGitProvider(container), new GitHubGitProvider(container)] - : [new LocalGitProvider(container)]; -} diff --git a/src/env/node/git/git.ts b/src/env/node/git/git.ts index 1fc364e..cc36f6b 100644 --- a/src/env/node/git/git.ts +++ b/src/env/node/git/git.ts @@ -1,14 +1,13 @@ -/* eslint-disable @typescript-eslint/naming-convention */ import { Uri, window, workspace } from 'vscode'; import { hrtime } from '@env/hrtime'; import { GlyphChars } from '../../../constants'; -import { Container } from '../../../container'; import { GitCommandOptions, GitErrorHandling } from '../../../git/commandOptions'; import { GitDiffFilter, GitRevision } from '../../../git/models'; import { GitBranchParser, GitLogParser, GitReflogParser, GitStashParser, GitTagParser } from '../../../git/parsers'; import { Logger } from '../../../logger'; -import { Strings, Versions } from '../../../system'; import { dirname, isAbsolute, isFolderGlob, joinPaths, normalizePath, splitPath } from '../../../system/path'; +import { getDurationMilliseconds } from '../../../system/string'; +import { compare, fromString } from '../../../system/version'; import { GitLocation } from './locator'; import { fsExists, run, RunError, RunOptions } from './shell'; @@ -48,128 +47,12 @@ const GitWarnings = { notAGitCommand: /'.+' is not a git command/i, }; -// A map of running git commands -- avoids running duplicate overlaping commands -const pendingCommands = new Map>(); - -export async function git(options: GitCommandOptions, ...args: any[]): Promise { - if (Container.instance.vsls.isMaybeGuest) { - if (options.local !== true) { - const guest = await Container.instance.vsls.guest(); - if (guest !== undefined) { - return guest.git(options, ...args); - } - } else { - // Since we will have a live share path here, just blank it out - options.cwd = ''; - } - } - - const start = hrtime(); - - const { configs, correlationKey, errors: errorHandling, encoding, ...opts } = options; - - const runOpts: RunOptions = { - ...opts, - encoding: (encoding ?? 'utf8') === 'utf8' ? 'utf8' : 'buffer', - // Adds GCM environment variables to avoid any possible credential issues -- from https://github.com/Microsoft/vscode/issues/26573#issuecomment-338686581 - // Shouldn't *really* be needed but better safe than sorry - env: { - ...process.env, - ...(options.env ?? emptyObj), - GCM_INTERACTIVE: 'NEVER', - GCM_PRESERVE_CREDS: 'TRUE', - LC_ALL: 'C', - }, - }; - - const gitCommand = `[${runOpts.cwd}] git ${args.join(' ')}`; - - const command = `${correlationKey !== undefined ? `${correlationKey}:` : ''}${gitCommand}`; - - let waiting; - let promise = pendingCommands.get(command); - if (promise === undefined) { - waiting = false; - - // Fixes https://github.com/eamodio/vscode-gitlens/issues/73 & https://github.com/eamodio/vscode-gitlens/issues/161 - // See https://stackoverflow.com/questions/4144417/how-to-handle-asian-characters-in-file-names-in-git-on-os-x - args.splice( - 0, - 0, - '-c', - 'core.quotepath=false', - '-c', - 'color.ui=false', - ...(configs !== undefined ? configs : emptyArray), - ); - - if (process.platform === 'win32') { - args.splice(0, 0, '-c', 'core.longpaths=true'); - } - - promise = run(await Git.path(), args, encoding ?? 'utf8', runOpts); - - pendingCommands.set(command, promise); - } else { - waiting = true; - Logger.debug(`[GIT ] ${gitCommand} ${GlyphChars.Dot} waiting...`); - } - - let exception: Error | undefined; - try { - return (await promise) as TOut; - } catch (ex) { - exception = ex; - - switch (errorHandling) { - case GitErrorHandling.Ignore: - exception = undefined; - return '' as TOut; - - case GitErrorHandling.Throw: - throw ex; - - default: { - const result = defaultExceptionHandler(ex, options.cwd, start); - exception = undefined; - return result as TOut; - } - } - } finally { - pendingCommands.delete(command); - - const duration = Strings.getDurationMilliseconds(start); - const slow = duration > Logger.slowCallWarningThreshold; - const status = - slow || waiting ? ` (${slow ? `slow${waiting ? ', waiting' : ''}` : ''}${waiting ? 'waiting' : ''})` : ''; - - if (exception != null) { - Logger.error( - '', - `[GIT ] [${runOpts.cwd}] git ${(exception.message || exception.toString() || '') - .trim() - .replace(/fatal: /g, '') - .replace(/\r?\n|\r/g, ` ${GlyphChars.Dot} `)} ${GlyphChars.Dot} ${duration} ms${status}`, - ); - } else if (slow) { - Logger.warn(`[GIT ] ${gitCommand} ${GlyphChars.Dot} ${duration} ms${status}`); - } else { - Logger.log(`[GIT ] ${gitCommand} ${GlyphChars.Dot} ${duration} ms${status}`); - } - Logger.logGitCommand( - `${gitCommand}${exception != null ? ` ${GlyphChars.Dot} FAILED` : ''}${waiting ? ' (waited)' : ''}`, - duration, - exception, - ); - } -} - function defaultExceptionHandler(ex: Error, cwd: string | undefined, start?: [number, number]): string { const msg = ex.message || ex.toString(); if (msg != null && msg.length !== 0) { for (const warning of Object.values(GitWarnings)) { if (warning.test(msg)) { - const duration = start !== undefined ? `${Strings.getDurationMilliseconds(start)} ms` : ''; + const duration = start !== undefined ? `${getDurationMilliseconds(start)} ms` : ''; Logger.warn( `[${cwd}] Git ${msg .trim() @@ -192,42 +75,148 @@ function defaultExceptionHandler(ex: Error, cwd: string | undefined, start?: [nu throw ex; } -export namespace Git { - let gitLocator!: () => Promise; - export function setLocator(locator: () => Promise): void { - gitLocator = locator; +export class Git { + // A map of running git commands -- avoids running duplicate overlaping commands + private readonly pendingCommands = new Map>(); + + async git(options: GitCommandOptions, ...args: any[]): Promise { + const start = hrtime(); + + const { configs, correlationKey, errors: errorHandling, encoding, ...opts } = options; + + const runOpts: RunOptions = { + ...opts, + encoding: (encoding ?? 'utf8') === 'utf8' ? 'utf8' : 'buffer', + // Adds GCM environment variables to avoid any possible credential issues -- from https://github.com/Microsoft/vscode/issues/26573#issuecomment-338686581 + // Shouldn't *really* be needed but better safe than sorry + env: { + ...process.env, + ...(options.env ?? emptyObj), + GCM_INTERACTIVE: 'NEVER', + GCM_PRESERVE_CREDS: 'TRUE', + LC_ALL: 'C', + }, + }; + + const gitCommand = `[${runOpts.cwd}] git ${args.join(' ')}`; + + const command = `${correlationKey !== undefined ? `${correlationKey}:` : ''}${gitCommand}`; + + let waiting; + let promise = this.pendingCommands.get(command); + if (promise === undefined) { + waiting = false; + + // Fixes https://github.com/eamodio/vscode-gitlens/issues/73 & https://github.com/eamodio/vscode-gitlens/issues/161 + // See https://stackoverflow.com/questions/4144417/how-to-handle-asian-characters-in-file-names-in-git-on-os-x + args.splice( + 0, + 0, + '-c', + 'core.quotepath=false', + '-c', + 'color.ui=false', + ...(configs !== undefined ? configs : emptyArray), + ); + + if (process.platform === 'win32') { + args.splice(0, 0, '-c', 'core.longpaths=true'); + } + + promise = run(await this.path(), args, encoding ?? 'utf8', runOpts); + + this.pendingCommands.set(command, promise); + } else { + waiting = true; + Logger.debug(`[GIT ] ${gitCommand} ${GlyphChars.Dot} waiting...`); + } + + let exception: Error | undefined; + try { + return (await promise) as TOut; + } catch (ex) { + exception = ex; + + switch (errorHandling) { + case GitErrorHandling.Ignore: + exception = undefined; + return '' as TOut; + + case GitErrorHandling.Throw: + throw ex; + + default: { + const result = defaultExceptionHandler(ex, options.cwd, start); + exception = undefined; + return result as TOut; + } + } + } finally { + this.pendingCommands.delete(command); + + const duration = getDurationMilliseconds(start); + const slow = duration > Logger.slowCallWarningThreshold; + const status = + slow || waiting + ? ` (${slow ? `slow${waiting ? ', waiting' : ''}` : ''}${waiting ? 'waiting' : ''})` + : ''; + + if (exception != null) { + Logger.error( + '', + `[GIT ] [${runOpts.cwd}] git ${(exception.message || exception.toString() || '') + .trim() + .replace(/fatal: /g, '') + .replace(/\r?\n|\r/g, ` ${GlyphChars.Dot} `)} ${GlyphChars.Dot} ${duration} ms${status}`, + ); + } else if (slow) { + Logger.warn(`[GIT ] ${gitCommand} ${GlyphChars.Dot} ${duration} ms${status}`); + } else { + Logger.log(`[GIT ] ${gitCommand} ${GlyphChars.Dot} ${duration} ms${status}`); + } + Logger.logGitCommand( + `${gitCommand}${exception != null ? ` ${GlyphChars.Dot} FAILED` : ''}${waiting ? ' (waited)' : ''}`, + duration, + exception, + ); + } + } + + private gitLocator!: () => Promise; + setLocator(locator: () => Promise): void { + this.gitLocator = locator; } - export async function path(): Promise { - return (await gitLocator()).path; + async path(): Promise { + return (await this.gitLocator()).path; } - export async function version(): Promise { - return (await gitLocator()).version; + async version(): Promise { + return (await this.gitLocator()).version; } - export async function isAtLeastVersion(minimum: string): Promise { - const result = Versions.compare(Versions.fromString(await Git.version()), Versions.fromString(minimum)); + async isAtLeastVersion(minimum: string): Promise { + const result = compare(fromString(await this.version()), fromString(minimum)); return result !== -1; } // Git commands - export function add(repoPath: string | undefined, pathspec: string) { - return git({ cwd: repoPath }, 'add', '-A', '--', pathspec); + add(repoPath: string | undefined, pathspec: string) { + return this.git({ cwd: repoPath }, 'add', '-A', '--', pathspec); } - export function apply(repoPath: string | undefined, patch: string, options: { allowConflicts?: boolean } = {}) { + apply(repoPath: string | undefined, patch: string, options: { allowConflicts?: boolean } = {}) { const params = ['apply', '--whitespace=warn']; if (options.allowConflicts) { params.push('-3'); } - return git({ cwd: repoPath, stdin: patch }, ...params); + return this.git({ cwd: repoPath, stdin: patch }, ...params); } - const ignoreRevsFileMap = new Map(); + private readonly ignoreRevsFileMap = new Map(); - export async function blame( + async blame( repoPath: string | undefined, fileName: string, ref?: string, @@ -249,14 +238,14 @@ export namespace Git { const index = params.indexOf('--ignore-revs-file'); if (index !== -1) { // Ensure the version of Git supports the --ignore-revs-file flag, otherwise the blame will fail - let supported = await Git.isAtLeastVersion('2.23'); + let supported = await this.isAtLeastVersion('2.23'); if (supported) { let ignoreRevsFile = params[index + 1]; if (!isAbsolute(ignoreRevsFile)) { ignoreRevsFile = joinPaths(repoPath ?? '', ignoreRevsFile); } - const exists = ignoreRevsFileMap.get(ignoreRevsFile); + const exists = this.ignoreRevsFileMap.get(ignoreRevsFile); if (exists !== undefined) { supported = exists; } else { @@ -267,7 +256,7 @@ export namespace Git { supported = false; } - ignoreRevsFileMap.set(ignoreRevsFile, supported); + this.ignoreRevsFileMap.set(ignoreRevsFile, supported); } } @@ -284,16 +273,16 @@ export namespace Git { params.push('--contents', '-'); // Get the file contents for the staged version using `:` - stdin = await Git.show(repoPath, fileName, ':'); + stdin = await this.show(repoPath, fileName, ':'); } else { params.push(ref); } } - return git({ cwd: root, stdin: stdin }, ...params, '--', file); + return this.git({ cwd: root, stdin: stdin }, ...params, '--', file); } - export function blame__contents( + blame__contents( repoPath: string | undefined, fileName: string, contents: string, @@ -322,7 +311,7 @@ export namespace Git { // Pipe the blame contents to stdin params.push('--contents', '-'); - return git( + return this.git( { cwd: root, stdin: contents, correlationKey: options.correlationKey }, ...params, '--', @@ -330,7 +319,7 @@ export namespace Git { ); } - export function branch__containsOrPointsAt( + branch__containsOrPointsAt( repoPath: string, ref: string, { @@ -348,14 +337,14 @@ export namespace Git { params.push(name); } - return git( + return this.git( { cwd: repoPath, configs: ['-c', 'color.branch=false'], errors: GitErrorHandling.Ignore }, ...params, ); } - export function check_ignore(repoPath: string, ...files: string[]) { - return git( + check_ignore(repoPath: string, ...files: string[]) { + return this.git( { cwd: repoPath, errors: GitErrorHandling.Ignore, stdin: files.join('\0') }, 'check-ignore', '-z', @@ -363,15 +352,11 @@ export namespace Git { ); } - export function check_mailmap(repoPath: string, author: string) { - return git({ cwd: repoPath, errors: GitErrorHandling.Ignore, local: true }, 'check-mailmap', author); + check_mailmap(repoPath: string, author: string) { + return this.git({ cwd: repoPath, errors: GitErrorHandling.Ignore }, 'check-mailmap', author); } - export async function check_ref_format( - ref: string, - repoPath?: string, - options: { branch?: boolean } = { branch: true }, - ) { + async check_ref_format(ref: string, repoPath?: string, options: { branch?: boolean } = { branch: true }) { const params = ['check-ref-format']; if (options.branch) { params.push('--branch'); @@ -380,8 +365,8 @@ export namespace Git { } try { - const data = await git( - { cwd: repoPath ?? '', errors: GitErrorHandling.Throw, local: true }, + const data = await this.git( + { cwd: repoPath ?? '', errors: GitErrorHandling.Throw }, ...params, ref, ); @@ -391,7 +376,7 @@ export namespace Git { } } - export function checkout( + checkout( repoPath: string, ref: string, { createBranch, fileName }: { createBranch?: string; fileName?: string } = {}, @@ -409,11 +394,11 @@ export namespace Git { } } - return git({ cwd: repoPath }, ...params); + return this.git({ cwd: repoPath }, ...params); } - export async function config__get(key: string, repoPath?: string, options: { local?: boolean } = {}) { - const data = await git( + async config__get(key: string, repoPath?: string, options: { local?: boolean } = {}) { + const data = await this.git( { cwd: repoPath ?? '', errors: GitErrorHandling.Ignore, local: options.local }, 'config', '--get', @@ -422,8 +407,8 @@ export namespace Git { return data.length === 0 ? undefined : data.trim(); } - export async function config__get_regex(pattern: string, repoPath?: string, options: { local?: boolean } = {}) { - const data = await git( + async config__get_regex(pattern: string, repoPath?: string, options: { local?: boolean } = {}) { + const data = await this.git( { cwd: repoPath ?? '', errors: GitErrorHandling.Ignore, local: options.local }, 'config', '--get-regex', @@ -432,7 +417,7 @@ export namespace Git { return data.length === 0 ? undefined : data.trim(); } - export async function diff( + async diff( repoPath: string, fileName: string, ref1?: string, @@ -471,7 +456,7 @@ export namespace Git { } try { - return await git( + return await this.git( { cwd: repoPath, configs: ['-c', 'color.diff=false'], @@ -488,7 +473,7 @@ export namespace Git { // If the bad ref is trying to find a parent ref, assume we hit to the last commit, so try again using the root sha if (ref === ref1 && ref != null && ref.endsWith('^')) { - return Git.diff(repoPath, fileName, rootSha, ref2, options); + return this.diff(repoPath, fileName, rootSha, ref2, options); } } @@ -496,7 +481,7 @@ export namespace Git { } } - export async function diff__contents( + async diff__contents( repoPath: string, fileName: string, ref: string, @@ -524,7 +509,7 @@ export namespace Git { params.push('--no-index'); try { - return await git( + return await this.git( { cwd: repoPath, configs: ['-c', 'color.diff=false'], @@ -548,7 +533,7 @@ export namespace Git { // If the bad ref is trying to find a parent ref, assume we hit to the last commit, so try again using the root sha if (matchedRef === ref && matchedRef != null && matchedRef.endsWith('^')) { - return Git.diff__contents(repoPath, fileName, rootSha, contents, options); + return this.diff__contents(repoPath, fileName, rootSha, contents, options); } } @@ -556,7 +541,7 @@ export namespace Git { } } - export function diff__name_status( + diff__name_status( repoPath: string, ref1?: string, ref2?: string, @@ -578,17 +563,17 @@ export namespace Git { params.push(ref2); } - return git({ cwd: repoPath, configs: ['-c', 'color.diff=false'] }, ...params, '--'); + return this.git({ cwd: repoPath, configs: ['-c', 'color.diff=false'] }, ...params, '--'); } - export async function diff__shortstat(repoPath: string, ref?: string) { + async diff__shortstat(repoPath: string, ref?: string) { const params = ['diff', '--shortstat', '--no-ext-diff']; if (ref) { params.push(ref); } try { - return await git({ cwd: repoPath, configs: ['-c', 'color.diff=false'] }, ...params, '--'); + return await this.git({ cwd: repoPath, configs: ['-c', 'color.diff=false'] }, ...params, '--'); } catch (ex) { const msg: string = ex?.toString() ?? ''; if (GitErrors.noMergeBase.test(msg)) { @@ -599,7 +584,7 @@ export namespace Git { } } - export function difftool( + difftool( repoPath: string, fileName: string, tool: string, @@ -616,19 +601,19 @@ export namespace Git { params.push(options.ref2); } - return git({ cwd: repoPath }, ...params, '--', fileName); + return this.git({ cwd: repoPath }, ...params, '--', fileName); } - export function difftool__dir_diff(repoPath: string, tool: string, ref1: string, ref2?: string) { + difftool__dir_diff(repoPath: string, tool: string, ref1: string, ref2?: string) { const params = ['difftool', '--dir-diff', `--tool=${tool}`, ref1]; if (ref2) { params.push(ref2); } - return git({ cwd: repoPath }, ...params); + return this.git({ cwd: repoPath }, ...params); } - export async function fetch( + async fetch( repoPath: string, options: | { all?: boolean; branch?: undefined; prune?: boolean; remote?: string } @@ -652,7 +637,7 @@ export namespace Git { params.push('-u', options.remote, `${options.upstream}:${options.branch}`); try { - void (await git({ cwd: repoPath }, ...params)); + void (await this.git({ cwd: repoPath }, ...params)); return; } catch (ex) { const msg: string = ex?.toString() ?? ''; @@ -675,19 +660,19 @@ export namespace Git { params.push('--all'); } - void (await git({ cwd: repoPath }, ...params)); + void (await this.git({ cwd: repoPath }, ...params)); } - export function for_each_ref__branch(repoPath: string, options: { all: boolean } = { all: false }) { + for_each_ref__branch(repoPath: string, options: { all: boolean } = { all: false }) { const params = ['for-each-ref', `--format=${GitBranchParser.defaultFormat}`, 'refs/heads']; if (options.all) { params.push('refs/remotes'); } - return git({ cwd: repoPath }, ...params); + return this.git({ cwd: repoPath }, ...params); } - export function log( + log( repoPath: string, ref: string | undefined, { @@ -767,14 +752,14 @@ export namespace Git { } } - return git( + return this.git( { cwd: repoPath, configs: ['-c', 'diff.renameLimit=0', '-c', 'log.showSignature=false'] }, ...params, '--', ); } - export function log__file( + log__file( repoPath: string, fileName: string, ref: string | undefined, @@ -879,10 +864,10 @@ export namespace Git { params.push('--', file); } - return git({ cwd: root, configs: ['-c', 'log.showSignature=false'] }, ...params); + return this.git({ cwd: root, configs: ['-c', 'log.showSignature=false'] }, ...params); } - export async function log__file_recent( + async log__file_recent( repoPath: string, fileName: string, { @@ -906,7 +891,7 @@ export namespace Git { params.push(ref); } - const data = await git( + const data = await this.git( { cwd: repoPath, configs: ['-c', 'log.showSignature=false'], errors: GitErrorHandling.Ignore }, ...params, '--', @@ -915,13 +900,7 @@ export namespace Git { return data.length === 0 ? undefined : data.trim(); } - export async function log__find_object( - repoPath: string, - objectId: string, - ref: string, - ordering: string | null, - file?: string, - ) { + async log__find_object(repoPath: string, objectId: string, ref: string, ordering: string | null, file?: string) { const params = ['log', '-n1', '--no-renames', '--format=%H', `--find-object=${objectId}`, ref]; if (ordering) { @@ -932,21 +911,21 @@ export namespace Git { params.push('--', file); } - const data = await git( + const data = await this.git( { cwd: repoPath, configs: ['-c', 'log.showSignature=false'], errors: GitErrorHandling.Ignore }, ...params, ); return data.length === 0 ? undefined : data.trim(); } - export async function log__recent(repoPath: string, ordering?: string | null) { + async log__recent(repoPath: string, ordering?: string | null) { const params = ['log', '-n1', '--format=%H']; if (ordering) { params.push(`--${ordering}-order`); } - const data = await git( + const data = await this.git( { cwd: repoPath, configs: ['-c', 'log.showSignature=false'], errors: GitErrorHandling.Ignore }, ...params, '--', @@ -955,14 +934,14 @@ export namespace Git { return data.length === 0 ? undefined : data.trim(); } - export async function log__recent_committerdate(repoPath: string, ordering?: string | null) { + async log__recent_committerdate(repoPath: string, ordering?: string | null) { const params = ['log', '-n1', '--format=%ct']; if (ordering) { params.push(`--${ordering}-order`); } - const data = await git( + const data = await this.git( { cwd: repoPath, configs: ['-c', 'log.showSignature=false'], errors: GitErrorHandling.Ignore }, ...params, '--', @@ -971,7 +950,7 @@ export namespace Git { return data.length === 0 ? undefined : data.trim(); } - export function log__search( + log__search( repoPath: string, search: string[] = emptyArray, { @@ -1000,22 +979,22 @@ export namespace Git { params.push(`--${ordering}-order`); } - return git( + return this.git( { cwd: repoPath, configs: useShow ? undefined : ['-c', 'log.showSignature=false'] }, ...params, ...search, ); } - // export function log__shortstat(repoPath: string, options: { ref?: string }) { + // log__shortstat(repoPath: string, options: { ref?: string }) { // const params = ['log', '--shortstat', '--oneline']; // if (options.ref && !GitRevision.isUncommittedStaged(options.ref)) { // params.push(options.ref); // } - // return git({ cwd: repoPath, configs: ['-c', 'log.showSignature=false'] }, ...params, '--'); + // return this.git({ cwd: repoPath, configs: ['-c', 'log.showSignature=false'] }, ...params, '--'); // } - export async function ls_files( + async ls_files( repoPath: string, fileName: string, { ref, untracked }: { ref?: string; untracked?: boolean } = {}, @@ -1029,44 +1008,44 @@ export namespace Git { params.push('-o'); } - const data = await git({ cwd: repoPath, errors: GitErrorHandling.Ignore }, ...params, '--', fileName); + const data = await this.git( + { cwd: repoPath, errors: GitErrorHandling.Ignore }, + ...params, + '--', + fileName, + ); return data.length === 0 ? undefined : data.trim(); } - export function ls_remote(repoPath: string, remote: string, ref?: string) { - return git({ cwd: repoPath }, 'ls-remote', remote, ref); + ls_remote(repoPath: string, remote: string, ref?: string) { + return this.git({ cwd: repoPath }, 'ls-remote', remote, ref); } - export function ls_remote__HEAD(repoPath: string, remote: string) { - return git({ cwd: repoPath }, 'ls-remote', '--symref', remote, 'HEAD'); + ls_remote__HEAD(repoPath: string, remote: string) { + return this.git({ cwd: repoPath }, 'ls-remote', '--symref', remote, 'HEAD'); } - export async function ls_tree(repoPath: string, ref: string, { fileName }: { fileName?: string } = {}) { + async ls_tree(repoPath: string, ref: string, { fileName }: { fileName?: string } = {}) { const params = ['ls-tree']; if (fileName) { params.push('-l', ref, '--', fileName); } else { params.push('-lrt', ref, '--'); } - const data = await git({ cwd: repoPath, errors: GitErrorHandling.Ignore }, ...params); + const data = await this.git({ cwd: repoPath, errors: GitErrorHandling.Ignore }, ...params); return data.length === 0 ? undefined : data.trim(); } - export function merge_base( - repoPath: string, - ref1: string, - ref2: string, - { forkPoint }: { forkPoint?: boolean } = {}, - ) { + merge_base(repoPath: string, ref1: string, ref2: string, { forkPoint }: { forkPoint?: boolean } = {}) { const params = ['merge-base']; if (forkPoint) { params.push('--fork-point'); } - return git({ cwd: repoPath }, ...params, ref1, ref2); + return this.git({ cwd: repoPath }, ...params, ref1, ref2); } - export function reflog( + reflog( repoPath: string, { all, @@ -1098,31 +1077,31 @@ export namespace Git { params.push(branch); } - return git({ cwd: repoPath, configs: ['-c', 'log.showSignature=false'] }, ...params, '--'); + return this.git({ cwd: repoPath, configs: ['-c', 'log.showSignature=false'] }, ...params, '--'); } - export function remote(repoPath: string): Promise { - return git({ cwd: repoPath }, 'remote', '-v'); + remote(repoPath: string): Promise { + return this.git({ cwd: repoPath }, 'remote', '-v'); } - export function remote__add(repoPath: string, name: string, url: string) { - return git({ cwd: repoPath }, 'remote', 'add', name, url); + remote__add(repoPath: string, name: string, url: string) { + return this.git({ cwd: repoPath }, 'remote', 'add', name, url); } - export function remote__prune(repoPath: string, remoteName: string) { - return git({ cwd: repoPath }, 'remote', 'prune', remoteName); + remote__prune(repoPath: string, remoteName: string) { + return this.git({ cwd: repoPath }, 'remote', 'prune', remoteName); } - export function remote__get_url(repoPath: string, remote: string): Promise { - return git({ cwd: repoPath }, 'remote', 'get-url', remote); + remote__get_url(repoPath: string, remote: string): Promise { + return this.git({ cwd: repoPath }, 'remote', 'get-url', remote); } - export function reset(repoPath: string | undefined, fileName: string) { - return git({ cwd: repoPath }, 'reset', '-q', '--', fileName); + reset(repoPath: string | undefined, fileName: string) { + return this.git({ cwd: repoPath }, 'reset', '-q', '--', fileName); } - export async function rev_list__count(repoPath: string, ref: string): Promise { - let data = await git( + async rev_list__count(repoPath: string, ref: string): Promise { + let data = await this.git( { cwd: repoPath, errors: GitErrorHandling.Ignore }, 'rev-list', '--count', @@ -1136,11 +1115,11 @@ export namespace Git { return isNaN(result) ? undefined : result; } - export async function rev_list__left_right( + async rev_list__left_right( repoPath: string, refs: string[], ): Promise<{ ahead: number; behind: number } | undefined> { - const data = await git( + const data = await this.git( { cwd: repoPath, errors: GitErrorHandling.Ignore }, 'rev-list', '--left-right', @@ -1164,12 +1143,12 @@ export namespace Git { return result; } - export async function rev_parse__currentBranch( + async rev_parse__currentBranch( repoPath: string, ordering: string | null, ): Promise<[string, string | undefined] | undefined> { try { - const data = await git( + const data = await this.git( { cwd: repoPath, errors: GitErrorHandling.Throw }, 'rev-parse', '--abbrev-ref', @@ -1188,17 +1167,17 @@ export namespace Git { } try { - const data = await symbolic_ref(repoPath, 'HEAD'); + const data = await this.symbolic_ref(repoPath, 'HEAD'); if (data != null) return [data.trim(), undefined]; } catch {} try { - const data = await symbolic_ref(repoPath, 'refs/remotes/origin/HEAD'); + const data = await this.symbolic_ref(repoPath, 'refs/remotes/origin/HEAD'); if (data != null) return [data.trim().substr('origin/'.length), undefined]; } catch (ex) { if (/is not a symbolic ref/.test(ex.stderr)) { try { - const data = await ls_remote__HEAD(repoPath, 'origin'); + const data = await this.ls_remote__HEAD(repoPath, 'origin'); if (data != null) { const match = /ref:\s(\S+)\s+HEAD/m.exec(data); if (match != null) { @@ -1210,8 +1189,8 @@ export namespace Git { } } - const defaultBranch = (await config__get('init.defaultBranch', repoPath, { local: true })) ?? 'main'; - const branchConfig = await config__get_regex(`branch\\.${defaultBranch}\\.+`, repoPath, { + const defaultBranch = (await this.config__get('init.defaultBranch', repoPath)) ?? 'main'; + const branchConfig = await this.config__get_regex(`branch\\.${defaultBranch}\\.+`, repoPath, { local: true, }); @@ -1233,7 +1212,7 @@ export namespace Git { } if (GitWarnings.headNotABranch.test(msg)) { - const sha = await log__recent(repoPath, ordering); + const sha = await this.log__recent(repoPath, ordering); if (sha === undefined) return undefined; return [`(HEAD detached at ${GitRevision.shorten(sha)})`, sha]; @@ -1244,16 +1223,16 @@ export namespace Git { } } - export async function rev_parse__git_dir(cwd: string): Promise { - const data = await git({ cwd: cwd, errors: GitErrorHandling.Ignore }, 'rev-parse', '--git-dir'); + async rev_parse__git_dir(cwd: string): Promise { + const data = await this.git({ cwd: cwd, errors: GitErrorHandling.Ignore }, 'rev-parse', '--git-dir'); // Make sure to normalize: https://github.com/git-for-windows/git/issues/2478 // Keep trailing spaces which are part of the directory name return data.length === 0 ? undefined : normalizePath(data.trimLeft().replace(/[\r|\n]+$/, '')); } - export async function rev_parse__show_toplevel(cwd: string): Promise { + async rev_parse__show_toplevel(cwd: string): Promise { try { - const data = await git( + const data = await this.git( { cwd: cwd, errors: GitErrorHandling.Throw }, 'rev-parse', '--show-toplevel', @@ -1275,19 +1254,15 @@ export namespace Git { exists = await fsExists(cwd); } while (!exists); - return rev_parse__show_toplevel(cwd); + return this.rev_parse__show_toplevel(cwd); } } return undefined; } } - export async function rev_parse__verify( - repoPath: string, - ref: string, - fileName?: string, - ): Promise { - const data = await git( + async rev_parse__verify(repoPath: string, ref: string, fileName?: string): Promise { + const data = await this.git( { cwd: repoPath, errors: GitErrorHandling.Ignore }, 'rev-parse', '--verify', @@ -1297,11 +1272,11 @@ export namespace Git { return data.length === 0 ? undefined : data.trim(); } - export function shortlog(repoPath: string) { - return git({ cwd: repoPath }, 'shortlog', '-sne', '--all', '--no-merges', 'HEAD'); + shortlog(repoPath: string) { + return this.git({ cwd: repoPath }, 'shortlog', '-sne', '--all', '--no-merges', 'HEAD'); } - export async function show( + async show( repoPath: string | undefined, fileName: string, ref: string, @@ -1325,12 +1300,12 @@ export namespace Git { const args = ref.endsWith(':') ? `${ref}./${file}` : `${ref}:./${file}`; try { - const data = await git(opts, 'show', '--textconv', args, '--'); + const data = await this.git(opts, 'show', '--textconv', args, '--'); return data; } catch (ex) { const msg: string = ex?.toString() ?? ''; if (ref === ':' && GitErrors.badRevision.test(msg)) { - return Git.show(repoPath, fileName, 'HEAD:', options); + return this.show(repoPath, fileName, 'HEAD:', options); } if ( @@ -1345,7 +1320,7 @@ export namespace Git { } } - export function show__diff( + show__diff( repoPath: string, fileName: string, ref: string, @@ -1366,31 +1341,27 @@ export namespace Git { params.push(originalFileName); } - return git({ cwd: repoPath }, ...params); + return this.git({ cwd: repoPath }, ...params); } - export function show__name_status(repoPath: string, fileName: string, ref: string) { - return git({ cwd: repoPath }, 'show', '--name-status', '--format=', ref, '--', fileName); + show__name_status(repoPath: string, fileName: string, ref: string) { + return this.git({ cwd: repoPath }, 'show', '--name-status', '--format=', ref, '--', fileName); } - export function show_ref__tags(repoPath: string) { - return git({ cwd: repoPath, errors: GitErrorHandling.Ignore }, 'show-ref', '--tags'); + show_ref__tags(repoPath: string) { + return this.git({ cwd: repoPath, errors: GitErrorHandling.Ignore }, 'show-ref', '--tags'); } - export function stash__apply( - repoPath: string, - stashName: string, - deleteAfter: boolean, - ): Promise { + stash__apply(repoPath: string, stashName: string, deleteAfter: boolean): Promise { if (!stashName) return Promise.resolve(undefined); - return git({ cwd: repoPath }, 'stash', deleteAfter ? 'pop' : 'apply', stashName); + return this.git({ cwd: repoPath }, 'stash', deleteAfter ? 'pop' : 'apply', stashName); } - export async function stash__delete(repoPath: string, stashName: string, ref?: string) { + async stash__delete(repoPath: string, stashName: string, ref?: string) { if (!stashName) return undefined; if (ref) { - const stashRef = await git( + const stashRef = await this.git( { cwd: repoPath, errors: GitErrorHandling.Ignore }, 'show', '--format=%H', @@ -1402,17 +1373,17 @@ export namespace Git { } } - return git({ cwd: repoPath }, 'stash', 'drop', stashName); + return this.git({ cwd: repoPath }, 'stash', 'drop', stashName); } - export function stash__list( + stash__list( repoPath: string, { format = GitStashParser.defaultFormat, similarityThreshold, }: { format?: string; similarityThreshold?: number | null } = {}, ) { - return git( + return this.git( { cwd: repoPath }, 'stash', 'list', @@ -1422,7 +1393,7 @@ export namespace Git { ); } - export async function stash__push( + async stash__push( repoPath: string, message?: string, { @@ -1447,7 +1418,7 @@ export namespace Git { } if (stdin && pathspecs != null && pathspecs.length !== 0) { - void (await git( + void (await this.git( { cwd: repoPath, stdin: pathspecs.join('\0') }, ...params, '--pathspec-from-file=-', @@ -1462,10 +1433,10 @@ export namespace Git { params.push(...pathspecs); } - void (await git({ cwd: repoPath }, ...params)); + void (await this.git({ cwd: repoPath }, ...params)); } - export async function status( + async status( repoPath: string, porcelainVersion: number = 1, { similarityThreshold }: { similarityThreshold?: number | null } = {}, @@ -1476,18 +1447,18 @@ export namespace Git { '--branch', '-u', ]; - if (await Git.isAtLeastVersion('2.18')) { + if (await this.isAtLeastVersion('2.18')) { params.push(`--find-renames${similarityThreshold == null ? '' : `=${similarityThreshold}%`}`); } - return git( + return this.git( { cwd: repoPath, configs: ['-c', 'color.status=false'], env: { GIT_OPTIONAL_LOCKS: '0' } }, ...params, '--', ); } - export async function status__file( + async status__file( repoPath: string, fileName: string, porcelainVersion: number = 1, @@ -1496,11 +1467,11 @@ export namespace Git { const [file, root] = splitPath(fileName, repoPath, true); const params = ['status', porcelainVersion >= 2 ? `--porcelain=v${porcelainVersion}` : '--porcelain']; - if (await Git.isAtLeastVersion('2.18')) { + if (await this.isAtLeastVersion('2.18')) { params.push(`--find-renames${similarityThreshold == null ? '' : `=${similarityThreshold}%`}`); } - return git( + return this.git( { cwd: root, configs: ['-c', 'color.status=false'], env: { GIT_OPTIONAL_LOCKS: '0' } }, ...params, '--', @@ -1508,31 +1479,31 @@ export namespace Git { ); } - export function symbolic_ref(repoPath: string, ref: string) { - return git({ cwd: repoPath }, 'symbolic-ref', '--short', ref); + symbolic_ref(repoPath: string, ref: string) { + return this.git({ cwd: repoPath }, 'symbolic-ref', '--short', ref); } - export function tag(repoPath: string) { - return git({ cwd: repoPath }, 'tag', '-l', `--format=${GitTagParser.defaultFormat}`); + tag(repoPath: string) { + return this.git({ cwd: repoPath }, 'tag', '-l', `--format=${GitTagParser.defaultFormat}`); } - export async function readDotGitFile( + async readDotGitFile( repoPath: string, paths: string[], options?: { numeric?: false; throw?: boolean; trim?: boolean }, ): Promise; - export async function readDotGitFile( + async readDotGitFile( repoPath: string, path: string[], options?: { numeric: true; throw?: boolean; trim?: boolean }, ): Promise; - export async function readDotGitFile( + async readDotGitFile( repoPath: string, pathParts: string[], options?: { numeric?: boolean; throw?: boolean; trim?: boolean }, ): Promise { try { - const bytes = await workspace.fs.readFile(Uri.file(joinPaths(...[repoPath, '.git', ...pathParts]))); + const bytes = await workspace.fs.readFile(Uri.file(joinPaths(repoPath, '.git', ...pathParts))); let contents = textDecoder.decode(bytes); contents = options?.trim ?? true ? contents.trim() : contents; diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index ef2738e..9d7b398 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -36,7 +36,7 @@ import { RevisionUriData, ScmRepository, } from '../../../git/gitProvider'; -import { GitProviderService, isUriRegex } from '../../../git/gitProviderService'; +import { GitProviderService } from '../../../git/gitProviderService'; import { encodeGitLensRevisionUriAuthority, GitUri } from '../../../git/gitUri'; import { BranchSortOptions, @@ -92,12 +92,24 @@ import { RemoteProvider, RichRemoteProvider } from '../../../git/remotes/provide import { SearchPattern } from '../../../git/search'; import { LogCorrelationContext, Logger } from '../../../logger'; import { Messages } from '../../../messages'; -import { Arrays, debug, Functions, gate, Iterables, log, Strings, Versions } from '../../../system'; -import { filterMap } from '../../../system/array'; -import { dirname, isAbsolute, isFolderGlob, normalizePath, relative, splitPath } from '../../../system/path'; -import { any, PromiseOrValue } from '../../../system/promise'; -import { CharCode, equalsIgnoreCase } from '../../../system/string'; +import { countStringLength, filterMap } from '../../../system/array'; +import { gate } from '../../../system/decorators/gate'; +import { debug, log } from '../../../system/decorators/log'; +import { filterMap as filterMapIterable, find, first, last, some } from '../../../system/iterable'; +import { + dirname, + getBestPath, + isAbsolute, + isFolderGlob, + maybeUri, + normalizePath, + relative, + splitPath, +} from '../../../system/path'; +import { any, PromiseOrValue, wait } from '../../../system/promise'; +import { equalsIgnoreCase, getDurationMilliseconds, md5, splitSingle } from '../../../system/string'; import { PathTrie } from '../../../system/trie'; +import { compare, fromString } from '../../../system/version'; import { CachedBlame, CachedDiff, @@ -111,6 +123,7 @@ import { fsExists, RunError } from './shell'; const emptyPromise: Promise = Promise.resolve(undefined); const emptyPagedResult: PagedResult = Object.freeze({ values: [] }); +const slash = 47; const RepoSearchWarnings = { doesNotExist: /no such file or directory/i, @@ -135,7 +148,7 @@ export class LocalGitProvider implements GitProvider, Disposable { DocumentSchemes.Git, DocumentSchemes.GitLens, DocumentSchemes.PRs, - DocumentSchemes.Vsls, + // DocumentSchemes.Vsls, ]; private _onDidChangeRepository = new EventEmitter(); @@ -164,8 +177,8 @@ export class LocalGitProvider implements GitProvider, Disposable { private _disposables: Disposable[] = []; - constructor(private readonly container: Container) { - Git.setLocator(this.ensureGit.bind(this)); + constructor(protected readonly container: Container, protected readonly git: Git) { + this.git.setLocator(this.ensureGit.bind(this)); } dispose() { @@ -277,7 +290,7 @@ export class LocalGitProvider implements GitProvider, Disposable { }, 1000); if (cc != null) { - cc.exitDetails = ` ${GlyphChars.Dot} Git found (${Strings.getDurationMilliseconds(start)} ms): ${ + cc.exitDetails = ` ${GlyphChars.Dot} Git found (${getDurationMilliseconds(start)} ms): ${ location.version } @ ${location.path === 'git' ? 'PATH' : location.path}`; } else { @@ -285,12 +298,12 @@ export class LocalGitProvider implements GitProvider, Disposable { cc, `Git found: ${location.version} @ ${location.path === 'git' ? 'PATH' : location.path} ${ GlyphChars.Dot - } ${Strings.getDurationMilliseconds(start)} ms`, + } ${getDurationMilliseconds(start)} ms`, ); } // Warn if git is less than v2.7.2 - if (Versions.compare(Versions.fromString(location.version), Versions.fromString('2.7.2')) === -1) { + if (compare(fromString(location.version), fromString('2.7.2')) === -1) { Logger.log(cc, `Git version (${location.version}) is outdated`); void Messages.showGitVersionUnsupportedErrorMessage(location.version, '2.7.2'); } @@ -398,7 +411,7 @@ export class LocalGitProvider implements GitProvider, Disposable { }; const excludedPaths = [ - ...Iterables.filterMap(Object.entries(excludedConfig), ([key, value]) => { + ...filterMapIterable(Object.entries(excludedConfig), ([key, value]) => { if (!value) return undefined; if (key.startsWith('**/')) return key.substring(3); return key; @@ -490,26 +503,16 @@ export class LocalGitProvider implements GitProvider, Disposable { }); } - canHandlePathOrUri(pathOrUri: string | Uri): string | undefined { - let scheme; - if (typeof pathOrUri === 'string') { - const match = isUriRegex.exec(pathOrUri); - if (match == null) return pathOrUri; - - [, scheme] = match; - } else { - ({ scheme } = pathOrUri); - } - + canHandlePathOrUri(scheme: string, pathOrUri: string | Uri): string | undefined { if (!this.supportedSchemes.includes(scheme)) return undefined; - return typeof pathOrUri === 'string' ? pathOrUri : pathOrUri.fsPath; + return typeof pathOrUri === 'string' ? pathOrUri : getBestPath(pathOrUri); } getAbsoluteUri(pathOrUri: string | Uri, base: string | Uri): Uri { // Convert the base to a Uri if it isn't one if (typeof base === 'string') { // If it looks like a Uri parse it - if (isUriRegex.test(base)) { + if (maybeUri(base)) { base = Uri.parse(base, true); } else { if (!isAbsolute(base)) { @@ -522,18 +525,12 @@ export class LocalGitProvider implements GitProvider, Disposable { } // Short-circuit if the path is relative - if (typeof pathOrUri === 'string' && !isAbsolute(pathOrUri) && !isUriRegex.test(pathOrUri)) { + if (typeof pathOrUri === 'string' && !isAbsolute(pathOrUri)) { return Uri.joinPath(base, pathOrUri); } const relativePath = this.getRelativePath(pathOrUri, base); - - const uri = Uri.joinPath(base, relativePath); - // TODO@eamodio We need to move live share support to a separate provider - if (this.container.vsls.isMaybeGuest) { - return uri.with({ scheme: DocumentSchemes.Vsls }); - } - return uri; + return Uri.joinPath(base, relativePath); } @log() @@ -543,11 +540,11 @@ export class LocalGitProvider implements GitProvider, Disposable { // TODO@eamodio Align this with isTrackedCore? if (!ref || (GitRevision.isUncommitted(ref) && !GitRevision.isUncommittedStaged(ref))) { // Make sure the file exists in the repo - let data = await Git.ls_files(repoPath, path); + let data = await this.git.ls_files(repoPath, path); if (data != null) return this.getAbsoluteUri(path, repoPath); // Check if the file exists untracked - data = await Git.ls_files(repoPath, path, { untracked: true }); + data = await this.git.ls_files(repoPath, path, { untracked: true }); if (data != null) return this.getAbsoluteUri(path, repoPath); return undefined; @@ -562,7 +559,7 @@ export class LocalGitProvider implements GitProvider, Disposable { // Convert the base to a Uri if it isn't one if (typeof base === 'string') { // If it looks like a Uri parse it - if (isUriRegex.test(base)) { + if (maybeUri(base)) { base = Uri.parse(base, true); } else { if (!isAbsolute(base)) { @@ -576,7 +573,7 @@ export class LocalGitProvider implements GitProvider, Disposable { // Convert the path to a Uri if it isn't one if (typeof pathOrUri === 'string') { - if (isUriRegex.test(pathOrUri)) { + if (maybeUri(pathOrUri)) { pathOrUri = Uri.parse(pathOrUri, true); } else { if (!isAbsolute(pathOrUri)) return normalizePath(pathOrUri); @@ -597,7 +594,7 @@ export class LocalGitProvider implements GitProvider, Disposable { } path = normalizePath(this.getAbsoluteUri(path, repoPath).fsPath); - if (path.charCodeAt(0) !== CharCode.Slash) { + if (path.charCodeAt(0) !== slash) { path = `/${path}`; } @@ -622,22 +619,22 @@ export class LocalGitProvider implements GitProvider, Disposable { let data; let ref; do { - data = await Git.ls_files(repoPath, fileName); + data = await this.git.ls_files(repoPath, fileName); if (data != null) { - fileName = Strings.splitSingle(data, '\n')[0]; + fileName = splitSingle(data, '\n')[0]; break; } // TODO: Add caching // Get the most recent commit for this file name - ref = await Git.log__file_recent(repoPath, fileName, { + ref = await this.git.log__file_recent(repoPath, fileName, { ordering: this.container.config.advanced.commitOrdering, similarityThreshold: this.container.config.advanced.similarityThreshold, }); if (ref == null) return undefined; // Now check if that commit had any renames - data = await Git.log__file(repoPath, '.', ref, { + data = await this.git.log__file(repoPath, '.', ref, { filters: ['R', 'C', 'D'], format: 'simple', limit: 1, @@ -658,12 +655,12 @@ export class LocalGitProvider implements GitProvider, Disposable { @log() async addRemote(repoPath: string, name: string, url: string): Promise { - await Git.remote__add(repoPath, name, url); + await this.git.remote__add(repoPath, name, url); } @log() async pruneRemote(repoPath: string, remoteName: string): Promise { - await Git.remote__prune(repoPath, remoteName); + await this.git.remote__prune(repoPath, remoteName); } @log() @@ -678,10 +675,12 @@ export class LocalGitProvider implements GitProvider, Disposable { ref1 = `${ref1}^`; } + const [path, root] = splitPath(uri.fsPath, uri.repoPath); + let patch; try { - patch = await Git.diff(uri.repoPath, uri.fsPath, ref1, ref2); - void (await Git.apply(uri.repoPath, patch)); + patch = await this.git.diff(root, path, ref1, ref2); + void (await this.git.apply(root, patch)); } catch (ex) { const msg: string = ex?.toString() ?? ''; if (patch && /patch does not apply/i.test(msg)) { @@ -695,7 +694,7 @@ export class LocalGitProvider implements GitProvider, Disposable { if (result.title === 'Yes') { try { - void (await Git.apply(uri.repoPath, patch, { allowConflicts: true })); + void (await this.git.apply(root, patch, { allowConflicts: true })); return; } catch (e) { @@ -712,7 +711,7 @@ export class LocalGitProvider implements GitProvider, Disposable { @log() async branchContainsCommit(repoPath: string, name: string, ref: string): Promise { - let data = await Git.branch__containsOrPointsAt(repoPath, ref, { mode: 'contains', name: name }); + let data = await this.git.branch__containsOrPointsAt(repoPath, ref, { mode: 'contains', name: name }); data = data?.trim(); return Boolean(data); } @@ -726,7 +725,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const cc = Logger.getCorrelationContext(); try { - await Git.checkout(repoPath, ref, options); + await this.git.checkout(repoPath, ref, options); } catch (ex) { const msg: string = ex?.toString() ?? ''; if (/overwritten by checkout/i.test(msg)) { @@ -774,7 +773,7 @@ export class LocalGitProvider implements GitProvider, Disposable { async excludeIgnoredUris(repoPath: string, uris: Uri[]): Promise { const paths = new Map(uris.map(u => [normalizePath(u.fsPath), u])); - const data = await Git.check_ignore(repoPath, ...paths.keys()); + const data = await this.git.check_ignore(repoPath, ...paths.keys()); if (data == null) return uris; const ignored = data.split('\0').filter((i?: T): i is T => Boolean(i)); @@ -799,7 +798,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const branch = await repo?.getBranch(branchRef?.name); if (!branch?.remote && branch?.upstream == null) return undefined; - return Git.fetch(repoPath, { + return this.git.fetch(repoPath, { branch: branch.getNameWithoutRemote(), remote: branch.getRemoteName()!, upstream: branch.getTrackingWithoutRemote()!, @@ -807,7 +806,7 @@ export class LocalGitProvider implements GitProvider, Disposable { }); } - return Git.fetch(repoPath, opts); + return this.git.fetch(repoPath, opts); } @gate() @@ -822,7 +821,7 @@ export class LocalGitProvider implements GitProvider, Disposable { uri = stats?.type === FileType.Directory ? uri : Uri.file(dirname(uri.fsPath)); } - repoPath = await Git.rev_parse__show_toplevel(uri.fsPath); + repoPath = await this.git.rev_parse__show_toplevel(uri.fsPath); if (!repoPath) return undefined; if (isWindows) { @@ -870,7 +869,7 @@ export class LocalGitProvider implements GitProvider, Disposable { return; } - if (Strings.equalsIgnoreCase(uri.fsPath, resolvedPath)) { + if (equalsIgnoreCase(uri.fsPath, resolvedPath)) { Logger.debug(cc, `No symlink detected; repoPath=${repoPath}`); resolve(repoPath); return; @@ -898,7 +897,7 @@ export class LocalGitProvider implements GitProvider, Disposable { repoPath: string, refs: string[], ): Promise<{ ahead: number; behind: number } | undefined> { - return Git.rev_list__left_right(repoPath, refs); + return this.git.rev_list__left_right(repoPath, refs); } @gate() @@ -957,7 +956,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const [file, root] = paths; try { - const data = await Git.blame(root, file, uri.sha, { + const data = await this.git.blame(root, file, uri.sha, { args: this.container.config.advanced.blame.customArguments, ignoreWhitespace: this.container.config.blame.ignoreWhitespace, }); @@ -988,7 +987,7 @@ export class LocalGitProvider implements GitProvider, Disposable { async getBlameForFileContents(uri: GitUri, contents: string): Promise { const cc = Logger.getCorrelationContext(); - const key = `blame:${Strings.md5(contents)}`; + const key = `blame:${md5(contents)}`; const doc = await this.container.tracker.getOrAdd(uri); if (this.useCaching) { @@ -1037,7 +1036,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const [file, root] = paths; try { - const data = await Git.blame__contents(root, file, contents, { + const data = await this.git.blame__contents(root, file, contents, { args: this.container.config.advanced.blame.customArguments, correlationKey: `:${key}`, ignoreWhitespace: this.container.config.blame.ignoreWhitespace, @@ -1093,21 +1092,21 @@ export class LocalGitProvider implements GitProvider, Disposable { } const lineToBlame = editorLine + 1; - const fileName = uri.fsPath; + const [path, root] = splitPath(uri.fsPath, uri.repoPath); try { - const data = await Git.blame(uri.repoPath, fileName, uri.sha, { + const data = await this.git.blame(root, path, uri.sha, { args: this.container.config.advanced.blame.customArguments, ignoreWhitespace: this.container.config.blame.ignoreWhitespace, startLine: lineToBlame, endLine: lineToBlame, }); - const blame = GitBlameParser.parse(data, uri.repoPath, fileName, await this.getCurrentUser(uri.repoPath!)); + const blame = GitBlameParser.parse(data, root, path, await this.getCurrentUser(root)); if (blame == null) return undefined; return { - author: Iterables.first(blame.authors.values()), - commit: Iterables.first(blame.commits.values()), + author: first(blame.authors.values()), + commit: first(blame.commits.values()), line: blame.lines[editorLine], }; } catch { @@ -1144,21 +1143,21 @@ export class LocalGitProvider implements GitProvider, Disposable { } const lineToBlame = editorLine + 1; - const fileName = uri.fsPath; + const [path, root] = splitPath(uri.fsPath, uri.repoPath); try { - const data = await Git.blame__contents(uri.repoPath, fileName, contents, { + const data = await this.git.blame__contents(root, path, contents, { args: this.container.config.advanced.blame.customArguments, ignoreWhitespace: this.container.config.blame.ignoreWhitespace, startLine: lineToBlame, endLine: lineToBlame, }); - const blame = GitBlameParser.parse(data, uri.repoPath, fileName, await this.getCurrentUser(uri.repoPath!)); + const blame = GitBlameParser.parse(data, root, path, await this.getCurrentUser(root)); if (blame == null) return undefined; return { - author: Iterables.first(blame.authors.values()), - commit: Iterables.first(blame.commits.values()), + author: first(blame.authors.values()), + commit: first(blame.commits.values()), line: blame.lines[editorLine], }; } catch { @@ -1237,14 +1236,14 @@ export class LocalGitProvider implements GitProvider, Disposable { } = await this.getBranches(repoPath, { filter: b => b.current }); if (branch != null) return branch; - const data = await Git.rev_parse__currentBranch(repoPath, this.container.config.advanced.commitOrdering); + const data = await this.git.rev_parse__currentBranch(repoPath, this.container.config.advanced.commitOrdering); if (data == null) return undefined; const [name, upstream] = data[0].split('\n'); if (GitBranch.isDetached(name)) { const [rebaseStatus, committerDate] = await Promise.all([ this.getRebaseStatus(repoPath), - Git.log__recent_committerdate(repoPath, this.container.config.advanced.commitOrdering), + this.git.log__recent_committerdate(repoPath, this.container.config.advanced.commitOrdering), ]); branch = new GitBranch( @@ -1280,12 +1279,12 @@ export class LocalGitProvider implements GitProvider, Disposable { if (resultsPromise == null) { async function load(this: LocalGitProvider): Promise> { try { - const data = await Git.for_each_ref__branch(repoPath!, { all: true }); + const data = await this.git.for_each_ref__branch(repoPath!, { all: true }); // If we don't get any data, assume the repo doesn't have any commits yet so check if we have a current branch if (data == null || data.length === 0) { let current; - const data = await Git.rev_parse__currentBranch( + const data = await this.git.rev_parse__currentBranch( repoPath!, this.container.config.advanced.commitOrdering, ); @@ -1293,7 +1292,10 @@ export class LocalGitProvider implements GitProvider, Disposable { const [name, upstream] = data[0].split('\n'); const [rebaseStatus, committerDate] = await Promise.all([ GitBranch.isDetached(name) ? this.getRebaseStatus(repoPath!) : undefined, - Git.log__recent_committerdate(repoPath!, this.container.config.advanced.commitOrdering), + this.git.log__recent_committerdate( + repoPath!, + this.container.config.advanced.commitOrdering, + ), ]); current = new GitBranch( @@ -1349,7 +1351,7 @@ export class LocalGitProvider implements GitProvider, Disposable { @log() async getChangedFilesCount(repoPath: string, ref?: string): Promise { - const data = await Git.diff__shortstat(repoPath, ref); + const data = await this.git.diff__shortstat(repoPath, ref); if (!data) return undefined; return GitDiffParser.parseShortStat(data); @@ -1360,7 +1362,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const log = await this.getLog(repoPath, { limit: 2, ref: ref }); if (log == null) return undefined; - return log.commits.get(ref) ?? Iterables.first(log.commits.values()); + return log.commits.get(ref) ?? first(log.commits.values()); } @log() @@ -1369,7 +1371,7 @@ export class LocalGitProvider implements GitProvider, Disposable { ref: string, options?: { mode?: 'contains' | 'pointsAt'; remotes?: boolean }, ): Promise { - const data = await Git.branch__containsOrPointsAt(repoPath, ref, options); + const data = await this.git.branch__containsOrPointsAt(repoPath, ref, options); if (!data) return []; return filterMap(data.split('\n'), b => b.trim() || undefined); @@ -1377,7 +1379,7 @@ export class LocalGitProvider implements GitProvider, Disposable { @log() getCommitCount(repoPath: string, ref: string): Promise { - return Git.rev_list__count(repoPath, ref); + return this.git.rev_list__count(repoPath, ref); } @log() @@ -1388,8 +1390,10 @@ export class LocalGitProvider implements GitProvider, Disposable { ): Promise { const cc = Logger.getCorrelationContext(); + const [path, root] = splitPath(uri.fsPath, repoPath); + try { - const log = await this.getLogForFile(repoPath, uri.fsPath, { + const log = await this.getLogForFile(root, path, { limit: 2, ref: options?.ref, range: options?.range, @@ -1406,7 +1410,7 @@ export class LocalGitProvider implements GitProvider, Disposable { } } - return commit ?? Iterables.first(log.commits.values()); + return commit ?? first(log.commits.values()); } catch (ex) { Logger.error(ex, cc); return undefined; @@ -1415,7 +1419,9 @@ export class LocalGitProvider implements GitProvider, Disposable { @log() async getOldestUnpushedRefForFile(repoPath: string, uri: Uri): Promise { - const data = await Git.log__file(repoPath, uri.fsPath, '@{push}..', { + const [path, root] = splitPath(uri.fsPath, repoPath); + + const data = await this.git.log__file(root, path, '@{push}..', { format: 'refs', ordering: this.container.config.advanced.commitOrdering, renames: true, @@ -1440,7 +1446,7 @@ export class LocalGitProvider implements GitProvider, Disposable { try { const currentUser = await this.getCurrentUser(repoPath); - const data = await Git.log(repoPath, options?.ref, { + const data = await this.git.log(repoPath, options?.ref, { all: options?.all, format: options?.stats ? 'shortlog+stats' : 'shortlog', }); @@ -1481,7 +1487,7 @@ export class LocalGitProvider implements GitProvider, Disposable { user = { name: undefined, email: undefined }; try { - const data = await Git.config__get_regex('^user\\.', repoPath, { local: true }); + const data = await this.git.config__get_regex('^user\\.', repoPath, { local: true }); if (data) { let key: string; let value: string; @@ -1513,7 +1519,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const author = `${user.name} <${user.email}>`; // Check if there is a mailmap for the current user - const mappedAuthor = await Git.check_mailmap(repoPath, author); + const mappedAuthor = await this.git.check_mailmap(repoPath, author); if (mappedAuthor != null && mappedAuthor.length !== 0 && author !== mappedAuthor) { const match = mappedAuthorRegex.exec(mappedAuthor); if (match != null) { @@ -1539,14 +1545,14 @@ export class LocalGitProvider implements GitProvider, Disposable { if (!remote) { try { - const data = await Git.symbolic_ref(repoPath, 'HEAD'); + const data = await this.git.symbolic_ref(repoPath, 'HEAD'); if (data != null) return data.trim(); } catch {} } remote = remote ?? 'origin'; try { - const data = await Git.ls_remote__HEAD(repoPath, remote); + const data = await this.git.ls_remote__HEAD(repoPath, remote); if (data == null) return undefined; const match = /ref:\s(\S+)\s+HEAD/m.exec(data); @@ -1621,10 +1627,10 @@ export class LocalGitProvider implements GitProvider, Disposable { key: string, cc: LogCorrelationContext | undefined, ): Promise { - const [file, root] = splitPath(fileName, repoPath); + const [path, root] = splitPath(fileName, repoPath); try { - const data = await Git.diff(root, file, ref1, ref2, { + const data = await this.git.diff(root, path, ref1, ref2, { ...options, filters: ['M'], linesOfContext: 0, @@ -1658,7 +1664,7 @@ export class LocalGitProvider implements GitProvider, Disposable { async getDiffForFileContents(uri: GitUri, ref: string, contents: string): Promise { const cc = Logger.getCorrelationContext(); - const key = `diff:${Strings.md5(contents)}`; + const key = `diff:${md5(contents)}`; const doc = await this.container.tracker.getOrAdd(uri); if (this.useCaching) { @@ -1710,10 +1716,10 @@ export class LocalGitProvider implements GitProvider, Disposable { key: string, cc: LogCorrelationContext | undefined, ): Promise { - const [file, root] = splitPath(fileName, repoPath); + const [path, root] = splitPath(fileName, repoPath); try { - const data = await Git.diff__contents(root, file, ref, contents, { + const data = await this.git.diff__contents(root, path, ref, contents, { ...options, filters: ['M'], similarityThreshold: this.container.config.advanced.similarityThreshold, @@ -1769,7 +1775,7 @@ export class LocalGitProvider implements GitProvider, Disposable { options?: { filters?: GitDiffFilter[]; similarityThreshold?: number }, ): Promise { try { - const data = await Git.diff__name_status(repoPath, ref1, ref2, { + const data = await this.git.diff__name_status(repoPath, ref1, ref2, { similarityThreshold: this.container.config.advanced.similarityThreshold, ...options, }); @@ -1784,7 +1790,9 @@ export class LocalGitProvider implements GitProvider, Disposable { async getFileStatusForCommit(repoPath: string, uri: Uri, ref: string): Promise { if (ref === GitRevision.deletedOrMissing || GitRevision.isUncommitted(ref)) return undefined; - const data = await Git.show__name_status(repoPath, uri.fsPath, ref); + const [path, root] = splitPath(uri.fsPath, repoPath); + + const data = await this.git.show__name_status(root, path, ref); if (!data) return undefined; const files = GitDiffParser.parseNameStatus(data, repoPath); @@ -1809,7 +1817,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const repo = this._repoInfoCache.get(repoPath); if (repo?.gitDir != null) return repo.gitDir; - const gitDir = normalizePath((await Git.rev_parse__git_dir(repoPath)) || '.git'); + const gitDir = normalizePath((await this.git.rev_parse__git_dir(repoPath)) || '.git'); this._repoInfoCache.set(repoPath, { ...repo, gitDir: gitDir }); return gitDir; @@ -1835,7 +1843,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const limit = options?.limit ?? this.container.config.advanced.maxListItems ?? 0; try { - const data = await Git.log(repoPath, options?.ref, { + const data = await this.git.log(repoPath, options?.ref, { ...options, limit: limit, merges: options?.merges == null ? true : options.merges, @@ -1888,7 +1896,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const limit = options?.limit ?? this.container.config.advanced.maxListItems ?? 0; try { - const data = await Git.log(repoPath, options?.ref, { + const data = await this.git.log(repoPath, options?.ref, { authors: options?.authors, format: 'refs', limit: limit, @@ -1922,7 +1930,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const moreUntil = limit != null && typeof limit === 'object' ? limit.until : undefined; let moreLimit = typeof limit === 'number' ? limit : undefined; - if (moreUntil && Iterables.some(log.commits.values(), c => c.ref === moreUntil)) { + if (moreUntil && some(log.commits.values(), c => c.ref === moreUntil)) { return log; } @@ -1940,7 +1948,7 @@ export class LocalGitProvider implements GitProvider, Disposable { return moreLog; } - const ref = Iterables.last(log.commits.values())?.ref; + const ref = last(log.commits.values())?.ref; const moreLog = await this.getLog(log.repoPath, { ...options, limit: moreUntil == null ? moreLimit : 0, @@ -2068,7 +2076,7 @@ export class LocalGitProvider implements GitProvider, Disposable { args.push(...files); } - const data = await Git.log__search(repoPath, args, { + const data = await this.git.log__search(repoPath, args, { ordering: this.container.config.advanced.commitOrdering, ...options, limit: limit, @@ -2240,7 +2248,7 @@ export class LocalGitProvider implements GitProvider, Disposable { let i = 0; const authors = new Map(); const commits = new Map( - Iterables.filterMap<[string, GitLogCommit], [string, GitLogCommit]>( + filterMapIterable<[string, GitLogCommit], [string, GitLogCommit]>( log.commits.entries(), ([ref, c]) => { if (skip) { @@ -2333,7 +2341,7 @@ export class LocalGitProvider implements GitProvider, Disposable { range = new Range(range.end, range.start); } - const data = await Git.log__file(root, file, ref, { + const data = await this.git.log__file(root, file, ref, { ordering: this.container.config.advanced.commitOrdering, ...options, firstParent: options.renames, @@ -2399,13 +2407,13 @@ export class LocalGitProvider implements GitProvider, Disposable { const moreUntil = limit != null && typeof limit === 'object' ? limit.until : undefined; let moreLimit = typeof limit === 'number' ? limit : undefined; - if (moreUntil && Iterables.some(log.commits.values(), c => c.ref === moreUntil)) { + if (moreUntil && some(log.commits.values(), c => c.ref === moreUntil)) { return log; } moreLimit = moreLimit ?? this.container.config.advanced.maxSearchItems ?? 0; - const ref = Iterables.last(log.commits.values())?.ref; + const ref = last(log.commits.values())?.ref; const moreLog = await this.getLogForFile(log.repoPath, fileName, { ...options, limit: moreUntil == null ? moreLimit : 0, @@ -2442,7 +2450,7 @@ export class LocalGitProvider implements GitProvider, Disposable { }; if (options.renames) { - const renamed = Iterables.find( + const renamed = find( moreLog.commits.values(), c => Boolean(c.originalFileName) && c.originalFileName !== fileName, ); @@ -2462,7 +2470,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const cc = Logger.getCorrelationContext(); try { - const data = await Git.merge_base(repoPath, ref1, ref2, options); + const data = await this.git.merge_base(repoPath, ref1, ref2, options); if (data == null) return undefined; return data.split('\n')[0].trim() || undefined; @@ -2477,7 +2485,7 @@ export class LocalGitProvider implements GitProvider, Disposable { async getMergeStatus(repoPath: string): Promise { let status = this.useCaching ? this._mergeStatusCache.get(repoPath) : undefined; if (status === undefined) { - const merge = await Git.rev_parse__verify(repoPath, 'MERGE_HEAD'); + const merge = await this.git.rev_parse__verify(repoPath, 'MERGE_HEAD'); if (merge != null) { const [branch, mergeBase, possibleSourceBranches] = await Promise.all([ this.getBranch(repoPath), @@ -2515,17 +2523,17 @@ export class LocalGitProvider implements GitProvider, Disposable { async getRebaseStatus(repoPath: string): Promise { let status = this.useCaching ? this._rebaseStatusCache.get(repoPath) : undefined; if (status === undefined) { - const rebase = await Git.rev_parse__verify(repoPath, 'REBASE_HEAD'); + const rebase = await this.git.rev_parse__verify(repoPath, 'REBASE_HEAD'); if (rebase != null) { let [mergeBase, branch, onto, stepsNumber, stepsMessage, stepsTotal] = await Promise.all([ this.getMergeBase(repoPath, 'REBASE_HEAD', 'HEAD'), - Git.readDotGitFile(repoPath, ['rebase-merge', 'head-name']), - Git.readDotGitFile(repoPath, ['rebase-merge', 'onto']), - Git.readDotGitFile(repoPath, ['rebase-merge', 'msgnum'], { numeric: true }), - Git.readDotGitFile(repoPath, ['rebase-merge', 'message'], { throw: true }).catch(() => - Git.readDotGitFile(repoPath, ['rebase-merge', 'message-squashed']), - ), - Git.readDotGitFile(repoPath, ['rebase-merge', 'end'], { numeric: true }), + this.git.readDotGitFile(repoPath, ['rebase-merge', 'head-name']), + this.git.readDotGitFile(repoPath, ['rebase-merge', 'onto']), + this.git.readDotGitFile(repoPath, ['rebase-merge', 'msgnum'], { numeric: true }), + this.git + .readDotGitFile(repoPath, ['rebase-merge', 'message'], { throw: true }) + .catch(() => this.git.readDotGitFile(repoPath, ['rebase-merge', 'message-squashed'])), + this.git.readDotGitFile(repoPath, ['rebase-merge', 'end'], { numeric: true }), ]); if (branch == null || onto == null) return undefined; @@ -2651,7 +2659,7 @@ export class LocalGitProvider implements GitProvider, Disposable { } const fileName = GitUri.relativeTo(uri, repoPath); - let data = await Git.log__file(repoPath, fileName, ref, { + let data = await this.git.log__file(repoPath, fileName, ref, { filters: filters, format: 'simple', limit: skip + 1, @@ -2664,7 +2672,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const [nextRef, file, status] = GitLogParser.parseSimple(data, skip); // If the file was deleted, check for a possible rename if (status === 'D') { - data = await Git.log__file(repoPath, '.', nextRef, { + data = await this.git.log__file(repoPath, '.', nextRef, { filters: ['R', 'C'], format: 'simple', limit: 1, @@ -2904,7 +2912,7 @@ export class LocalGitProvider implements GitProvider, Disposable { // TODO: Add caching let data; try { - data = await Git.log__file(repoPath, path, ref, { + data = await this.git.log__file(repoPath, path, ref, { firstParent: firstParent, format: 'simple', limit: skip + 2, @@ -2922,7 +2930,7 @@ export class LocalGitProvider implements GitProvider, Disposable { } } - ref = await Git.log__file_recent(repoPath, path, { + ref = await this.git.log__file_recent(repoPath, path, { ordering: this.container.config.advanced.commitOrdering, }); return GitUri.fromFile(path, repoPath, ref ?? GitRevision.deletedOrMissing); @@ -2950,7 +2958,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const limit = options?.limit ?? this.container.config.advanced.maxListItems ?? 0; try { // Pass a much larger limit to reflog, because we aggregate the data and we won't know how many lines we'll need - const data = await Git.reflog(repoPath, { + const data = await this.git.reflog(repoPath, { ordering: this.container.config.advanced.commitOrdering, ...options, limit: limit * 100, @@ -3010,7 +3018,7 @@ export class LocalGitProvider implements GitProvider, Disposable { const providers = options?.providers ?? RemoteProviderFactory.loadProviders(configuration.get('remotes', null)); try { - const data = await Git.remote(repoPath); + const data = await this.git.remote(repoPath); const remotes = GitRemoteParser.parse(data, repoPath, RemoteProviderFactory.factory(providers)); if (remotes == null) return []; @@ -3028,7 +3036,9 @@ export class LocalGitProvider implements GitProvider, Disposable { @gate() @log() getRevisionContent(repoPath: string, path: string, ref: string): Promise { - return Git.show(repoPath, path, ref, { encoding: 'buffer' }); + [path, repoPath] = splitPath(path, repoPath); + + return this.git.show(repoPath, path, ref, { encoding: 'buffer' }); } @gate() @@ -3038,7 +3048,7 @@ export class LocalGitProvider implements GitProvider, Disposable { let stash = this.useCaching ? this._stashesCache.get(repoPath) : undefined; if (stash === undefined) { - const data = await Git.stash__list(repoPath, { + const data = await this.git.stash__list(repoPath, { similarityThreshold: this.container.config.advanced.similarityThreshold, }); stash = GitStashParser.parse(data, repoPath); @@ -3053,9 +3063,11 @@ export class LocalGitProvider implements GitProvider, Disposable { @log() async getStatusForFile(repoPath: string, path: string): Promise { - const porcelainVersion = (await Git.isAtLeastVersion('2.11')) ? 2 : 1; + const porcelainVersion = (await this.git.isAtLeastVersion('2.11')) ? 2 : 1; + + [path, repoPath] = splitPath(path, repoPath); - const data = await Git.status__file(repoPath, path, porcelainVersion, { + const data = await this.git.status__file(repoPath, path, porcelainVersion, { similarityThreshold: this.container.config.advanced.similarityThreshold, }); const status = GitStatusParser.parse(data, repoPath, porcelainVersion); @@ -3066,9 +3078,11 @@ export class LocalGitProvider implements GitProvider, Disposable { @log() async getStatusForFiles(repoPath: string, pathOrGlob: string): Promise { - const porcelainVersion = (await Git.isAtLeastVersion('2.11')) ? 2 : 1; + const porcelainVersion = (await this.git.isAtLeastVersion('2.11')) ? 2 : 1; + + [pathOrGlob, repoPath] = splitPath(pathOrGlob, repoPath); - const data = await Git.status__file(repoPath, pathOrGlob, porcelainVersion, { + const data = await this.git.status__file(repoPath, pathOrGlob, porcelainVersion, { similarityThreshold: this.container.config.advanced.similarityThreshold, }); const status = GitStatusParser.parse(data, repoPath, porcelainVersion); @@ -3081,9 +3095,9 @@ export class LocalGitProvider implements GitProvider, Disposable { async getStatusForRepo(repoPath: string | undefined): Promise { if (repoPath == null) return undefined; - const porcelainVersion = (await Git.isAtLeastVersion('2.11')) ? 2 : 1; + const porcelainVersion = (await this.git.isAtLeastVersion('2.11')) ? 2 : 1; - const data = await Git.status(repoPath, porcelainVersion, { + const data = await this.git.status(repoPath, porcelainVersion, { similarityThreshold: this.container.config.advanced.similarityThreshold, }); const status = GitStatusParser.parse(data, repoPath, porcelainVersion); @@ -3116,7 +3130,7 @@ export class LocalGitProvider implements GitProvider, Disposable { if (resultsPromise == null) { async function load(this: LocalGitProvider): Promise> { try { - const data = await Git.tag(repoPath!); + const data = await this.git.tag(repoPath!); return { values: GitTagParser.parse(data, repoPath!) ?? [] }; } catch (ex) { this._tagsCache.delete(repoPath!); @@ -3152,7 +3166,7 @@ export class LocalGitProvider implements GitProvider, Disposable { async getTreeEntryForRevision(repoPath: string, path: string, ref: string): Promise { if (repoPath == null || !path) return undefined; - const data = await Git.ls_tree(repoPath, ref, { fileName: path }); + const data = await this.git.ls_tree(repoPath, ref, { fileName: path }); const trees = GitTreeParser.parse(data); return trees?.length ? trees[0] : undefined; } @@ -3161,7 +3175,7 @@ export class LocalGitProvider implements GitProvider, Disposable { async getTreeForRevision(repoPath: string, ref: string): Promise { if (repoPath == null) return []; - const data = await Git.ls_tree(repoPath, ref); + const data = await this.git.ls_tree(repoPath, ref); return GitTreeParser.parse(data) ?? []; } @@ -3254,14 +3268,14 @@ export class LocalGitProvider implements GitProvider, Disposable { } // 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)); + let tracked = Boolean(await this.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)); + tracked = Boolean(await this.git.ls_files(newRepoPath, newRelativePath)); if (tracked) { repository = await this.container.git.getOrOpenRepository(Uri.file(path), true); if (repository != null) { @@ -3274,10 +3288,10 @@ export class LocalGitProvider implements GitProvider, Disposable { } if (!tracked && ref && !GitRevision.isUncommitted(ref)) { - tracked = Boolean(await Git.ls_files(repoPath, relativePath, { ref: ref })); + tracked = Boolean(await this.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}^` })); + tracked = Boolean(await this.git.ls_files(repoPath, relativePath, { ref: `${ref}^` })); } } @@ -3311,8 +3325,8 @@ export class LocalGitProvider implements GitProvider, Disposable { @log() async getDiffTool(repoPath?: string): Promise { return ( - (await Git.config__get('diff.guitool', repoPath, { local: true })) ?? - Git.config__get('diff.tool', repoPath, { local: true }) + (await this.git.config__get('diff.guitool', repoPath, { local: true })) ?? + this.git.config__get('diff.tool', repoPath, { local: true }) ); } @@ -3322,18 +3336,20 @@ export class LocalGitProvider implements GitProvider, Disposable { uri: Uri, options?: { ref1?: string; ref2?: string; staged?: boolean; tool?: string }, ): Promise { + const [path, root] = splitPath(uri.fsPath, repoPath); + try { let tool = options?.tool; if (!tool) { const cc = Logger.getCorrelationContext(); - tool = this.container.config.advanced.externalDiffTool || (await this.getDiffTool(repoPath)); + tool = this.container.config.advanced.externalDiffTool || (await this.getDiffTool(root)); if (tool == null) throw new Error('No diff tool found'); Logger.log(cc, `Using tool=${tool}`); } - await Git.difftool(repoPath, uri.fsPath, tool, options); + await this.git.difftool(root, path, tool, options); } catch (ex) { const msg: string = ex?.toString() ?? ''; if (msg === 'No diff tool found' || /Unknown .+? tool/.test(msg)) { @@ -3368,7 +3384,7 @@ export class LocalGitProvider implements GitProvider, Disposable { Logger.log(cc, `Using tool=${tool}`); } - await Git.difftool__dir_diff(repoPath, tool, ref1, ref2); + await this.git.difftool__dir_diff(repoPath, tool, ref1, ref2); } catch (ex) { const msg: string = ex?.toString() ?? ''; if (msg === 'No diff tool found' || /Unknown .+? tool/.test(msg)) { @@ -3400,15 +3416,15 @@ export class LocalGitProvider implements GitProvider, Disposable { if (pathOrUri == null) { if (GitRevision.isSha(ref) || !GitRevision.isShaLike(ref) || ref.endsWith('^3')) return ref; - return (await Git.rev_parse__verify(repoPath, ref)) ?? ref; + return (await this.git.rev_parse__verify(repoPath, ref)) ?? ref; } const path = normalizePath(this.getRelativePath(pathOrUri, repoPath)); - const blob = await Git.rev_parse__verify(repoPath, ref, path); + const blob = await this.git.rev_parse__verify(repoPath, ref, path); if (blob == null) return GitRevision.deletedOrMissing; - let promise: Promise = Git.log__find_object( + let promise: Promise = this.git.log__find_object( repoPath, blob, ref, @@ -3416,7 +3432,7 @@ export class LocalGitProvider implements GitProvider, Disposable { path, ); if (options?.timeout != null) { - promise = Promise.race([promise, Functions.wait(options.timeout)]); + promise = Promise.race([promise, wait(options.timeout)]); } return (await promise) ?? ref; @@ -3424,7 +3440,7 @@ export class LocalGitProvider implements GitProvider, Disposable { @log() validateBranchOrTagName(repoPath: string, ref: string): Promise { - return Git.check_ref_format(ref, repoPath); + return this.git.check_ref_format(ref, repoPath); } @log() @@ -3432,17 +3448,20 @@ export class LocalGitProvider implements GitProvider, Disposable { if (ref == null || ref.length === 0) return false; if (ref === GitRevision.deletedOrMissing || GitRevision.isUncommitted(ref)) return true; - return (await Git.rev_parse__verify(repoPath, ref)) != null; + return (await this.git.rev_parse__verify(repoPath, ref)) != null; } @log() async stageFile(repoPath: string, pathOrUri: string | Uri): Promise { - await Git.add(repoPath, typeof pathOrUri === 'string' ? pathOrUri : splitPath(pathOrUri.fsPath, repoPath)[0]); + await this.git.add( + repoPath, + typeof pathOrUri === 'string' ? pathOrUri : splitPath(pathOrUri.fsPath, repoPath)[0], + ); } @log() async stageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise { - await Git.add( + await this.git.add( repoPath, typeof directoryOrUri === 'string' ? directoryOrUri : splitPath(directoryOrUri.fsPath, repoPath)[0], ); @@ -3450,12 +3469,15 @@ export class LocalGitProvider implements GitProvider, Disposable { @log() async unStageFile(repoPath: string, pathOrUri: string | Uri): Promise { - await Git.reset(repoPath, typeof pathOrUri === 'string' ? pathOrUri : splitPath(pathOrUri.fsPath, repoPath)[0]); + await this.git.reset( + repoPath, + typeof pathOrUri === 'string' ? pathOrUri : splitPath(pathOrUri.fsPath, repoPath)[0], + ); } @log() async unStageDirectory(repoPath: string, directoryOrUri: string | Uri): Promise { - await Git.reset( + await this.git.reset( repoPath, typeof directoryOrUri === 'string' ? directoryOrUri : splitPath(directoryOrUri.fsPath, repoPath)[0], ); @@ -3464,7 +3486,7 @@ export class LocalGitProvider implements GitProvider, Disposable { @log() async stashApply(repoPath: string, stashName: string, options?: { deleteAfter?: boolean }): Promise { try { - await Git.stash__apply(repoPath, stashName, Boolean(options?.deleteAfter)); + await this.git.stash__apply(repoPath, stashName, Boolean(options?.deleteAfter)); } catch (ex) { if (ex instanceof Error) { const msg: string = ex.message ?? ''; @@ -3500,7 +3522,7 @@ export class LocalGitProvider implements GitProvider, Disposable { @log() async stashDelete(repoPath: string, stashName: string, ref?: string): Promise { - await Git.stash__delete(repoPath, stashName, ref); + await this.git.stash__delete(repoPath, stashName, ref); } @log({ args: { 2: uris => uris?.length } }) @@ -3510,7 +3532,7 @@ export class LocalGitProvider implements GitProvider, Disposable { uris?: Uri[], options?: { includeUntracked?: boolean; keepIndex?: boolean }, ): Promise { - if (uris == null) return Git.stash__push(repoPath, message, options); + if (uris == null) return this.git.stash__push(repoPath, message, options); await this.ensureGitVersion( '2.13.2', @@ -3521,9 +3543,9 @@ export class LocalGitProvider implements GitProvider, Disposable { const pathspecs = uris.map(u => `./${splitPath(u.fsPath, repoPath)[0]}`); const stdinVersion = '2.30.0'; - const stdin = await Git.isAtLeastVersion(stdinVersion); + const stdin = await this.git.isAtLeastVersion(stdinVersion); // If we don't support stdin, then error out if we are over the maximum allowed git cli length - if (!stdin && Arrays.countStringLength(pathspecs) > maxGitCliLength) { + if (!stdin && countStringLength(pathspecs) > maxGitCliLength) { await this.ensureGitVersion( stdinVersion, `Stashing so many files (${pathspecs.length}) at once`, @@ -3531,7 +3553,7 @@ export class LocalGitProvider implements GitProvider, Disposable { ); } - return Git.stash__push(repoPath, message, { + return this.git.stash__push(repoPath, message, { ...options, pathspecs: pathspecs, stdin: stdin, @@ -3597,10 +3619,10 @@ export class LocalGitProvider implements GitProvider, Disposable { } private async ensureGitVersion(version: string, prefix: string, suffix: string): Promise { - if (await Git.isAtLeastVersion(version)) return; + if (await this.git.isAtLeastVersion(version)) return; throw new Error( - `${prefix} requires a newer version of Git (>= ${version}) than is currently installed (${await Git.version()}).${suffix}`, + `${prefix} requires a newer version of Git (>= ${version}) than is currently installed (${await this.git.version()}).${suffix}`, ); } } diff --git a/src/env/node/git/vslsGitProvider.ts b/src/env/node/git/vslsGitProvider.ts new file mode 100644 index 0000000..c65aa6f --- /dev/null +++ b/src/env/node/git/vslsGitProvider.ts @@ -0,0 +1,95 @@ +import { FileType, Uri, workspace } from 'vscode'; +import { DocumentSchemes } from '../../../constants'; +import { Container } from '../../../container'; +import { GitCommandOptions } from '../../../git/commandOptions'; +import { GitProviderDescriptor, GitProviderId } from '../../../git/gitProvider'; +import { Repository } from '../../../git/models/repository'; +import { Logger } from '../../../logger'; +import { addVslsPrefixIfNeeded, dirname } from '../../../system/path'; +import { Git } from './git'; +import { LocalGitProvider } from './localGitProvider'; + +export class VslsGit extends Git { + constructor(private readonly localGit: Git) { + super(); + } + + override async git(options: GitCommandOptions, ...args: any[]): Promise { + if (options.local) { + // Since we will have a live share path here, just blank it out + options.cwd = ''; + return this.localGit.git(options, ...args); + } + + const guest = await Container.instance.vsls.guest(); + if (guest == null) { + debugger; + throw new Error('No guest'); + } + + return guest.git(options, ...args); + } +} + +export class VslsGitProvider extends LocalGitProvider { + override readonly descriptor: GitProviderDescriptor = { id: GitProviderId.Vsls, name: 'Live Share' }; + override readonly supportedSchemes: string[] = [DocumentSchemes.Vsls, DocumentSchemes.VslsScc]; + + override async discoverRepositories(uri: Uri): Promise { + if (!this.supportedSchemes.includes(uri.scheme)) return []; + + const cc = Logger.getCorrelationContext(); + + try { + const guest = await this.container.vsls.guest(); + const repositories = await guest?.getRepositoriesForUri(uri); + if (repositories == null || repositories.length === 0) return []; + + return repositories.map(r => + this.openRepository(undefined, Uri.parse(r.folderUri, true), r.root, undefined, r.closed), + ); + } catch (ex) { + Logger.error(ex, cc); + debugger; + + return []; + } + } + + override getAbsoluteUri(pathOrUri: string | Uri, base: string | Uri): Uri { + pathOrUri = addVslsPrefixIfNeeded(pathOrUri); + + const scheme = + (typeof base !== 'string' ? base.scheme : undefined) ?? + (typeof pathOrUri !== 'string' ? pathOrUri.scheme : undefined) ?? + DocumentSchemes.Vsls; + + return super.getAbsoluteUri(pathOrUri, base).with({ scheme: scheme }); + } + + override async findRepositoryUri(uri: Uri, isDirectory?: boolean): Promise { + const cc = Logger.getCorrelationContext(); + + let repoPath: string | undefined; + try { + if (!isDirectory) { + try { + const stats = await workspace.fs.stat(uri); + uri = stats?.type === FileType.Directory ? uri : uri.with({ path: dirname(uri.fsPath) }); + } catch {} + } + + repoPath = await this.git.rev_parse__show_toplevel(uri.fsPath); + if (!repoPath) return undefined; + + return repoPath ? Uri.parse(repoPath, true) : undefined; + } catch (ex) { + Logger.error(ex, cc); + return undefined; + } + } + + override getLastFetchedTimestamp(_repoPath: string): Promise { + return Promise.resolve(undefined); + } +} diff --git a/src/env/node/providers.ts b/src/env/node/providers.ts new file mode 100644 index 0000000..2f0fd5f --- /dev/null +++ b/src/env/node/providers.ts @@ -0,0 +1,34 @@ +import { Container } from '../../container'; +import { GitCommandOptions } from '../../git/commandOptions'; +import { GitProvider } from '../../git/gitProvider'; +import { GitHubGitProvider } from '../../premium/github/githubGitProvider'; +import { Git } from './git/git'; +import { LocalGitProvider } from './git/localGitProvider'; +import { VslsGit, VslsGitProvider } from './git/vslsGitProvider'; + +let gitInstance: Git | undefined; +function ensureGit() { + if (gitInstance == null) { + gitInstance = new Git(); + } + return gitInstance; +} + +export function git(_options: GitCommandOptions, ..._args: any[]): Promise { + return ensureGit().git(_options, ..._args); +} + +export function getSupportedGitProviders(container: Container): GitProvider[] { + const git = ensureGit(); + + const providers: GitProvider[] = [ + new LocalGitProvider(container, git), + new VslsGitProvider(container, new VslsGit(git)), + ]; + + if (container.config.experimental.virtualRepositories.enabled) { + providers.push(new GitHubGitProvider(container)); + } + + return providers; +} diff --git a/src/git/gitProvider.ts b/src/git/gitProvider.ts index 48d4107..d4bdf00 100644 --- a/src/git/gitProvider.ts +++ b/src/git/gitProvider.ts @@ -37,6 +37,7 @@ import { SearchPattern } from './search'; export const enum GitProviderId { Git = 'git', GitHub = 'github', + Vsls = 'vsls', } export interface GitProviderDescriptor { @@ -93,8 +94,8 @@ export interface GitProvider extends Disposable { getOpenScmRepositories(): Promise; getOrOpenScmRepository(repoPath: string): Promise; - canHandlePathOrUri(pathOrUri: string | Uri): string | undefined; - getAbsoluteUri(pathOrUri: string | Uri, base?: string | Uri): Uri; + canHandlePathOrUri(scheme: string, pathOrUri: string | Uri): string | undefined; + getAbsoluteUri(pathOrUri: string | Uri, base: string | Uri): Uri; getBestRevisionUri(repoPath: string, path: string, ref: string | undefined): Promise; getRelativePath(pathOrUri: string | Uri, base: string | Uri): string; getRevisionUri(repoPath: string, path: string, ref: string): Uri; diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index 8ce9a32..6bf6e0c 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -32,11 +32,9 @@ import { groupByFilterMap, groupByMap } from '../system/array'; import { gate } from '../system/decorators/gate'; import { debug, log } from '../system/decorators/log'; import { count, filter, first, flatMap, map } from '../system/iterable'; -import { dirname, getBestPath, isAbsolute, normalizePath } from '../system/path'; +import { dirname, getBestPath, getScheme, isAbsolute, maybeUri } 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'; import { @@ -81,8 +79,6 @@ import { RemoteProviders } from './remotes/factory'; import { Authentication, RemoteProvider, RichRemoteProvider } from './remotes/provider'; import { SearchPattern } from './search'; -export const isUriRegex = /^(\w[\w\d+.-]{1,}?):\/\//; - const maxDefaultBranchWeight = 100; const weightedDefaultBranches = new Map([ ['master', maxDefaultBranchWeight], @@ -133,11 +129,13 @@ export class GitProviderService implements Disposable { return this._onDidChangeRepository.event; } + readonly supportedSchemes = new Set(); + private readonly _disposable: Disposable; + private readonly _pendingRepositories = new Map>(); private readonly _providers = new Map(); private readonly _repositories = new Repositories(); private readonly _richRemotesCache = new Map | null>(); - private readonly _supportedSchemes = new Set(); private readonly _visitedPaths = new VisitedPathsTrie(); constructor(private readonly container: Container) { @@ -290,7 +288,7 @@ export class GitProviderService implements Disposable { this._providers.set(id, provider); for (const scheme of provider.supportedSchemes) { - this._supportedSchemes.add(scheme); + this.supportedSchemes.add(scheme); } const disposables = []; @@ -569,17 +567,24 @@ export class GitProviderService implements Disposable { // private _pathToProvider = new Map(); private getProvider(repoPath: string | Uri): GitProviderResult { - if (repoPath == null || (typeof repoPath !== 'string' && !this._supportedSchemes.has(repoPath.scheme))) { + 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) ?? DocumentSchemes.File; + } else { + ({ scheme } = repoPath); + } + // const key = repoPath.toString(); // let providerResult = this._pathToProvider.get(key); // if (providerResult != null) return providerResult; for (const provider of this._providers.values()) { - const path = provider.canHandlePathOrUri(repoPath); + const path = provider.canHandlePathOrUri(scheme, repoPath); if (path == null) continue; const providerResult: GitProviderResult = { provider: provider, path: path }; @@ -621,7 +626,7 @@ export class GitProviderService implements Disposable { getAbsoluteUri(pathOrUri: string | Uri, base?: string | Uri): Uri { if (base == null) { if (typeof pathOrUri === 'string') { - if (isUriRegex.test(pathOrUri)) return Uri.parse(pathOrUri, true); + if (maybeUri(pathOrUri)) return Uri.parse(pathOrUri, true); // I think it is safe to assume this should be file:// return Uri.file(pathOrUri); @@ -1611,8 +1616,6 @@ export class GitProviderService implements Disposable { return (editor != null ? this.getRepository(editor.document.uri) : undefined) ?? this.highlander; } - private _pendingRepositories = new Map>(); - @log({ exit: r => `returned ${r?.path}` }) async getOrOpenRepository(uri: Uri, detectNested?: boolean): Promise { const cc = Logger.getCorrelationContext(); @@ -1642,17 +1645,7 @@ export class GitProviderService implements Disposable { 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 })); - } - } + const root = this._repositories.getClosest(provider.getAbsoluteUri(uri, repoUri)); Logger.log(cc, `Repository found in '${repoUri.toString(false)}'`); repository = provider.openRepository(root?.folder, repoUri, false); @@ -1830,7 +1823,7 @@ export class GitProviderService implements Disposable { } isTrackable(uri: Uri): boolean { - if (!this._supportedSchemes.has(uri.scheme)) return false; + if (!this.supportedSchemes.has(uri.scheme)) return false; const { provider } = this.getProvider(uri); return provider.isTrackable(uri); diff --git a/src/git/gitUri.ts b/src/git/gitUri.ts index b56ae37..9450330 100644 --- a/src/git/gitUri.ts +++ b/src/git/gitUri.ts @@ -230,15 +230,6 @@ export class GitUri extends (Uri as any as UriEx) { return Container.instance.git.getAbsoluteUri(this.fsPath, this.repoPath); } - static file(path: string, useVslsScheme?: boolean) { - const uri = Uri.file(path); - if (Container.instance.vsls.isMaybeGuest && useVslsScheme !== false) { - return uri.with({ scheme: DocumentSchemes.Vsls }); - } - - return uri; - } - static fromCommit(commit: GitCommit, previous: boolean = false) { if (!previous) return new GitUri(commit.uri, commit); diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index a503b4f..2ce341e 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -529,8 +529,7 @@ export class Repository implements Disposable { @gate() async getLastFetched(): Promise { if (this._lastFetched == null) { - const hasRemotes = await this.hasRemotes(); - if (!hasRemotes || this.container.vsls.isMaybeGuest) return 0; + if (!(await this.hasRemotes())) return 0; } try { diff --git a/src/premium/github/githubGitProvider.ts b/src/premium/github/githubGitProvider.ts index c67b81d..dce0b2f 100644 --- a/src/premium/github/githubGitProvider.ts +++ b/src/premium/github/githubGitProvider.ts @@ -30,7 +30,6 @@ import { RepositoryOpenEvent, ScmRepository, } from '../../git/gitProvider'; -import { isUriRegex } from '../../git/gitProviderService'; import { GitUri } from '../../git/gitUri'; import { BranchSortOptions, @@ -74,7 +73,7 @@ import { RemoteProvider, RichRemoteProvider } from '../../git/remotes/provider'; import { SearchPattern } from '../../git/search'; import { LogCorrelationContext, Logger } from '../../logger'; import { debug, gate, Iterables, log } from '../../system'; -import { isAbsolute, isFolderGlob, normalizePath, relative } from '../../system/path'; +import { isAbsolute, isFolderGlob, maybeUri, normalizePath, relative } from '../../system/path'; import { CharCode } from '../../system/string'; import { CachedBlame, CachedLog, GitDocumentState } from '../../trackers/gitDocumentTracker'; import { TrackedDocument } from '../../trackers/trackedDocument'; @@ -135,7 +134,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { } async discoverRepositories(uri: Uri): Promise { - if (uri.scheme !== DocumentSchemes.Virtual) return []; + if (!this.supportedSchemes.includes(uri.scheme)) return []; try { void (await this.ensureRepositoryContext(uri.toString())); @@ -172,17 +171,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { return undefined; } - canHandlePathOrUri(pathOrUri: string | Uri): string | undefined { - let scheme; - if (typeof pathOrUri === 'string') { - const match = isUriRegex.exec(pathOrUri); - if (match == null) return undefined; - - [, scheme] = match; - } else { - ({ scheme } = pathOrUri); - } - + canHandlePathOrUri(scheme: string, pathOrUri: string | Uri): string | undefined { if (!this.supportedSchemes.includes(scheme)) return undefined; return typeof pathOrUri === 'string' ? pathOrUri : pathOrUri.toString(); } @@ -191,7 +180,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { // Convert the base to a Uri if it isn't one if (typeof base === 'string') { // If it looks like a Uri parse it, otherwise throw - if (isUriRegex.test(base)) { + if (maybeUri(base)) { base = Uri.parse(base, true); } else { debugger; @@ -199,7 +188,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { } } - if (typeof pathOrUri === 'string' && !isUriRegex.test(pathOrUri) && !isAbsolute(pathOrUri)) { + if (typeof pathOrUri === 'string' && !maybeUri(pathOrUri) && !isAbsolute(pathOrUri)) { return Uri.joinPath(base, pathOrUri); } @@ -216,7 +205,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { // Convert the base to a Uri if it isn't one if (typeof base === 'string') { // If it looks like a Uri parse it, otherwise throw - if (isUriRegex.test(base)) { + if (maybeUri(base)) { base = Uri.parse(base, true); } else { debugger; @@ -228,7 +217,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { // Convert the path to a Uri if it isn't one if (typeof pathOrUri === 'string') { - if (isUriRegex.test(pathOrUri)) { + if (maybeUri(pathOrUri)) { pathOrUri = Uri.parse(pathOrUri, true); } else { pathOrUri = normalizePath(pathOrUri); diff --git a/src/repositories.ts b/src/repositories.ts index f6093c3..521f278 100644 --- a/src/repositories.ts +++ b/src/repositories.ts @@ -2,7 +2,7 @@ import { Uri } from 'vscode'; import { DocumentSchemes } from './constants'; import { isLinux } from './env/node/platform'; import { Repository } from './git/models/repository'; -import { normalizePath } from './system/path'; +import { addVslsPrefixIfNeeded, normalizePath } from './system/path'; import { UriTrie } from './system/trie'; // TODO@eamodio don't import from string here since it will break the tests because of ESM dependencies // import { CharCode } from './string'; @@ -44,6 +44,19 @@ export function normalizeRepoUri(uri: Uri): { path: string; ignoreCase: boolean const authority = uri.authority?.split('+', 1)[0]; return { path: authority ? `${authority}${path}` : path.slice(1), ignoreCase: false }; } + case DocumentSchemes.Vsls: + case DocumentSchemes.VslsScc: + // Check if this is a root live share folder, if so add the required prefix (required to match repos correctly) + path = addVslsPrefixIfNeeded(uri.path); + + if (path.charCodeAt(path.length - 1) === slash) { + path = path.slice(1, -1); + } else { + path = path.slice(1); + } + + return { path: path, ignoreCase: false }; + default: path = uri.path; if (path.charCodeAt(path.length - 1) === slash) { diff --git a/src/system/event.ts b/src/system/event.ts index 8cb6662..97d810a 100644 --- a/src/system/event.ts +++ b/src/system/event.ts @@ -14,3 +14,24 @@ export function once(event: Event): Event { return result; }; } + +export function promisify(event: Event): Promise { + return new Promise(resolve => once(event)(resolve)); +} + +export function until(event: Event, predicate: (e: T) => boolean): Event { + return (listener: (e: T) => unknown, thisArgs?: unknown, disposables?: Disposable[]) => { + const result = event( + e => { + if (predicate(e)) { + result.dispose(); + } + return listener.call(thisArgs, e); + }, + null, + disposables, + ); + + return result; + }; +} diff --git a/src/system/function.ts b/src/system/function.ts index f14827d..42b5f04 100644 --- a/src/system/function.ts +++ b/src/system/function.ts @@ -13,19 +13,6 @@ interface PropOfValue { value: string | undefined; } -export function cachedOnce(fn: (...args: any[]) => Promise, seed: T): (...args: any[]) => Promise { - let cached: T | undefined = seed; - return (...args: any[]) => { - if (cached !== undefined) { - const promise = Promise.resolve(cached); - cached = undefined; - - return promise; - } - return fn(...args); - }; -} - export interface DebounceOptions { leading?: boolean; maxWait?: number; @@ -164,40 +151,3 @@ export function disposableInterval(fn: (...args: any[]) => void, ms: number): Di return disposable; } - -export function progress(promise: Promise, intervalMs: number, onProgress: () => boolean): Promise { - return new Promise((resolve, reject) => { - let timer: ReturnType | undefined; - timer = setInterval(() => { - if (onProgress()) { - if (timer != null) { - clearInterval(timer); - timer = undefined; - } - } - }, intervalMs); - - promise.then( - () => { - if (timer != null) { - clearInterval(timer); - timer = undefined; - } - - resolve(promise); - }, - ex => { - if (timer != null) { - clearInterval(timer); - timer = undefined; - } - - reject(ex); - }, - ); - }); -} - -export async function wait(ms: number) { - await new Promise(resolve => setTimeout(resolve, ms)); -} diff --git a/src/system/path.ts b/src/system/path.ts index 48aa18c..0baf8cd 100644 --- a/src/system/path.ts +++ b/src/system/path.ts @@ -1,16 +1,51 @@ -import { basename, dirname } from 'path'; +import { isAbsolute as _isAbsolute, basename, dirname } from 'path'; import { Uri } from 'vscode'; import { isLinux, isWindows } from '@env/platform'; import { DocumentSchemes } from '../constants'; // TODO@eamodio don't import from string here since it will break the tests because of ESM dependencies // import { CharCode } from './string'; -export { basename, dirname, extname, isAbsolute, join as joinPaths } from 'path'; +export { basename, dirname, extname, join as joinPaths } from 'path'; + +const slash = 47; //slash; const driveLetterNormalizeRegex = /(?<=^\/?)([A-Z])(?=:\/)/; +const hasSchemeRegex = /^([a-zA-Z][\w+.-]+):/; const pathNormalizeRegex = /\\/g; -const slash = 47; //slash; -const uriSchemeRegex = /^(\w[\w\d+.-]{1,}?):\/\//; +const vslsHasPrefixRegex = /^[/|\\]~(?:\d+?|external)(?:[/|\\]|$)/; +const vslsRootUriRegex = /^[/|\\]~(?:\d+?|external)(?:[/|\\]|$)/; + +export function addVslsPrefixIfNeeded(path: string): string; +export function addVslsPrefixIfNeeded(uri: Uri): Uri; +export function addVslsPrefixIfNeeded(pathOrUri: string | Uri): string | Uri; +export function addVslsPrefixIfNeeded(pathOrUri: string | Uri): string | Uri { + if (typeof pathOrUri === 'string') { + if (maybeUri(pathOrUri)) { + pathOrUri = Uri.parse(pathOrUri); + } + } + + if (typeof pathOrUri === 'string') { + if (hasVslsPrefix(pathOrUri)) return pathOrUri; + + pathOrUri = normalizePath(pathOrUri); + return `/~0${pathOrUri.charCodeAt(0) === slash ? pathOrUri : `/${pathOrUri}`}`; + } + + let path = pathOrUri.fsPath; + if (hasVslsPrefix(path)) return pathOrUri; + + path = normalizePath(path); + return pathOrUri.with({ path: `/~0${path.charCodeAt(0) === slash ? path : `/${path}`}` }); +} + +export function hasVslsPrefix(path: string): boolean { + return vslsHasPrefixRegex.test(path); +} + +export function isVslsRoot(path: string): boolean { + return vslsRootUriRegex.test(path); +} export function commonBase(s1: string, s2: string, delimiter: string, ignoreCase?: boolean): string | undefined { const index = commonBaseIndex(s1, s2, delimiter, ignoreCase); @@ -40,7 +75,11 @@ export function commonBaseIndex(s1: string, s2: string, delimiter: string, ignor } export function getBestPath(uri: Uri): string { - return uri.scheme === DocumentSchemes.File ? uri.fsPath : uri.path; + return normalizePath(uri.scheme === DocumentSchemes.File ? uri.fsPath : uri.path); +} + +export function getScheme(path: string): string | undefined { + return hasSchemeRegex.exec(path)?.[1]; } export function isChild(path: string, base: string | Uri): boolean; @@ -54,7 +93,7 @@ export function isChild(pathOrUri: string | Uri, base: string | Uri): boolean { return ( isDescendent(pathOrUri, base) && (typeof pathOrUri === 'string' ? pathOrUri : pathOrUri.path) - .substr(base.length + (base.endsWith('/') ? 0 : 1)) + .substr(base.length + (base.charCodeAt(base.length - 1) === slash ? 0 : 1)) .split('/').length === 1 ); } @@ -62,7 +101,7 @@ export function isChild(pathOrUri: string | Uri, base: string | Uri): boolean { return ( isDescendent(pathOrUri, base) && (typeof pathOrUri === 'string' ? pathOrUri : pathOrUri.path) - .substr(base.path.length + (base.path.endsWith('/') ? 0 : 1)) + .substr(base.path.length + (base.path.charCodeAt(base.path.length - 1) === slash ? 0 : 1)) .split('/').length === 1 ); } @@ -89,26 +128,40 @@ export function isDescendent(pathOrUri: string | Uri, base: string | Uri): boole return ( base.length === 1 || (typeof pathOrUri === 'string' ? pathOrUri : pathOrUri.path).startsWith( - base.endsWith('/') ? base : `${base}/`, + base.charCodeAt(base.length - 1) === slash ? base : `${base}/`, ) ); } if (typeof pathOrUri === 'string') { - return base.path.length === 1 || pathOrUri.startsWith(base.path.endsWith('/') ? base.path : `${base.path}/`); + return ( + base.path.length === 1 || + pathOrUri.startsWith(base.path.charCodeAt(base.path.length - 1) === slash ? base.path : `${base.path}/`) + ); } return ( base.scheme === pathOrUri.scheme && base.authority === pathOrUri.authority && - (base.path.length === 1 || pathOrUri.path.startsWith(base.path.endsWith('/') ? base.path : `${base.path}/`)) + (base.path.length === 1 || + pathOrUri.path.startsWith( + base.path.charCodeAt(base.path.length - 1) === slash ? base.path : `${base.path}/`, + )) ); } +export function isAbsolute(path: string): boolean { + return !maybeUri(path) && _isAbsolute(path); +} + export function isFolderGlob(path: string): boolean { return basename(path) === '*'; } +export function maybeUri(path: string): boolean { + return hasSchemeRegex.test(path); +} + export function normalizePath(path: string): string { if (!path) return path; @@ -119,15 +172,15 @@ export function normalizePath(path: string): string { if (isWindows) { // Ensure that drive casing is normalized (lower case) - path = path.replace(driveLetterNormalizeRegex, drive => drive.toLowerCase()); + path = path.replace(driveLetterNormalizeRegex, d => d.toLowerCase()); } return path; } export function relative(from: string, to: string, ignoreCase?: boolean): string { - from = uriSchemeRegex.test(from) ? Uri.parse(from, true).path : normalizePath(from); - to = uriSchemeRegex.test(to) ? Uri.parse(to, true).path : normalizePath(to); + from = hasSchemeRegex.test(from) ? Uri.parse(from, true).path : normalizePath(from); + to = hasSchemeRegex.test(to) ? Uri.parse(to, true).path : normalizePath(to); const index = commonBaseIndex(`${to}/`, `${from}/`, '/', ignoreCase); return index > 0 ? to.substring(index + 1) : to; @@ -140,16 +193,29 @@ export function splitPath( ignoreCase?: boolean, ): [string, string] { if (repoPath) { - path = normalizePath(path); - repoPath = normalizePath(repoPath); + path = hasSchemeRegex.test(path) ? Uri.parse(path, true).path : normalizePath(path); + + let repoUri; + if (hasSchemeRegex.test(repoPath)) { + repoUri = Uri.parse(repoPath, true); + repoPath = getBestPath(repoUri); + } else { + repoPath = normalizePath(repoPath); + } const index = commonBaseIndex(`${repoPath}/`, `${path}/`, '/', ignoreCase); if (index > 0) { repoPath = path.substring(0, index); path = path.substring(index + 1); + } else if (path.charCodeAt(0) === slash) { + path = path.slice(1); + } + + if (repoUri != null) { + repoPath = repoUri.with({ path: repoPath }).toString(); } } else { - repoPath = normalizePath(splitOnBaseIfMissing ? dirname(path) : repoPath ?? ''); + repoPath = normalizePath(splitOnBaseIfMissing ? dirname(path) : ''); path = normalizePath(splitOnBaseIfMissing ? basename(path) : path); } diff --git a/src/system/promise.ts b/src/system/promise.ts index 55b8897..7634702 100644 --- a/src/system/promise.ts +++ b/src/system/promise.ts @@ -101,10 +101,58 @@ export function cancellable( }); } +export interface Deferred { + promise: Promise; + fulfill: (value: T) => void; + cancel(): void; +} + +export function defer(): Deferred { + const deferred: Deferred = { promise: undefined!, fulfill: undefined!, cancel: undefined! }; + deferred.promise = new Promise((resolve, reject) => { + deferred.fulfill = resolve; + deferred.cancel = reject; + }); + return deferred; +} + export function isPromise(obj: PromiseLike | T): obj is Promise { return obj instanceof Promise || typeof (obj as PromiseLike)?.then === 'function'; } +export function progress(promise: Promise, intervalMs: number, onProgress: () => boolean): Promise { + return new Promise((resolve, reject) => { + let timer: ReturnType | undefined; + timer = setInterval(() => { + if (onProgress()) { + if (timer != null) { + clearInterval(timer); + timer = undefined; + } + } + }, intervalMs); + + promise.then( + () => { + if (timer != null) { + clearInterval(timer); + timer = undefined; + } + + resolve(promise); + }, + ex => { + if (timer != null) { + clearInterval(timer); + timer = undefined; + } + + reject(ex); + }, + ); + }); +} + export function raceAll( promises: Promise[], timeout?: number, @@ -168,6 +216,10 @@ export async function raceAll( ); } +export async function wait(ms: number): Promise { + await new Promise(resolve => setTimeout(resolve, ms)); +} + export class AggregateError extends Error { constructor(readonly errors: Error[]) { super(`AggregateError(${errors.length})\n${errors.map(e => `\t${String(e)}`).join('\n')}`); diff --git a/src/trackers/documentTracker.ts b/src/trackers/documentTracker.ts index 96a3d9f..00497db 100644 --- a/src/trackers/documentTracker.ts +++ b/src/trackers/documentTracker.ts @@ -16,7 +16,7 @@ import { workspace, } from 'vscode'; import { configuration } from '../configuration'; -import { ContextKeys, DocumentSchemes, isActiveDocument, isTextEditor, setContext } from '../constants'; +import { ContextKeys, isActiveDocument, isTextEditor, setContext } from '../constants'; import { Container } from '../container'; import { RepositoriesChangeEvent } from '../git/gitProviderService'; import { GitUri } from '../git/gitUri'; @@ -170,9 +170,7 @@ export class DocumentTracker implements Disposable { private async onTextDocumentChanged(e: TextDocumentChangeEvent) { const { scheme } = e.document.uri; - if (scheme !== DocumentSchemes.File && scheme !== DocumentSchemes.Git && scheme !== DocumentSchemes.Vsls) { - return; - } + if (!this.container.git.supportedSchemes.has(scheme)) return; const doc = await (this._documentMap.get(e.document) ?? this.addCore(e.document)); doc.reset('document'); diff --git a/src/vsls/guest.ts b/src/vsls/guest.ts index 94b940e..0de47a5 100644 --- a/src/vsls/guest.ts +++ b/src/vsls/guest.ts @@ -1,12 +1,11 @@ -import { CancellationToken, Disposable, Uri, window, WorkspaceFolder } from 'vscode'; +import { CancellationToken, Disposable, Uri, window } from 'vscode'; import type { LiveShare, SharedServiceProxy } from '../@types/vsls'; import { Container } from '../container'; import { GitCommandOptions } from '../git/commandOptions'; -import { Repository, RepositoryChangeEvent } from '../git/models'; import { Logger } from '../logger'; import { debug, log } from '../system'; import { VslsHostService } from './host'; -import { GitCommandRequestType, RepositoriesInFolderRequestType, RepositoryProxy, RequestType } from './protocol'; +import { GetRepositoriesForUriRequestType, GitCommandRequestType, RepositoryProxy, RequestType } from './protocol'; export class VslsGuestService implements Disposable { @log() @@ -64,28 +63,12 @@ export class VslsGuestService implements Disposable { } @log() - async getRepositoriesInFolder( - folder: WorkspaceFolder, - onAnyRepositoryChanged: (repo: Repository, e: RepositoryChangeEvent) => void, - ): Promise { - const response = await this.sendRequest(RepositoriesInFolderRequestType, { - folderUri: folder.uri.toString(true), + async getRepositoriesForUri(uri: Uri): Promise { + const response = await this.sendRequest(GetRepositoriesForUriRequestType, { + folderUri: uri.toString(), }); - return response.repositories.map( - (r: RepositoryProxy) => - new Repository( - this.container, - onAnyRepositoryChanged, - // TODO@eamodio add live share provider - undefined!, - folder, - Uri.parse(r.uri), - r.root, - !window.state.focused, - r.closed, - ), - ); + return response.repositories; } @debug() diff --git a/src/vsls/host.ts b/src/vsls/host.ts index 8da5e3d..fb206f3 100644 --- a/src/vsls/host.ts +++ b/src/vsls/host.ts @@ -1,21 +1,21 @@ import { CancellationToken, Disposable, Uri, workspace, WorkspaceFoldersChangeEvent } from 'vscode'; -import { git } from '@env/git'; +import { git } from '@env/providers'; import type { LiveShare, SharedService } from '../@types/vsls'; import { Container } from '../container'; import { Logger } from '../logger'; import { debug, log } from '../system/decorators/log'; -import { filterMap, join } from '../system/iterable'; -import { normalizePath } from '../system/path'; +import { join } from '../system/iterable'; +import { isVslsRoot, normalizePath } from '../system/path'; import { + GetRepositoriesForUriRequest, + GetRepositoriesForUriRequestType, + GetRepositoriesForUriResponse, GitCommandRequest, GitCommandRequestType, GitCommandResponse, - RepositoriesInFolderRequest, - RepositoriesInFolderRequestType, - RepositoriesInFolderResponse, + RepositoryProxy, RequestType, } from './protocol'; -import { vslsUriRootRegex } from './vsls'; const defaultWhitelistFn = () => true; const gitWhitelist = new Map boolean>([ @@ -45,6 +45,7 @@ const gitWhitelist = new Map boolean>([ ]); const leadingSlashRegex = /^[/|\\]/; +const slash = 47; //CharCode.Slash; export class VslsHostService implements Disposable { static ServiceId = 'proxy'; @@ -75,7 +76,7 @@ export class VslsHostService implements Disposable { this._disposable = Disposable.from(workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); this.onRequest(GitCommandRequestType, this.onGitCommandRequest.bind(this)); - this.onRequest(RepositoriesInFolderRequestType, this.onRepositoriesInFolderRequest.bind(this)); + this.onRequest(GetRepositoriesForUriRequestType, this.onGetRepositoriesForUriRequest.bind(this)); void this.onWorkspaceFoldersChanged(); } @@ -101,7 +102,7 @@ export class VslsHostService implements Disposable { @debug() private onWorkspaceFoldersChanged(_e?: WorkspaceFoldersChangeEvent) { - if (workspace.workspaceFolders === undefined || workspace.workspaceFolders.length === 0) return; + if (workspace.workspaceFolders == null || workspace.workspaceFolders.length === 0) return; const cc = Logger.getCorrelationContext(); @@ -112,7 +113,7 @@ export class VslsHostService implements Disposable { let sharedPath; for (const f of workspace.workspaceFolders) { localPath = normalizePath(f.uri.fsPath); - sharedPath = normalizePath(this.convertLocalUriToShared(f.uri).fsPath); + sharedPath = normalizePath(this.convertLocalUriToShared(f.uri).toString()); Logger.debug(cc, `shared='${sharedPath}' \u2194 local='${localPath}'`); this._localToSharedPaths.set(localPath, sharedPath); @@ -136,10 +137,10 @@ export class VslsHostService implements Disposable { const { options, args } = request; const fn = gitWhitelist.get(request.args[0]); - if (fn === undefined || !fn(request.args)) throw new Error(`Git ${request.args[0]} command is not allowed`); + if (fn == null || !fn(request.args)) throw new Error(`Git ${request.args[0]} command is not allowed`); let isRootWorkspace = false; - if (options.cwd !== undefined && options.cwd.length > 0 && this._sharedToLocalPaths !== undefined) { + if (options.cwd != null && options.cwd.length > 0 && this._sharedToLocalPaths != null) { // This is all so ugly, but basically we are converting shared paths to local paths if (this._sharedPathsRegex?.test(options.cwd)) { options.cwd = normalizePath(options.cwd).replace(this._sharedPathsRegex, (match, shared) => { @@ -151,8 +152,8 @@ export class VslsHostService implements Disposable { return local != null ? local : shared; }); } else if (leadingSlashRegex.test(options.cwd)) { - const localCwd = this._sharedToLocalPaths.get('/~0'); - if (localCwd !== undefined) { + const localCwd = this._sharedToLocalPaths.get('vsls:/~0'); + if (localCwd != null) { isRootWorkspace = true; options.cwd = normalizePath(this.container.git.getAbsoluteUri(options.cwd, localCwd).fsPath); } @@ -192,9 +193,9 @@ export class VslsHostService implements Disposable { let data = await git(options, ...args); if (typeof data === 'string') { // And then we convert local paths to shared paths - if (this._localPathsRegex !== undefined && data.length > 0) { + if (this._localPathsRegex != null && data.length > 0) { data = data.replace(this._localPathsRegex, (match, local) => { - const shared = this._localToSharedPaths.get(local); + const shared = this._localToSharedPaths.get(normalizePath(local)); return shared != null ? shared : local; }); } @@ -207,30 +208,26 @@ export class VslsHostService implements Disposable { // eslint-disable-next-line @typescript-eslint/require-await @log() - private async onRepositoriesInFolderRequest( - request: RepositoriesInFolderRequest, + private async onGetRepositoriesForUriRequest( + request: GetRepositoriesForUriRequest, _cancellation: CancellationToken, - ): Promise { - const uri = this.convertSharedUriToLocal(Uri.parse(request.folderUri)); - const normalized = normalizePath(uri.fsPath).toLowerCase(); - - const repos = [ - ...filterMap(this.container.git.repositories, r => { - if (!r.id.startsWith(normalized)) return undefined; - - const vslsUri = this.convertLocalUriToShared(r.folder?.uri ?? r.uri); - return { - folderUri: vslsUri.toString(true), - uri: vslsUri.toString(), - root: r.root, - closed: r.closed, - }; - }), - ]; - - return { - repositories: repos, - }; + ): Promise { + const repositories: RepositoryProxy[] = []; + + const uri = this.convertSharedUriToLocal(Uri.parse(request.folderUri, true)); + const repository = this.container.git.getRepository(uri); + + if (repository != null) { + const vslsUri = this.convertLocalUriToShared(repository.uri); + repositories.push({ + folderUri: vslsUri.toString(), + // uri: vslsUri.toString(), + root: repository.root, + closed: repository.closed, + }); + } + + return { repositories: repositories }; } @debug({ @@ -267,17 +264,22 @@ export class VslsHostService implements Disposable { } private convertSharedUriToLocal(sharedUri: Uri) { - if (vslsUriRootRegex.test(sharedUri.path)) { + if (isVslsRoot(sharedUri.path)) { sharedUri = sharedUri.with({ path: `${sharedUri.path}/` }); } const localUri = this._api.convertSharedUriToLocal(sharedUri); - const localPath = localUri.path; + let localPath = localUri.path; const sharedPath = sharedUri.path; if (localPath.endsWith(sharedPath)) { - return localUri.with({ path: localPath.substr(0, localPath.length - sharedPath.length) }); + localPath = localPath.substr(0, localPath.length - sharedPath.length); } - return localUri; + + if (localPath.charCodeAt(localPath.length - 1) === slash) { + localPath = localPath.slice(0, -1); + } + + return localUri.with({ path: localPath }); } } diff --git a/src/vsls/protocol.ts b/src/vsls/protocol.ts index ba606cd..86aece8 100644 --- a/src/vsls/protocol.ts +++ b/src/vsls/protocol.ts @@ -19,20 +19,21 @@ export const GitCommandRequestType = new RequestType('repositories/inFolder'); diff --git a/src/vsls/vsls.ts b/src/vsls/vsls.ts index 52795f3..806a5aa 100644 --- a/src/vsls/vsls.ts +++ b/src/vsls/vsls.ts @@ -3,13 +3,13 @@ import type { LiveShare, LiveShareExtension, SessionChangeEvent } from '../@type import { ContextKeys, DocumentSchemes, setContext } from '../constants'; import { Container } from '../container'; import { Logger } from '../logger'; -import { debug, timeout } from '../system'; +import { debug } from '../system/decorators/log'; +import { timeout } from '../system/decorators/timeout'; +import { once } from '../system/event'; +import { defer, Deferred } from '../system/promise'; import { VslsGuestService } from './guest'; import { VslsHostService } from './host'; -export const vslsUriPrefixRegex = /^[/|\\]~(?:\d+?|external)(?:[/|\\]|$)/; -export const vslsUriRootRegex = /^[/|\\]~(?:\d+?|external)$/; - export interface ContactPresence { status: ContactPresenceStatus; statusText: string; @@ -32,20 +32,20 @@ function contactStatusToPresence(status: string | undefined): ContactPresence { } export class VslsController implements Disposable { + private _api: Promise | undefined; private _disposable: Disposable; private _guest: VslsGuestService | undefined; private _host: VslsHostService | undefined; - - private _onReady: (() => void) | undefined; - private _waitForReady: Promise | undefined; - - private _api: Promise | undefined; + private _ready: Deferred; constructor(private readonly container: Container) { - this._disposable = Disposable.from(container.onReady(this.onReady, this)); + this._ready = defer(); + this._disposable = Disposable.from(once(container.onReady)(this.onReady, this)); } dispose() { + this._ready.fulfill(); + this._disposable.dispose(); this._host?.dispose(); this._guest?.dispose(); @@ -56,22 +56,20 @@ export class VslsController implements Disposable { } private async initialize() { - try { - // If we have a vsls: workspace open, we might be a guest, so wait until live share transitions into a mode - if (workspace.workspaceFolders?.some(f => f.uri.scheme === DocumentSchemes.Vsls)) { - this.setReadonly(true); - this._waitForReady = new Promise(resolve => (this._onReady = resolve)); - } + // If we have a vsls: workspace open, we might be a guest, so wait until live share transitions into a mode + if (workspace.workspaceFolders?.some(f => f.uri.scheme === DocumentSchemes.Vsls)) { + this.setReadonly(true); + } + try { this._api = this.getLiveShareApi(); const api = await this._api; if (api == null) { + debugger; + void setContext(ContextKeys.Vsls, false); // Tear it down if we can't talk to live share - if (this._onReady !== undefined) { - this._onReady(); - this._waitForReady = undefined; - } + this._ready.fulfill(); return; } @@ -82,8 +80,47 @@ export class VslsController implements Disposable { this._disposable, api.onDidChangeSession(e => this.onLiveShareSessionChanged(api, e), this), ); + void this.onLiveShareSessionChanged(api, { session: api.session }); } catch (ex) { Logger.error(ex); + debugger; + } + } + + private async onLiveShareSessionChanged(api: LiveShare, e: SessionChangeEvent) { + this._host?.dispose(); + this._host = undefined; + this._guest?.dispose(); + this._guest = undefined; + + switch (e.session.role) { + case 1 /*Role.Host*/: + this.setReadonly(false); + void setContext(ContextKeys.Vsls, 'host'); + if (this.container.config.liveshare.allowGuestAccess) { + this._host = await VslsHostService.share(api, this.container); + } + + this._ready.fulfill(); + + break; + + case 2 /*Role.Guest*/: + this.setReadonly(true); + void setContext(ContextKeys.Vsls, 'guest'); + this._guest = await VslsGuestService.connect(api, this.container); + + this._ready.fulfill(); + + break; + + default: + this.setReadonly(false); + void setContext(ContextKeys.Vsls, true); + + this._ready = defer(); + + break; } } @@ -92,17 +129,13 @@ export class VslsController implements Disposable { const extension = extensions.getExtension('ms-vsliveshare.vsliveshare'); if (extension != null) { const liveshareExtension = extension.isActive ? extension.exports : await extension.activate(); - return (await liveshareExtension.getApi('1.0.3015')) ?? undefined; + return (await liveshareExtension.getApi('1.0.4753')) ?? undefined; } } catch {} return undefined; } - get isMaybeGuest() { - return this._guest !== undefined || this._waitForReady !== undefined; - } - private _readonly: boolean = false; get readonly() { return this._readonly; @@ -112,9 +145,16 @@ export class VslsController implements Disposable { void setContext(ContextKeys.Readonly, value ? true : undefined); } + async guest() { + if (this._guest != null) return this._guest; + + await this._ready.promise; + return this._guest; + } + @debug() async getContact(email: string | undefined) { - if (email === undefined) return undefined; + if (email == null) return undefined; const api = await this._api; if (api == null) return undefined; @@ -171,47 +211,4 @@ export class VslsController implements Disposable { return api.share(); } - - async guest() { - if (this._waitForReady !== undefined) { - await this._waitForReady; - this._waitForReady = undefined; - } - - return this._guest; - } - - host() { - return this._host; - } - - private async onLiveShareSessionChanged(api: LiveShare, e: SessionChangeEvent) { - this._host?.dispose(); - this._guest?.dispose(); - - switch (e.session.role) { - case 1 /*Role.Host*/: - this.setReadonly(false); - void setContext(ContextKeys.Vsls, 'host'); - if (this.container.config.liveshare.allowGuestAccess) { - this._host = await VslsHostService.share(api, this.container); - } - break; - case 2 /*Role.Guest*/: - this.setReadonly(true); - void setContext(ContextKeys.Vsls, 'guest'); - this._guest = await VslsGuestService.connect(api, this.container); - break; - - default: - this.setReadonly(false); - void setContext(ContextKeys.Vsls, true); - break; - } - - if (this._onReady !== undefined) { - this._onReady(); - this._onReady = undefined; - } - } }