From b935a0537509242724110918cbe336868ad1c98e Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sat, 28 Oct 2017 02:16:22 -0400 Subject: [PATCH] Moves onDidChangeRepo to Repository.onDidChange Adds onDidChange to GitService Adds better multi-root support Reworks custom view eventing --- CHANGELOG.md | 5 +- package.json | 15 +++- src/git/gitContextTracker.ts | 95 +++++++++++++--------- src/git/models/repository.ts | 168 +++++++++++++++++++++++++++++++------- src/gitService.ts | 185 +++++++++++++++++------------------------- src/views/explorerNode.ts | 27 +++++- src/views/fileHistoryNode.ts | 48 +++++++++-- src/views/gitExplorer.ts | 119 +++++++++++++++++---------- src/views/historyNode.ts | 20 +++-- src/views/repositoriesNode.ts | 15 ++-- src/views/repositoryNode.ts | 55 +++++++++++-- src/views/statusNode.ts | 78 ++++++++++-------- 12 files changed, 537 insertions(+), 293 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 353d985..eb515d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ ATTENTION! To support multi-root workspaces some underlying fundamentals had to - Adds support to the `Compare File with Branch...` command (`gitlens.diffWithBranch`) work with renamed files -- closes [#165](https://github.com/eamodio/vscode-gitlens/issues/165) - Adds `Compare File with Branch...` command (`gitlens.diffWithBranch`) to source control resource context menu - Adds `Open Repository in Remote` command (`gitlens.openRepoInRemote`) to repository node(s) of the `GitLens` custom view +- Adds `Enable Automatic Refresh` command (`gitlens.gitExplorer.setAutoRefreshToOn`) to the `GitLens` custom view regardless of the current view +- Adds `Disable Automatic Refresh` command (`gitlens.gitExplorer.setAutoRefreshToOff`) to the `GitLens` custom view regardless of the current view ### Changed - `GitLens` custom view will no longer show if there is no Git repository -- closes [#159](https://github.com/eamodio/vscode-gitlens/issues/159) @@ -28,7 +30,8 @@ ATTENTION! To support multi-root workspaces some underlying fundamentals had to ### Fixed - Fixes jumpy code lens when deleting characters from a line with a Git code lens -- Fixes? [#178](https://github.com/eamodio/vscode-gitlens/issues/178) - Slight but noticeable keyboard lag with Gitlens +- Fixes [#178](https://github.com/eamodio/vscode-gitlens/issues/178) - Slight but noticeable keyboard lag with Gitlens +- Fixes issue where using the `Refresh` command on a `GitLens` custom view node refreshed the whole view, rather than just the node ## [5.7.1] - 2017-10-19 ### Fixed diff --git a/package.json b/package.json index 45148e1..2151f7e 100644 --- a/package.json +++ b/package.json @@ -1148,6 +1148,11 @@ } }, { + "command": "gitlens.gitExplorer.refreshNode", + "title": "Refresh", + "category": "GitLens" + }, + { "command": "gitlens.gitExplorer.setFilesLayoutToAuto", "title": "Show Files in Automatic View", "category": "GitLens" @@ -1410,6 +1415,10 @@ "when": "false" }, { + "command": "gitlens.gitExplorer.refreshNode", + "when": "false" + }, + { "command": "gitlens.gitExplorer.switchToHistoryView", "when": "gitlens:gitExplorer:view == repository" }, @@ -1709,12 +1718,12 @@ }, { "command": "gitlens.gitExplorer.setAutoRefreshToOn", - "when": "view == gitlens.gitExplorer && gitlens:gitExplorer:view == repository && config.gitlens.gitExplorer.autoRefresh && !gitlens:gitExplorer:autoRefresh", + "when": "view == gitlens.gitExplorer && config.gitlens.gitExplorer.autoRefresh && !gitlens:gitExplorer:autoRefresh", "group": "2_gitlens" }, { "command": "gitlens.gitExplorer.setAutoRefreshToOff", - "when": "view == gitlens.gitExplorer && gitlens:gitExplorer:view == repository && config.gitlens.gitExplorer.autoRefresh && gitlens:gitExplorer:autoRefresh", + "when": "view == gitlens.gitExplorer && config.gitlens.gitExplorer.autoRefresh && gitlens:gitExplorer:autoRefresh", "group": "2_gitlens" } ], @@ -1965,7 +1974,7 @@ "group": "1_gitlens@2" }, { - "command": "gitlens.gitExplorer.refresh", + "command": "gitlens.gitExplorer.refreshNode", "when": "view == gitlens.gitExplorer && viewItem != gitlens:commit-file && viewItem != gitlens:stash-file && viewItem != gitlens:status-file", "group": "9_gitlens@1" } diff --git a/src/git/gitContextTracker.ts b/src/git/gitContextTracker.ts index b7bd187..ef7263d 100644 --- a/src/git/gitContextTracker.ts +++ b/src/git/gitContextTracker.ts @@ -3,7 +3,7 @@ import { Functions } from '../system'; import { Disposable, Event, EventEmitter, TextDocumentChangeEvent, TextEditor, window, workspace } from 'vscode'; import { TextDocumentComparer } from '../comparers'; import { CommandContext, isTextEditor, setCommandContext } from '../constants'; -import { GitService, GitUri, RepoChangedReasons } from '../gitService'; +import { GitChangeEvent, GitChangeReason, GitService, GitUri, Repository, RepositoryChangeEvent } from '../gitService'; // import { Logger } from '../logger'; export enum BlameabilityChangeReason { @@ -19,6 +19,20 @@ export interface BlameabilityChangeEvent { reason: BlameabilityChangeReason; } +interface Context { + editor?: TextEditor; + repo?: Repository; + repoDisposable?: Disposable; + state: ContextState; + uri?: GitUri; +} + +interface ContextState { + blameable?: boolean; + dirty: boolean; + tracked?: boolean; +} + export class GitContextTracker extends Disposable { private _onDidChangeBlameability = new EventEmitter(); @@ -26,7 +40,7 @@ export class GitContextTracker extends Disposable { return this._onDidChangeBlameability.event; } - private _context: { editor?: TextEditor, uri?: GitUri, blameable?: boolean, dirty: boolean, tracked?: boolean } = { dirty: false }; + private _context: Context = { state: { dirty: false } }; private _disposable: Disposable | undefined; private _gitEnabled: boolean; @@ -81,28 +95,26 @@ export class GitContextTracker extends Disposable { this.updateBlameability(BlameabilityChangeReason.BlameFailed, false); } - private onRepoChanged(reasons: RepoChangedReasons[]) { - if (reasons.includes(RepoChangedReasons.CacheReset) || reasons.includes(RepoChangedReasons.Unknown)) { - this.updateContext(BlameabilityChangeReason.RepoChanged, this._context.editor); - - return; + private onGitChanged(e: GitChangeEvent) { + if (e.reason === GitChangeReason.RemoteCache || e.reason === GitChangeReason.Repositories) { + this.updateRemotes(); } + } - // TODO: Support multi-root - if (!reasons.includes(RepoChangedReasons.Remotes) && !reasons.includes(RepoChangedReasons.Repositories)) return; - - this.updateRemotes(this._context.uri); + private onRepoChanged(e: RepositoryChangeEvent) { + this.updateContext(BlameabilityChangeReason.RepoChanged, this._context.editor); + this.updateRemotes(); } private onTextDocumentChanged(e: TextDocumentChangeEvent) { if (this._context.editor === undefined || !TextDocumentComparer.equals(this._context.editor.document, e.document)) return; // If we haven't changed state, kick out - if (this._context.dirty === e.document.isDirty) return; + if (this._context.state.dirty === e.document.isDirty) return; // Logger.log('GitContextTracker.onTextDocumentChanged', 'Dirty state changed', e); - this._context.dirty = e.document.isDirty; + this._context.state.dirty = e.document.isDirty; this.updateBlameability(BlameabilityChangeReason.DocumentChanged); } @@ -110,16 +122,29 @@ export class GitContextTracker extends Disposable { let tracked: boolean; if (force || this._context.editor !== editor) { this._context.editor = editor; + this._context.repo = undefined; + if (this._context.repoDisposable !== undefined) { + this._context.repoDisposable.dispose(); + this._context.repoDisposable = undefined; + } if (editor !== undefined) { - this._context.uri = await GitUri.fromUri(editor.document.uri, this.git); - this._context.dirty = editor.document.isDirty; + const uri = editor.document.uri; + + const repo = await this.git.getRepository(uri); + if (repo !== undefined) { + this._context.repo = repo; + this._context.repoDisposable = repo.onDidChange(this.onRepoChanged, this); + } + + this._context.uri = await GitUri.fromUri(uri, this.git); + this._context.state.dirty = editor.document.isDirty; tracked = await this.git.isTracked(this._context.uri); } else { this._context.uri = undefined; - this._context.dirty = false; - this._context.blameable = false; + this._context.state.dirty = false; + this._context.state.blameable = false; tracked = false; } } @@ -130,23 +155,23 @@ export class GitContextTracker extends Disposable { : false; } - if (this._context.tracked !== tracked) { - this._context.tracked = tracked; + if (this._context.state.tracked !== tracked) { + this._context.state.tracked = tracked; setCommandContext(CommandContext.ActiveFileIsTracked, tracked); } this.updateBlameability(reason, undefined, force); - this.updateRemotes(this._context.uri); + this.updateRemotes(); } private updateBlameability(reason: BlameabilityChangeReason, blameable?: boolean, force: boolean = false) { if (blameable === undefined) { - blameable = this._context.tracked && !this._context.dirty; + blameable = this._context.state.tracked && !this._context.state.dirty; } - if (!force && this._context.blameable === blameable) return; + if (!force && this._context.state.blameable === blameable) return; - this._context.blameable = blameable; + this._context.state.blameable = blameable; setCommandContext(CommandContext.ActiveIsBlameable, blameable); this._onDidChangeBlameability.fire({ @@ -156,29 +181,23 @@ export class GitContextTracker extends Disposable { }); } - private async updateRemotes(uri: GitUri | undefined) { - const repositories = await this.git.getRepositories(); - + private async updateRemotes() { let hasRemotes = false; - if (uri !== undefined && this.git.isTrackable(uri)) { - const remotes = await this.git.getRemotes(uri.repoPath); + if (this._context.repo !== undefined) { + const remotes = await this.git.getRemotes(this._context.repo.path); - setCommandContext(CommandContext.ActiveHasRemotes, remotes.length !== 0); + hasRemotes = remotes.length !== 0; + setCommandContext(CommandContext.ActiveHasRemotes, hasRemotes); } else { - if (repositories.length === 1) { - const remotes = await this.git.getRemotes(repositories[0].path); - hasRemotes = remotes.length !== 0; - - setCommandContext(CommandContext.ActiveHasRemotes, hasRemotes); - } - else { - setCommandContext(CommandContext.ActiveHasRemotes, false); - } + setCommandContext(CommandContext.ActiveHasRemotes, false); } if (!hasRemotes) { + const repositories = await this.git.getRepositories(); for (const repo of repositories) { + if (repo === this._context.repo) continue; + const remotes = await this.git.getRemotes(repo.path); hasRemotes = remotes.length !== 0; diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index 90b54cf..b09b959 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -2,14 +2,52 @@ import { Functions } from '../../system'; import { Disposable, Event, EventEmitter, RelativePattern, Uri, workspace, WorkspaceFolder } from 'vscode'; +export enum RepositoryChange { + // FileSystem = 'file-system', + Repository = 'repository', + Stashes = 'stashes' +} + +export class RepositoryChangeEvent { + + readonly changes: RepositoryChange[] = []; + + constructor(public repository?: Repository) { } + + changed(change: RepositoryChange, solely: boolean = false) { + if (solely) return this.changes.length === 1 && this.changes[0] === change; + + return this.changes.includes(change); + + // const changed = this.changes.includes(change); + // if (changed) return true; + + // if (change === RepositoryChange.Repository) { + // return this.changes.includes(RepositoryChange.Stashes); + // } + + // return false; + } +} + +export interface RepositoryFileSystemChangeEvent { + repository?: Repository; + uris: Uri[]; +} + export enum RepositoryStorage { StatusNode = 'statusNode' } export class Repository extends Disposable { - private _onDidChangeFileSystem = new EventEmitter(); - get onDidChangeFileSystem(): Event { + private _onDidChange = new EventEmitter(); + get onDidChange(): Event { + return this._onDidChange.event; + } + + private _onDidChangeFileSystem = new EventEmitter(); + get onDidChangeFileSystem(): Event { return this._onDidChangeFileSystem.event; } @@ -18,14 +56,17 @@ export class Repository extends Disposable { readonly storage: Map = new Map(); private readonly _disposable: Disposable; + private _fireChangeDebounced: ((e: RepositoryChangeEvent) => void) | undefined = undefined; + private _fireFileSystemChangeDebounced: ((e: RepositoryFileSystemChangeEvent) => void) | undefined = undefined; + private _fsWatchCounter = 0; private _fsWatcherDisposable: Disposable | undefined; - private _pendingChanges: { repo: boolean, fs: boolean } = { repo: false, fs: false }; + private _pendingChanges: { repo?: RepositoryChangeEvent, fs?: RepositoryFileSystemChangeEvent } = { }; private _suspended: boolean; constructor( private readonly folder: WorkspaceFolder, public readonly path: string, - readonly onRepoChanged: (uri: Uri) => void, + private readonly onAnyRepositoryChanged: () => void, suspended: boolean ) { super(() => this.dispose()); @@ -35,14 +76,12 @@ export class Repository extends Disposable { this._suspended = suspended; const watcher = workspace.createFileSystemWatcher(new RelativePattern(folder, '**/.git/{index,HEAD,refs/stash,refs/heads/**,refs/remotes/**}')); - const subscriptions = [ + this._disposable = Disposable.from( watcher, - watcher.onDidChange(onRepoChanged), - watcher.onDidCreate(onRepoChanged), - watcher.onDidDelete(onRepoChanged) - ]; - - this._disposable = Disposable.from(...subscriptions); + watcher.onDidChange(this.onRepositoryChanged, this), + watcher.onDidCreate(this.onRepositoryChanged, this), + watcher.onDidDelete(this.onRepositoryChanged, this) + ); } dispose() { @@ -58,45 +97,112 @@ export class Repository extends Disposable { this._disposable && this._disposable.dispose(); } + private onFileSystemChanged(uri: Uri) { + // Ignore .git changes + if (/\.git/.test(uri.fsPath)) return; + + this.fireFileSystemChange(uri); + } + + private onRepositoryChanged(uri: Uri) { + if (uri !== undefined && uri.path.endsWith('ref/stash')) { + this.fireChange(RepositoryChange.Stashes); + + return; + } + + this.onAnyRepositoryChanged(); + + this.fireChange(RepositoryChange.Repository); + } + + private fireChange(reason: RepositoryChange) { + if (this._fireChangeDebounced === undefined) { + this._fireChangeDebounced = Functions.debounce(this.fireChangeCore, 250); + } + + if (this._pendingChanges.repo === undefined) { + this._pendingChanges.repo = new RepositoryChangeEvent(this); + } + + const e = this._pendingChanges.repo; + + if (!e.changes.includes(reason)) { + e.changes.push(reason); + } + + if (this._suspended) return; + + this._fireChangeDebounced(e); + } + + private fireChangeCore(e: RepositoryChangeEvent) { + this._pendingChanges.repo = undefined; + + this._onDidChange.fire(e); + } + + private fireFileSystemChange(uri: Uri) { + if (this._fireFileSystemChangeDebounced === undefined) { + this._fireFileSystemChangeDebounced = Functions.debounce(this.fireFileSystemChangeCore, 2500); + } + + if (this._pendingChanges.fs === undefined) { + this._pendingChanges.fs = { repository: this, uris: [] }; + } + + const e = this._pendingChanges.fs; + e.uris.push(uri); + + if (this._suspended) return; + + this._fireFileSystemChangeDebounced(e); + } + + private fireFileSystemChangeCore(e: RepositoryFileSystemChangeEvent) { + this._pendingChanges.fs = undefined; + + this._onDidChangeFileSystem.fire(e); + } + + containsUri(uri: Uri) { + return this.folder === workspace.getWorkspaceFolder(uri); + } + resume() { if (!this._suspended) return; this._suspended = false; // If we've come back into focus and we are dirty, fire the change events - if (this._pendingChanges.fs) { - this._pendingChanges.fs = false; - this._onDidChangeFileSystem.fire(); + + if (this._pendingChanges.repo !== undefined) { + this._fireChangeDebounced!(this._pendingChanges.repo); + } + + if (this._pendingChanges.fs !== undefined) { + this._fireFileSystemChangeDebounced!(this._pendingChanges.fs); } } startWatchingFileSystem() { + this._fsWatchCounter++; if (this._fsWatcherDisposable !== undefined) return; - const debouncedFn = Functions.debounce((uri: Uri) => this._onDidChangeFileSystem.fire(uri), 2500); - const fn = (uri: Uri) => { - // Ignore .git changes - if (/\.git/.test(uri.fsPath)) return; - - if (this._suspended) { - this._pendingChanges.fs = true; - return; - } - - debouncedFn(uri); - }; - const watcher = workspace.createFileSystemWatcher(new RelativePattern(this.folder, `**`)); this._fsWatcherDisposable = Disposable.from( watcher, - watcher.onDidChange(fn), - watcher.onDidCreate(fn), - watcher.onDidDelete(fn) + watcher.onDidChange(this.onFileSystemChanged, this), + watcher.onDidCreate(this.onFileSystemChanged, this), + watcher.onDidDelete(this.onFileSystemChanged, this) ); } stopWatchingFileSystem() { - this._fsWatcherDisposable && this._fsWatcherDisposable.dispose(); + if (this._fsWatcherDisposable === undefined) return; + if (--this._fsWatchCounter > 0) return; + + this._fsWatcherDisposable.dispose(); this._fsWatcherDisposable = undefined; } diff --git a/src/gitService.ts b/src/gitService.ts index 221ae3f..a641a9d 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -65,12 +65,14 @@ export enum GitRepoSearchBy { Sha = 'sha' } -export enum RepoChangedReasons { - CacheReset = 'cache-reset', - Remotes = 'remotes', - Repositories = 'Repositories', - Stash = 'stash', - Unknown = '' +export enum GitChangeReason { + GitCache = 'git-cache', + RemoteCache = 'remote-cache', + Repositories = 'repositories' +} + +export interface GitChangeEvent { + reason: GitChangeReason; } export class GitService extends Disposable { @@ -86,17 +88,15 @@ export class GitService extends Disposable { return this._onDidBlameFail.event; } - // TODO: Support multi-root { repo, reasons }[]? - private _onDidChangeRepo = new EventEmitter(); - get onDidChangeRepo(): Event { - return this._onDidChangeRepo.event; + private _onDidChange = new EventEmitter(); + get onDidChange(): Event { + return this._onDidChange.event; } private _cacheDisposable: Disposable | undefined; private _disposable: Disposable | undefined; private _documentKeyMap: Map; private _gitCache: Map; - private _pendingChanges: { repo: boolean } = { repo: false }; private _remotesCache: Map; private _repositories: Map; private _repositoriesPromise: Promise | undefined; @@ -140,17 +140,22 @@ export class GitService extends Disposable { this._versionedUriCache.clear(); } - public get repoPath(): string | undefined { + get repoPath(): string | undefined { if (this._repositories.size !== 1) return undefined; const repo = Iterables.first(this._repositories.values()); return repo === undefined ? undefined : repo.path; } - public get UseCaching() { + get UseCaching() { return this.config.advanced.caching.enabled; } + private onAnyRepositoryChanged() { + this._gitCache.clear(); + this._trackedCache.clear(); + } + private onConfigurationChanged() { const encoding = workspace.getConfiguration('files').get('encoding', 'utf8'); setDefaultEncoding(encoding); @@ -184,13 +189,40 @@ export class GitService extends Disposable { if (this.config.blame.ignoreWhitespace !== ignoreWhitespace) { this._gitCache.clear(); - this.fireRepoChange(RepoChangedReasons.CacheReset); + + this.fireChange(GitChangeReason.GitCache); } } private onRemoteProvidersChanged() { this._remotesCache.clear(); - this.fireRepoChange(RepoChangedReasons.Remotes); + + this.fireChange(GitChangeReason.RemoteCache); + } + + private onTextDocumentChanged(e: TextDocumentChangeEvent) { + let key = this._documentKeyMap.get(e.document); + if (key === undefined) { + key = this.getCacheEntryKey(e.document.uri); + this._documentKeyMap.set(e.document, key); + } + + // Don't remove broken blame on change (since otherwise we'll have to run the broken blame again) + const entry = this._gitCache.get(key); + if (entry === undefined || entry.hasErrors) return; + + if (this._gitCache.delete(key)) { + Logger.log(`Clear cache entry for '${key}', reason=${RemoveCacheReason[RemoveCacheReason.DocumentChanged]}`); + } + } + + private onTextDocumentClosed(document: TextDocument) { + this._documentKeyMap.delete(document); + + const key = this.getCacheEntryKey(document.uri); + if (this._gitCache.delete(key)) { + Logger.log(`Clear cache entry for '${key}', reason=${RemoveCacheReason[RemoveCacheReason.DocumentClosed]}`); + } } private onWindowStateChanged(e: WindowState) { @@ -201,17 +233,7 @@ export class GitService extends Disposable { this._repositories.forEach(r => r && r.suspend()); } - const suspended = !e.focused; - const changed = suspended !== this._suspended; - this._suspended = suspended; - - if (suspended || !changed) return; - - // If we've come back into focus and we are dirty, fire the change events - if (this._pendingChanges.repo) { - this._pendingChanges.repo = false; - this._fireRepoChangeDebounced!(); - } + this._suspended = !e.focused; } private async onWorkspaceFoldersChanged(e?: WorkspaceFoldersChangeEvent) { @@ -234,7 +256,7 @@ export class GitService extends Disposable { this._repositories.set(fsPath, undefined); } else { - this._repositories.set(fsPath, new Repository(f, rp, this.onRepoChanged.bind(this), this._suspended)); + this._repositories.set(fsPath, new Repository(f, rp, this.onAnyRepositoryChanged.bind(this), this._suspended)); } } @@ -253,82 +275,12 @@ export class GitService extends Disposable { await setCommandContext(CommandContext.HasRepository, hasRepository); if (!initializing) { - this.fireRepoChange(RepoChangedReasons.Repositories); + this.fireChange(GitChangeReason.Repositories); } } - private onTextDocumentChanged(e: TextDocumentChangeEvent) { - let key = this._documentKeyMap.get(e.document); - if (key === undefined) { - key = this.getCacheEntryKey(e.document.uri); - this._documentKeyMap.set(e.document, key); - } - - // Don't remove broken blame on change (since otherwise we'll have to run the broken blame again) - const entry = this._gitCache.get(key); - if (entry === undefined || entry.hasErrors) return; - - if (this._gitCache.delete(key)) { - Logger.log(`Clear cache entry for '${key}', reason=${RemoveCacheReason[RemoveCacheReason.DocumentChanged]}`); - } - } - - private onTextDocumentClosed(document: TextDocument) { - this._documentKeyMap.delete(document); - - const key = this.getCacheEntryKey(document.uri); - if (this._gitCache.delete(key)) { - Logger.log(`Clear cache entry for '${key}', reason=${RemoveCacheReason[RemoveCacheReason.DocumentClosed]}`); - } - } - - private onRepoChanged(uri: Uri) { - if (uri !== undefined && uri.path.endsWith('ref/stash')) { - this.fireRepoChange(RepoChangedReasons.Stash); - - return; - } - - this._gitCache.clear(); - this._trackedCache.clear(); - - this.fireRepoChange(); - } - - private _fireRepoChangeDebounced: (() => void) | undefined = undefined; - private _repoChangedReasons: RepoChangedReasons[] = []; - - private fireRepoChange(reason: RepoChangedReasons = RepoChangedReasons.Unknown) { - if (this._fireRepoChangeDebounced === undefined) { - this._fireRepoChangeDebounced = Functions.debounce(this.fireRepoChangeCore, 250); - } - - if (!this._repoChangedReasons.includes(reason)) { - this._repoChangedReasons.push(reason); - } - - if (this._suspended) { - this._pendingChanges.repo = true; - return; - } - - return this._fireRepoChangeDebounced(); - } - - private fireRepoChangeCore() { - const reasons = this._repoChangedReasons; - this._repoChangedReasons = []; - - this._onDidChangeRepo.fire(reasons); - } - - public async getRepositories(): Promise { - if (this._repositoriesPromise !== undefined) { - await this._repositoriesPromise; - this._repositoriesPromise = undefined; - } - - return [...Iterables.filter(this._repositories.values(), r => r !== undefined) as Iterable]; + private fireChange(reason: GitChangeReason) { + this._onDidChange.fire({ reason: reason }); } checkoutFile(uri: GitUri, sha?: string) { @@ -962,15 +914,8 @@ export class GitService extends Disposable { if (typeof filePathOrUri === 'string') return this.getRepoPathCore(filePathOrUri, false); - const folder = workspace.getWorkspaceFolder(filePathOrUri); - if (folder !== undefined) { - if (this._repositoriesPromise !== undefined) { - await this._repositoriesPromise; - } - - const rp = this._repositories.get(folder.uri.fsPath); - if (rp !== undefined) return rp.path; - } + const repo = await this.getRepository(filePathOrUri); + if (repo !== undefined) return repo.path; return this.getRepoPathCore(filePathOrUri.fsPath, false); } @@ -979,6 +924,28 @@ export class GitService extends Disposable { return Git.revparse_toplevel(isDirectory ? filePath : path.dirname(filePath)); } + async getRepositories(): Promise { + const repositories = await this.getRepositoriesCore(); + return [...Iterables.filter(repositories.values(), r => r !== undefined) as Iterable]; + } + + private async getRepositoriesCore(): Promise> { + if (this._repositoriesPromise !== undefined) { + await this._repositoriesPromise; + this._repositoriesPromise = undefined; + } + + return this._repositories; + } + + async getRepository(uri: Uri): Promise { + const folder = workspace.getWorkspaceFolder(uri); + if (folder === undefined) return undefined; + + const repositories = await this.getRepositoriesCore(); + return repositories.get(folder.uri.fsPath); + } + async getStashList(repoPath: string | undefined): Promise { Logger.log(`getStashList('${repoPath}')`); if (repoPath === undefined) return undefined; diff --git a/src/views/explorerNode.ts b/src/views/explorerNode.ts index c378e4f..ac5b0a8 100644 --- a/src/views/explorerNode.ts +++ b/src/views/explorerNode.ts @@ -1,5 +1,5 @@ 'use strict'; -import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Command, Disposable, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { GlyphChars } from '../constants'; import { GitUri } from '../gitService'; import { RefreshNodeCommandArgs } from './gitExplorer'; @@ -27,11 +27,25 @@ export declare type ResourceType = 'gitlens:status-file-commits' | 'gitlens:status-upstream'; -export abstract class ExplorerNode { +export abstract class ExplorerNode extends Disposable { abstract readonly resourceType: ResourceType; - constructor(public readonly uri: GitUri) { } + protected children: ExplorerNode[] | undefined; + protected disposable: Disposable | undefined; + + constructor(public readonly uri: GitUri) { + super(() => this.dispose()); + } + + dispose() { + if (this.disposable !== undefined) { + this.disposable.dispose(); + this.disposable = undefined; + } + + this.resetChildren(); + } abstract getChildren(): ExplorerNode[] | Promise; abstract getTreeItem(): TreeItem | Promise; @@ -39,6 +53,13 @@ export abstract class ExplorerNode { getCommand(): Command | undefined { return undefined; } + + resetChildren() { + if (this.children !== undefined) { + this.children.forEach(c => c.dispose()); + this.children = undefined; + } + } } export class MessageNode extends ExplorerNode { diff --git a/src/views/fileHistoryNode.ts b/src/views/fileHistoryNode.ts index 97895e8..3daea90 100644 --- a/src/views/fileHistoryNode.ts +++ b/src/views/fileHistoryNode.ts @@ -1,9 +1,11 @@ 'use strict'; import { Iterables } from '../system'; -import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode'; import { ExplorerNode, MessageNode, ResourceType } from './explorerNode'; -import { GitService, GitUri } from '../gitService'; +import { GitUri, Repository, RepositoryChange, RepositoryChangeEvent } from '../gitService'; +import { GitExplorer } from './gitExplorer'; +import { Logger } from '../logger'; export class FileHistoryNode extends ExplorerNode { @@ -11,28 +13,58 @@ export class FileHistoryNode extends ExplorerNode { constructor( uri: GitUri, - protected readonly context: ExtensionContext, - protected readonly git: GitService + private repo: Repository, + private readonly explorer: GitExplorer ) { super(uri); } async getChildren(): Promise { - const log = await this.git.getLogForFile(this.uri.repoPath, this.uri.fsPath, this.uri.sha); + const log = await this.explorer.git.getLogForFile(this.uri.repoPath, this.uri.fsPath, this.uri.sha); if (log === undefined) return [new MessageNode('No file history')]; - return [...Iterables.map(log.commits.values(), c => new CommitFileNode(c.fileStatuses[0], c, this.context, this.git, CommitFileNodeDisplayAs.CommitLabel | CommitFileNodeDisplayAs.StatusIcon))]; + return [...Iterables.map(log.commits.values(), c => new CommitFileNode(c.fileStatuses[0], c, this.explorer.context, this.explorer.git, CommitFileNodeDisplayAs.CommitLabel | CommitFileNodeDisplayAs.StatusIcon))]; } getTreeItem(): TreeItem { + if (this.disposable !== undefined) { + this.disposable.dispose(); + this.disposable = undefined; + } + + // We only need to subscribe if auto-refresh is enabled, because if it becomes enabled we will be refreshed + if (this.explorer.autoRefresh) { + this.disposable = Disposable.from( + this.explorer.onDidChangeAutoRefresh(this.onAutoRefreshChanged, this), + this.repo.onDidChange(this.onRepoChanged, this) + ); + } + const item = new TreeItem(`${this.uri.getFormattedPath()}`, TreeItemCollapsibleState.Expanded); item.contextValue = this.resourceType; item.iconPath = { - dark: this.context.asAbsolutePath('images/dark/icon-history.svg'), - light: this.context.asAbsolutePath('images/light/icon-history.svg') + dark: this.explorer.context.asAbsolutePath('images/dark/icon-history.svg'), + light: this.explorer.context.asAbsolutePath('images/light/icon-history.svg') }; return item; } + + private onAutoRefreshChanged() { + if (this.disposable === undefined) return; + + // If auto-refresh changes, just kill the subscriptions + // (if it was enabled -- we will get refreshed so we don't have to worry about re-hooking it up here) + this.disposable.dispose(); + this.disposable = undefined; + } + + private onRepoChanged(e: RepositoryChangeEvent) { + if (e.changed(RepositoryChange.Stashes, true)) return; + + Logger.log(`RepositoryNode.onRepoChanged(${e.changes.join()}); triggering node refresh`); + + this.explorer.refreshNode(this); + } } \ No newline at end of file diff --git a/src/views/gitExplorer.ts b/src/views/gitExplorer.ts index 14b532a..430a281 100644 --- a/src/views/gitExplorer.ts +++ b/src/views/gitExplorer.ts @@ -6,7 +6,7 @@ import { UriComparer } from '../comparers'; import { ExtensionKey, GitExplorerFilesLayout, IConfig } from '../configuration'; import { CommandContext, GlyphChars, setCommandContext, WorkspaceState } from '../constants'; import { BranchHistoryNode, CommitFileNode, CommitNode, ExplorerNode, HistoryNode, MessageNode, RepositoriesNode, RepositoryNode, StashNode } from './explorerNodes'; -import { GitService, GitUri, RepoChangedReasons } from '../gitService'; +import { GitChangeEvent, GitChangeReason, GitService, GitUri } from '../gitService'; import { Logger } from '../logger'; export * from './explorerNodes'; @@ -42,14 +42,19 @@ export class GitExplorer implements TreeDataProvider { private _root?: ExplorerNode; private _view: GitExplorerView | undefined; + private _onDidChangeAutoRefresh = new EventEmitter(); + public get onDidChangeAutoRefresh(): Event { + return this._onDidChangeAutoRefresh.event; + } + private _onDidChangeTreeData = new EventEmitter(); public get onDidChangeTreeData(): Event { return this._onDidChangeTreeData.event; } - constructor(private readonly context: ExtensionContext, private readonly git: GitService) { + constructor(public readonly context: ExtensionContext, public readonly git: GitService) { commands.registerCommand('gitlens.gitExplorer.setAutoRefreshToOn', () => this.setAutoRefresh(this.git.config.gitExplorer.autoRefresh, true), this); - commands.registerCommand('gitlens.gitExplorer.setAutoRefreshToOff', () => this.setAutoRefresh(this.git.config.gitExplorer.autoRefresh, true), this); + commands.registerCommand('gitlens.gitExplorer.setAutoRefreshToOff', () => this.setAutoRefresh(this.git.config.gitExplorer.autoRefresh, false), this); commands.registerCommand('gitlens.gitExplorer.setFilesLayoutToAuto', () => this.setFilesLayout(GitExplorerFilesLayout.Auto), this); commands.registerCommand('gitlens.gitExplorer.setFilesLayoutToList', () => this.setFilesLayout(GitExplorerFilesLayout.List), this); commands.registerCommand('gitlens.gitExplorer.setFilesLayoutToTree', () => this.setFilesLayout(GitExplorerFilesLayout.Tree), this); @@ -79,6 +84,14 @@ export class GitExplorer implements TreeDataProvider { this.onConfigurationChanged(); } + get autoRefresh() { + return this._config.gitExplorer.autoRefresh && this.context.workspaceState.get(WorkspaceState.GitExplorerAutoRefresh, true); + } + + get config() { + return this._config.gitExplorer; + } + async getTreeItem(node: ExplorerNode): Promise { return node.getTreeItem(); } @@ -116,10 +129,10 @@ export class GitExplorer implements TreeDataProvider { if (repositories.length === 1) { const repo = repositories[0]; - return new RepositoryNode(new GitUri(Uri.file(repo.path), { repoPath: repo.path, fileName: repo.path }), repo, this.context, this.git); + return new RepositoryNode(new GitUri(Uri.file(repo.path), { repoPath: repo.path, fileName: repo.path }), repo, this); } - return new RepositoriesNode(repositories, this.context, this.git); + return new RepositoriesNode(repositories, this); } } } @@ -130,27 +143,22 @@ export class GitExplorer implements TreeDataProvider { // If we do have a visible trackable editor, don't change from the last state (avoids issues when focus switches to the problems/output/debug console panes) if (editor.document === undefined || !this.git.isTrackable(editor.document.uri)) return this._root; - let uri = this.git.getGitUriForFile(editor.document.uri); - if (uri === undefined) { - const repoPath = await this.git.getRepoPath(editor.document.uri); - if (repoPath === undefined) return undefined; - - uri = new GitUri(editor.document.uri, { repoPath: repoPath, fileName: editor.document.uri.fsPath }); - } + const repo = await this.git.getRepository(editor.document.uri); + if (repo === undefined) return undefined; + const uri = this.git.getGitUriForFile(editor.document.uri) || new GitUri(editor.document.uri, { repoPath: repo.path, fileName: editor.document.uri.fsPath }); if (UriComparer.equals(uri, this._root && this._root.uri)) return this._root; - return new HistoryNode(uri, this.context, this.git); + return new HistoryNode(uri, repo, this); } private async onActiveEditorChanged(editor: TextEditor | undefined) { if (this._view !== GitExplorerView.History) return; const root = await this.getRootNode(editor); - if (root === this._root) return; + if (!this.setRoot(root)) return; - this._root = root; - this.refresh(RefreshReason.ActiveEditorChanged, undefined, root); + this.refresh(RefreshReason.ActiveEditorChanged, root); } private onConfigurationChanged() { @@ -178,64 +186,83 @@ export class GitExplorer implements TreeDataProvider { } } - private onRepoChanged(reasons: RepoChangedReasons[]) { - if (this._view !== GitExplorerView.Repository) return; + private onGitChanged(e: GitChangeEvent) { + if (this._root === undefined || this._view !== GitExplorerView.Repository || e.reason !== GitChangeReason.Repositories) return; - // If we are changing the set of repositories then force a root node reset - if (reasons.includes(RepoChangedReasons.Repositories)) { - this._root = undefined; - } + this.clearRoot(); - Logger.log(`GitExplorer[view=${this._view}].onRepoChanged(${reasons.join()})`); + Logger.log(`GitExplorer[view=${this._view}].onGitChanged(${e.reason})`); this.refresh(RefreshReason.RepoChanged); } private onVisibleEditorsChanged(editors: TextEditor[]) { - if (this._view !== GitExplorerView.History) return; + if (this._root === undefined || this._view !== GitExplorerView.History) return; // If we have no visible editors, or no trackable visible editors reset the view if (editors.length === 0 || !editors.some(e => e.document && this.git.isTrackable(e.document.uri))) { - if (this._root === undefined) return; + this.clearRoot(); - this._root = undefined; this.refresh(RefreshReason.VisibleEditorsChanged); } } - async refresh(reason: RefreshReason | undefined, node?: ExplorerNode, root?: ExplorerNode) { + async refresh(reason: RefreshReason | undefined, root?: ExplorerNode) { if (reason === undefined) { reason = RefreshReason.Command; } Logger.log(`GitExplorer[view=${this._view}].refresh`, `reason='${reason}'`); if (this._root === undefined || (root === undefined && this._view === GitExplorerView.History)) { - this._root = await this.getRootNode(window.activeTextEditor); + this.clearRoot(); + this.setRoot(await this.getRootNode(window.activeTextEditor)); } - this._onDidChangeTreeData.fire(node); + this._onDidChangeTreeData.fire(); } - refreshNode(node: ExplorerNode, args: RefreshNodeCommandArgs) { - if (node instanceof BranchHistoryNode) { + refreshNode(node: ExplorerNode, args?: RefreshNodeCommandArgs) { + Logger.log(`GitExplorer[view=${this._view}].refreshNode`); + + if (args !== undefined && node instanceof BranchHistoryNode) { node.maxCount = args.maxCount; } - this.refresh(RefreshReason.NodeCommand, node); + this._onDidChangeTreeData.fire(node); } async reset(view: GitExplorerView, force: boolean = false) { this.setView(view); - if (force) { - this._root = undefined; + if (force && this._root !== undefined) { + this.clearRoot(); } - this._root = await this.getRootNode(window.activeTextEditor); + + this.setRoot(await this.getRootNode(window.activeTextEditor)); + if (force) { this.refresh(RefreshReason.ViewChanged); } } + private clearRoot() { + if (this._root === undefined) return; + + this._root.dispose(); + this._root = undefined; + } + + private setRoot(root: ExplorerNode | undefined): boolean { + if (this._root === root) return false; + + if (this._root !== undefined) { + this._root.dispose(); + } + + this._root = root; + return true; + } + setView(view: GitExplorerView) { if (this._view === view) return; @@ -347,29 +374,33 @@ export class GitExplorer implements TreeDataProvider { private _autoRefreshDisposable: Disposable | undefined; - private async setAutoRefresh(enabled: boolean, userToggle: boolean = false) { + private async setAutoRefresh(enabled: boolean, workspaceEnabled?: boolean) { if (this._autoRefreshDisposable !== undefined) { this._autoRefreshDisposable.dispose(); this._autoRefreshDisposable = undefined; } + let toggled = false; if (enabled) { - enabled = this.context.workspaceState.get(WorkspaceState.GitExplorerAutoRefresh, true); + if (workspaceEnabled === undefined) { + workspaceEnabled = this.context.workspaceState.get(WorkspaceState.GitExplorerAutoRefresh, true); + } + else { + toggled = workspaceEnabled; + await this.context.workspaceState.update(WorkspaceState.GitExplorerAutoRefresh, workspaceEnabled); - if (userToggle) { - enabled = !enabled; - await this.context.workspaceState.update(WorkspaceState.GitExplorerAutoRefresh, enabled); + this._onDidChangeAutoRefresh.fire(); } - if (enabled) { - this._autoRefreshDisposable = this.git.onDidChangeRepo(this.onRepoChanged, this); + if (workspaceEnabled) { + this._autoRefreshDisposable = this.git.onDidChange(this.onGitChanged, this); this.context.subscriptions.push(this._autoRefreshDisposable); } } - setCommandContext(CommandContext.GitExplorerAutoRefresh, enabled); + setCommandContext(CommandContext.GitExplorerAutoRefresh, enabled && workspaceEnabled); - if (userToggle) { + if (toggled) { this.refresh(RefreshReason.AutoRefreshChanged); } } diff --git a/src/views/historyNode.ts b/src/views/historyNode.ts index f920f65..3746c13 100644 --- a/src/views/historyNode.ts +++ b/src/views/historyNode.ts @@ -1,8 +1,9 @@ 'use strict'; -import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExplorerNode, ResourceType } from './explorerNode'; import { FileHistoryNode } from './fileHistoryNode'; -import { GitService, GitUri } from '../gitService'; +import { GitUri, Repository } from '../gitService'; +import { GitExplorer } from './gitExplorer'; export class HistoryNode extends ExplorerNode { @@ -10,14 +11,19 @@ export class HistoryNode extends ExplorerNode { constructor( uri: GitUri, - protected readonly context: ExtensionContext, - protected readonly git: GitService + private repo: Repository, + private readonly explorer: GitExplorer ) { super(uri); } async getChildren(): Promise { - return [new FileHistoryNode(this.uri, this.context, this.git)]; + this.resetChildren(); + + this.children = [ + new FileHistoryNode(this.uri, this.repo, this.explorer) + ]; + return this.children; } getTreeItem(): TreeItem { @@ -25,8 +31,8 @@ export class HistoryNode extends ExplorerNode { item.contextValue = this.resourceType; item.iconPath = { - dark: this.context.asAbsolutePath('images/dark/icon-history.svg'), - light: this.context.asAbsolutePath('images/light/icon-history.svg') + dark: this.explorer.context.asAbsolutePath('images/dark/icon-history.svg'), + light: this.explorer.context.asAbsolutePath('images/light/icon-history.svg') }; return item; diff --git a/src/views/repositoriesNode.ts b/src/views/repositoriesNode.ts index 63b0e69..7470449 100644 --- a/src/views/repositoriesNode.ts +++ b/src/views/repositoriesNode.ts @@ -1,7 +1,8 @@ 'use strict'; -import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; +import { TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { GitService, GitUri, Repository } from '../gitService'; +import { GitExplorer } from './gitExplorer'; +import { GitUri, Repository } from '../gitService'; import { RepositoryNode } from './repositoryNode'; export class RepositoriesNode extends ExplorerNode { @@ -10,16 +11,18 @@ export class RepositoriesNode extends ExplorerNode { constructor( private readonly repositories: Repository[], - protected readonly context: ExtensionContext, - protected readonly git: GitService + private readonly explorer: GitExplorer ) { super(undefined!); } async getChildren(): Promise { - return this.repositories + this.resetChildren(); + + this.children = this.repositories .sort((a, b) => a.index - b.index) - .map(repo => new RepositoryNode(new GitUri(Uri.file(repo.path), { repoPath: repo.path, fileName: repo.path }), repo, this.context, this.git)); + .map(repo => new RepositoryNode(new GitUri(Uri.file(repo.path), { repoPath: repo.path, fileName: repo.path }), repo, this.explorer)); + return this.children; } getTreeItem(): TreeItem { diff --git a/src/views/repositoryNode.ts b/src/views/repositoryNode.ts index 8b428a2..145f244 100644 --- a/src/views/repositoryNode.ts +++ b/src/views/repositoryNode.ts @@ -1,13 +1,15 @@ 'use strict'; import { Strings } from '../system'; -import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { BranchesNode } from './branchesNode'; import { GlyphChars } from '../constants'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { GitService, GitUri, Repository } from '../gitService'; +import { GitExplorer } from './gitExplorer'; +import { GitUri, Repository, RepositoryChange, RepositoryChangeEvent } from '../gitService'; import { RemotesNode } from './remotesNode'; import { StatusNode } from './statusNode'; import { StashesNode } from './stashesNode'; +import { Logger } from '../logger'; export class RepositoryNode extends ExplorerNode { @@ -16,24 +18,59 @@ export class RepositoryNode extends ExplorerNode { constructor( uri: GitUri, private repo: Repository, - protected readonly context: ExtensionContext, - protected readonly git: GitService + private readonly explorer: GitExplorer ) { super(uri); } async getChildren(): Promise { - return [ - new StatusNode(this.uri, this.repo, this.context, this.git), - new BranchesNode(this.uri, this.context, this.git), - new RemotesNode(this.uri, this.context, this.git), - new StashesNode(this.uri, this.context, this.git) + this.resetChildren(); + + this.children = [ + new StatusNode(this.uri, this.repo, this, this.explorer), + new BranchesNode(this.uri, this.explorer.context, this.explorer.git), + new RemotesNode(this.uri, this.explorer.context, this.explorer.git), + new StashesNode(this.uri, this.explorer.context, this.explorer.git) ]; + return this.children; } getTreeItem(): TreeItem { + if (this.disposable !== undefined) { + this.disposable.dispose(); + this.disposable = undefined; + } + + // We only need to subscribe if auto-refresh is enabled, because if it becomes enabled we will be refreshed + if (this.explorer.autoRefresh) { + this.disposable = Disposable.from( + this.explorer.onDidChangeAutoRefresh(this.onAutoRefreshChanged, this), + this.repo.onDidChange(this.onRepoChanged, this) + ); + } + const item = new TreeItem(`Repository ${Strings.pad(GlyphChars.Dash, 1, 1)} ${this.repo.name || this.uri.repoPath}`, TreeItemCollapsibleState.Expanded); item.contextValue = this.resourceType; return item; } + + private onAutoRefreshChanged() { + if (this.disposable === undefined) return; + + // If auto-refresh changes, just kill the subscriptions + // (if it was enabled -- we will get refreshed so we don't have to worry about re-hooking it up here) + this.disposable.dispose(); + this.disposable = undefined; + } + + private onRepoChanged(e: RepositoryChangeEvent) { + Logger.log(`RepositoryNode.onRepoChanged(${e.changes.join()}); triggering node refresh`); + + let node: ExplorerNode | undefined; + if (this.children !== undefined && e.changed(RepositoryChange.Stashes, true)) { + node = this.children.find(c => c instanceof StashesNode); + } + + this.explorer.refreshNode(node || this); + } } \ No newline at end of file diff --git a/src/views/statusNode.ts b/src/views/statusNode.ts index a5c18dd..c403d48 100644 --- a/src/views/statusNode.ts +++ b/src/views/statusNode.ts @@ -1,7 +1,7 @@ -import { commands, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; -import { WorkspaceState } from '../constants'; +import { commands, Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { GitService, GitStatus, GitUri, Repository, RepositoryStorage } from '../gitService'; +import { GitExplorer } from './gitExplorer'; +import { GitStatus, GitUri, Repository, RepositoryFileSystemChangeEvent } from '../gitService'; import { Logger } from '../logger'; import { StatusFilesNode } from './statusFilesNode'; import { StatusUpstreamNode } from './statusUpstreamNode'; @@ -13,58 +13,59 @@ export class StatusNode extends ExplorerNode { constructor( uri: GitUri, private repo: Repository, - protected readonly context: ExtensionContext, - protected readonly git: GitService + private parent: ExplorerNode, + private readonly explorer: GitExplorer ) { super(uri); } async getChildren(): Promise { - const status = await this.git.getStatusForRepo(this.uri.repoPath!); - if (status === undefined) return []; + this.resetChildren(); - const children: ExplorerNode[] = []; + this.children = []; + + const status = await this.explorer.git.getStatusForRepo(this.uri.repoPath!); + if (status === undefined) return this.children; if (status.state.behind) { - children.push(new StatusUpstreamNode(status, 'behind', this.context, this.git)); + this.children.push(new StatusUpstreamNode(status, 'behind', this.explorer.context, this.explorer.git)); } if (status.state.ahead) { - children.push(new StatusUpstreamNode(status, 'ahead', this.context, this.git)); + this.children.push(new StatusUpstreamNode(status, 'ahead', this.explorer.context, this.explorer.git)); } if (status.state.ahead || (status.files.length !== 0 && this.includeWorkingTree)) { const range = status.upstream ? `${status.upstream}..${status.branch}` : undefined; - children.push(new StatusFilesNode(status, range, this.context, this.git)); + this.children.push(new StatusFilesNode(status, range, this.explorer.context, this.explorer.git)); } - return children; + return this.children; } private _status: GitStatus | undefined; async getTreeItem(): Promise < TreeItem > { - const status = await this.git.getStatusForRepo(this.uri.repoPath!); - if (status === undefined) return new TreeItem('No repo status'); - - const subscription = this.repo.storage.get(RepositoryStorage.StatusNode); - if (subscription !== undefined) { - subscription.dispose(); - this.repo.storage.delete(RepositoryStorage.StatusNode); + if (this.disposable !== undefined) { + this.disposable.dispose(); + this.disposable = undefined; } - if (this.includeWorkingTree) { + const status = await this.explorer.git.getStatusForRepo(this.uri.repoPath!); + if (status === undefined) return new TreeItem('No repo status'); + + if (this.explorer.autoRefresh && this.includeWorkingTree) { this._status = status; - if (this.git.config.gitExplorer.autoRefresh && this.context.workspaceState.get(WorkspaceState.GitExplorerAutoRefresh, true)) { - const subscription = this.repo.onDidChangeFileSystem(this.onFileSystemChanged, this); - this.repo.storage.set(RepositoryStorage.StatusNode, subscription); - this.context.subscriptions.push(subscription); + this.disposable = Disposable.from( + this.explorer.onDidChangeAutoRefresh(this.onAutoRefreshChanged, this), + this.repo.onDidChangeFileSystem(this.onFileSystemChanged, this), + { dispose: () => this.repo.stopWatchingFileSystem() } + ); - this.repo.startWatchingFileSystem(); - } + this.repo.startWatchingFileSystem(); } let hasChildren = false; @@ -98,32 +99,41 @@ export class StatusNode extends ExplorerNode { item.contextValue = this.resourceType; item.iconPath = { - dark: this.context.asAbsolutePath(`images/dark/icon-repo${iconSuffix}.svg`), - light: this.context.asAbsolutePath(`images/light/icon-repo${iconSuffix}.svg`) + dark: this.explorer.context.asAbsolutePath(`images/dark/icon-repo${iconSuffix}.svg`), + light: this.explorer.context.asAbsolutePath(`images/light/icon-repo${iconSuffix}.svg`) }; return item; } private get includeWorkingTree(): boolean { - return this.git.config.gitExplorer.includeWorkingTree; + return this.explorer.config.includeWorkingTree; + } + + private onAutoRefreshChanged() { + if (this.disposable === undefined) return; + + // If auto-refresh changes, just kill the subscriptions + // (if it was enabled -- we will get refreshed so we don't have to worry about re-hooking it up here) + this.disposable.dispose(); + this.disposable = undefined; } - private async onFileSystemChanged(uri?: Uri) { - const status = await this.git.getStatusForRepo(this.uri.repoPath!); + private async onFileSystemChanged(e: RepositoryFileSystemChangeEvent) { + const status = await this.explorer.git.getStatusForRepo(this.uri.repoPath!); // If we haven't changed from having some working changes to none or vice versa then just refresh the node // This is because of https://github.com/Microsoft/vscode/issues/34789 if (this._status !== undefined && status !== undefined && ((this._status.files.length === status.files.length) || (this._status.files.length > 0 && status.files.length > 0))) { - Logger.log(`GitExplorer.StatusNode.onFileSystemChanged(${uri && uri.fsPath}); triggering node refresh`); + Logger.log(`StatusNode.onFileSystemChanged; triggering node refresh`); commands.executeCommand('gitlens.gitExplorer.refreshNode', this); return; } - Logger.log(`GitExplorer.StatusNode.onFileSystemChanged(${uri && uri.fsPath}); triggering refresh`); - commands.executeCommand('gitlens.gitExplorer.refresh'); + Logger.log(`StatusNode.onFileSystemChanged; triggering parent node refresh`); + commands.executeCommand('gitlens.gitExplorer.refreshNode', this.parent); } } \ No newline at end of file