From 3d6b1b71c5d902499908171291cbc98f0b0ab81d Mon Sep 17 00:00:00 2001 From: Joao Moreno Date: Thu, 16 Feb 2017 10:15:38 +0100 Subject: [PATCH] git: wrap up nogit state --- extensions/git/src/main.ts | 3 +- extensions/git/src/model.ts | 166 ++++++++++++++++++------------ extensions/git/src/scmProvider.ts | 4 +- extensions/git/src/statusbar.ts | 5 +- extensions/git/src/util.ts | 2 + 5 files changed, 107 insertions(+), 73 deletions(-) diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 5c9a5a98254..058f8130e2c 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -65,7 +65,8 @@ async function init(disposables: Disposable[]): Promise { checkoutStatusBar, syncStatusBar, autoFetcher, - mergeDecorator + mergeDecorator, + model ); } diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index 10ae49695ae..b5bcd13bc66 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -7,7 +7,7 @@ import { Uri, EventEmitter, Event, SCMResource, SCMResourceDecorations, SCMResourceGroup, Disposable, window, workspace } from 'vscode'; import { Repository, Ref, Branch, Remote, PushOptions, Commit, GitErrorCodes } from './git'; -import { anyEvent, eventToPromise, filterEvent, mapEvent } from './util'; +import { anyEvent, eventToPromise, filterEvent, mapEvent, EmptyDisposable, combinedDisposable } from './util'; import { memoize, throttle, debounce } from './decorators'; import { watch } from './watch'; import * as path from 'path'; @@ -20,6 +20,12 @@ function getIconUri(iconName: string, theme: string): Uri { return Uri.file(path.join(iconsRootPath, theme, `${iconName}.svg`)); } +export enum State { + Uninitialized, + Idle, + NotAGitRepository +} + export enum Status { INDEX_MODIFIED, INDEX_ADDED, @@ -139,7 +145,7 @@ export class MergeGroup extends ResourceGroup { static readonly ID = 'merge'; - constructor(resources: Resource[]) { + constructor(resources: Resource[] = []) { super(MergeGroup.ID, localize('merge changes', "Merge Changes"), resources); } } @@ -148,7 +154,7 @@ export class IndexGroup extends ResourceGroup { static readonly ID = 'index'; - constructor(resources: Resource[]) { + constructor(resources: Resource[] = []) { super(IndexGroup.ID, localize('staged changes', "Staged Changes"), resources); } } @@ -157,7 +163,7 @@ export class WorkingTreeGroup extends ResourceGroup { static readonly ID = 'workingTree'; - constructor(resources: Resource[]) { + constructor(resources: Resource[] = []) { super(WorkingTreeGroup.ID, localize('changes', "Changes"), resources); } } @@ -176,6 +182,7 @@ export enum Operation { Push = 1 << 10, Sync = 1 << 11, Init = 1 << 12, + UpdateModel = 1 << 13 } export interface Operations { @@ -212,16 +219,18 @@ export interface CommitOptions { signoff?: boolean; } -export enum State { - Uninitialized, - Idle, - NotAGitRepository -} +export class Model implements Disposable { -export class Model { + private _onDidChangeState = new EventEmitter(); + readonly onDidChangeState: Event = this._onDidChangeState.event; - private _onDidChange = new EventEmitter(); - readonly onDidChange: Event = this._onDidChange.event; + private _onDidChangeResources = new EventEmitter(); + readonly onDidChangeResources: Event = this._onDidChangeResources.event; + + @memoize + get onDidChange(): Event { + return anyEvent(this.onDidChangeState, this.onDidChangeResources); + } private _onRunOperation = new EventEmitter(); readonly onRunOperation: Event = this._onRunOperation.event; @@ -229,9 +238,6 @@ export class Model { private _onDidRunOperation = new EventEmitter(); readonly onDidRunOperation: Event = this._onDidRunOperation.event; - private _onDidChangeState = new EventEmitter(); - readonly onDidChangeState: Event = this._onDidChangeState.event; - @memoize get onDidChangeOperations(): Event { return anyEvent(this.onRunOperation as Event, this.onDidRunOperation as Event); @@ -262,6 +268,21 @@ export class Model { return result; } + private _HEAD: Branch | undefined; + get HEAD(): Branch | undefined { + return this._HEAD; + } + + private _refs: Ref[] = []; + get refs(): Ref[] { + return this._refs; + } + + private _remotes: Remote[] = []; + get remotes(): Remote[] { + return this._remotes; + } + private _operations = new OperationsImpl(); get operations(): Operations { return this._operations; } @@ -272,65 +293,33 @@ export class Model { set state(state: State) { this._state = state; this._onDidChangeState.fire(state); + + this._HEAD = undefined; + this._refs = []; + this._remotes = []; + this._mergeGroup = new MergeGroup(); + this._indexGroup = new IndexGroup(); + this._workingTreeGroup = new WorkingTreeGroup(); + this._onDidChangeResources.fire(this.resources); } - private disposables: Disposable[] = []; + private repositoryDisposable: Disposable = EmptyDisposable; constructor( private repository: Repository, private onWorkspaceChange: Event ) { - this.initialize().catch(err => console.error(err)); - } - - private async initialize(): Promise { - try { - this.repositoryRoot = await this.repository.getRoot(); - this.state = State.Idle; - - /* 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(this.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(this.onWorkspaceChange, uri => !/\/\.git\//.test(uri.fsPath)); - onNonGitChange(this.onFSChange, this, this.disposables); - - this.status(); - } catch (err) { - if (err.gitErrorCode === GitErrorCodes.NotAGitRepository) { - this.state = State.NotAGitRepository; - } else { - throw err; - } - } - } - - private _HEAD: Branch | undefined; - get HEAD(): Branch | undefined { - return this._HEAD; - } - - private _refs: Ref[] = []; - get refs(): Ref[] { - return this._refs; - } - - private _remotes: Remote[] = []; - get remotes(): Remote[] { - return this._remotes; + this.status(); } @throttle async init(): Promise { - await this.run(Operation.Init, () => this.repository.init()); + if (this.state !== State.NotAGitRepository) { + return; + } + + await this.repository.init(); + await this.status(); } @throttle @@ -432,14 +421,23 @@ export class Model { await this.run(Operation.Sync, () => this.repository.sync()); } - private async run(operation: Operation, fn: () => Promise = () => Promise.resolve()): Promise { + private async run(operation: Operation, runOperation: () => Promise = () => Promise.resolve()): Promise { return window.withScmProgress(async () => { this._operations = this._operations.start(operation); this._onRunOperation.fire(operation); try { - await fn(); + await this.assertIdleState(); + await runOperation(); await this.update(); + } catch (err) { + if (err.gitErrorCode === GitErrorCodes.NotAGitRepository) { + // TODO@Joao! + this.repositoryDisposable.dispose(); + this.state = State.NotAGitRepository; + } else { + throw err; + } } finally { this._operations = this._operations.end(operation); this._onDidRunOperation.fire(operation); @@ -447,6 +445,37 @@ export class Model { }); } + /* 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. + */ + private async assertIdleState(): Promise { + if (this.state === State.Idle) { + return; + } + + const repositoryRoot = await this.repository.getRoot(); + + this.repositoryDisposable.dispose(); + this.repositoryRoot = repositoryRoot; + + const disposables: Disposable[] = []; + const gitPath = path.join(repositoryRoot, '.git'); + const { event, disposable: watcher } = watch(gitPath); + disposables.push(watcher); + + 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, disposables); + + const onNonGitChange = filterEvent(this.onWorkspaceChange, uri => !/\/\.git\//.test(uri.fsPath)); + onNonGitChange(this.onFSChange, this, disposables); + + this.repositoryDisposable = combinedDisposable(disposables); + this.state = State.Idle; + } + @throttle private async update(): Promise { const status = await this.repository.getStatus(); @@ -511,8 +540,7 @@ export class Model { this._mergeGroup = new MergeGroup(merge); this._indexGroup = new IndexGroup(index); this._workingTreeGroup = new WorkingTreeGroup(workingTree); - - this._onDidChange.fire(this.resources); + this._onDidChangeResources.fire(this.resources); } private onFSChange(uri: Uri): void { @@ -547,4 +575,8 @@ export class Model { await eventToPromise(this.onDidRunOperation); } } + + dispose(): void { + this.repositoryDisposable.dispose(); + } } \ No newline at end of file diff --git a/extensions/git/src/scmProvider.ts b/extensions/git/src/scmProvider.ts index 61ccb32ab21..71dda0dec4c 100644 --- a/extensions/git/src/scmProvider.ts +++ b/extensions/git/src/scmProvider.ts @@ -8,7 +8,7 @@ import { scm, Uri, Disposable, SCMProvider, SCMResourceGroup, Event, ProviderResult, workspace } from 'vscode'; import { Model, Resource, ResourceGroup, State } from './model'; import { CommandCenter } from './commands'; -import { anyEvent, mapEvent } from './util'; +import { mapEvent } from './util'; export class GitSCMProvider implements SCMProvider { @@ -17,7 +17,7 @@ export class GitSCMProvider implements SCMProvider { get resources(): SCMResourceGroup[] { return this.model.resources; } get onDidChange(): Event { - return mapEvent(anyEvent(this.model.onDidChange, this.model.onDidChangeState), () => this.model.resources); + return mapEvent(this.model.onDidChange, () => this.model.resources); } get label(): string { return 'Git'; } diff --git a/extensions/git/src/statusbar.ts b/extensions/git/src/statusbar.ts index f3171809317..00dda46a5ad 100644 --- a/extensions/git/src/statusbar.ts +++ b/extensions/git/src/statusbar.ts @@ -30,9 +30,7 @@ export class CheckoutStatusBar { const HEAD = this.model.HEAD; if (!HEAD) { - this.raw.command = ''; - this.raw.color = 'rgb(100, 100, 100)'; - this.raw.text = 'unknown'; + this.raw.hide(); return; } @@ -48,6 +46,7 @@ export class CheckoutStatusBar { (this.model.workingTreeGroup.resources.length > 0 ? '*' : '') + (this.model.indexGroup.resources.length > 0 ? '+' : '') + (this.model.mergeGroup.resources.length > 0 ? '!' : ''); + this.raw.show(); } dispose(): void { diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index e4940255602..8cc69d1f43c 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -28,6 +28,8 @@ export function combinedDisposable(disposables: IDisposable[]): IDisposable { return toDisposable(() => dispose(disposables)); } +export const EmptyDisposable = toDisposable(() => null); + export function mapEvent(event: Event, map: (i: I) => O): Event { return (listener, thisArgs = null, disposables?) => event(i => listener.call(thisArgs, map(i)), null, disposables); } -- GitLab