mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
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:
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
173
extensions/git/src/decorationProvider.ts
Normal file
173
extensions/git/src/decorationProvider.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user