diff --git a/CHANGELOG.md b/CHANGELOG.md index 9044b4e..99034e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [Unreleased] -## [5.2.0-beta] +## [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 file-based view of all the changed files in the working tree (enabled via `"gitlens.insiders": true`) and/or files in commits that haven't yet been pushed upstream + - 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 `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.statusFileFormat` setting to the format of the status of a working or committed file in the `GitLens` custom view ### Changed - Changes the sorting (now alphabetical) of files shown in the `GitLens` custom view diff --git a/README.md b/README.md index 08018cc..5b2f479 100644 --- a/README.md +++ b/README.md @@ -126,13 +126,17 @@ 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 upstream tracking branch (if available), and its upstream status (if available) + - 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 indicator dots on the repository icon which denote the following: - `None` - up-to-date with the upstream - `Green` - ahead of the upstream - `Red` - behind the upstream - `Yellow` - both ahead of and behind the upstream - - Provides additional nodes, if the current branch is not synchronized with the upstream, to quickly see and explore the specific commits ahead and/or behind the upstream + - Provides additional upstream status nodes, if the current branch is tracking a remote branch and + - 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 - Provides a context menu with `Open Repository in Remote`, and `Refresh` commands - `Branches` node — provides a list of the local branches @@ -357,6 +361,7 @@ GitLens is highly customizable and provides many configuration settings to allow |`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 |`gitlens.gitExplorer.stashFormat`|Specifies the format of stashed 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.stashFileFormat`|Specifies the format of a stashed file in the `GitLens` custom view
Available tokens
${file} - file name
${filePath} - file name and path
${path} - file path +|`gitlens.gitExplorer.statusFileFormat`|Specifies the format of the status of a working or committed file in the `GitLens` custom view
Available tokens
${file} - file name
${filePath} - file name and path
${path} - file path
${working} - optional indicator if the file is uncommitted ### Custom Remotes Settings diff --git a/images/dark/icon-diff.svg b/images/dark/icon-diff.svg new file mode 100644 index 0000000..055180d --- /dev/null +++ b/images/dark/icon-diff.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/images/light/icon-diff.svg b/images/light/icon-diff.svg new file mode 100644 index 0000000..384d6c2 --- /dev/null +++ b/images/light/icon-diff.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/package.json b/package.json index 384916d..2e807e0 100644 --- a/package.json +++ b/package.json @@ -443,6 +443,11 @@ "default": "${filePath}", "description": "Specifies the format of a stashed file in the `GitLens` custom view\nAvailable tokens\n ${file} - file name\n ${filePath} - file name and path\n ${path} - file path" }, + "gitlens.gitExplorer.statusFileFormat": { + "type": "string", + "default": "${working}${filePath}", + "description": "Specifies the format of the status of a working or committed file in the `GitLens` custom view\nAvailable tokens\n ${file} - file name\n ${filePath} - file name and path\n ${path} - file path\n ${working} - optional indicator if the file is uncommitted" + }, "gitlens.gitExplorer.view": { "type": "string", "default": "repository", diff --git a/src/configuration.ts b/src/configuration.ts index 935cc02..0e2cf2d 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -324,6 +324,7 @@ export interface IConfig { commitFileFormat: string; stashFormat: string; stashFileFormat: string; + statusFileFormat: string; // dateFormat: string | null; }; diff --git a/src/constants.ts b/src/constants.ts index 6e7cefe..9f433be 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -81,10 +81,12 @@ export type GlyphChars = '\u21a9' | '\u21e8' | '\u2191' | '\u2197' | + '\u2217' | '\u2713' | '\u2014' | '\u2022' | '\u2026' | + '\u270E' | '\u00a0' | '\u200b'; export const GlyphChars = { @@ -97,10 +99,12 @@ export const GlyphChars = { ArrowRightHollow: '\u21e8' as GlyphChars, ArrowUp: '\u2191' as GlyphChars, ArrowUpRight: '\u2197' as GlyphChars, + Asterisk: '\u2217' as GlyphChars, Check: '\u2713' as GlyphChars, Dash: '\u2014' as GlyphChars, Dot: '\u2022' as GlyphChars, Ellipsis: '\u2026' as GlyphChars, + Pensil: '\u270E' as GlyphChars, Space: '\u00a0' as GlyphChars, ZeroWidthSpace: '\u200b' as GlyphChars }; diff --git a/src/git/formatters/status.ts b/src/git/formatters/status.ts index 4cd0393..f38f6c3 100644 --- a/src/git/formatters/status.ts +++ b/src/git/formatters/status.ts @@ -1,7 +1,8 @@ 'use strict'; import { Strings } from '../../system'; +import { GlyphChars } from '../../constants'; import { Formatter, IFormatOptions } from './formatter'; -import { GitStatusFile, IGitStatusFile } from '../models/status'; +import { GitStatusFile, IGitStatusFile, IGitStatusFileWithCommit } from '../models/status'; import * as path from 'path'; export interface IStatusFormatOptions extends IFormatOptions { @@ -29,6 +30,11 @@ export class StatusFileFormatter extends Formatter { private onConfigurationChanged() { const cfg = workspace.getConfiguration().get(ExtensionKey)!; - if (!Objects.areEquivalent(cfg.gitExplorer, this._config && this._config.gitExplorer)) { + if (!Objects.areEquivalent(cfg.gitExplorer, this._config && this._config.gitExplorer) || + !Objects.areEquivalent(cfg.insiders, this._config && this._config.insiders)) { setTimeout(() => { this._root = this.getRootNode(window.activeTextEditor); this.refresh(); diff --git a/src/views/statusFileCommitsNode.ts b/src/views/statusFileCommitsNode.ts index 0b0506c..49c3f6c 100644 --- a/src/views/statusFileCommitsNode.ts +++ b/src/views/statusFileCommitsNode.ts @@ -3,7 +3,7 @@ import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } fr import { Commands, DiffWithPreviousCommandArgs } from '../commands'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { getGitStatusIcon, GitBranch, GitLogCommit, GitService, GitUri, IGitStatusFile, StatusFileFormatter } from '../gitService'; +import { getGitStatusIcon, GitBranch, GitLogCommit, GitService, GitUri, IGitStatusFile, IGitStatusFileWithCommit, StatusFileFormatter } from '../gitService'; import * as path from 'path'; export class StatusFileCommitsNode extends ExplorerNode { @@ -26,7 +26,7 @@ export class StatusFileCommitsNode extends ExplorerNode { } async getTreeItem(): Promise { - const item = new TreeItem(StatusFileFormatter.fromTemplate(this.git.config.gitExplorer.commitFileFormat, this.status), TreeItemCollapsibleState.Collapsed); + const item = new TreeItem(this.label, TreeItemCollapsibleState.Collapsed); item.contextValue = this.resourceType; const icon = getGitStatusIcon(this.status.status); @@ -41,9 +41,20 @@ export class StatusFileCommitsNode extends ExplorerNode { item.command = this.getCommand(); } + // Only cache the label for a single refresh + this._label = undefined; + return item; } + private _label: string | undefined; + get label() { + if (this._label === undefined) { + this._label = StatusFileFormatter.fromTemplate(this.git.config.gitExplorer.statusFileFormat, { ...this.status, commit: this.commit } as IGitStatusFileWithCommit); + } + return this._label; + } + get commit() { return this.commits[0]; } diff --git a/src/views/statusFilesNode.ts b/src/views/statusFilesNode.ts index baf73c5..a665801 100644 --- a/src/views/statusFilesNode.ts +++ b/src/views/statusFilesNode.ts @@ -2,13 +2,9 @@ import { Arrays, Iterables, Objects } from '../system'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { ExplorerNode, ResourceType, ShowAllNode } from './explorerNode'; -import { GitBranch, GitLog, GitLogCommit, GitService, GitStatus, GitUri, IGitStatusFile } from '../gitService'; +import { GitBranch, GitLog, GitLogCommit, GitService, GitStatus, GitUri, IGitStatusFileWithCommit } from '../gitService'; import { StatusFileCommitsNode } from './statusFileCommitsNode'; -interface IGitStatusFileWithCommit extends IGitStatusFile { - commit: GitLogCommit; -} - export class StatusFilesNode extends ExplorerNode { readonly resourceType: ResourceType = 'gitlens:status-files'; @@ -42,7 +38,7 @@ export class StatusFilesNode extends ExplorerNode { statuses = []; } - if (this.status.files.length !== 0) { + if (this.status.files.length !== 0 && this.git.config.insiders) { 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; })); @@ -56,6 +52,8 @@ export class StatusFilesNode extends ExplorerNode { statuses => new StatusFileCommitsNode(this.uri.repoPath!, statuses[statuses.length - 1], statuses.map(s => s.commit), this.context, this.git, this.branch)) ]; + children.sort((a: StatusFileCommitsNode, b: StatusFileCommitsNode) => (a.commit.isUncommitted ? -1 : 1) - (b.commit.isUncommitted ? -1 : 1) || a.label!.localeCompare(b.label!)); + if (log !== undefined && log.truncated) { children.push(new ShowAllNode('Show All Changes', this, this.context)); } @@ -63,8 +61,17 @@ export class StatusFilesNode extends ExplorerNode { } async getTreeItem(): Promise { - const item = new TreeItem(`Changed Files`, TreeItemCollapsibleState.Collapsed); + const stats = await this.git.getChangedFilesCount(this.status.repoPath, this.git.config.insiders ? this.status.upstream : this.range); + const files = (stats === undefined) ? 0 : stats.files; + + const label = `${files} file${files > 1 ? 's' : ''} changed`; // ${this.status.upstream === undefined ? '' : ` (ahead of ${this.status.upstream})`}`; + const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed); item.contextValue = this.resourceType; + item.iconPath = { + dark: this.context.asAbsolutePath(`images/dark/icon-diff.svg`), + light: this.context.asAbsolutePath(`images/light/icon-diff.svg`) + }; + return item; } } \ No newline at end of file diff --git a/src/views/statusNode.ts b/src/views/statusNode.ts index 4599f46..703c87f 100644 --- a/src/views/statusNode.ts +++ b/src/views/statusNode.ts @@ -30,11 +30,11 @@ export class StatusNode extends ExplorerNode { children.push(new StatusUpstreamNode(status, 'ahead', this.context, this.git)); } - if (status.files.length !== 0 || status.state.ahead && this.git.config.insiders) { + if (status.state.ahead || (status.files.length !== 0 && this.git.config.insiders)) { const range = status.state.ahead ? `${status.upstream}..${status.branch}` : undefined; - children.splice(0, 0, new StatusFilesNode(status, range, this.context, this.git)); + children.push(new StatusFilesNode(status, range, this.context, this.git)); } return children; @@ -45,14 +45,16 @@ export class StatusNode extends ExplorerNode { if (status === undefined) return new TreeItem('No repo status'); let hasChildren = false; + const hasWorkingChanges = status.files.length !== 0 && this.git.config.insiders; let label = ''; let iconSuffix = ''; if (status.upstream) { if (!status.state.ahead && !status.state.behind) { - label = `${status.branch} is up-to-date with ${status.upstream}`; + label = `${status.branch}${hasWorkingChanges ? ' has uncommitted changes and' : ''} is up-to-date with ${status.upstream}`; } else { - label = `${status.branch} is not up-to-date with ${status.upstream}`; + label = `${status.branch}${hasWorkingChanges ? ' has uncommitted changes and' : ''} is not up-to-date with ${status.upstream}`; + hasChildren = true; if (status.state.ahead && status.state.behind) { iconSuffix = '-yellow'; @@ -66,14 +68,10 @@ export class StatusNode extends ExplorerNode { } } else { - label = `${status.branch} is up-to-date`; + label = `${status.branch} ${hasWorkingChanges ? 'has uncommitted changes' : 'is clean'}`; } - if (this.git.config.insiders) { - hasChildren = hasChildren || status.files.length !== 0; - } - - const item = new TreeItem(label, hasChildren ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None); + const item = new TreeItem(label, (hasChildren || hasWorkingChanges) ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None); item.contextValue = this.resourceType; item.iconPath = { diff --git a/src/views/statusUpstreamNode.ts b/src/views/statusUpstreamNode.ts index 3529b51..a94303a 100644 --- a/src/views/statusUpstreamNode.ts +++ b/src/views/statusUpstreamNode.ts @@ -43,8 +43,8 @@ export class StatusUpstreamNode extends ExplorerNode { async getTreeItem(): Promise { const label = this.direction === 'ahead' - ? `${this.status.state.ahead} commit${this.status.state.ahead > 1 ? 's' : ''} ahead (local changes)` // of ${this.status.upstream}` - : `${this.status.state.behind} commit${this.status.state.behind > 1 ? 's' : ''} behind (remote changes)`; // ${this.status.upstream}`; + ? `${this.status.state.ahead} commit${this.status.state.ahead > 1 ? 's' : ''} (ahead of ${this.status.upstream})` + : `${this.status.state.behind} commit${this.status.state.behind > 1 ? 's' : ''} (behind ${this.status.upstream})`; const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed); item.contextValue = this.resourceType;