From fb0feb1c862beed99b184148bf332246d71ff9de Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Thu, 28 Mar 2019 00:19:29 -0500 Subject: [PATCH] Show git clone progress bar and percentage --- extensions/git/src/commands.ts | 4 ++-- extensions/git/src/git.ts | 38 ++++++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 15c9d701e3e..7e68bb9eb41 100755 --- 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 } from 'vscode'; +import { Uri, commands, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation, TextEditor, MessageOptions, WorkspaceFolder, Progress } from 'vscode'; import { Git, CommitOptions, Stash, ForcePushMode } from './git'; import { Repository, Resource, ResourceGroupType } from './repository'; import { Model } from './model'; @@ -478,7 +478,7 @@ export class CommandCenter { const repositoryPath = await window.withProgress( opts, - (_, token) => this.git.clone(url!, parentPath, token) + (progress: Progress<{ message?: string, increment: number }>, token) => this.git.clone(url!, parentPath, progress, token) ); const choices = []; diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 3132b8cdf86..a12abd7a570 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -12,10 +12,12 @@ import { EventEmitter } from 'events'; import iconv = require('iconv-lite'); import * as filetype from 'file-type'; import { assign, groupBy, denodeify, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent } from './util'; -import { CancellationToken, Uri } from 'vscode'; +import { CancellationToken, Uri, Progress } from 'vscode'; +import * as nls from 'vscode-nls'; import { detectEncoding } from './encoding'; import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status } from './api/git'; +const localize = nls.loadMessageBundle(); const readfile = denodeify(fs.readFile); export interface IGit { @@ -159,9 +161,10 @@ export interface SpawnOptions extends cp.SpawnOptions { encoding?: string; log?: boolean; cancellationToken?: CancellationToken; + progress?: Progress<{ message?: string, increment: number }>; } -async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToken): Promise> { +async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToken, progress?: Progress<{ message?: string, increment: number }>): Promise> { if (!child.stdout || !child.stderr) { throw new GitError({ message: 'Failed to get stdout or stderr from git process.' }); } @@ -182,6 +185,9 @@ 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)); @@ -194,7 +200,27 @@ async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToke }), new Promise(c => { const buffers: Buffer[] = []; - on(child.stderr, 'data', (b: Buffer) => buffers.push(b)); + 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; + } + } + }); + }); once(child.stderr, 'close', () => c(Buffer.concat(buffers).toString('utf8'))); }) ]) as Promise<[number, Buffer, string]>; @@ -335,7 +361,7 @@ export class Git { return; } - async clone(url: string, parentPath: string, cancellationToken?: CancellationToken): Promise { + async clone(url: string, parentPath: string, progress: Progress<{ message?: string, increment: number }>, cancellationToken?: CancellationToken): Promise { let baseFolderName = decodeURI(url).replace(/^.*\//, '').replace(/\.git$/, '') || 'repository'; let folderName = baseFolderName; let folderPath = path.join(parentPath, folderName); @@ -349,7 +375,7 @@ export class Git { await mkdirp(parentPath); try { - await this.exec(parentPath, ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath], { cancellationToken }); + await this.exec(parentPath, ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath, '--progress'], { cancellationToken, progress }); } catch (err) { if (err.stderr) { err.stderr = err.stderr.replace(/^Cloning.+$/m, '').trim(); @@ -388,7 +414,7 @@ export class Git { child.stdin.end(options.input, 'utf8'); } - const bufferResult = await exec(child, options.cancellationToken); + const bufferResult = await exec(child, options.cancellationToken, options.progress); if (options.log !== false && bufferResult.stderr.length > 0) { this.log(`${bufferResult.stderr}\n`);