mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-08 02:43:11 -05:00
git: improve fs events reaction
This commit is contained in:
@@ -126,7 +126,7 @@ export class CommandCenter {
|
||||
@CommandCenter.Command('git.refresh')
|
||||
@CommandCenter.CatchErrors
|
||||
async refresh(): Promise<void> {
|
||||
await this.model.update();
|
||||
await this.model.status();
|
||||
}
|
||||
|
||||
@CommandCenter.Command('git.openChange')
|
||||
|
||||
@@ -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<Uri>) {
|
||||
this.listener = onWorkspaceChange(this.eventuallyUpdateModel, this);
|
||||
}
|
||||
|
||||
@debounce(1000)
|
||||
private eventuallyUpdateModel(): void {
|
||||
this.updateModelAndWait();
|
||||
}
|
||||
|
||||
@decorate(throttle)
|
||||
private async updateModelAndWait(): Promise<void> {
|
||||
await this.model.update();
|
||||
await new Promise(c => setTimeout(c, 8000));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.listener.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async function init(disposables: Disposable[]): Promise<void> {
|
||||
const rootPath = workspace.rootPath;
|
||||
|
||||
|
||||
@@ -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<Uri>
|
||||
) {
|
||||
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<void> {
|
||||
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<void> {
|
||||
await this.run(Operation.Status);
|
||||
}
|
||||
|
||||
@decorate(throttle)
|
||||
async stage(...resources: Resource[]): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void>): Promise<void> {
|
||||
private async run(operation: Operation, fn: () => Promise<void> = () => Promise.resolve()): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
23
extensions/git/src/watch.ts
Normal file
23
extensions/git/src/watch.ts
Normal file
@@ -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<FSEvent>; disposable: Disposable; } {
|
||||
const emitter = new EventEmitter<FSEvent>();
|
||||
const event = emitter.event;
|
||||
const watcher = fs.watch(path, (eventType, filename) => emitter.fire({ eventType, filename }));
|
||||
const disposable = new Disposable(() => watcher.close());
|
||||
|
||||
return { event, disposable };
|
||||
}
|
||||
Reference in New Issue
Block a user