Merge branch 'git-perf'

This commit is contained in:
Joao Moreno 2017-04-19 08:48:49 +02:00
commit 0be6167d9e
11 changed files with 294 additions and 76 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

@ -618,6 +618,11 @@
"type": "boolean",
"description": "%config.ignoreLegacyWarning%",
"default": false
},
"git.ignoreLimitWarning": {
"type": "boolean",
"description": "%config.ignoreLimitWarning%",
"default": false
}
}
}
@ -627,6 +632,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

@ -36,5 +36,6 @@
"config.confirmSync": "Confirm before synchronizing git repositories",
"config.countBadge": "Controls the git badge counter",
"config.checkoutType": "Controls what type of branches are listed",
"config.ignoreLegacyWarning": "Ignores the legacy Git warning"
"config.ignoreLegacyWarning": "Ignores the legacy Git warning",
"config.ignoreLimitWarning": "Ignores the warning when there are too many changes in a repository"
}

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,36 @@ 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(limit = 5000): Promise<{ status: IFileStatus[]; didHitLimit: boolean; }> {
return new Promise<{ status: IFileStatus[]; didHitLimit: boolean; }>((c, e) => {
const parser = new GitStatusParser();
const child = this.stream(['status', '-z', '-u']);
function readName(): string {
const start = i;
let c: string;
while ((c = status.charAt(i)) !== '\u0000') { i++; }
return status.substring(start, i++);
}
const onExit = exitCode => {
if (exitCode !== 0) {
e(new GitError({ message: 'Could not get git status.', exitCode }));
}
while (i < status.length) {
current = {
x: status.charAt(i++),
y: status.charAt(i++),
path: ''
c({ status: parser.status, didHitLimit: false });
};
i++;
const onData = (raw: string) => {
parser.update(raw);
if (current.x === 'R') {
current.rename = readName();
}
if (parser.status.length > 5000) {
child.removeListener('exit', onExit);
child.stdout.removeListener('data', onData);
child.kill();
current.path = readName();
c({ status: parser.status.slice(0, 5000), didHitLimit: true });
}
};
// 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;
child.stdout.setEncoding('utf8');
child.stdout.on('data', onData);
child.on('error', e);
child.on('exit', onExit);
});
}
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

@ -351,6 +351,8 @@ export class Model implements Disposable {
}
private onWorkspaceChange: Event<Uri>;
private isRepositoryHuge = false;
private didWarnAboutLimit = false;
private repositoryDisposable: Disposable = EmptyDisposable;
private disposables: Disposable[] = [];
@ -454,10 +456,10 @@ export class Model implements Disposable {
@throttle
async fetch(): Promise<void> {
try {
await this.run(Operation.Fetch, () => this.repository.fetch());
await this.run(Operation.Fetch, () => this.repository.fetch());
} catch (err) {
// noop
}
}
}
async pull(rebase?: boolean): Promise<void> {
@ -586,12 +588,32 @@ export class Model implements Disposable {
onNonGitChange(this.onFSChange, this, disposables);
this.repositoryDisposable = combinedDisposable(disposables);
this.isRepositoryHuge = false;
this.didWarnAboutLimit = false;
this.state = State.Idle;
}
@throttle
private async updateModelState(): Promise<void> {
const status = await this.repository.getStatus();
const { status, didHitLimit } = await this.repository.getStatus();
const config = workspace.getConfiguration('git');
const shouldIgnore = config.get<boolean>('ignoreLimitWarning') === true;
this.isRepositoryHuge = didHitLimit;
if (didHitLimit && !shouldIgnore && !this.didWarnAboutLimit) {
const ok = { title: localize('ok', "OK"), isCloseAffordance: true };
const neverAgain = { title: localize('neveragain', "Never Show Again") };
window.showWarningMessage(localize('huge', "The git repository at '{0}' has too many active changes, only a subset of Git features will be enabled.", this.repository.root), ok, neverAgain).then(result => {
if (result === neverAgain) {
config.update('ignoreLimitWarning', true, false);
}
});
this.didWarnAboutLimit = true;
}
let HEAD: Branch | undefined;
try {
@ -664,6 +686,10 @@ export class Model implements Disposable {
return;
}
if (this.isRepositoryHuge) {
return;
}
if (!this.operations.isIdle()) {
return;
}

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

@ -6,3 +6,4 @@
/// <reference path='../../../../src/vs/vscode.d.ts'/>
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
/// <reference types='@types/node'/>
/// <reference types='@types/mocha'/>

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