From 393ec351f0a00ceb99631a3b8f3a2e2b8c963e4a Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sat, 23 Sep 2017 16:29:56 -0400 Subject: [PATCH] Adds gitlens.gitExplorer.includeWorkingTree setting Adds auto-update for working trree Fixes issues with working tree status --- CHANGELOG.md | 5 +++-- README.md | 5 +++-- package.json | 5 +++++ src/configuration.ts | 1 + src/git/git.ts | 8 ++++++++ src/gitService.ts | 43 ++++++++++++++++++++++++++++++++++++---- src/views/gitExplorer.ts | 22 ++++++++++----------- src/views/statusFilesNode.ts | 18 +++++++++++------ src/views/statusNode.ts | 47 +++++++++++++++++++++++++++++++++++++------- 9 files changed, 122 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8984edd..2ab7e57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [5.2.0-beta] - 2017-09-20 ### Added -- Adds working tree status (enabled via `"gitlens.insiders": true`) to the `Repository Status` node in the `GitLens` custom view - Adds new `Changed Files` node to the `Repository Status` node of the `GitLens` custom view's `Repository View` -- closes [#139](https://github.com/eamodio/vscode-gitlens/issues/139) - Provides a at-a-glance view of all "working" changes - Expands to a file-based view of all changed files in the working tree (enabled via `"gitlens.insiders": true`) and/or all files in all commits ahead of the upstream +- Adds optional (on by default) working tree status information to the `Repository Status` node in the `GitLens` custom view +- Adds `auto` value to `gitlens.gitExplorer.view` setting - closes [#150](https://github.com/eamodio/vscode-gitlens/issues/150) - Adds `gitlens.gitExplorer.enabled` setting to specify whether or not to show the `GitLens` custom view - closes [#144](https://github.com/eamodio/vscode-gitlens/issues/144) +- Adds `gitlens.gitExplorer.includeWorkingTree` setting to specify whether or not to include working tree files inside the `Repository Status` node of the `GitLens` custom view - Adds `gitlens.gitExplorer.statusFileFormat` setting to the format of the status of a working or committed file in the `GitLens` custom view -- Adds `auto` value to `gitlens.gitExplorer.view` setting - closes [#150](https://github.com/eamodio/vscode-gitlens/issues/150) ### Changed - Changes the sorting (now alphabetical) of files shown in the `GitLens` custom view diff --git a/README.md b/README.md index f43a069..dc1edae 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ GitLens provides an unobtrusive blame annotation at the end of the current line, ![GitLens Repository view](https://raw.githubusercontent.com/eamodio/vscode-gitlens/master/images/screenshot-git-custom-view-repository.png) - `Repository Status` node — provides the status of the repository - - Provides the name of the current branch, its working tree status (enabled via `"gitlens.insiders": true`), and its upstream tracking branch and status (if available) + - Provides the name of the current branch, [optionally](#gitlens-custom-view-settings) its working tree status, and its upstream tracking branch and status (if available) - Provides indicator dots on the repository icon which denote the following: - `None` - up-to-date with the upstream - `Green` - ahead of the upstream @@ -136,7 +136,7 @@ GitLens provides an unobtrusive blame annotation at the end of the current line, - is behind the upstream — quickly see and explore the specific commits behind the upstream (i.e. commits that haven't been pulled) - is ahead of the upstream — quickly see and explore the specific commits ahead of the upstream (i.e. commits that haven't been pushed) - `Changed Files` node — provides a at-a-glance view of all "working" changes - - Expands to a file-based view of all changed files in the working tree (enabled via `"gitlens.insiders": true`) and/or all files in all commits ahead of the upstream + - Expands to a file-based view of all changed files in the working tree ([optionally](#gitlens-custom-view-settings)) and/or all files in all commits ahead of the upstream - Provides a context menu with `Open Repository in Remote`, and `Refresh` commands - `Branches` node — provides a list of the local branches @@ -356,6 +356,7 @@ GitLens is highly customizable and provides many configuration settings to allow |-----|------------ |`gitlens.gitExplorer.enabled`|Specifies whether or not to show the `GitLens` custom view" |`gitlens.gitExplorer.view`|Specifies the starting view (mode) of the `GitLens` custom view
`auto` - shows the last selected view, defaults to `repository`
`history` - shows the commit history of the active file
`repository` - shows a repository explorer" +|`gitlens.gitExplorer.includeWorkingTree`|Specifies whether or not to include working tree files inside the `Repository Status` node of the `GitLens` custom view |`gitlens.gitExplorer.showTrackingBranch`|Specifies whether or not to show the tracking branch when displaying local branches in the `GitLens` custom view" |`gitlens.gitExplorer.commitFormat`|Specifies the format of committed changes in the `GitLens` custom view
Available tokens
${id} - commit id
${author} - commit author
${message} - commit message
${ago} - relative commit date (e.g. 1 day ago)
${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)
${authorAgo} - commit author, relative commit date
See https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting |`gitlens.gitExplorer.commitFileFormat`|Specifies the format of a committed file in the `GitLens` custom view
Available tokens
${file} - file name
${filePath} - file name and path
${path} - file path diff --git a/package.json b/package.json index d97a085..fa10788 100644 --- a/package.json +++ b/package.json @@ -428,6 +428,11 @@ "default": true, "description": "Specifies whether or not to show the `GitLens` custom view" }, + "gitlens.gitExplorer.includeWorkingTree": { + "type": "boolean", + "default": true, + "description": "Specifies whether or not to include working tree files inside the `Repository Status` node of the `GitLens` custom view" + }, "gitlens.gitExplorer.showTrackingBranch": { "type": "boolean", "default": true, diff --git a/src/configuration.ts b/src/configuration.ts index 0e2cf2d..7de453f 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -319,6 +319,7 @@ export interface IConfig { gitExplorer: { enabled: boolean; view: GitExplorerView; + includeWorkingTree: boolean; showTrackingBranch: boolean; commitFormat: string; commitFileFormat: string; diff --git a/src/git/git.ts b/src/git/git.ts index bc65f1e..f7098c7 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -340,6 +340,14 @@ export class Git { return gitCommand({ cwd: repoPath }, ...params, ...search); } + static log_shortstat(repoPath: string, sha?: string) { + const params = [`log`, `--shortstat`, `--oneline`]; + if (sha) { + params.push(sha); + } + return gitCommand({ cwd: repoPath }, ...params); + } + static async ls_files(repoPath: string, fileName: string): Promise { try { return await gitCommand({ cwd: repoPath, overrideErrorHandling: true }, 'ls-files', fileName); diff --git a/src/gitService.ts b/src/gitService.ts index b64cdbf..41b3598 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -86,6 +86,11 @@ export class GitService extends Disposable { return this._onDidChangeGitCache.event; } + private _onDidChangeFileSystem = new EventEmitter(); + get onDidChangeFileSystem(): Event { + return this._onDidChangeFileSystem.event; + } + private _onDidChangeRepo = new EventEmitter(); get onDidChangeRepo(): Event { return this._onDidChangeRepo.event; @@ -121,14 +126,16 @@ export class GitService extends Disposable { } dispose() { + this.stopWatchingFileSystem(); + + this._repoWatcher && this._repoWatcher.dispose(); + this._repoWatcher = undefined; + this._disposable && this._disposable.dispose(); this._cacheDisposable && this._cacheDisposable.dispose(); this._cacheDisposable = undefined; - this._repoWatcher && this._repoWatcher.dispose(); - this._repoWatcher = undefined; - this._gitCache.clear(); this._remotesCache.clear(); this._uriCache.clear(); @@ -602,7 +609,8 @@ export class GitService extends Disposable { } async getChangedFilesCount(repoPath: string, sha?: string): Promise { - return GitDiffParser.parseShortStat(await Git.diff_shortstat(repoPath, sha)); + const data = await Git.diff_shortstat(repoPath, sha); + return GitDiffParser.parseShortStat(data); } async getConfig(key: string, repoPath?: string): Promise { @@ -1034,6 +1042,33 @@ export class GitService extends Disposable { return Git.difftool_dirDiff(repoPath, sha1, sha2); } + private _fsWatcherDisposable: Disposable | undefined; + + startWatchingFileSystem() { + 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; + + debouncedFn(uri); + }; + + const watcher = workspace.createFileSystemWatcher(`**`); + this._fsWatcherDisposable = Disposable.from( + watcher, + watcher.onDidChange(fn), + watcher.onDidCreate(fn), + watcher.onDidDelete(fn) + ); + } + + stopWatchingFileSystem() { + this._fsWatcherDisposable && this._fsWatcherDisposable.dispose(); + this._fsWatcherDisposable = undefined; + } + stashApply(repoPath: string, stashName: string, deleteAfter: boolean = false) { Logger.log(`stashApply('${repoPath}', ${stashName}, ${deleteAfter})`); diff --git a/src/views/gitExplorer.ts b/src/views/gitExplorer.ts index 8e83ba0..5154f71 100644 --- a/src/views/gitExplorer.ts +++ b/src/views/gitExplorer.ts @@ -81,14 +81,14 @@ export class GitExplorer implements TreeDataProvider { } private getRootNode(editor?: TextEditor): ExplorerNode | undefined { - const uri = new GitUri(Uri.file(this.git.repoPath), { repoPath: this.git.repoPath, fileName: this.git.repoPath }); - switch (this._view) { - case GitExplorerView.History: return this.getHistoryNode(editor || window.activeTextEditor); - case GitExplorerView.Repository: return new RepositoryNode(uri, this.context, this.git); - } + case GitExplorerView.History: + return this.getHistoryNode(editor || window.activeTextEditor); - return undefined; + default: + const uri = new GitUri(Uri.file(this.git.repoPath), { repoPath: this.git.repoPath, fileName: this.git.repoPath }); + return new RepositoryNode(uri, this.context, this.git); + } } private getHistoryNode(editor: TextEditor | undefined): ExplorerNode | undefined { @@ -114,11 +114,7 @@ export class GitExplorer implements TreeDataProvider { private onConfigurationChanged() { const cfg = workspace.getConfiguration().get(ExtensionKey)!; - let changed = false; - if (!Objects.areEquivalent(cfg.gitExplorer, this._config && this._config.gitExplorer) || - !Objects.areEquivalent(cfg.insiders, this._config && this._config.insiders)) { - changed = true; - } + const changed = !Objects.areEquivalent(cfg.gitExplorer, this._config && this._config.gitExplorer); this._config = cfg; @@ -165,6 +161,10 @@ export class GitExplorer implements TreeDataProvider { this._view = view; setCommandContext(CommandContext.GitExplorerView, this._view); + + if (view !== GitExplorerView.Repository) { + this.git.stopWatchingFileSystem(); + } } switchTo(view: GitExplorerView) { diff --git a/src/views/statusFilesNode.ts b/src/views/statusFilesNode.ts index 6a7d627..6de7780 100644 --- a/src/views/statusFilesNode.ts +++ b/src/views/statusFilesNode.ts @@ -38,7 +38,7 @@ export class StatusFilesNode extends ExplorerNode { statuses = []; } - if (this.status.files.length !== 0 && this.git.config.insiders) { + if (this.status.files.length !== 0 && this.includeWorkingTree) { statuses.splice(0, 0, ...this.status.files.map(s => { return { ...s, commit: new GitLogCommit('file', this.status.repoPath, GitService.uncommittedSha, s.fileName, 'You', new Date(), '', s.status, [s], s.originalFileName, 'HEAD', s.fileName) } as IGitStatusFileWithCommit; })); @@ -61,12 +61,13 @@ export class StatusFilesNode extends ExplorerNode { } async getTreeItem(): Promise { - // Start with any untracked files, since they won't be included in the next call - let files = (this.status.files === undefined) ? 0 : this.status.files.filter(s => s.status === '?').length; + let files = (this.status.files !== undefined && this.includeWorkingTree) ? this.status.files.length : 0; - const stats = await this.git.getChangedFilesCount(this.status.repoPath, this.git.config.insiders ? this.status.upstream : this.range); - if (stats !== undefined) { - files += stats.files; + if (this.status.upstream !== undefined) { + const stats = await this.git.getChangedFilesCount(this.status.repoPath, `${this.status.upstream}...`); + if (stats !== undefined) { + files += stats.files; + } } const label = `${files} file${files > 1 ? 's' : ''} changed`; // ${this.status.upstream === undefined ? '' : ` (ahead of ${this.status.upstream})`}`; @@ -79,4 +80,9 @@ export class StatusFilesNode extends ExplorerNode { return item; } + + private get includeWorkingTree(): boolean { + return this.git.config.gitExplorer.includeWorkingTree; + } + } \ No newline at end of file diff --git a/src/views/statusNode.ts b/src/views/statusNode.ts index 703c87f..10209ac 100644 --- a/src/views/statusNode.ts +++ b/src/views/statusNode.ts @@ -1,9 +1,11 @@ -import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { commands, Disposable, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { GitService, GitUri } from '../gitService'; +import { GitService, GitStatus, GitUri } from '../gitService'; import { StatusFilesNode } from './statusFilesNode'; import { StatusUpstreamNode } from './statusUpstreamNode'; +let _eventDisposable: Disposable | undefined; + export class StatusNode extends ExplorerNode { readonly resourceType: ResourceType = 'gitlens:status'; @@ -30,8 +32,8 @@ export class StatusNode extends ExplorerNode { children.push(new StatusUpstreamNode(status, 'ahead', this.context, this.git)); } - if (status.state.ahead || (status.files.length !== 0 && this.git.config.insiders)) { - const range = status.state.ahead + 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)); @@ -40,12 +42,26 @@ export class StatusNode extends ExplorerNode { return children; } - async getTreeItem(): Promise { + 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'); + if (_eventDisposable !== undefined) { + _eventDisposable.dispose(); + _eventDisposable = undefined; + } + + if (this.includeWorkingTree) { + this._status = status; + + _eventDisposable = this.git.onDidChangeFileSystem(this.onFileSystemChanged, this); + this.git.startWatchingFileSystem(); + } + let hasChildren = false; - const hasWorkingChanges = status.files.length !== 0 && this.git.config.insiders; + const hasWorkingChanges = status.files.length !== 0 && this.includeWorkingTree; let label = ''; let iconSuffix = ''; if (status.upstream) { @@ -68,7 +84,7 @@ export class StatusNode extends ExplorerNode { } } else { - label = `${status.branch} ${hasWorkingChanges ? 'has uncommitted changes' : 'is clean'}`; + label = `${status.branch} ${hasWorkingChanges ? 'has uncommitted changes' : this.includeWorkingTree ? 'has no changes' : 'has nothing to commit'}`; } const item = new TreeItem(label, (hasChildren || hasWorkingChanges) ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None); @@ -81,4 +97,21 @@ export class StatusNode extends ExplorerNode { return item; } + + private get includeWorkingTree(): boolean { + return this.git.config.gitExplorer.includeWorkingTree; + } + + private async onFileSystemChanged(uri: Uri) { + const status = await this.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))) { + commands.executeCommand('gitlens.gitExplorer.refreshNode', this); + } + + commands.executeCommand('gitlens.gitExplorer.refresh'); + } } \ No newline at end of file