diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 8b4a222441b..ed8f2a44a0f 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Uri, commands, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation, TextEditor, MessageOptions, WorkspaceFolder, Progress } from 'vscode'; +import { Uri, commands, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation, TextEditor, MessageOptions, WorkspaceFolder } from 'vscode'; import { Git, CommitOptions, Stash, ForcePushMode } from './git'; import { Repository, Resource, ResourceGroupType } from './repository'; import { Model } from './model'; @@ -493,7 +493,7 @@ export class CommandCenter { const repositoryPath = await window.withProgress( opts, - (progress: Progress<{ message?: string, increment: number }>, token) => this.git.clone(url!, parentPath, progress, token) + (progress, token) => this.git.clone(url!, parentPath, progress, token) ); let message = localize('proposeopen', "Would you like to open the cloned repository?"); diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index e70bd1e0c62..0696a0af5cd 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -16,9 +16,8 @@ import { CancellationToken, Progress } from 'vscode'; import { URI } from 'vscode-uri'; import { detectEncoding } from './encoding'; import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status } from './api/git'; -import * as nls from 'vscode-nls'; - -const localize = nls.loadMessageBundle(); +import * as byline from 'byline'; +import { StringDecoder } from 'string_decoder'; // https://github.com/microsoft/vscode/issues/65693 const MAX_CLI_LENGTH = 30000; @@ -166,10 +165,10 @@ export interface SpawnOptions extends cp.SpawnOptions { encoding?: string; log?: boolean; cancellationToken?: CancellationToken; - progress?: Progress<{ message?: string, increment: number }>; + onSpawn?: (childProcess: cp.ChildProcess) => void; } -async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToken, progress?: Progress<{ message?: string, increment: number }>): Promise> { +async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToken): Promise> { if (!child.stdout || !child.stderr) { throw new GitError({ message: 'Failed to get stdout or stderr from git process.' }); } @@ -190,9 +189,6 @@ async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToke disposables.push(toDisposable(() => ee.removeListener(name, fn))); }; - const cloneProgressOutput = ['Receiving objects', 'Resolving deltas']; - let prevInc = 0; - let result = Promise.all([ new Promise((c, e) => { once(child, 'error', cpErrorHandler(e)); @@ -205,27 +201,7 @@ async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToke }), new Promise(c => { const buffers: Buffer[] = []; - on(child.stderr, 'data', (b: Buffer) => { - buffers.push(b); - const s = b.toString(); - - // Check for git clone progress reporting - cloneProgressOutput.forEach(cloneOutput => { - if (s.startsWith(cloneOutput)) { - const idx = s.indexOf('%'); - const inc = parseInt(s.slice(idx - 3, idx)); - - if (progress) { - progress.report({ - message: localize(cloneOutput.toLowerCase(), cloneOutput) + ': ' + inc + '%', - increment: inc - prevInc - }); - - prevInc = inc; - } - } - }); - }); + on(child.stderr, 'data', (b: Buffer) => buffers.push(b)); once(child.stderr, 'close', () => c(Buffer.concat(buffers).toString('utf8'))); }) ]) as Promise<[number, Buffer, string]>; @@ -368,7 +344,7 @@ export class Git { return; } - async clone(url: string, parentPath: string, progress: Progress<{ message?: string, increment: number }>, cancellationToken?: CancellationToken): Promise { + async clone(url: string, parentPath: string, progress: Progress<{ increment: number }>, cancellationToken?: CancellationToken): Promise { let baseFolderName = decodeURI(url).replace(/[\/]+$/, '').replace(/^.*[\/\\]/, '').replace(/\.git$/, '') || 'repository'; let folderName = baseFolderName; let folderPath = path.join(parentPath, folderName); @@ -381,8 +357,36 @@ export class Git { await mkdirp(parentPath); + const onSpawn = (child: cp.ChildProcess) => { + const decoder = new StringDecoder('utf8'); + const lineStream = new byline.LineStream({ encoding: 'utf8' }); + child.stderr.on('data', (buffer: Buffer) => lineStream.write(decoder.write(buffer))); + + let totalProgress = 0; + let previousProgress = 0; + + lineStream.on('data', (line: string) => { + let match: RegExpMatchArray | null = null; + + if (match = /Counting objects:\s*(\d+)%/i.exec(line)) { + totalProgress = Math.floor(parseInt(match[1]) * 0.1); + } else if (match = /Compressing objects:\s*(\d+)%/i.exec(line)) { + totalProgress = 10 + Math.floor(parseInt(match[1]) * 0.1); + } else if (match = /Receiving objects:\s*(\d+)%/i.exec(line)) { + totalProgress = 20 + Math.floor(parseInt(match[1]) * 0.4); + } else if (match = /Resolving deltas:\s*(\d+)%/i.exec(line)) { + totalProgress = 60 + Math.floor(parseInt(match[1]) * 0.4); + } + + if (totalProgress !== previousProgress) { + progress.report({ increment: totalProgress - previousProgress }); + previousProgress = totalProgress; + } + }); + }; + try { - await this.exec(parentPath, ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath, '--progress'], { cancellationToken, progress }); + await this.exec(parentPath, ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath, '--progress'], { cancellationToken, onSpawn }); } catch (err) { if (err.stderr) { err.stderr = err.stderr.replace(/^Cloning.+$/m, '').trim(); @@ -428,11 +432,15 @@ export class Git { private async _exec(args: string[], options: SpawnOptions = {}): Promise> { const child = this.spawn(args, options); + if (options.onSpawn) { + options.onSpawn(child); + } + if (options.input) { child.stdin.end(options.input, 'utf8'); } - const bufferResult = await exec(child, options.cancellationToken, options.progress); + const bufferResult = await exec(child, options.cancellationToken); if (options.log !== false && bufferResult.stderr.length > 0) { this.log(`${bufferResult.stderr}\n`);