Refresh master with initial release/0.24 snapshot (#332)

* Initial port of release/0.24 source code

* Fix additional headers

* Fix a typo in launch.json
This commit is contained in:
Karl Burtram
2017-12-15 15:38:57 -08:00
committed by GitHub
parent 271b3a0b82
commit 6ad0df0e3e
7118 changed files with 107999 additions and 56466 deletions

View File

@@ -5,16 +5,23 @@
'use strict';
import { workspace, Disposable } from 'vscode';
import { workspace, Disposable, EventEmitter } from 'vscode';
import { GitErrorCodes } from './git';
import { Repository } from './repository';
import { throttle } from './decorators';
import { eventToPromise, filterEvent } from './util';
export class AutoFetcher {
private static Period = 3 * 60 * 1000 /* three minutes */;
private _onDidChange = new EventEmitter<boolean>();
private onDidChange = this._onDidChange.event;
private _enabled: boolean = false;
get enabled(): boolean { return this._enabled; }
set enabled(enabled: boolean) { this._enabled = enabled; this._onDidChange.fire(enabled); }
private disposables: Disposable[] = [];
private timer: NodeJS.Timer;
constructor(private repository: Repository) {
workspace.onDidChangeConfiguration(this.onConfiguration, this, this.disposables);
@@ -32,26 +39,41 @@ export class AutoFetcher {
}
enable(): void {
if (this.timer) {
if (this.enabled) {
return;
}
this.fetch();
this.timer = setInterval(() => this.fetch(), AutoFetcher.Period);
this.enabled = true;
this.run();
}
disable(): void {
clearInterval(this.timer);
this.enabled = false;
}
@throttle
private async fetch(): Promise<void> {
try {
await this.repository.fetch();
} catch (err) {
if (err.gitErrorCode === GitErrorCodes.AuthenticationFailed) {
this.disable();
private async run(): Promise<void> {
while (this.enabled) {
await this.repository.whenIdleAndFocused();
if (!this.enabled) {
return;
}
try {
await this.repository.fetch();
} catch (err) {
if (err.gitErrorCode === GitErrorCodes.AuthenticationFailed) {
this.disable();
}
}
if (!this.enabled) {
return;
}
const timeout = new Promise(c => setTimeout(c, AutoFetcher.Period));
const whenDisabled = eventToPromise(filterEvent(this.onDidChange, enabled => !enabled));
await Promise.race([timeout, whenDisabled]);
}
}

View File

@@ -5,11 +5,12 @@
'use strict';
import { Uri, commands, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation } from 'vscode';
import { Uri, commands, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation, TextEditor } from 'vscode';
import { Ref, RefType, Git, GitErrorCodes, Branch } from './git';
import { Repository, Resource, Status, CommitOptions, ResourceGroupType } from './repository';
import { Model } from './model';
import { toGitUri, fromGitUri } from './uri';
import { grep } from './util';
import { applyLineChanges, intersectDiffWithRange, toLineRanges, invertLineChange } from './staging';
import * as path from 'path';
import * as os from 'os';
@@ -92,11 +93,13 @@ class MergeItem implements QuickPickItem {
class CreateBranchItem implements QuickPickItem {
constructor(private cc: CommandCenter) { }
get label(): string { return localize('create branch', '$(plus) Create new branch'); }
get description(): string { return ''; }
async run(repository: Repository): Promise<void> {
await commands.executeCommand('git.branch');
await this.cc.branch(repository);
}
}
@@ -169,12 +172,14 @@ export class CommandCenter {
const opts: TextDocumentShowOptions = {
preserveFocus,
preview,
viewColumn: window.activeTextEditor && window.activeTextEditor.viewColumn || ViewColumn.One
viewColumn: ViewColumn.Active
};
const activeTextEditor = window.activeTextEditor;
if (preserveSelection && activeTextEditor && activeTextEditor.document.uri.fsPath === right.fsPath) {
// Check if active text editor has same path as other editor. we cannot compare via
// URI.toString() here because the schemas can be different. Instead we just go by path.
if (preserveSelection && activeTextEditor && activeTextEditor.document.uri.path === right.path) {
opts.selection = activeTextEditor.selection;
}
@@ -257,13 +262,20 @@ export class CommandCenter {
}
@command('git.clone')
async clone(): Promise<void> {
const url = await window.showInputBox({
prompt: localize('repourl', "Repository URL"),
ignoreFocusOut: true
});
async clone(url?: string): Promise<void> {
if (!url) {
url = await window.showInputBox({
prompt: localize('repourl', "Repository URL"),
ignoreFocusOut: true
});
}
if (!url) {
/* __GDPR__
"clone" : {
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_URL' });
return;
}
@@ -278,6 +290,11 @@ export class CommandCenter {
});
if (!parentPath) {
/* __GDPR__
"clone" : {
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' });
return;
}
@@ -295,14 +312,30 @@ export class CommandCenter {
const result = await window.showInformationMessage(localize('proposeopen', "Would you like to open the cloned repository?"), open);
const openFolder = result === open;
/* __GDPR__
"clone" : {
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: openFolder ? 1 : 0 });
if (openFolder) {
commands.executeCommand('vscode.openFolder', Uri.file(repositoryPath));
}
} catch (err) {
if (/already exists and is not an empty directory/.test(err && err.stderr || '')) {
/* __GDPR__
"clone" : {
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' });
} else {
/* __GDPR__
"clone" : {
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' });
}
throw err;
@@ -311,25 +344,44 @@ export class CommandCenter {
@command('git.init')
async init(): Promise<void> {
const value = workspace.workspaceFolders && workspace.workspaceFolders.length > 0
? workspace.workspaceFolders[0].uri.fsPath
: os.homedir();
const homeUri = Uri.file(os.homedir());
const defaultUri = workspace.workspaceFolders && workspace.workspaceFolders.length > 0
? Uri.file(workspace.workspaceFolders[0].uri.fsPath)
: homeUri;
const path = await window.showInputBox({
placeHolder: localize('path to init', "Folder path"),
prompt: localize('provide path', "Please provide a folder path to initialize a Git repository"),
value,
ignoreFocusOut: true
const result = await window.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
defaultUri,
openLabel: localize('init repo', "Initialize Repository")
});
if (!path) {
if (!result || result.length === 0) {
return;
}
const uri = result[0];
if (homeUri.toString().startsWith(uri.toString())) {
const yes = localize('create repo', "Initialize Repository");
const answer = await window.showWarningMessage(localize('are you sure', "This will create a Git repository in '{0}'. Are you sure you want to continue?", uri.fsPath), yes);
if (answer !== yes) {
return;
}
}
const path = uri.fsPath;
await this.git.init(path);
await this.model.tryOpenRepository(path);
}
@command('git.close', { repository: true })
async close(repository: Repository): Promise<void> {
this.model.close(repository);
}
@command('git.openFile')
async openFile(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
const preserveFocus = arg instanceof Resource;
@@ -364,11 +416,13 @@ export class CommandCenter {
for (const uri of uris) {
const opts: TextDocumentShowOptions = {
preserveFocus,
preview: preview,
viewColumn: activeTextEditor && activeTextEditor.viewColumn || ViewColumn.One
preview,
viewColumn: ViewColumn.Active
};
if (activeTextEditor && activeTextEditor.document.uri.fsPath === uri.fsPath) {
// Check if active text editor has same path as other editor. we cannot compare via
// URI.toString() here because the schemas can be different. Instead we just go by path.
if (activeTextEditor && activeTextEditor.document.uri.path === uri.path) {
opts.selection = activeTextEditor.selection;
}
@@ -451,12 +505,20 @@ export class CommandCenter {
}
const selection = resourceStates.filter(s => s instanceof Resource) as Resource[];
const mergeConflicts = selection.filter(s => s.resourceGroupType === ResourceGroupType.Merge);
const merge = selection.filter(s => s.resourceGroupType === ResourceGroupType.Merge);
const bothModified = merge.filter(s => s.type === Status.BOTH_MODIFIED);
const promises = bothModified.map(s => grep(s.resourceUri.fsPath, /^<{7}|^={7}|^>{7}/));
const unresolvedBothModified = await Promise.all<boolean>(promises);
const resolvedConflicts = bothModified.filter((s, i) => !unresolvedBothModified[i]);
const unresolvedConflicts = [
...merge.filter(s => s.type !== Status.BOTH_MODIFIED),
...bothModified.filter((s, i) => unresolvedBothModified[i])
];
if (mergeConflicts.length > 0) {
const message = mergeConflicts.length > 1
? localize('confirm stage files with merge conflicts', "Are you sure you want to stage {0} files with merge conflicts?", mergeConflicts.length)
: localize('confirm stage file with merge conflicts', "Are you sure you want to stage {0} with merge conflicts?", path.basename(mergeConflicts[0].resourceUri.fsPath));
if (unresolvedConflicts.length > 0) {
const message = unresolvedConflicts.length > 1
? localize('confirm stage files with merge conflicts', "Are you sure you want to stage {0} files with merge conflicts?", unresolvedConflicts.length)
: localize('confirm stage file with merge conflicts', "Are you sure you want to stage {0} with merge conflicts?", path.basename(unresolvedConflicts[0].resourceUri.fsPath));
const yes = localize('yes', "Yes");
const pick = await window.showWarningMessage(message, { modal: true }, yes);
@@ -466,10 +528,8 @@ export class CommandCenter {
}
}
const workingTree = selection
.filter(s => s.resourceGroupType === ResourceGroupType.WorkingTree);
const scmResources = [...workingTree, ...mergeConflicts];
const workingTree = selection.filter(s => s.resourceGroupType === ResourceGroupType.WorkingTree);
const scmResources = [...workingTree, ...resolvedConflicts, ...unresolvedConflicts];
if (!scmResources.length) {
return;
@@ -500,14 +560,39 @@ export class CommandCenter {
await repository.add([]);
}
@command('git.stageChange')
async stageChange(uri: Uri, changes: LineChange[], index: number): Promise<void> {
const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0];
if (!textEditor) {
return;
}
await this._stageChanges(textEditor, [changes[index]]);
}
@command('git.stageSelectedRanges', { diff: true })
async stageSelectedRanges(diffs: LineChange[]): Promise<void> {
async stageSelectedChanges(changes: LineChange[]): Promise<void> {
const textEditor = window.activeTextEditor;
if (!textEditor) {
return;
}
const modifiedDocument = textEditor.document;
const selectedLines = toLineRanges(textEditor.selections, modifiedDocument);
const selectedChanges = changes
.map(diff => selectedLines.reduce<LineChange | null>((result, range) => result || intersectDiffWithRange(modifiedDocument, diff, range), null))
.filter(d => !!d) as LineChange[];
if (!selectedChanges.length) {
return;
}
await this._stageChanges(textEditor, selectedChanges);
}
private async _stageChanges(textEditor: TextEditor, changes: LineChange[]): Promise<void> {
const modifiedDocument = textEditor.document;
const modifiedUri = modifiedDocument.uri;
@@ -517,28 +602,48 @@ export class CommandCenter {
const originalUri = toGitUri(modifiedUri, '~');
const originalDocument = await workspace.openTextDocument(originalUri);
const selectedLines = toLineRanges(textEditor.selections, modifiedDocument);
const selectedDiffs = diffs
.map(diff => selectedLines.reduce<LineChange | null>((result, range) => result || intersectDiffWithRange(modifiedDocument, diff, range), null))
.filter(d => !!d) as LineChange[];
if (!selectedDiffs.length) {
return;
}
const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs);
const result = applyLineChanges(originalDocument, modifiedDocument, changes);
await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
}
@command('git.revertChange')
async revertChange(uri: Uri, changes: LineChange[], index: number): Promise<void> {
const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0];
if (!textEditor) {
return;
}
await this._revertChanges(textEditor, [...changes.slice(0, index), ...changes.slice(index + 1)]);
}
@command('git.revertSelectedRanges', { diff: true })
async revertSelectedRanges(diffs: LineChange[]): Promise<void> {
async revertSelectedRanges(changes: LineChange[]): Promise<void> {
const textEditor = window.activeTextEditor;
if (!textEditor) {
return;
}
const modifiedDocument = textEditor.document;
const selections = textEditor.selections;
const selectedChanges = changes.filter(change => {
const modifiedRange = change.modifiedEndLineNumber === 0
? new Range(modifiedDocument.lineAt(change.modifiedStartLineNumber - 1).range.end, modifiedDocument.lineAt(change.modifiedStartLineNumber).range.start)
: new Range(modifiedDocument.lineAt(change.modifiedStartLineNumber - 1).range.start, modifiedDocument.lineAt(change.modifiedEndLineNumber - 1).range.end);
return selections.every(selection => !selection.intersection(modifiedRange));
});
if (selectedChanges.length === changes.length) {
return;
}
await this._revertChanges(textEditor, selectedChanges);
}
private async _revertChanges(textEditor: TextEditor, changes: LineChange[]): Promise<void> {
const modifiedDocument = textEditor.document;
const modifiedUri = modifiedDocument.uri;
@@ -548,19 +653,6 @@ export class CommandCenter {
const originalUri = toGitUri(modifiedUri, '~');
const originalDocument = await workspace.openTextDocument(originalUri);
const selections = textEditor.selections;
const selectedDiffs = diffs.filter(diff => {
const modifiedRange = diff.modifiedEndLineNumber === 0
? new Range(modifiedDocument.lineAt(diff.modifiedStartLineNumber - 1).range.end, modifiedDocument.lineAt(diff.modifiedStartLineNumber).range.start)
: new Range(modifiedDocument.lineAt(diff.modifiedStartLineNumber - 1).range.start, modifiedDocument.lineAt(diff.modifiedEndLineNumber - 1).range.end);
return selections.every(selection => !selection.intersection(modifiedRange));
});
if (selectedDiffs.length === diffs.length) {
return;
}
const basename = path.basename(modifiedUri.fsPath);
const message = localize('confirm revert', "Are you sure you want to revert the selected changes in {0}?", basename);
const yes = localize('revert', "Revert Changes");
@@ -570,10 +662,11 @@ export class CommandCenter {
return;
}
const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs);
const result = applyLineChanges(originalDocument, modifiedDocument, changes);
const edit = new WorkspaceEdit();
edit.replace(modifiedUri, new Range(new Position(0, 0), modifiedDocument.lineAt(modifiedDocument.lineCount - 1).range.end), result);
workspace.applyEdit(edit);
await modifiedDocument.save();
}
@command('git.unstage')
@@ -909,7 +1002,7 @@ export class CommandCenter {
const includeTags = checkoutType === 'all' || checkoutType === 'tags';
const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';
const createBranch = new CreateBranchItem();
const createBranch = new CreateBranchItem(this);
const heads = repository.refs.filter(ref => ref.type === RefType.Head)
.map(ref => new CheckoutItem(ref));
@@ -1174,6 +1267,19 @@ export class CommandCenter {
await repository.sync();
}
@command('git._syncAll')
async syncAll(): Promise<void> {
await Promise.all(this.model.repositories.map(async repository => {
const HEAD = repository.HEAD;
if (!HEAD || !HEAD.upstream) {
return;
}
await repository.sync();
}));
}
@command('git.publish', { repository: true })
async publish(repository: Repository): Promise<void> {
const remotes = repository.remotes;
@@ -1184,9 +1290,12 @@ export class CommandCenter {
}
const branchName = repository.HEAD && repository.HEAD.name || '';
const picks = repository.remotes.map(r => r.name);
const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
const choice = await window.showQuickPick(picks, { placeHolder });
const selectRemote = async () => {
const picks = repository.remotes.map(r => r.name);
const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
return await window.showQuickPick(picks, { placeHolder });
};
const choice = remotes.length === 1 ? remotes[0].name : await selectRemote();
if (!choice) {
return;
@@ -1200,27 +1309,27 @@ export class CommandCenter {
this.outputChannel.show();
}
@command('git.ignore', { repository: true })
async ignore(repository: Repository, ...resourceStates: SourceControlResourceState[]): Promise<void> {
@command('git.ignore')
async ignore(...resourceStates: SourceControlResourceState[]): Promise<void> {
if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
const uri = window.activeTextEditor && window.activeTextEditor.document.uri;
const resource = this.getSCMResource();
if (!uri) {
if (!resource) {
return;
}
return await repository.ignore([uri]);
resourceStates = [resource];
}
const uris = resourceStates
const resources = resourceStates
.filter(s => s instanceof Resource)
.map(r => r.resourceUri);
if (!uris.length) {
if (!resources.length) {
return;
}
await repository.ignore(uris);
await this.runByRepository(resources, async (repository, resources) => repository.ignore(resources));
}
@command('git.stash', { repository: true })
@@ -1302,6 +1411,11 @@ export class CommandCenter {
});
}
/* __GDPR__
"git.command" : {
"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });
return result.catch(async err => {
@@ -1389,7 +1503,7 @@ export class CommandCenter {
return result;
}
const tuple = result.filter(p => p[0] === repository)[0];
const tuple = result.filter(p => p.repository === repository)[0];
if (tuple) {
tuple.resources.push(resource);

View File

@@ -6,9 +6,10 @@
'use strict';
import { workspace, Uri, Disposable, Event, EventEmitter, window } from 'vscode';
import { debounce } from './decorators';
import { fromGitUri } from './uri';
import { Model, ModelChangeEvent } from './model';
import { debounce, throttle } from './decorators';
import { fromGitUri, toGitUri } from './uri';
import { Model, ModelChangeEvent, OriginalResourceChangeEvent } from './model';
import { filterEvent, eventToPromise } from './util';
interface CacheRow {
uri: Uri;
@@ -24,8 +25,8 @@ const FIVE_MINUTES = 1000 * 60 * 5;
export class GitContentProvider {
private onDidChangeEmitter = new EventEmitter<Uri>();
get onDidChange(): Event<Uri> { return this.onDidChangeEmitter.event; }
private _onDidChange = new EventEmitter<Uri>();
get onDidChange(): Event<Uri> { return this._onDidChange.event; }
private changedRepositoryRoots = new Set<string>();
private cache: Cache = Object.create(null);
@@ -34,6 +35,7 @@ export class GitContentProvider {
constructor(private model: Model) {
this.disposables.push(
model.onDidChangeRepository(this.onDidChangeRepository, this),
model.onDidChangeOriginalResource(this.onDidChangeOriginalResource, this),
workspace.registerTextDocumentContentProvider('git', this)
);
@@ -45,19 +47,33 @@ export class GitContentProvider {
this.eventuallyFireChangeEvents();
}
private onDidChangeOriginalResource({ uri }: OriginalResourceChangeEvent): void {
if (uri.scheme !== 'file') {
return;
}
this._onDidChange.fire(toGitUri(uri, '', true));
}
@debounce(1100)
private eventuallyFireChangeEvents(): void {
this.fireChangeEvents();
}
private fireChangeEvents(): void {
@throttle
private async fireChangeEvents(): Promise<void> {
if (!window.state.focused) {
const onDidFocusWindow = filterEvent(window.onDidChangeWindowState, e => e.focused);
await eventToPromise(onDidFocusWindow);
}
Object.keys(this.cache).forEach(key => {
const uri = this.cache[key].uri;
const fsPath = uri.fsPath;
for (const root of this.changedRepositoryRoots) {
if (fsPath.startsWith(root)) {
this.onDidChangeEmitter.fire(uri);
this._onDidChange.fire(uri);
return;
}
}
@@ -75,7 +91,7 @@ export class GitContentProvider {
const cacheKey = uri.toString();
const timestamp = new Date().getTime();
const cacheValue = { uri, timestamp };
const cacheValue: CacheRow = { uri, timestamp };
this.cache[cacheKey] = cacheValue;
@@ -101,7 +117,10 @@ export class GitContentProvider {
Object.keys(this.cache).forEach(key => {
const row = this.cache[key];
const isOpen = window.visibleTextEditors.some(e => e.document.uri.fsPath === row.uri.fsPath);
const { path } = fromGitUri(row.uri);
const isOpen = workspace.textDocuments
.filter(d => d.uri.scheme === 'file')
.some(d => d.uri.fsPath === path);
if (isOpen || now - row.timestamp < THREE_MINUTES) {
cache[row.uri.toString()] = row;

View File

@@ -0,0 +1,173 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { window, workspace, Uri, Disposable, Event, EventEmitter, DecorationData, DecorationProvider, ThemeColor } from 'vscode';
import { Repository, GitResourceGroup } from './repository';
import { Model } from './model';
import { debounce } from './decorators';
import { filterEvent } from './util';
class GitIgnoreDecorationProvider implements DecorationProvider {
private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
readonly onDidChangeDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;
private checkIgnoreQueue = new Map<string, { resolve: (status: boolean) => void, reject: (err: any) => void }>();
private disposables: Disposable[] = [];
constructor(private repository: Repository) {
this.disposables.push(
window.registerDecorationProvider(this),
filterEvent(workspace.onDidSaveTextDocument, e => e.fileName.endsWith('.gitignore'))(_ => this._onDidChangeDecorations.fire())
//todo@joh -> events when the ignore status actually changes, not only when the file changes
);
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
this.checkIgnoreQueue.clear();
}
provideDecoration(uri: Uri): Promise<DecorationData | undefined> {
return new Promise<boolean>((resolve, reject) => {
this.checkIgnoreQueue.set(uri.fsPath, { resolve, reject });
this.checkIgnoreSoon();
}).then(ignored => {
if (ignored) {
return <DecorationData>{
priority: 3,
color: new ThemeColor('gitDecoration.ignoredResourceForeground')
};
}
});
}
@debounce(500)
private checkIgnoreSoon(): void {
const queue = new Map(this.checkIgnoreQueue.entries());
this.checkIgnoreQueue.clear();
this.repository.checkIgnore([...queue.keys()]).then(ignoreSet => {
for (const [key, value] of queue.entries()) {
value.resolve(ignoreSet.has(key));
}
}, err => {
console.error(err);
for (const [, value] of queue.entries()) {
value.reject(err);
}
});
}
}
class GitDecorationProvider implements DecorationProvider {
private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
readonly onDidChangeDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;
private disposables: Disposable[] = [];
private decorations = new Map<string, DecorationData>();
constructor(private repository: Repository) {
this.disposables.push(
window.registerDecorationProvider(this),
repository.onDidRunOperation(this.onDidRunOperation, this)
);
}
private onDidRunOperation(): void {
let newDecorations = new Map<string, DecorationData>();
this.collectDecorationData(this.repository.indexGroup, newDecorations);
this.collectDecorationData(this.repository.workingTreeGroup, newDecorations);
this.collectDecorationData(this.repository.mergeGroup, newDecorations);
let uris: Uri[] = [];
newDecorations.forEach((value, uriString) => {
if (this.decorations.has(uriString)) {
this.decorations.delete(uriString);
} else {
uris.push(Uri.parse(uriString));
}
});
this.decorations.forEach((value, uriString) => {
uris.push(Uri.parse(uriString));
});
this.decorations = newDecorations;
this._onDidChangeDecorations.fire(uris);
}
private collectDecorationData(group: GitResourceGroup, bucket: Map<string, DecorationData>): void {
group.resourceStates.forEach(r => {
if (r.resourceDecoration) {
bucket.set(r.original.toString(), r.resourceDecoration);
}
});
}
provideDecoration(uri: Uri): DecorationData | undefined {
return this.decorations.get(uri.toString());
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
}
export class GitDecorations {
private configListener: Disposable;
private modelListener: Disposable[] = [];
private providers = new Map<Repository, Disposable>();
constructor(private model: Model) {
this.configListener = workspace.onDidChangeConfiguration(e => e.affectsConfiguration('git.decorations.enabled') && this.update());
this.update();
}
private update(): void {
const enabled = workspace.getConfiguration('git').get('decorations.enabled');
if (enabled) {
this.enable();
} else {
this.disable();
}
}
private enable(): void {
this.modelListener = [];
this.model.onDidOpenRepository(this.onDidOpenRepository, this, this.modelListener);
this.model.onDidCloseRepository(this.onDidCloseRepository, this, this.modelListener);
this.model.repositories.forEach(this.onDidOpenRepository, this);
}
private disable(): void {
this.modelListener.forEach(d => d.dispose());
this.providers.forEach(value => value.dispose());
this.providers.clear();
}
private onDidOpenRepository(repository: Repository): void {
const provider = new GitDecorationProvider(repository);
const ignoreProvider = new GitIgnoreDecorationProvider(repository);
this.providers.set(repository, Disposable.from(provider, ignoreProvider));
}
private onDidCloseRepository(repository: Repository): void {
const provider = this.providers.get(repository);
if (provider) {
provider.dispose();
this.providers.delete(repository);
}
}
dispose(): void {
this.configListener.dispose();
this.modelListener.forEach(d => d.dispose());
this.providers.forEach(value => value.dispose);
this.providers.clear();
}
}

View File

@@ -13,7 +13,6 @@ import { EventEmitter } from 'events';
import iconv = require('iconv-lite');
import { assign, uniqBy, groupBy, denodeify, IDisposable, toDisposable, dispose, mkdirp } from './util';
const readdir = denodeify<string[]>(fs.readdir);
const readfile = denodeify<string>(fs.readFile);
export interface IGit {
@@ -66,7 +65,7 @@ function findSpecificGit(path: string): Promise<IGit> {
const buffers: Buffer[] = [];
const child = cp.spawn(path, ['--version']);
child.stdout.on('data', (b: Buffer) => buffers.push(b));
child.on('error', e);
child.on('error', cpErrorHandler(e));
child.on('exit', code => code ? e(new Error('Not found')) : c({ path, version: parseVersion(Buffer.concat(buffers).toString('utf8').trim()) }));
});
}
@@ -82,12 +81,12 @@ function findGitDarwin(): Promise<IGit> {
function getVersion(path: string) {
// make sure git executes
cp.exec('git --version', (err, stdout: Buffer) => {
cp.exec('git --version', (err, stdout) => {
if (err) {
return e('git not found');
}
return c({ path, version: parseVersion(stdout.toString('utf8').trim()) });
return c({ path, version: parseVersion(stdout.trim()) });
});
}
@@ -118,48 +117,54 @@ function findSystemGitWin32(base: string): Promise<IGit> {
return findSpecificGit(path.join(base, 'Git', 'cmd', 'git.exe'));
}
function findGitHubGitWin32(): Promise<IGit> {
const github = path.join(process.env['LOCALAPPDATA'], 'GitHub');
return readdir(github).then(children => {
const git = children.filter(child => /^PortableGit/.test(child))[0];
if (!git) {
return Promise.reject<IGit>('Not found');
}
return findSpecificGit(path.join(github, git, 'cmd', 'git.exe'));
});
}
function findGitWin32(): Promise<IGit> {
return findSystemGitWin32(process.env['ProgramW6432'])
.then(void 0, () => findSystemGitWin32(process.env['ProgramFiles(x86)']))
.then(void 0, () => findSystemGitWin32(process.env['ProgramFiles']))
.then(void 0, () => findSpecificGit('git'))
.then(void 0, () => findGitHubGitWin32());
.then(void 0, () => findSpecificGit('git'));
}
export function findGit(hint: string | undefined): Promise<IGit> {
var first = hint ? findSpecificGit(hint) : Promise.reject<IGit>(null);
return first.then(void 0, () => {
switch (process.platform) {
case 'darwin': return findGitDarwin();
case 'win32': return findGitWin32();
default: return findSpecificGit('git');
}
});
return first
.then(void 0, () => {
switch (process.platform) {
case 'darwin': return findGitDarwin();
case 'win32': return findGitWin32();
default: return findSpecificGit('git');
}
})
.then(null, () => Promise.reject(new Error('Git installation not found.')));
}
export interface IExecutionResult {
exitCode: number;
stdout: string;
stderr: string;
}
async function exec(child: cp.ChildProcess, options: any = {}): Promise<IExecutionResult> {
function cpErrorHandler(cb: (reason?: any) => void): (reason?: any) => void {
return err => {
if (/ENOENT/.test(err.message)) {
err = new GitError({
error: err,
message: 'Failed to execute git (ENOENT)',
gitErrorCode: GitErrorCodes.NotAGitRepository
});
}
cb(err);
};
}
export interface SpawnOptions extends cp.SpawnOptions {
input?: string;
encoding?: string;
log?: boolean;
}
async function exec(child: cp.ChildProcess, options: SpawnOptions = {}): Promise<IExecutionResult> {
if (!child.stdout || !child.stderr) {
throw new GitError({
message: 'Failed to get stdout or stderr from git process.'
@@ -183,7 +188,7 @@ async function exec(child: cp.ChildProcess, options: any = {}): Promise<IExecuti
const [exitCode, stdout, stderr] = await Promise.all<any>([
new Promise<number>((c, e) => {
once(child, 'error', e);
once(child, 'error', cpErrorHandler(e));
once(child, 'exit', c);
}),
new Promise<string>(c => {
@@ -246,7 +251,7 @@ export class GitError {
gitCommand: this.gitCommand,
stdout: this.stdout,
stderr: this.stderr
}, [], 2);
}, null, 2);
if (this.error) {
result += (<any>this.error).stack;
@@ -350,17 +355,17 @@ export class Git {
return path.normalize(result.stdout.trim());
}
async exec(cwd: string, args: string[], options: any = {}): Promise<IExecutionResult> {
async exec(cwd: string, args: string[], options: SpawnOptions = {}): Promise<IExecutionResult> {
options = assign({ cwd }, options || {});
return await this._exec(args, options);
}
stream(cwd: string, args: string[], options: any = {}): cp.ChildProcess {
stream(cwd: string, args: string[], options: SpawnOptions = {}): cp.ChildProcess {
options = assign({ cwd }, options || {});
return this.spawn(args, options);
}
private async _exec(args: string[], options: any = {}): Promise<IExecutionResult> {
private async _exec(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult> {
const child = this.spawn(args, options);
if (options.input) {
@@ -387,7 +392,7 @@ export class Git {
return result;
}
spawn(args: string[], options: any = {}): cp.ChildProcess {
spawn(args: string[], options: SpawnOptions = {}): cp.ChildProcess {
if (!this.gitPath) {
throw new Error('git could not be found in the system.');
}
@@ -505,19 +510,19 @@ export class Repository {
}
// TODO@Joao: rename to exec
async run(args: string[], options: any = {}): Promise<IExecutionResult> {
async run(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult> {
return await this.git.exec(this.repositoryRoot, args, options);
}
stream(args: string[], options: any = {}): cp.ChildProcess {
stream(args: string[], options: SpawnOptions = {}): cp.ChildProcess {
return this.git.stream(this.repositoryRoot, args, options);
}
spawn(args: string[], options: any = {}): cp.ChildProcess {
spawn(args: string[], options: SpawnOptions = {}): cp.ChildProcess {
return this.git.spawn(args, options);
}
async config(scope: string, key: string, value: any, options: any): Promise<string> {
async config(scope: string, key: string, value: any, options: SpawnOptions): Promise<string> {
const args = ['config'];
if (scope) {
@@ -883,7 +888,8 @@ export class Repository {
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']);
const env = { GIT_OPTIONAL_LOCKS: '0' };
const child = this.stream(['status', '-z', '-u'], { env });
const onExit = exitCode => {
if (exitCode !== 0) {
@@ -919,7 +925,7 @@ export class Repository {
child.stderr.setEncoding('utf8');
child.stderr.on('data', raw => stderrData.push(raw as string));
child.on('error', e);
child.on('error', cpErrorHandler(e));
child.on('exit', onExit);
});
}

View File

@@ -12,6 +12,7 @@ import { findGit, Git, IGit } from './git';
import { Model } from './model';
import { CommandCenter } from './commands';
import { GitContentProvider } from './contentProvider';
import { GitDecorations } from './decorationProvider';
import { Askpass } from './askpass';
import { toDisposable } from './util';
import TelemetryReporter from 'vscode-extension-telemetry';
@@ -54,6 +55,7 @@ async function init(context: ExtensionContext, disposables: Disposable[]): Promi
disposables.push(
new CommandCenter(git, model, outputChannel, telemetryReporter),
new GitContentProvider(model),
new GitDecorations(model)
);
await checkGitVersion(info);
@@ -70,7 +72,7 @@ export function activate(context: ExtensionContext): any {
async function checkGitVersion(info: IGit): Promise<void> {
// {{SQL CARBON EDIT}}
// remove Git version check on since for Carbon
// remove Git version check for sqlops
return;
@@ -101,4 +103,4 @@ async function checkGitVersion(info: IGit): Promise<void> {
await config.update('ignoreLegacyWarning', true, true);
}
*/
}
}

View File

@@ -17,8 +17,16 @@ import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
class RepositoryPick implements QuickPickItem {
@memoize get label(): string { return path.basename(this.repository.root); }
@memoize get description(): string { return path.dirname(this.repository.root); }
@memoize get label(): string {
return path.basename(this.repository.root);
}
@memoize get description(): string {
return [this.repository.headLabel, this.repository.syncLabel]
.filter(l => !!l)
.join(' ');
}
constructor(public readonly repository: Repository) { }
}
@@ -27,10 +35,19 @@ export interface ModelChangeEvent {
uri: Uri;
}
export interface OriginalResourceChangeEvent {
repository: Repository;
uri: Uri;
}
interface OpenRepository extends Disposable {
repository: Repository;
}
function isParent(parent: string, child: string): boolean {
return child.startsWith(parent);
}
export class Model {
private _onDidOpenRepository = new EventEmitter<Repository>();
@@ -42,6 +59,9 @@ export class Model {
private _onDidChangeRepository = new EventEmitter<ModelChangeEvent>();
readonly onDidChangeRepository: Event<ModelChangeEvent> = this._onDidChangeRepository.event;
private _onDidChangeOriginalResource = new EventEmitter<OriginalResourceChangeEvent>();
readonly onDidChangeOriginalResource: Event<OriginalResourceChangeEvent> = this._onDidChangeOriginalResource.event;
private openRepositories: OpenRepository[] = [];
get repositories(): Repository[] { return this.openRepositories.map(r => r.repository); }
@@ -147,7 +167,9 @@ export class Model {
const activeRepositories = new Set<Repository>(activeRepositoriesList);
const openRepositoriesToDispose = removed
.map(folder => this.getOpenRepository(folder.uri))
.filter(r => !!r && !activeRepositories.has(r.repository)) as OpenRepository[];
.filter(r => !!r)
.filter(r => !activeRepositories.has(r!.repository))
.filter(r => !(workspace.workspaceFolders || []).some(f => isParent(f.uri.fsPath, r!.repository.root))) as OpenRepository[];
possibleRepositoryFolders.forEach(p => this.tryOpenRepository(p.uri.fsPath));
openRepositoriesToDispose.forEach(r => r.dispose());
@@ -203,10 +225,14 @@ export class Model {
const onDidDisappearRepository = filterEvent(repository.onDidChangeState, state => state === RepositoryState.Disposed);
const disappearListener = onDidDisappearRepository(() => dispose());
const changeListener = repository.onDidChangeRepository(uri => this._onDidChangeRepository.fire({ repository, uri }));
const originalResourceChangeListener = repository.onDidChangeOriginalResource(uri => this._onDidChangeOriginalResource.fire({ repository, uri }));
const dispose = () => {
disappearListener.dispose();
changeListener.dispose();
originalResourceChangeListener.dispose();
repository.dispose();
this.openRepositories = this.openRepositories.filter(e => e !== openRepository);
this._onDidCloseRepository.fire(repository);
};
@@ -216,6 +242,16 @@ export class Model {
this._onDidOpenRepository.fire(repository);
}
close(repository: Repository): void {
const openRepository = this.getOpenRepository(repository);
if (!openRepository) {
return;
}
openRepository.dispose();
}
async pickRepository(): Promise<Repository | undefined> {
if (this.openRepositories.length === 0) {
throw new Error(localize('no repositories', "There are no available repositories"));

View File

@@ -5,8 +5,8 @@
'use strict';
import { Uri, Command, EventEmitter, Event, scm, SourceControl, SourceControlInputBox, SourceControlResourceGroup, SourceControlResourceState, SourceControlResourceDecorations, Disposable, ProgressLocation, window, workspace, WorkspaceEdit } from 'vscode';
import { Repository as BaseRepository, Ref, Branch, Remote, Commit, GitErrorCodes, Stash } from './git';
import { Uri, Command, EventEmitter, Event, scm, SourceControl, SourceControlInputBox, SourceControlResourceGroup, SourceControlResourceState, SourceControlResourceDecorations, Disposable, ProgressLocation, window, workspace, WorkspaceEdit, ThemeColor, DecorationData } from 'vscode';
import { Repository as BaseRepository, Ref, Branch, Remote, Commit, GitErrorCodes, Stash, RefType, GitError } from './git';
import { anyEvent, filterEvent, eventToPromise, dispose, find } from './util';
import { memoize, throttle, debounce } from './decorators';
import { toGitUri } from './uri';
@@ -171,13 +171,104 @@ export class Resource implements SourceControlResourceState {
}
get decorations(): SourceControlResourceDecorations {
const light = { iconPath: this.getIconPath('light') };
const dark = { iconPath: this.getIconPath('dark') };
// TODO@joh, still requires restart/redraw in the SCM viewlet
const decorations = workspace.getConfiguration().get<boolean>('git.decorations.enabled');
const light = !decorations ? { iconPath: this.getIconPath('light') } : undefined;
const dark = !decorations ? { iconPath: this.getIconPath('dark') } : undefined;
const tooltip = this.tooltip;
const strikeThrough = this.strikeThrough;
const faded = this.faded;
const letter = this.letter;
const color = this.color;
return { strikeThrough, faded, tooltip, light, dark };
return { strikeThrough, faded, tooltip, light, dark, letter, color, source: 'git.resource' /*todo@joh*/ };
}
get letter(): string | undefined {
switch (this.type) {
case Status.INDEX_MODIFIED:
case Status.MODIFIED:
return 'M';
case Status.INDEX_ADDED:
return 'A';
case Status.INDEX_DELETED:
case Status.DELETED:
return 'D';
case Status.INDEX_RENAMED:
return 'R';
case Status.UNTRACKED:
return 'U';
case Status.IGNORED:
return 'I';
case Status.INDEX_COPIED:
case Status.BOTH_DELETED:
case Status.ADDED_BY_US:
case Status.DELETED_BY_THEM:
case Status.ADDED_BY_THEM:
case Status.DELETED_BY_US:
case Status.BOTH_ADDED:
case Status.BOTH_MODIFIED:
return 'C';
default:
return undefined;
}
}
get color(): ThemeColor | undefined {
switch (this.type) {
case Status.INDEX_MODIFIED:
case Status.MODIFIED:
return new ThemeColor('gitDecoration.modifiedResourceForeground');
case Status.INDEX_DELETED:
case Status.DELETED:
return new ThemeColor('gitDecoration.deletedResourceForeground');
case Status.INDEX_ADDED: // todo@joh - special color?
case Status.INDEX_RENAMED: // todo@joh - special color?
case Status.UNTRACKED:
return new ThemeColor('gitDecoration.untrackedResourceForeground');
case Status.IGNORED:
return new ThemeColor('gitDecoration.ignoredResourceForeground');
case Status.INDEX_COPIED:
case Status.BOTH_DELETED:
case Status.ADDED_BY_US:
case Status.DELETED_BY_THEM:
case Status.ADDED_BY_THEM:
case Status.DELETED_BY_US:
case Status.BOTH_ADDED:
case Status.BOTH_MODIFIED:
return new ThemeColor('gitDecoration.conflictingResourceForeground');
default:
return undefined;
}
}
get priority(): number {
switch (this.type) {
case Status.INDEX_MODIFIED:
case Status.MODIFIED:
return 2;
case Status.IGNORED:
return 3;
case Status.INDEX_COPIED:
case Status.BOTH_DELETED:
case Status.ADDED_BY_US:
case Status.DELETED_BY_THEM:
case Status.ADDED_BY_THEM:
case Status.DELETED_BY_US:
case Status.BOTH_ADDED:
case Status.BOTH_MODIFIED:
return 4;
default:
return 1;
}
}
get resourceDecoration(): DecorationData | undefined {
const title = this.tooltip;
const abbreviation = this.letter;
const color = this.color;
const priority = this.priority;
return { bubble: true, source: 'git.resource', title, abbreviation, color, priority };
}
constructor(
@@ -189,54 +280,34 @@ export class Resource implements SourceControlResourceState {
}
export enum Operation {
Status = 1 << 0,
Add = 1 << 1,
RevertFiles = 1 << 2,
Commit = 1 << 3,
Clean = 1 << 4,
Branch = 1 << 5,
Checkout = 1 << 6,
Reset = 1 << 7,
Fetch = 1 << 8,
Pull = 1 << 9,
Push = 1 << 10,
Sync = 1 << 11,
Show = 1 << 12,
Stage = 1 << 13,
GetCommitTemplate = 1 << 14,
DeleteBranch = 1 << 15,
Merge = 1 << 16,
Ignore = 1 << 17,
Tag = 1 << 18,
Stash = 1 << 19
Status = 'Status',
Add = 'Add',
RevertFiles = 'RevertFiles',
Commit = 'Commit',
Clean = 'Clean',
Branch = 'Branch',
Checkout = 'Checkout',
Reset = 'Reset',
Fetch = 'Fetch',
Pull = 'Pull',
Push = 'Push',
Sync = 'Sync',
Show = 'Show',
Stage = 'Stage',
GetCommitTemplate = 'GetCommitTemplate',
DeleteBranch = 'DeleteBranch',
Merge = 'Merge',
Ignore = 'Ignore',
Tag = 'Tag',
Stash = 'Stash',
CheckIgnore = 'CheckIgnore'
}
// function getOperationName(operation: Operation): string {
// switch (operation) {
// case Operation.Status: return 'Status';
// case Operation.Add: return 'Add';
// case Operation.RevertFiles: return 'RevertFiles';
// case Operation.Commit: return 'Commit';
// case Operation.Clean: return 'Clean';
// case Operation.Branch: return 'Branch';
// case Operation.Checkout: return 'Checkout';
// case Operation.Reset: return 'Reset';
// case Operation.Fetch: return 'Fetch';
// case Operation.Pull: return 'Pull';
// case Operation.Push: return 'Push';
// case Operation.Sync: return 'Sync';
// case Operation.Init: return 'Init';
// case Operation.Show: return 'Show';
// case Operation.Stage: return 'Stage';
// case Operation.GetCommitTemplate: return 'GetCommitTemplate';
// default: return 'unknown';
// }
// }
function isReadOnly(operation: Operation): boolean {
switch (operation) {
case Operation.Show:
case Operation.GetCommitTemplate:
case Operation.CheckIgnore:
return true;
default:
return false;
@@ -246,6 +317,7 @@ function isReadOnly(operation: Operation): boolean {
function shouldShowProgress(operation: Operation): boolean {
switch (operation) {
case Operation.Fetch:
case Operation.CheckIgnore:
return false;
default:
return true;
@@ -259,24 +331,36 @@ export interface Operations {
class OperationsImpl implements Operations {
constructor(private readonly operations: number = 0) {
// noop
private operations = new Map<Operation, number>();
start(operation: Operation): void {
this.operations.set(operation, (this.operations.get(operation) || 0) + 1);
}
start(operation: Operation): OperationsImpl {
return new OperationsImpl(this.operations | operation);
}
end(operation: Operation): void {
const count = (this.operations.get(operation) || 0) - 1;
end(operation: Operation): OperationsImpl {
return new OperationsImpl(this.operations & ~operation);
if (count <= 0) {
this.operations.delete(operation);
} else {
this.operations.set(operation, count);
}
}
isRunning(operation: Operation): boolean {
return (this.operations & operation) !== 0;
return this.operations.has(operation);
}
isIdle(): boolean {
return this.operations === 0;
const operations = this.operations.keys();
for (const operation of operations) {
if (!isReadOnly(operation)) {
return false;
}
}
return true;
}
}
@@ -302,6 +386,9 @@ export class Repository implements Disposable {
private _onDidChangeStatus = new EventEmitter<void>();
readonly onDidChangeStatus: Event<void> = this._onDidChangeStatus.event;
private _onDidChangeOriginalResource = new EventEmitter<Uri>();
readonly onDidChangeOriginalResource: Event<Uri> = this._onDidChangeOriginalResource.event;
private _onRunOperation = new EventEmitter<Operation>();
readonly onRunOperation: Event<Operation> = this._onRunOperation.event;
@@ -382,9 +469,7 @@ export class Repository implements Disposable {
const onRelevantGitChange = filterEvent(onRelevantRepositoryChange, uri => /\/\.git\//.test(uri.path));
onRelevantGitChange(this._onDidChangeRepository.fire, this._onDidChangeRepository, this.disposables);
const label = `${path.basename(repository.root)} (Git)`;
this._sourceControl = scm.createSourceControl('git', label);
this._sourceControl = scm.createSourceControl('git', 'Git', Uri.file(repository.root));
this._sourceControl.acceptInputCommand = { command: 'git.commitWithInput', title: localize('commit', "Commit"), arguments: [this._sourceControl] };
this._sourceControl.quickDiffProvider = this;
this.disposables.push(this._sourceControl);
@@ -449,6 +534,7 @@ export class Repository implements Disposable {
async stage(resource: Uri, contents: string): Promise<void> {
const relativePath = path.relative(this.repository.root, resource.fsPath).replace(/\\/g, '/');
await this.run(Operation.Stage, () => this.repository.stage(relativePath, contents));
this._onDidChangeOriginalResource.fire(resource);
}
async revert(resources: Uri[]): Promise<void> {
@@ -534,11 +620,7 @@ export class Repository implements Disposable {
@throttle
async fetch(): Promise<void> {
try {
await this.run(Operation.Fetch, () => this.repository.fetch());
} catch (err) {
// noop
}
await this.run(Operation.Fetch, () => this.repository.fetch());
}
@throttle
@@ -584,7 +666,7 @@ export class Repository implements Disposable {
async show(ref: string, filePath: string): Promise<string> {
return await this.run(Operation.Show, async () => {
const relativePath = path.relative(this.repository.root, filePath).replace(/\\/g, '/');
const configFiles = workspace.getConfiguration('files');
const configFiles = workspace.getConfiguration('files', Uri.file(filePath));
const encoding = configFiles.get<string>('encoding');
return await this.repository.buffer(`${ref}:${relativePath}`, encoding);
@@ -629,13 +711,58 @@ export class Repository implements Disposable {
});
}
checkIgnore(filePaths: string[]): Promise<Set<string>> {
return this.run(Operation.CheckIgnore, () => {
return new Promise<Set<string>>((resolve, reject) => {
filePaths = filePaths.filter(filePath => !path.relative(this.root, filePath).startsWith('..'));
if (filePaths.length === 0) {
// nothing left
return resolve(new Set<string>());
}
// https://git-scm.com/docs/git-check-ignore#git-check-ignore--z
const child = this.repository.stream(['check-ignore', '-z', '--stdin'], { stdio: [null, null, null] });
child.stdin.end(filePaths.join('\0'), 'utf8');
const onExit = exitCode => {
if (exitCode === 1) {
// nothing ignored
resolve(new Set<string>());
} else if (exitCode === 0) {
// paths are separated by the null-character
resolve(new Set<string>(data.split('\0')));
} else {
reject(new GitError({ stdout: data, stderr, exitCode }));
}
};
let data = '';
const onStdoutData = (raw: string) => {
data += raw;
};
child.stdout.setEncoding('utf8');
child.stdout.on('data', onStdoutData);
let stderr: string = '';
child.stderr.setEncoding('utf8');
child.stderr.on('data', raw => stderr += raw);
child.on('error', reject);
child.on('exit', onExit);
});
});
}
private async run<T>(operation: Operation, runOperation: () => Promise<T> = () => Promise.resolve<any>(null)): Promise<T> {
if (this.state !== RepositoryState.Idle) {
throw new Error('Repository not initialized');
}
const run = async () => {
this._operations = this._operations.start(operation);
this._operations.start(operation);
this._onRunOperation.fire(operation);
try {
@@ -653,7 +780,7 @@ export class Repository implements Disposable {
throw err;
} finally {
this._operations = this._operations.end(operation);
this._operations.end(operation);
this._onDidRunOperation.fire(operation);
}
};
@@ -818,7 +945,7 @@ export class Repository implements Disposable {
await timeout(5000);
}
private async whenIdleAndFocused(): Promise<void> {
async whenIdleAndFocused(): Promise<void> {
while (true) {
if (!this.operations.isIdle()) {
await eventToPromise(this.onDidRunOperation);
@@ -835,6 +962,36 @@ export class Repository implements Disposable {
}
}
get headLabel(): string {
const HEAD = this.HEAD;
if (!HEAD) {
return '';
}
const tag = this.refs.filter(iref => iref.type === RefType.Tag && iref.commit === HEAD.commit)[0];
const tagName = tag && tag.name;
const head = HEAD.name || tagName || (HEAD.commit || '').substr(0, 8);
return head
+ (this.workingTreeGroup.resourceStates.length > 0 ? '*' : '')
+ (this.indexGroup.resourceStates.length > 0 ? '+' : '')
+ (this.mergeGroup.resourceStates.length > 0 ? '!' : '');
}
get syncLabel(): string {
if (!this.HEAD
|| !this.HEAD.name
|| !this.HEAD.commit
|| !this.HEAD.upstream
|| !(this.HEAD.ahead || this.HEAD.behind)
) {
return '';
}
return `${this.HEAD.behind}${this.HEAD.ahead}`;
}
dispose(): void {
this.disposables = dispose(this.disposables);
}

View File

@@ -6,7 +6,7 @@
'use strict';
import { Disposable, Command, EventEmitter, Event } from 'vscode';
import { RefType, Branch } from './git';
import { Branch } from './git';
import { Repository, Operation } from './repository';
import { anyEvent, dispose } from './util';
import * as nls from 'vscode-nls';
@@ -24,20 +24,7 @@ class CheckoutStatusBar {
}
get command(): Command | undefined {
const HEAD = this.repository.HEAD;
if (!HEAD) {
return undefined;
}
const tag = this.repository.refs.filter(iref => iref.type === RefType.Tag && iref.commit === HEAD.commit)[0];
const tagName = tag && tag.name;
const head = HEAD.name || tagName || (HEAD.commit || '').substr(0, 8);
const title = '$(git-branch) '
+ head
+ (this.repository.workingTreeGroup.resourceStates.length > 0 ? '*' : '')
+ (this.repository.indexGroup.resourceStates.length > 0 ? '+' : '')
+ (this.repository.mergeGroup.resourceStates.length > 0 ? '!' : '');
const title = `$(git-branch) ${this.repository.headLabel}`;
return {
command: 'git.checkout',
@@ -112,7 +99,7 @@ class SyncStatusBar {
if (HEAD && HEAD.name && HEAD.commit) {
if (HEAD.upstream) {
if (HEAD.ahead || HEAD.behind) {
text += `${HEAD.behind}${HEAD.ahead}`;
text += this.repository.syncLabel;
}
command = 'git.sync';
tooltip = localize('sync changes', "Synchronize Changes");

View File

@@ -8,6 +8,7 @@
import { Event } from 'vscode';
import { dirname } from 'path';
import * as fs from 'fs';
import * as byline from 'byline';
export function log(...args: any[]): void {
console.log.apply(console, ['git:', ...args]);
@@ -188,4 +189,20 @@ export function find<T>(array: T[], fn: (t: T) => boolean): T | undefined {
});
return result;
}
export async function grep(filename: string, pattern: RegExp): Promise<boolean> {
return new Promise<boolean>((c, e) => {
const fileStream = fs.createReadStream(filename, { encoding: 'utf8' });
const stream = byline(fileStream);
stream.on('data', (line: string) => {
if (pattern.test(line)) {
fileStream.close();
c(true);
}
});
stream.on('error', e);
stream.on('end', () => c(false));
});
}