⚡ git: run status without buffering whole output
This commit is contained in:
parent
d2c40f06b3
commit
6146b9739a
18
.vscode/launch.json
vendored
18
.vscode/launch.json
vendored
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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);
|
||||
|
|
137
extensions/git/src/test/git.test.ts
Normal file
137
extensions/git/src/test/git.test.ts
Normal 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: '?' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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));
|
||||
// });
|
||||
// });
|
|
@ -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;
|
1
extensions/git/test/mocha.opts
Normal file
1
extensions/git/test/mocha.opts
Normal file
|
@ -0,0 +1 @@
|
|||
--ui tdd out/test
|
Loading…
Reference in a new issue