git: run status without buffering whole output

This commit is contained in:
Joao Moreno 2017-04-18 17:16:20 +02:00
parent d2c40f06b3
commit 6146b9739a
8 changed files with 252 additions and 79 deletions

18
.vscode/launch.json vendored
View file

@ -150,12 +150,28 @@
"--debug=5875"
],
"webRoot": "${workspaceRoot}"
},
{
"type": "node",
"request": "launch",
"name": "Git Unit Tests",
"protocol": "inspector",
"program": "${workspaceRoot}/extensions/git/node_modules/mocha/bin/_mocha",
"stopOnEntry": false,
"cwd": "${workspaceRoot}/extensions/git",
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/extensions/git/out/**/*.js"
]
}
],
"compounds": [
{
"name": "Debug VS Code Main and Renderer",
"configurations": ["Launch VS Code", "Attach to Main Process"]
"configurations": [
"Launch VS Code",
"Attach to Main Process"
]
}
]
}

View file

@ -627,6 +627,8 @@
"vscode-nls": "^2.0.1"
},
"devDependencies": {
"@types/node": "^7.0.4"
"@types/mocha": "^2.2.41",
"@types/node": "^7.0.4",
"mocha": "^3.2.0"
}
}
}

View file

@ -9,11 +9,9 @@ import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as cp from 'child_process';
import { EventEmitter } from 'events';
import { assign, uniqBy, groupBy, denodeify, IDisposable, toDisposable, dispose, mkdirp } from './util';
import { EventEmitter, Event } from 'vscode';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
const readdir = denodeify<string[]>(fs.readdir);
const readfile = denodeify<string>(fs.readFile);
@ -280,8 +278,8 @@ export class Git {
private version: string;
private env: any;
private _onOutput = new EventEmitter<string>();
get onOutput(): Event<string> { return this._onOutput.event; }
private _onOutput = new EventEmitter();
get onOutput(): EventEmitter { return this._onOutput; }
constructor(options: IGitOptions) {
this.gitPath = options.gitPath;
@ -394,7 +392,7 @@ export class Git {
}
private log(output: string): void {
this._onOutput.fire(output);
this._onOutput.emit('log', output);
}
}
@ -403,6 +401,72 @@ export interface Commit {
message: string;
}
export class GitStatusParser {
private lastRaw = '';
private result: IFileStatus[] = [];
get status(): IFileStatus[] {
return this.result;
}
update(raw: string): void {
let i = 0;
let nextI: number | undefined;
raw = this.lastRaw + raw;
while ((nextI = this.parseEntry(raw, i)) !== undefined) {
i = nextI;
}
this.lastRaw = raw.substr(i);
}
private parseEntry(raw: string, i: number): number | undefined {
if (i + 4 >= raw.length) {
return;
}
let lastIndex: number;
const entry: IFileStatus = {
x: raw.charAt(i++),
y: raw.charAt(i++),
rename: undefined,
path: ''
};
// space
i++;
if (entry.x === 'R') {
lastIndex = raw.indexOf('\0', i);
if (lastIndex === -1) {
return;
}
entry.rename = raw.substring(i, lastIndex);
i = lastIndex + 1;
}
lastIndex = raw.indexOf('\0', i);
if (lastIndex === -1) {
return;
}
entry.path = raw.substring(i, lastIndex);
// If path ends with slash, it must be a nested git repo
if (entry.path[entry.path.length - 1] !== '/') {
this.result.push(entry);
}
return lastIndex + 1;
}
}
export class Repository {
constructor(
@ -452,7 +516,7 @@ export class Repository {
const child = this.stream(['show', object]);
if (!child.stdout) {
return Promise.reject<string>(localize('errorBuffer', "Can't open file from git"));
return Promise.reject<string>('Can\'t open file from git');
}
return await this.doBuffer(object);
@ -717,44 +781,24 @@ export class Repository {
}
}
async getStatus(): Promise<IFileStatus[]> {
const executionResult = await this.run(['status', '-z', '-u']);
const status = executionResult.stdout;
const result: IFileStatus[] = [];
let current: IFileStatus;
let i = 0;
getStatus(): Promise<IFileStatus[]> {
return new Promise((c, e) => {
const parser = new GitStatusParser();
const child = this.stream(['status', '-z', '-u']);
child.stdout.setEncoding('utf8');
child.stdout.on('data', (raw: string) => {
parser.update(raw);
console.log('got', parser.status.length);
});
child.on('error', e);
child.on('exit', exitCode => {
if (exitCode !== 0) {
e(new GitError({ message: 'Could not get git status.', exitCode }));
}
function readName(): string {
const start = i;
let c: string;
while ((c = status.charAt(i)) !== '\u0000') { i++; }
return status.substring(start, i++);
}
while (i < status.length) {
current = {
x: status.charAt(i++),
y: status.charAt(i++),
path: ''
};
i++;
if (current.x === 'R') {
current.rename = readName();
}
current.path = readName();
// If path ends with slash, it must be a nested git repo
if (current.path[current.path.length - 1] === '/') {
continue;
}
result.push(current);
}
return result;
c(parser.status);
});
});
}
async getHEAD(): Promise<Ref> {

View file

@ -15,6 +15,7 @@ import { GitContentProvider } from './contentProvider';
import { AutoFetcher } from './autofetch';
import { MergeDecorator } from './merge';
import { Askpass } from './askpass';
import { toDisposable } from './util';
import TelemetryReporter from 'vscode-extension-telemetry';
import * as nls from 'vscode-nls';
@ -47,7 +48,10 @@ async function init(context: ExtensionContext, disposables: Disposable[]): Promi
const model = new Model(git, workspaceRootPath);
outputChannel.appendLine(localize('using git', "Using git {0} from {1}", info.version, info.path));
git.onOutput(str => outputChannel.append(str), null, disposables);
const onOutput = str => outputChannel.append(str);
git.onOutput.addListener('log', onOutput);
disposables.push(toDisposable(() => git.onOutput.removeListener('log', onOutput)));
const commandCenter = new CommandCenter(git, model, outputChannel, telemetryReporter);
const statusBarCommands = new StatusBarCommands(model);

View file

@ -0,0 +1,137 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { GitStatusParser } from '../git';
import * as assert from 'assert';
suite('git', () => {
suite('GitStatusParser', () => {
test('empty parser', () => {
const parser = new GitStatusParser();
assert.deepEqual(parser.status, []);
});
test('empty parser 2', () => {
const parser = new GitStatusParser();
parser.update('');
assert.deepEqual(parser.status, []);
});
test('simple', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('simple 2', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt\0');
parser.update('?? file2.txt\0');
parser.update('?? file3.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('empty lines', () => {
const parser = new GitStatusParser();
parser.update('');
parser.update('?? file.txt\0');
parser.update('');
parser.update('');
parser.update('?? file2.txt\0');
parser.update('');
parser.update('?? file3.txt\0');
parser.update('');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('combined', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt\0?? file2.txt\0?? file3.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('split 1', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt\0?? file2');
parser.update('.txt\0?? file3.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('split 2', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt');
parser.update('\0?? file2.txt\0?? file3.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('split 3', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt\0?? file2.txt\0?? file3.txt');
parser.update('\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('rename', () => {
const parser = new GitStatusParser();
parser.update('R newfile.txt\0file.txt\0?? file2.txt\0?? file3.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: 'newfile.txt', x: 'R', y: ' ' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('rename split', () => {
const parser = new GitStatusParser();
parser.update('R newfile.txt\0fil');
parser.update('e.txt\0?? file2.txt\0?? file3.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: 'newfile.txt', x: 'R', y: ' ' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('rename split 3', () => {
const parser = new GitStatusParser();
parser.update('?? file2.txt\0R new');
parser.update('file.txt\0fil');
parser.update('e.txt\0?? file3.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file.txt', rename: 'newfile.txt', x: 'R', y: ' ' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
});
});

View file

@ -1,18 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// import * as assert from 'assert';
// import * as vscode from 'vscode';
// import * as myExtension from '../src/extension';
// Defines a Mocha test suite to group tests of similar kind together
// suite("Extension Tests", () => {
// // Defines a Mocha unit test
// test("Something 1", () => {
// assert.equal(-1, [1, 2, 3].indexOf(5));
// assert.equal(-1, [1, 2, 3].indexOf(0));
// });
// });

View file

@ -1,13 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const testRunner = require('vscode/lib/testrunner');
testRunner.configure({
ui: 'tdd',
useColors: true
});
module.exports = testRunner;

View file

@ -0,0 +1 @@
--ui tdd out/test