diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index fb2d8dacb26..bcf4188bd11 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -126,7 +126,7 @@ export class CommandCenter { @CommandCenter.Command('git.refresh') @CommandCenter.CatchErrors async refresh(): Promise { - await this.model.update(); + await this.model.status(); } @CommandCenter.Command('git.openChange') diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 56e22324722..9ae2c38e69a 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -5,44 +5,19 @@ 'use strict'; -import { ExtensionContext, workspace, Uri, window, Disposable, Event } from 'vscode'; +import { ExtensionContext, workspace, window, Disposable } from 'vscode'; import { findGit, Git } from './git'; import { Model } from './model'; import { GitSCMProvider } from './scmProvider'; import { CommandCenter } from './commands'; import { CheckoutStatusBar, SyncStatusBar } from './statusbar'; -import { filterEvent, anyEvent, throttle } from './util'; +import { filterEvent, anyEvent } from './util'; import { GitContentProvider } from './contentProvider'; import { AutoFetcher } from './autofetch'; import * as nls from 'vscode-nls'; -import { decorate, debounce } from 'core-decorators'; nls.config(); -class Watcher { - - private listener: Disposable; - - constructor(private model: Model, onWorkspaceChange: Event) { - this.listener = onWorkspaceChange(this.eventuallyUpdateModel, this); - } - - @debounce(1000) - private eventuallyUpdateModel(): void { - this.updateModelAndWait(); - } - - @decorate(throttle) - private async updateModelAndWait(): Promise { - await this.model.update(); - await new Promise(c => setTimeout(c, 8000)); - } - - dispose(): void { - this.listener.dispose(); - } -} - async function init(disposables: Disposable[]): Promise { const rootPath = workspace.rootPath; diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index e17deb419ef..754317ae6f0 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -7,7 +7,8 @@ import { Uri, EventEmitter, Event, SCMResource, SCMResourceDecorations, SCMResourceGroup, Disposable } from 'vscode'; import { Repository, IRef, IBranch, IRemote, IPushOptions } from './git'; -import { throttle, anyEvent, eventToPromise } from './util'; +import { throttle, anyEvent, eventToPromise, filterEvent, mapEvent } from './util'; +import { watch } from './watch'; import { decorate, memoize, debounce } from 'core-decorators'; import * as path from 'path'; @@ -242,7 +243,22 @@ export class Model { private repository: Repository, onWorkspaceChange: Event ) { - onWorkspaceChange(this.onWorkspaceChange, this, this.disposables); + /* We use the native Node `watch` for faster, non debounced events. + * That way we hopefully get the events during the operations we're + * performing, thus sparing useless `git status` calls to refresh + * the model's state. + */ + const gitPath = path.join(_repositoryRoot, '.git'); + const { event, disposable } = watch(gitPath); + const onGitChange = mapEvent(event, ({ filename }) => Uri.file(path.join(gitPath, filename))); + const onRelevantGitChange = filterEvent(onGitChange, uri => !/\/\.git\/index\.lock$/.test(uri.fsPath)); + onRelevantGitChange(this.onFSChange, this, this.disposables); + this.disposables.push(disposable); + + const onNonGitChange = filterEvent(onWorkspaceChange, uri => !/\/\.git\//.test(uri.fsPath)); + onNonGitChange(this.onFSChange, this, this.disposables); + + this.status(); } get repositoryRoot(): string { @@ -265,90 +281,18 @@ export class Model { } @decorate(throttle) - async update(): Promise { - await this.run(Operation.Status, async () => { - const status = await this.repository.getStatus(); - let HEAD: IBranch | undefined; - - try { - HEAD = await this.repository.getHEAD(); - - if (HEAD.name) { - try { - HEAD = await this.repository.getBranch(HEAD.name); - } catch (err) { - // noop - } - } - } catch (err) { - // noop - } - - const [refs, remotes] = await Promise.all([this.repository.getRefs(), this.repository.getRemotes()]); - - this._HEAD = HEAD; - this._refs = refs; - this._remotes = remotes; - - const index: Resource[] = []; - const workingTree: Resource[] = []; - const merge: Resource[] = []; - - status.forEach(raw => { - const uri = Uri.file(path.join(this.repositoryRoot, raw.path)); - - switch (raw.x + raw.y) { - case '??': return workingTree.push(new Resource(uri, Status.UNTRACKED)); - case '!!': return workingTree.push(new Resource(uri, Status.IGNORED)); - case 'DD': return merge.push(new Resource(uri, Status.BOTH_DELETED)); - case 'AU': return merge.push(new Resource(uri, Status.ADDED_BY_US)); - case 'UD': return merge.push(new Resource(uri, Status.DELETED_BY_THEM)); - case 'UA': return merge.push(new Resource(uri, Status.ADDED_BY_THEM)); - case 'DU': return merge.push(new Resource(uri, Status.DELETED_BY_US)); - case 'AA': return merge.push(new Resource(uri, Status.BOTH_ADDED)); - case 'UU': return merge.push(new Resource(uri, Status.BOTH_MODIFIED)); - } - - let isModifiedInIndex = false; - - switch (raw.x) { - case 'M': index.push(new Resource(uri, Status.INDEX_MODIFIED)); isModifiedInIndex = true; break; - case 'A': index.push(new Resource(uri, Status.INDEX_ADDED)); break; - case 'D': index.push(new Resource(uri, Status.INDEX_DELETED)); break; - case 'R': index.push(new Resource(uri, Status.INDEX_RENAMED/*, raw.rename*/)); break; - case 'C': index.push(new Resource(uri, Status.INDEX_COPIED)); break; - } - - switch (raw.y) { - case 'M': workingTree.push(new Resource(uri, Status.MODIFIED/*, raw.rename*/)); break; - case 'D': workingTree.push(new Resource(uri, Status.DELETED/*, raw.rename*/)); break; - } - }); - - this._mergeGroup = new MergeGroup(merge); - this._indexGroup = new IndexGroup(index); - this._workingTreeGroup = new WorkingTreeGroup(workingTree); - - this._onDidChange.fire(this.resources); - }); + async status(): Promise { + await this.run(Operation.Status); } @decorate(throttle) async stage(...resources: Resource[]): Promise { - await this.run(Operation.Stage, async () => { - const paths = resources.map(r => r.uri.fsPath); - await this.repository.add(paths); - await this.update(); - }); + await this.run(Operation.Stage, () => this.repository.add(resources.map(r => r.uri.fsPath))); } @decorate(throttle) async unstage(...resources: Resource[]): Promise { - await this.run(Operation.Unstage, async () => { - const paths = resources.map(r => r.uri.fsPath); - await this.repository.revertFiles('HEAD', paths); - await this.update(); - }); + await this.run(Operation.Unstage, () => this.repository.revertFiles('HEAD', resources.map(r => r.uri.fsPath))); } @decorate(throttle) @@ -359,7 +303,6 @@ export class Model { } await this.repository.commit(message, opts); - await this.update(); }); } @@ -393,72 +336,132 @@ export class Model { } await Promise.all(promises); - await this.update(); }); } @decorate(throttle) async branch(name: string): Promise { - await this.run(Operation.Branch, async () => { - await this.repository.branch(name, true); - await this.update(); - }); + await this.run(Operation.Branch, () => this.repository.branch(name, true)); } @decorate(throttle) async checkout(treeish: string): Promise { - await this.run(Operation.Checkout, async () => { - await this.repository.checkout(treeish, []); - await this.update(); - }); + await this.run(Operation.Checkout, () => this.repository.checkout(treeish, [])); } @decorate(throttle) async fetch(): Promise { - await this.run(Operation.Fetch, async () => { - await this.repository.fetch(); - await this.update(); - }); + await this.run(Operation.Fetch, () => this.repository.fetch()); } @decorate(throttle) async sync(): Promise { - await this.run(Operation.Sync, async () => { - await this.repository.sync(); - await this.update(); - }); + await this.run(Operation.Sync, () => this.repository.sync()); } @decorate(throttle) async push(remote?: string, name?: string, options?: IPushOptions): Promise { - await this.run(Operation.Push, async () => { - await this.repository.push(remote, name, options); - await this.update(); - }); + await this.run(Operation.Push, () => this.repository.push(remote, name, options)); } - private async run(operation: Operation, fn: () => Promise): Promise { + private async run(operation: Operation, fn: () => Promise = () => Promise.resolve()): Promise { this._operations = this._operations.start(operation); this._onRunOperation.fire(operation); try { await fn(); + await this.update(); } finally { this._operations = this._operations.end(operation); this._onDidRunOperation.fire(operation); } } + @decorate(throttle) + private async update(): Promise { + const status = await this.repository.getStatus(); + let HEAD: IBranch | undefined; + + try { + HEAD = await this.repository.getHEAD(); + + if (HEAD.name) { + try { + HEAD = await this.repository.getBranch(HEAD.name); + } catch (err) { + // noop + } + } + } catch (err) { + // noop + } + + const [refs, remotes] = await Promise.all([this.repository.getRefs(), this.repository.getRemotes()]); + + this._HEAD = HEAD; + this._refs = refs; + this._remotes = remotes; + + const index: Resource[] = []; + const workingTree: Resource[] = []; + const merge: Resource[] = []; + + status.forEach(raw => { + const uri = Uri.file(path.join(this.repositoryRoot, raw.path)); + + switch (raw.x + raw.y) { + case '??': return workingTree.push(new Resource(uri, Status.UNTRACKED)); + case '!!': return workingTree.push(new Resource(uri, Status.IGNORED)); + case 'DD': return merge.push(new Resource(uri, Status.BOTH_DELETED)); + case 'AU': return merge.push(new Resource(uri, Status.ADDED_BY_US)); + case 'UD': return merge.push(new Resource(uri, Status.DELETED_BY_THEM)); + case 'UA': return merge.push(new Resource(uri, Status.ADDED_BY_THEM)); + case 'DU': return merge.push(new Resource(uri, Status.DELETED_BY_US)); + case 'AA': return merge.push(new Resource(uri, Status.BOTH_ADDED)); + case 'UU': return merge.push(new Resource(uri, Status.BOTH_MODIFIED)); + } + + let isModifiedInIndex = false; + + switch (raw.x) { + case 'M': index.push(new Resource(uri, Status.INDEX_MODIFIED)); isModifiedInIndex = true; break; + case 'A': index.push(new Resource(uri, Status.INDEX_ADDED)); break; + case 'D': index.push(new Resource(uri, Status.INDEX_DELETED)); break; + case 'R': index.push(new Resource(uri, Status.INDEX_RENAMED/*, raw.rename*/)); break; + case 'C': index.push(new Resource(uri, Status.INDEX_COPIED)); break; + } + + switch (raw.y) { + case 'M': workingTree.push(new Resource(uri, Status.MODIFIED/*, raw.rename*/)); break; + case 'D': workingTree.push(new Resource(uri, Status.DELETED/*, raw.rename*/)); break; + } + }); + + this._mergeGroup = new MergeGroup(merge); + this._indexGroup = new IndexGroup(index); + this._workingTreeGroup = new WorkingTreeGroup(workingTree); + + this._onDidChange.fire(this.resources); + } + + private onFSChange(uri: Uri): void { + if (!this.operations.isIdle()) { + return; + } + + this.eventuallyUpdateWhenIdleAndWait(); + } + @debounce(1000) - private onWorkspaceChange(): void { + private eventuallyUpdateWhenIdleAndWait(): void { this.updateWhenIdleAndWait(); } @decorate(throttle) private async updateWhenIdleAndWait(): Promise { await this.whenIdle(); - await this.update(); - await new Promise(c => setTimeout(c, 7000)); + await this.status(); + await new Promise(c => setTimeout(c, 5000)); } private async whenIdle(): Promise { diff --git a/extensions/git/src/scmProvider.ts b/extensions/git/src/scmProvider.ts index 776f8ca97bd..c9a4234e10d 100644 --- a/extensions/git/src/scmProvider.ts +++ b/extensions/git/src/scmProvider.ts @@ -18,7 +18,6 @@ export class GitSCMProvider implements SCMProvider { get label(): string { return 'Git'; } constructor(private model: Model, private commandCenter: CommandCenter) { - model.update(); scm.registerSCMProvider('git', this); } diff --git a/extensions/git/src/watch.ts b/extensions/git/src/watch.ts new file mode 100644 index 00000000000..a3a51a8aea7 --- /dev/null +++ b/extensions/git/src/watch.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { EventEmitter, Event, Disposable } from 'vscode'; +import * as fs from 'fs'; + +export interface FSEvent { + eventType: string; + filename: string; +} + +export function watch(path: string): { event: Event; disposable: Disposable; } { + const emitter = new EventEmitter(); + const event = emitter.event; + const watcher = fs.watch(path, (eventType, filename) => emitter.fire({ eventType, filename })); + const disposable = new Disposable(() => watcher.close()); + + return { event, disposable }; +}