Skip to content

Commit f901a47

Browse files
committed
💄 git clone progress
1 parent 471b263 commit f901a47

2 files changed

Lines changed: 42 additions & 34 deletions

File tree

extensions/git/src/commands.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { Uri, commands, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation, TextEditor, MessageOptions, WorkspaceFolder, Progress } from 'vscode';
6+
import { Uri, commands, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation, TextEditor, MessageOptions, WorkspaceFolder } from 'vscode';
77
import { Git, CommitOptions, Stash, ForcePushMode } from './git';
88
import { Repository, Resource, ResourceGroupType } from './repository';
99
import { Model } from './model';
@@ -493,7 +493,7 @@ export class CommandCenter {
493493

494494
const repositoryPath = await window.withProgress(
495495
opts,
496-
(progress: Progress<{ message?: string, increment: number }>, token) => this.git.clone(url!, parentPath, progress, token)
496+
(progress, token) => this.git.clone(url!, parentPath, progress, token)
497497
);
498498

499499
let message = localize('proposeopen', "Would you like to open the cloned repository?");

extensions/git/src/git.ts

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ import { CancellationToken, Progress } from 'vscode';
1616
import { URI } from 'vscode-uri';
1717
import { detectEncoding } from './encoding';
1818
import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status } from './api/git';
19-
import * as nls from 'vscode-nls';
20-
21-
const localize = nls.loadMessageBundle();
19+
import * as byline from 'byline';
20+
import { StringDecoder } from 'string_decoder';
2221

2322
// https://github.com/microsoft/vscode/issues/65693
2423
const MAX_CLI_LENGTH = 30000;
@@ -166,10 +165,10 @@ export interface SpawnOptions extends cp.SpawnOptions {
166165
encoding?: string;
167166
log?: boolean;
168167
cancellationToken?: CancellationToken;
169-
progress?: Progress<{ message?: string, increment: number }>;
168+
onSpawn?: (childProcess: cp.ChildProcess) => void;
170169
}
171170

172-
async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToken, progress?: Progress<{ message?: string, increment: number }>): Promise<IExecutionResult<Buffer>> {
171+
async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToken): Promise<IExecutionResult<Buffer>> {
173172
if (!child.stdout || !child.stderr) {
174173
throw new GitError({ message: 'Failed to get stdout or stderr from git process.' });
175174
}
@@ -190,9 +189,6 @@ async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToke
190189
disposables.push(toDisposable(() => ee.removeListener(name, fn)));
191190
};
192191

193-
const cloneProgressOutput = ['Receiving objects', 'Resolving deltas'];
194-
let prevInc = 0;
195-
196192
let result = Promise.all<any>([
197193
new Promise<number>((c, e) => {
198194
once(child, 'error', cpErrorHandler(e));
@@ -205,27 +201,7 @@ async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToke
205201
}),
206202
new Promise<string>(c => {
207203
const buffers: Buffer[] = [];
208-
on(child.stderr, 'data', (b: Buffer) => {
209-
buffers.push(b);
210-
const s = b.toString();
211-
212-
// Check for git clone progress reporting
213-
cloneProgressOutput.forEach(cloneOutput => {
214-
if (s.startsWith(cloneOutput)) {
215-
const idx = s.indexOf('%');
216-
const inc = parseInt(s.slice(idx - 3, idx));
217-
218-
if (progress) {
219-
progress.report({
220-
message: localize(cloneOutput.toLowerCase(), cloneOutput) + ': ' + inc + '%',
221-
increment: inc - prevInc
222-
});
223-
224-
prevInc = inc;
225-
}
226-
}
227-
});
228-
});
204+
on(child.stderr, 'data', (b: Buffer) => buffers.push(b));
229205
once(child.stderr, 'close', () => c(Buffer.concat(buffers).toString('utf8')));
230206
})
231207
]) as Promise<[number, Buffer, string]>;
@@ -368,7 +344,7 @@ export class Git {
368344
return;
369345
}
370346

371-
async clone(url: string, parentPath: string, progress: Progress<{ message?: string, increment: number }>, cancellationToken?: CancellationToken): Promise<string> {
347+
async clone(url: string, parentPath: string, progress: Progress<{ increment: number }>, cancellationToken?: CancellationToken): Promise<string> {
372348
let baseFolderName = decodeURI(url).replace(/[\/]+$/, '').replace(/^.*[\/\\]/, '').replace(/\.git$/, '') || 'repository';
373349
let folderName = baseFolderName;
374350
let folderPath = path.join(parentPath, folderName);
@@ -381,8 +357,36 @@ export class Git {
381357

382358
await mkdirp(parentPath);
383359

360+
const onSpawn = (child: cp.ChildProcess) => {
361+
const decoder = new StringDecoder('utf8');
362+
const lineStream = new byline.LineStream({ encoding: 'utf8' });
363+
child.stderr.on('data', (buffer: Buffer) => lineStream.write(decoder.write(buffer)));
364+
365+
let totalProgress = 0;
366+
let previousProgress = 0;
367+
368+
lineStream.on('data', (line: string) => {
369+
let match: RegExpMatchArray | null = null;
370+
371+
if (match = /Counting objects:\s*(\d+)%/i.exec(line)) {
372+
totalProgress = Math.floor(parseInt(match[1]) * 0.1);
373+
} else if (match = /Compressing objects:\s*(\d+)%/i.exec(line)) {
374+
totalProgress = 10 + Math.floor(parseInt(match[1]) * 0.1);
375+
} else if (match = /Receiving objects:\s*(\d+)%/i.exec(line)) {
376+
totalProgress = 20 + Math.floor(parseInt(match[1]) * 0.4);
377+
} else if (match = /Resolving deltas:\s*(\d+)%/i.exec(line)) {
378+
totalProgress = 60 + Math.floor(parseInt(match[1]) * 0.4);
379+
}
380+
381+
if (totalProgress !== previousProgress) {
382+
progress.report({ increment: totalProgress - previousProgress });
383+
previousProgress = totalProgress;
384+
}
385+
});
386+
};
387+
384388
try {
385-
await this.exec(parentPath, ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath, '--progress'], { cancellationToken, progress });
389+
await this.exec(parentPath, ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath, '--progress'], { cancellationToken, onSpawn });
386390
} catch (err) {
387391
if (err.stderr) {
388392
err.stderr = err.stderr.replace(/^Cloning.+$/m, '').trim();
@@ -428,11 +432,15 @@ export class Git {
428432
private async _exec(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
429433
const child = this.spawn(args, options);
430434

435+
if (options.onSpawn) {
436+
options.onSpawn(child);
437+
}
438+
431439
if (options.input) {
432440
child.stdin.end(options.input, 'utf8');
433441
}
434442

435-
const bufferResult = await exec(child, options.cancellationToken, options.progress);
443+
const bufferResult = await exec(child, options.cancellationToken);
436444

437445
if (options.log !== false && bufferResult.stderr.length > 0) {
438446
this.log(`${bufferResult.stderr}\n`);

0 commit comments

Comments
 (0)