diff --git a/src/env/browser/providers.ts b/src/env/browser/providers.ts index e85bc0a..25ae9cb 100644 --- a/src/env/browser/providers.ts +++ b/src/env/browser/providers.ts @@ -8,6 +8,16 @@ export function git(_options: GitCommandOptions, ..._args: any[]): Promise { + return Promise.resolve([[''], 0]); +} + export async function getSupportedGitProviders(container: Container): Promise { return [new GitHubGitProvider(container)]; } diff --git a/src/env/node/git/vslsGitProvider.ts b/src/env/node/git/vslsGitProvider.ts index 52ff6a3..7071a42 100644 --- a/src/env/node/git/vslsGitProvider.ts +++ b/src/env/node/git/vslsGitProvider.ts @@ -1,7 +1,8 @@ +import type { ChildProcess } from 'child_process'; import { FileType, Uri, workspace } from 'vscode'; import { Schemes } from '../../../constants'; import { Container } from '../../../container'; -import type { GitCommandOptions } from '../../../git/commandOptions'; +import type { GitCommandOptions, GitSpawnOptions } from '../../../git/commandOptions'; import type { GitProviderDescriptor } from '../../../git/gitProvider'; import { GitProviderId } from '../../../git/gitProvider'; import type { Repository } from '../../../git/models/repository'; @@ -31,6 +32,28 @@ export class VslsGit extends Git { return guest.git(options, ...args); } + + // eslint-disable-next-line @typescript-eslint/require-await + override async gitSpawn(_options: GitSpawnOptions, ..._args: any[]): Promise { + debugger; + throw new Error('Git spawn not supported in Live Share'); + } + + override async logStreamTo( + repoPath: string, + sha: string, + limit: number, + options?: { configs?: readonly string[]; stdin?: string }, + ...args: string[] + ): Promise<[data: string[], count: number]> { + const guest = await Container.instance.vsls.guest(); + if (guest == null) { + debugger; + throw new Error('No guest'); + } + + return guest.gitLogStreamTo(repoPath, sha, limit, options, ...args); + } } export class VslsGitProvider extends LocalGitProvider { diff --git a/src/env/node/providers.ts b/src/env/node/providers.ts index a617a00..e7404f8 100644 --- a/src/env/node/providers.ts +++ b/src/env/node/providers.ts @@ -15,8 +15,18 @@ function ensureGit() { return gitInstance; } -export function git(_options: GitCommandOptions, ..._args: any[]): Promise { - return ensureGit().git(_options, ..._args); +export function git(options: GitCommandOptions, ...args: any[]): Promise { + return ensureGit().git(options, ...args); +} + +export function gitLogStreamTo( + repoPath: string, + sha: string, + limit: number, + options?: { configs?: readonly string[]; stdin?: string }, + ...args: string[] +): Promise<[data: string[], count: number]> { + return ensureGit().logStreamTo(repoPath, sha, limit, options, ...args); } export async function getSupportedGitProviders(container: Container): Promise { diff --git a/src/vsls/guest.ts b/src/vsls/guest.ts index 93f744c..e4537bf 100644 --- a/src/vsls/guest.ts +++ b/src/vsls/guest.ts @@ -8,7 +8,7 @@ import { Logger } from '../system/logger'; import { getLogScope } from '../system/logger.scope'; import { VslsHostService } from './host'; import type { RepositoryProxy, RequestType } from './protocol'; -import { GetRepositoriesForUriRequestType, GitCommandRequestType } from './protocol'; +import { GetRepositoriesForUriRequestType, GitCommandRequestType, GitLogStreamToCommandRequestType } from './protocol'; export class VslsGuestService implements Disposable { @log() @@ -70,6 +70,26 @@ export class VslsGuestService implements Disposable { } @log() + async gitLogStreamTo( + repoPath: string, + sha: string, + limit: number, + options?: { configs?: readonly string[]; stdin?: string }, + ...args: string[] + ): Promise<[data: string[], count: number]> { + const response = await this.sendRequest(GitLogStreamToCommandRequestType, { + __type: 'gitlens', + repoPath: repoPath, + sha: sha, + limit: limit, + options: options, + args: args, + }); + + return [response.data, response.count]; + } + + @log() async getRepositoriesForUri(uri: Uri): Promise { const response = await this.sendRequest(GetRepositoriesForUriRequestType, { __type: 'gitlens', diff --git a/src/vsls/host.ts b/src/vsls/host.ts index e1883ee..c38f51b 100644 --- a/src/vsls/host.ts +++ b/src/vsls/host.ts @@ -1,6 +1,6 @@ import type { CancellationToken, WorkspaceFoldersChangeEvent } from 'vscode'; import { Disposable, Uri, workspace } from 'vscode'; -import { git } from '@env/providers'; +import { git, gitLogStreamTo } from '@env/providers'; import type { LiveShare, SharedService } from '../@types/vsls'; import type { Container } from '../container'; import { debug, log } from '../system/decorators/log'; @@ -13,10 +13,12 @@ import type { GetRepositoriesForUriResponse, GitCommandRequest, GitCommandResponse, + GitLogStreamToCommandRequest, + GitLogStreamToCommandResponse, RepositoryProxy, RequestType, } from './protocol'; -import { GetRepositoriesForUriRequestType, GitCommandRequestType } from './protocol'; +import { GetRepositoriesForUriRequestType, GitCommandRequestType, GitLogStreamToCommandRequestType } from './protocol'; const defaultWhitelistFn = () => true; const gitWhitelist = new Map boolean>([ @@ -42,6 +44,7 @@ const gitWhitelist = new Map boolean>([ ['status', defaultWhitelistFn], ['symbolic-ref', defaultWhitelistFn], ['tag', args => args[1] === '-l'], + ['worktree', args => args[1] === 'list'], ]); const leadingSlashRegex = /^[/|\\]/; @@ -76,6 +79,7 @@ export class VslsHostService implements Disposable { this._disposable = Disposable.from(workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); this.onRequest(GitCommandRequestType, this.onGitCommandRequest.bind(this)); + this.onRequest(GitLogStreamToCommandRequestType, this.onGitLogStreamToCommandRequest.bind(this)); this.onRequest(GetRepositoriesForUriRequestType, this.onGetRepositoriesForUriRequest.bind(this)); this.onWorkspaceFoldersChanged(); @@ -142,65 +146,16 @@ export class VslsHostService implements Disposable { request: GitCommandRequest, _cancellation: CancellationToken, ): Promise { - const { options, args } = request; - const fn = gitWhitelist.get(request.args[0]); if (fn == null || !fn(request.args)) throw new Error(`Git ${request.args[0]} command is not allowed`); - let isRootWorkspace = false; - 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: string) => { - if (!isRootWorkspace) { - isRootWorkspace = shared === '/~0'; - } - - const local = this._sharedToLocalPaths.get(shared); - return local != null ? local : shared; - }); - } else if (leadingSlashRegex.test(options.cwd)) { - const localCwd = this._sharedToLocalPaths.get('vsls:/~0'); - if (localCwd != null) { - isRootWorkspace = true; - options.cwd = normalizePath(this.container.git.getAbsoluteUri(options.cwd, localCwd).fsPath); - } - } - } - - let files = false; - let i = -1; - for (const arg of args) { - i++; - if (arg === '--') { - files = true; - continue; - } - - if (!files) continue; - - if (typeof arg === 'string') { - // If we are the "root" workspace, then we need to remove the leading slash off the path (otherwise it will not be treated as a relative path) - if (isRootWorkspace && leadingSlashRegex.test(arg[0])) { - args.splice(i, 1, arg.substr(1)); - } - - if (this._sharedPathsRegex?.test(arg)) { - args.splice( - i, - 1, - normalizePath(arg).replace(this._sharedPathsRegex, (match, shared: string) => { - const local = this._sharedToLocalPaths.get(shared); - return local != null ? local : shared; - }), - ); - } - } - } + const { options, args } = request; + const [cwd, isRootWorkspace] = this.convertGitCommandCwd(options.cwd); + options.cwd = cwd; - let data = await git(options, ...args); + let data = await git(options, ...this.convertGitCommandArgs(args, isRootWorkspace)); if (typeof data === 'string') { - // And then we convert local paths to shared paths + // Convert local paths to shared paths if (this._localPathsRegex != null && data.length > 0) { data = data.replace(this._localPathsRegex, (match, local: string) => { const shared = this._localToSharedPaths.get(normalizePath(local)); @@ -214,6 +169,33 @@ export class VslsHostService implements Disposable { return { data: data.toString('binary'), isBuffer: true }; } + @log() + private async onGitLogStreamToCommandRequest( + request: GitLogStreamToCommandRequest, + _cancellation: CancellationToken, + ): Promise { + const { options, args } = request; + const [cwd, isRootWorkspace] = this.convertGitCommandCwd(request.repoPath); + + let [data, count] = await gitLogStreamTo( + cwd, + request.sha, + request.limit, + options, + ...this.convertGitCommandArgs(args, isRootWorkspace), + ); + if (this._localPathsRegex != null && data.length > 0) { + // Convert local paths to shared paths + data = data.map(d => + d.replace(this._localPathsRegex!, (match, local: string) => { + const shared = this._localToSharedPaths.get(normalizePath(local)); + return shared != null ? shared : local; + }), + ); + } + return { data: data, count: count }; + } + // eslint-disable-next-line @typescript-eslint/require-await @log() private async onGetRepositoriesForUriRequest( @@ -271,6 +253,67 @@ export class VslsHostService implements Disposable { return sharedUri; } + private convertGitCommandCwd(cwd: string): [cwd: string, root: boolean]; + private convertGitCommandCwd(cwd: string | undefined): [cwd: string | undefined, root: boolean]; + private convertGitCommandCwd(cwd: string | undefined): [cwd: string | undefined, root: boolean] { + let isRootWorkspace = false; + if (cwd != null && 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(cwd)) { + cwd = normalizePath(cwd).replace(this._sharedPathsRegex, (match, shared: string) => { + if (!isRootWorkspace) { + isRootWorkspace = shared === '/~0'; + } + + const local = this._sharedToLocalPaths.get(shared); + return local != null ? local : shared; + }); + } else if (leadingSlashRegex.test(cwd)) { + const localCwd = this._sharedToLocalPaths.get('vsls:/~0'); + if (localCwd != null) { + isRootWorkspace = true; + cwd = normalizePath(this.container.git.getAbsoluteUri(cwd, localCwd).fsPath); + } + } + } + + return [cwd, isRootWorkspace]; + } + + private convertGitCommandArgs(args: any[], isRootWorkspace: boolean): any[] { + let files = false; + let i = -1; + for (const arg of args) { + i++; + if (arg === '--') { + files = true; + continue; + } + + if (!files) continue; + + if (typeof arg === 'string') { + // If we are the "root" workspace, then we need to remove the leading slash off the path (otherwise it will not be treated as a relative path) + if (isRootWorkspace && leadingSlashRegex.test(arg[0])) { + args.splice(i, 1, arg.substr(1)); + } + + if (this._sharedPathsRegex?.test(arg)) { + args.splice( + i, + 1, + normalizePath(arg).replace(this._sharedPathsRegex, (match, shared: string) => { + const local = this._sharedToLocalPaths.get(shared); + return local != null ? local : shared; + }), + ); + } + } + } + + return args; + } + private convertSharedUriToLocal(sharedUri: Uri) { if (isVslsRoot(sharedUri.path)) { sharedUri = sharedUri.with({ path: `${sharedUri.path}/` }); diff --git a/src/vsls/protocol.ts b/src/vsls/protocol.ts index e38e0c3..1baa7e4 100644 --- a/src/vsls/protocol.ts +++ b/src/vsls/protocol.ts @@ -17,6 +17,24 @@ export interface GitCommandResponse { export const GitCommandRequestType = new RequestType('git'); +export interface GitLogStreamToCommandRequest { + repoPath: string; + sha: string; + limit: number; + options?: { configs?: readonly string[]; stdin?: string }; + args: string[]; +} + +export interface GitLogStreamToCommandResponse { + data: string[]; + count: number; +} + +export const GitLogStreamToCommandRequestType = new RequestType< + GitLogStreamToCommandRequest, + GitLogStreamToCommandResponse +>('git/logStreamTo'); + export interface RepositoryProxy { folderUri: string; /** @deprecated */