From 71d17bcc2fe40edc96de59c085e56be952d4b794 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sun, 17 Sep 2017 02:06:18 -0400 Subject: [PATCH] Closes #139 - adds changed files node to repository status Reworks commit-file nodes --- CHANGELOG.md | 2 ++ package.json | 47 ++++++++++++++++++++++++- src/git/formatters/commit.ts | 2 +- src/gitService.ts | 1 + src/system/array.ts | 2 +- src/views/branchHistoryNode.ts | 18 ++++++---- src/views/branchesNode.ts | 8 +++-- src/views/commitFileNode.ts | 47 +++++++++++++++++++++---- src/views/commitNode.ts | 46 +++++++++---------------- src/views/explorerNode.ts | 9 +++-- src/views/explorerNodes.ts | 4 ++- src/views/fileHistoryNode.ts | 12 ++++--- src/views/historyNode.ts | 8 +++-- src/views/remoteNode.ts | 9 +++-- src/views/remotesNode.ts | 6 +++- src/views/repositoryNode.ts | 8 +++-- src/views/stashFileNode.ts | 19 +++++++++-- src/views/stashNode.ts | 8 +++-- src/views/stashesNode.ts | 8 +++-- src/views/statusFileCommitsNode.ts | 68 ++++++++++++++++++++++++++++++++++++ src/views/statusFilesNode.ts | 70 ++++++++++++++++++++++++++++++++++++++ src/views/statusNode.ts | 26 +++++++++++--- src/views/statusUpstreamNode.ts | 16 ++++++--- 23 files changed, 365 insertions(+), 79 deletions(-) create mode 100644 src/views/statusFileCommitsNode.ts create mode 100644 src/views/statusFilesNode.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4732b53..caa7449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [Unreleased] ### Added +- 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 and/or files in commits that haven't yet been pushed 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) ## [5.1.0] - 2017-09-15 diff --git a/package.json b/package.json index ea61120..a17d4c2 100644 --- a/package.json +++ b/package.json @@ -1743,8 +1743,53 @@ "group": "1_gitlens@1" }, { + "command": "gitlens.gitExplorer.openChanges", + "when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file", + "group": "1_gitlens@1" + }, + { + "command": "gitlens.gitExplorer.openChangesWithWorking", + "when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file", + "group": "1_gitlens@2" + }, + { + "command": "gitlens.gitExplorer.openFile", + "when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file", + "group": "2_gitlens@1" + }, + { + "command": "gitlens.gitExplorer.openFileRevision", + "when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file", + "group": "2_gitlens@2" + }, + { + "command": "gitlens.openFileInRemote", + "when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:status-file", + "group": "3_gitlens@1" + }, + { + "command": "gitlens.showQuickFileHistory", + "when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file && gitlens:gitExplorer:view == repository", + "group": "5_gitlens@1" + }, + { + "command": "gitlens.showQuickCommitFileDetails", + "when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file", + "group": "5_gitlens@2" + }, + { + "command": "gitlens.gitExplorer.openFile", + "when": "view == gitlens.gitExplorer && viewItem == gitlens:status-file-commits", + "group": "1_gitlens@1" + }, + { + "command": "gitlens.openFileInRemote", + "when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:status-file-commits", + "group": "1_gitlens@2" + }, + { "command": "gitlens.gitExplorer.refresh", - "when": "view == gitlens.gitExplorer && viewItem != gitlens:commit-file && viewItem != gitlens:stash-file", + "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/formatters/commit.ts b/src/git/formatters/commit.ts index a25aa8a..e896c7f 100644 --- a/src/git/formatters/commit.ts +++ b/src/git/formatters/commit.ts @@ -39,7 +39,7 @@ export class CommitFormatter extends Formatter } get id() { - return this._item.shortSha; + return this._item.isUncommitted ? 'index' : this._item.shortSha; } get message() { diff --git a/src/gitService.ts b/src/gitService.ts index fb16e7b..f7a2608 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -74,6 +74,7 @@ export const RepoChangedReasons = { export class GitService extends Disposable { static fakeSha = 'ffffffffffffffffffffffffffffffffffffffff'; + static uncommittedSha = '0000000000000000000000000000000000000000'; private _onDidBlameFail = new EventEmitter(); get onDidBlameFail(): Event { diff --git a/src/system/array.ts b/src/system/array.ts index 8a7695f..13afa42 100644 --- a/src/system/array.ts +++ b/src/system/array.ts @@ -1,7 +1,7 @@ 'use strict'; export namespace Arrays { - export function groupBy(array: T[], accessor: (item: T) => any): T[] { + export function groupBy(array: T[], accessor: (item: T) => string): { [key: string]: T[] } { return array.reduce((previous, current) => { const value = accessor(current); previous[value] = previous[value] || []; diff --git a/src/views/branchHistoryNode.ts b/src/views/branchHistoryNode.ts index 5cff697..27ee27a 100644 --- a/src/views/branchHistoryNode.ts +++ b/src/views/branchHistoryNode.ts @@ -3,7 +3,7 @@ import { Iterables } from '../system'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { CommitNode } from './commitNode'; import { GlyphChars } from '../constants'; -import { ExplorerNode, ResourceType, ShowAllCommitsNode } from './explorerNode'; +import { ExplorerNode, ResourceType, ShowAllNode } from './explorerNode'; import { GitBranch, GitService, GitUri } from '../gitService'; export class BranchHistoryNode extends ExplorerNode { @@ -12,7 +12,12 @@ export class BranchHistoryNode extends ExplorerNode { maxCount: number | undefined = undefined; - constructor(public readonly branch: GitBranch, uri: GitUri, private readonly template: string, protected readonly context: ExtensionContext, protected readonly git: GitService) { + constructor( + public readonly branch: GitBranch, + uri: GitUri, + protected readonly context: ExtensionContext, + protected readonly git: GitService + ) { super(uri); } @@ -20,10 +25,11 @@ export class BranchHistoryNode extends ExplorerNode { const log = await this.git.getLogForRepo(this.uri.repoPath!, this.branch.name, this.maxCount); if (log === undefined) return []; - const children = Iterables.map(log.commits.values(), c => new CommitNode(c, this.template, this.context, this.git, this.branch)); - if (!log.truncated) return [...children]; - - return [...children, new ShowAllCommitsNode(this, this.context)]; + const children: (CommitNode | ShowAllNode)[] = [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.context, this.git, this.branch))]; + if (log.truncated) { + children.push(new ShowAllNode('Show All Commits', this, this.context)); + } + return children; } async getTreeItem(): Promise { diff --git a/src/views/branchesNode.ts b/src/views/branchesNode.ts index 991e527..5b95393 100644 --- a/src/views/branchesNode.ts +++ b/src/views/branchesNode.ts @@ -9,7 +9,11 @@ export class BranchesNode extends ExplorerNode { readonly resourceType: ResourceType = 'gitlens:branches'; - constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { + constructor( + uri: GitUri, + protected readonly context: ExtensionContext, + protected readonly git: GitService + ) { super(uri); } @@ -18,7 +22,7 @@ export class BranchesNode extends ExplorerNode { if (branches === undefined) return []; branches.sort((a, b) => (a.current ? -1 : 1) - (b.current ? -1 : 1) || a.name.localeCompare(b.name)); - return [...Iterables.filterMap(branches, b => b.remote ? undefined : new BranchHistoryNode(b, this.uri, this.git.config.gitExplorer.commitFormat, this.context, this.git))]; + return [...Iterables.filterMap(branches, b => b.remote ? undefined : new BranchHistoryNode(b, this.uri, this.context, this.git))]; } async getTreeItem(): Promise { diff --git a/src/views/commitFileNode.ts b/src/views/commitFileNode.ts index 3ffb5b9..bc2feac 100644 --- a/src/views/commitFileNode.ts +++ b/src/views/commitFileNode.ts @@ -2,19 +2,36 @@ import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { Commands, DiffWithPreviousCommandArgs } from '../commands'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { getGitStatusIcon, GitBranch, GitCommit, GitService, GitUri, IGitStatusFile, StatusFileFormatter } from '../gitService'; +import { CommitFormatter, getGitStatusIcon, GitBranch, GitCommit, GitService, GitUri, ICommitFormatOptions, IGitStatusFile, StatusFileFormatter } from '../gitService'; import * as path from 'path'; +export enum CommitFileNodeDisplayAs { + CommitLabel = 1 << 0, + CommitIcon = 1 << 1, + FileLabel = 1 << 2, + StatusIcon = 1 << 3, + + Commit = CommitLabel | CommitIcon, + File = FileLabel | StatusIcon +} + export class CommitFileNode extends ExplorerNode { readonly resourceType: ResourceType = 'gitlens:commit-file'; - constructor(public readonly status: IGitStatusFile, public commit: GitCommit, protected readonly context: ExtensionContext, protected readonly git: GitService, public readonly branch?: GitBranch) { + constructor( + public readonly status: IGitStatusFile, + public commit: GitCommit, + protected readonly context: ExtensionContext, + protected readonly git: GitService, + private displayAs: CommitFileNodeDisplayAs = CommitFileNodeDisplayAs.Commit, + public readonly branch?: GitBranch + ) { super(new GitUri(Uri.file(path.resolve(commit.repoPath, status.fileName)), { repoPath: commit.repoPath, fileName: status.fileName, sha: commit.sha })); } - getChildren(): Promise { - return Promise.resolve([]); + async getChildren(): Promise { + return []; } async getTreeItem(): Promise { @@ -25,10 +42,20 @@ export class CommitFileNode extends ExplorerNode { } } - const item = new TreeItem(StatusFileFormatter.fromTemplate(this.git.config.gitExplorer.commitFileFormat, this.status), TreeItemCollapsibleState.None); + const label = (this.displayAs & CommitFileNodeDisplayAs.CommitLabel) + ? CommitFormatter.fromTemplate(this.getCommitTemplate(), this.commit, { + truncateMessageAtNewLine: true, + dataFormat: this.git.config.defaultDateFormat + } as ICommitFormatOptions) + : StatusFileFormatter.fromTemplate(this.getCommitFileTemplate(), this.status); + + const item = new TreeItem(label, TreeItemCollapsibleState.None); item.contextValue = this.resourceType; - const icon = getGitStatusIcon(this.status.status); + const icon = (this.displayAs & CommitFileNodeDisplayAs.CommitIcon) + ? 'icon-commit.svg' + : getGitStatusIcon(this.status.status); + item.iconPath = { dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)), light: this.context.asAbsolutePath(path.join('images', 'light', icon)) @@ -39,6 +66,14 @@ export class CommitFileNode extends ExplorerNode { return item; } + protected getCommitTemplate() { + return this.git.config.gitExplorer.commitFormat; + } + + protected getCommitFileTemplate() { + return this.git.config.gitExplorer.commitFileFormat; + } + getCommand(): Command | undefined { return { title: 'Compare File with Previous Revision', diff --git a/src/views/commitNode.ts b/src/views/commitNode.ts index 7b93c53..5bbdf1f 100644 --- a/src/views/commitNode.ts +++ b/src/views/commitNode.ts @@ -2,58 +2,44 @@ import { Iterables } from '../system'; import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Commands, DiffWithPreviousCommandArgs } from '../commands'; -import { CommitFileNode } from './commitFileNode'; +import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { CommitFormatter, getGitStatusIcon, GitBranch, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService'; -import * as path from 'path'; +import { CommitFormatter, GitBranch, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService'; export class CommitNode extends ExplorerNode { readonly resourceType: ResourceType = 'gitlens:commit'; - constructor(public readonly commit: GitLogCommit, private readonly template: string, protected readonly context: ExtensionContext, protected readonly git: GitService, public readonly branch?: GitBranch) { + constructor( + public readonly commit: GitLogCommit, + protected readonly context: ExtensionContext, + protected readonly git: GitService, + public readonly branch?: GitBranch + ) { super(new GitUri(commit.uri, commit)); } async getChildren(): Promise { - if (this.commit.type === 'file') Promise.resolve([]); - const log = await this.git.getLogForRepo(this.commit.repoPath, this.commit.sha, 1); if (log === undefined) return []; const commit = Iterables.first(log.commits.values()); if (commit === undefined) return []; - return [...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git, this.branch))]; + return [...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git, CommitFileNodeDisplayAs.File, this.branch))]; } getTreeItem(): TreeItem { - const item = new TreeItem(CommitFormatter.fromTemplate(this.template, this.commit, { + const item = new TreeItem(CommitFormatter.fromTemplate(this.git.config.gitExplorer.commitFormat, this.commit, { truncateMessageAtNewLine: true, dataFormat: this.git.config.defaultDateFormat - } as ICommitFormatOptions)); - - if (this.commit.type === 'file') { - item.collapsibleState = TreeItemCollapsibleState.None; - item.command = this.getCommand(); - const resourceType: ResourceType = 'gitlens:commit-file'; - item.contextValue = resourceType; + } as ICommitFormatOptions), TreeItemCollapsibleState.Collapsed); - const icon = getGitStatusIcon(this.commit.status!); - item.iconPath = { - dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)), - light: this.context.asAbsolutePath(path.join('images', 'light', icon)) - }; - } - else { - item.collapsibleState = TreeItemCollapsibleState.Collapsed; - item.contextValue = this.resourceType; - - item.iconPath = { - dark: this.context.asAbsolutePath('images/dark/icon-commit.svg'), - light: this.context.asAbsolutePath('images/light/icon-commit.svg') - }; - } + item.contextValue = this.resourceType; + item.iconPath = { + dark: this.context.asAbsolutePath('images/dark/icon-commit.svg'), + light: this.context.asAbsolutePath('images/light/icon-commit.svg') + }; return item; } diff --git a/src/views/explorerNode.ts b/src/views/explorerNode.ts index b720375..3c71eff 100644 --- a/src/views/explorerNode.ts +++ b/src/views/explorerNode.ts @@ -20,6 +20,9 @@ export declare type ResourceType = 'gitlens:stash-file' | 'gitlens:stashes' | 'gitlens:status' | + 'gitlens:status-file' | + 'gitlens:status-files' | + 'gitlens:status-file-commits' | 'gitlens:status-upstream'; export abstract class ExplorerNode { @@ -88,11 +91,11 @@ export class PagerNode extends ExplorerNode { } } -export class ShowAllCommitsNode extends PagerNode { +export class ShowAllNode extends PagerNode { args: RefreshNodeCommandArgs = { maxCount: 0 }; - constructor(node: ExplorerNode, context: ExtensionContext) { - super(`Show All Commits ${GlyphChars.Space}${GlyphChars.Dash}${GlyphChars.Space} this may take a while`, node, context); + constructor(message: string, node: ExplorerNode, context: ExtensionContext) { + super(`${message} ${GlyphChars.Space}${GlyphChars.Dash}${GlyphChars.Space} this may take a while`, node, context); } } \ No newline at end of file diff --git a/src/views/explorerNodes.ts b/src/views/explorerNodes.ts index e08134e..7fb4d53 100644 --- a/src/views/explorerNodes.ts +++ b/src/views/explorerNodes.ts @@ -10,8 +10,10 @@ export * from './historyNode'; export * from './remoteNode'; export * from './remotesNode'; export * from './repositoryNode'; +export * from './stashesNode'; export * from './stashFileNode'; export * from './stashNode'; -export * from './stashesNode'; +export * from './statusFileCommitsNode'; +export * from './statusFilesNode'; export * from './statusNode'; export * from './statusUpstreamNode'; \ No newline at end of file diff --git a/src/views/fileHistoryNode.ts b/src/views/fileHistoryNode.ts index 8497d59..97895e8 100644 --- a/src/views/fileHistoryNode.ts +++ b/src/views/fileHistoryNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { Iterables } from '../system'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { CommitNode } from './commitNode'; +import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode'; import { ExplorerNode, MessageNode, ResourceType } from './explorerNode'; import { GitService, GitUri } from '../gitService'; @@ -9,15 +9,19 @@ export class FileHistoryNode extends ExplorerNode { readonly resourceType: ResourceType = 'gitlens:file-history'; - constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { + constructor( + uri: GitUri, + protected readonly context: ExtensionContext, + protected readonly git: GitService + ) { super(uri); - } + } async getChildren(): Promise { const log = await this.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 CommitNode(c, this.git.config.gitExplorer.commitFormat, this.context, this.git))]; + return [...Iterables.map(log.commits.values(), c => new CommitFileNode(c.fileStatuses[0], c, this.context, this.git, CommitFileNodeDisplayAs.CommitLabel | CommitFileNodeDisplayAs.StatusIcon))]; } getTreeItem(): TreeItem { diff --git a/src/views/historyNode.ts b/src/views/historyNode.ts index 96acc31..f920f65 100644 --- a/src/views/historyNode.ts +++ b/src/views/historyNode.ts @@ -8,9 +8,13 @@ export class HistoryNode extends ExplorerNode { readonly resourceType: ResourceType = 'gitlens:history'; - constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { + constructor( + uri: GitUri, + protected readonly context: ExtensionContext, + protected readonly git: GitService + ) { super(uri); - } + } async getChildren(): Promise { return [new FileHistoryNode(this.uri, this.context, this.git)]; diff --git a/src/views/remoteNode.ts b/src/views/remoteNode.ts index 01544f3..f23dce2 100644 --- a/src/views/remoteNode.ts +++ b/src/views/remoteNode.ts @@ -10,7 +10,12 @@ export class RemoteNode extends ExplorerNode { readonly resourceType: ResourceType = 'gitlens:remote'; - constructor(public readonly remote: GitRemote, uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { + constructor( + public readonly remote: GitRemote, + uri: GitUri, + protected readonly context: ExtensionContext, + protected readonly git: GitService + ) { super(uri); } @@ -19,7 +24,7 @@ export class RemoteNode extends ExplorerNode { if (branches === undefined) return []; branches.sort((a, b) => a.name.localeCompare(b.name)); - return [...Iterables.filterMap(branches, b => !b.remote || !b.name.startsWith(this.remote.name) ? undefined : new BranchHistoryNode(b, this.uri, this.git.config.gitExplorer.commitFormat, this.context, this.git))]; + return [...Iterables.filterMap(branches, b => !b.remote || !b.name.startsWith(this.remote.name) ? undefined : new BranchHistoryNode(b, this.uri, this.context, this.git))]; } getTreeItem(): TreeItem { diff --git a/src/views/remotesNode.ts b/src/views/remotesNode.ts index ab5a547..5bd7b26 100644 --- a/src/views/remotesNode.ts +++ b/src/views/remotesNode.ts @@ -9,7 +9,11 @@ export class RemotesNode extends ExplorerNode { readonly resourceType: ResourceType = 'gitlens:remotes'; - constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { + constructor( + uri: GitUri, + protected readonly context: ExtensionContext, + protected readonly git: GitService + ) { super(uri); } diff --git a/src/views/repositoryNode.ts b/src/views/repositoryNode.ts index d69519e..551fe38 100644 --- a/src/views/repositoryNode.ts +++ b/src/views/repositoryNode.ts @@ -12,9 +12,13 @@ export class RepositoryNode extends ExplorerNode { readonly resourceType: ResourceType = 'gitlens:repository'; - constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { + constructor( + uri: GitUri, + protected readonly context: ExtensionContext, + protected readonly git: GitService + ) { super(uri); - } + } async getChildren(): Promise { return [ diff --git a/src/views/stashFileNode.ts b/src/views/stashFileNode.ts index 16dbbd5..ca8b05b 100644 --- a/src/views/stashFileNode.ts +++ b/src/views/stashFileNode.ts @@ -2,13 +2,26 @@ import { ExtensionContext } from 'vscode'; import { ResourceType } from './explorerNode'; import { GitService, GitStashCommit, IGitStatusFile } from '../gitService'; -import { CommitFileNode } from './commitFileNode'; +import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode'; export class StashFileNode extends CommitFileNode { readonly resourceType: ResourceType = 'gitlens:stash-file'; - constructor(readonly status: IGitStatusFile, readonly commit: GitStashCommit, readonly context: ExtensionContext, readonly git: GitService) { - super(status, commit, context, git); + constructor( + readonly status: IGitStatusFile, + readonly commit: GitStashCommit, + readonly context: ExtensionContext, + readonly git: GitService + ) { + super(status, commit, context, git, CommitFileNodeDisplayAs.File); + } + + protected getCommitTemplate() { + return this.git.config.gitExplorer.stashFormat; + } + + protected getCommitFileTemplate() { + return this.git.config.gitExplorer.stashFileFormat; } } \ No newline at end of file diff --git a/src/views/stashNode.ts b/src/views/stashNode.ts index 1bb609f..deefd1c 100644 --- a/src/views/stashNode.ts +++ b/src/views/stashNode.ts @@ -9,7 +9,11 @@ export class StashNode extends ExplorerNode { readonly resourceType: ResourceType = 'gitlens:stash'; - constructor(public readonly commit: GitStashCommit, protected readonly context: ExtensionContext, protected readonly git: GitService) { + constructor( + public readonly commit: GitStashCommit, + protected readonly context: ExtensionContext, + protected readonly git: GitService + ) { super(new GitUri(commit.uri, commit)); } @@ -27,7 +31,7 @@ export class StashNode extends ExplorerNode { } } - return Promise.resolve(statuses.map(s => new StashFileNode(s, this.commit, this.context, this.git))); + return statuses.map(s => new StashFileNode(s, this.commit, this.context, this.git)); } getTreeItem(): TreeItem { diff --git a/src/views/stashesNode.ts b/src/views/stashesNode.ts index 42f1035..e8b64eb 100644 --- a/src/views/stashesNode.ts +++ b/src/views/stashesNode.ts @@ -9,9 +9,13 @@ export class StashesNode extends ExplorerNode { readonly resourceType: ResourceType = 'gitlens:stashes'; - constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { + constructor( + uri: GitUri, + protected readonly context: ExtensionContext, + protected readonly git: GitService + ) { super(uri); - } + } async getChildren(): Promise { const stash = await this.git.getStashList(this.uri.repoPath!); diff --git a/src/views/statusFileCommitsNode.ts b/src/views/statusFileCommitsNode.ts new file mode 100644 index 0000000..0b0506c --- /dev/null +++ b/src/views/statusFileCommitsNode.ts @@ -0,0 +1,68 @@ +'use strict'; +import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; +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 * as path from 'path'; + +export class StatusFileCommitsNode extends ExplorerNode { + + readonly resourceType: ResourceType = 'gitlens:status-file-commits'; + + constructor( + repoPath: string, + public readonly status: IGitStatusFile, + public commits: GitLogCommit[], + protected readonly context: ExtensionContext, + protected readonly git: GitService, + public readonly branch?: GitBranch + ) { + super(new GitUri(Uri.file(path.resolve(repoPath, status.fileName)), { repoPath: repoPath, fileName: status.fileName, sha: 'HEAD' })); + } + + async getChildren(): Promise { + return this.commits.map(c => new CommitFileNode(this.status, c, this.context, this.git, CommitFileNodeDisplayAs.Commit, this.branch)); + } + + async getTreeItem(): Promise { + const item = new TreeItem(StatusFileFormatter.fromTemplate(this.git.config.gitExplorer.commitFileFormat, this.status), TreeItemCollapsibleState.Collapsed); + item.contextValue = this.resourceType; + + const icon = getGitStatusIcon(this.status.status); + item.iconPath = { + dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)), + light: this.context.asAbsolutePath(path.join('images', 'light', icon)) + }; + + if (this.commits.length === 1 && this.commits[0].isUncommitted) { + item.collapsibleState = TreeItemCollapsibleState.None; + item.contextValue = 'gitlens:status-file' as ResourceType; + item.command = this.getCommand(); + } + + return item; + } + + get commit() { + return this.commits[0]; + } + + getCommand(): Command | undefined { + return { + title: 'Compare File with Previous Revision', + command: Commands.DiffWithPrevious, + arguments: [ + GitUri.fromFileStatus(this.status, this.uri.repoPath!), + { + commit: this.commit, + line: 0, + showOptions: { + preserveFocus: true, + preview: true + } + } as DiffWithPreviousCommandArgs + ] + }; + } +} \ No newline at end of file diff --git a/src/views/statusFilesNode.ts b/src/views/statusFilesNode.ts new file mode 100644 index 0000000..baf73c5 --- /dev/null +++ b/src/views/statusFilesNode.ts @@ -0,0 +1,70 @@ +'use strict'; +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 { StatusFileCommitsNode } from './statusFileCommitsNode'; + +interface IGitStatusFileWithCommit extends IGitStatusFile { + commit: GitLogCommit; +} + +export class StatusFilesNode extends ExplorerNode { + + readonly resourceType: ResourceType = 'gitlens:status-files'; + + maxCount: number | undefined = undefined; + + constructor( + public readonly status: GitStatus, + public readonly range: string | undefined, + protected readonly context: ExtensionContext, + protected readonly git: GitService, + public readonly branch?: GitBranch + ) { + super(new GitUri(Uri.file(status.repoPath), { repoPath: status.repoPath, fileName: status.repoPath })); + } + + async getChildren(): Promise { + let statuses: IGitStatusFileWithCommit[]; + let log: GitLog | undefined; + if (this.range !== undefined) { + log = await this.git.getLogForRepo(this.status.repoPath, this.range, this.maxCount); + if (log === undefined) return []; + + statuses = Array.from(Iterables.flatMap(log.commits.values(), c => { + return c.fileStatuses.map(s => { + return { ...s, commit: c } as IGitStatusFileWithCommit; + }); + })); + } + else { + statuses = []; + } + + if (this.status.files.length !== 0) { + 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; + })); + } + statuses.sort((a, b) => b.commit.date.getTime() - a.commit.date.getTime()); + + const groups = Arrays.groupBy(statuses, s => s.fileName); + + const children: (StatusFileCommitsNode | ShowAllNode)[] = [ + ...Iterables.map(Objects.values(groups), + statuses => new StatusFileCommitsNode(this.uri.repoPath!, statuses[statuses.length - 1], statuses.map(s => s.commit), this.context, this.git, this.branch)) + ]; + + if (log !== undefined && log.truncated) { + children.push(new ShowAllNode('Show All Changes', this, this.context)); + } + return children; + } + + async getTreeItem(): Promise { + const item = new TreeItem(`Changed Files`, TreeItemCollapsibleState.Collapsed); + item.contextValue = this.resourceType; + return item; + } +} \ No newline at end of file diff --git a/src/views/statusNode.ts b/src/views/statusNode.ts index e74b977..4599f46 100644 --- a/src/views/statusNode.ts +++ b/src/views/statusNode.ts @@ -1,28 +1,40 @@ import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExplorerNode, ResourceType } from './explorerNode'; import { GitService, GitUri } from '../gitService'; +import { StatusFilesNode } from './statusFilesNode'; import { StatusUpstreamNode } from './statusUpstreamNode'; export class StatusNode extends ExplorerNode { readonly resourceType: ResourceType = 'gitlens:status'; - constructor(uri: GitUri, protected readonly context: ExtensionContext, protected readonly git: GitService) { + constructor( + uri: GitUri, + protected readonly context: ExtensionContext, + protected readonly git: GitService + ) { super(uri); - } + } async getChildren(): Promise { const status = await this.git.getStatusForRepo(this.uri.repoPath!); if (status === undefined) return []; - const children = []; + const children: ExplorerNode[] = []; if (status.state.behind) { - children.push(new StatusUpstreamNode(status, 'behind', this.git.config.gitExplorer.commitFormat, this.context, this.git)); + children.push(new StatusUpstreamNode(status, 'behind', this.context, this.git)); } if (status.state.ahead) { - children.push(new StatusUpstreamNode(status, 'ahead', this.git.config.gitExplorer.commitFormat, this.context, this.git)); + children.push(new StatusUpstreamNode(status, 'ahead', this.context, this.git)); + } + + if (status.files.length !== 0 || status.state.ahead && 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)); } return children; @@ -57,6 +69,10 @@ export class StatusNode extends ExplorerNode { label = `${status.branch} is up-to-date`; } + if (this.git.config.insiders) { + hasChildren = hasChildren || status.files.length !== 0; + } + const item = new TreeItem(label, hasChildren ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None); item.contextValue = this.resourceType; diff --git a/src/views/statusUpstreamNode.ts b/src/views/statusUpstreamNode.ts index 3925e67..3529b51 100644 --- a/src/views/statusUpstreamNode.ts +++ b/src/views/statusUpstreamNode.ts @@ -1,15 +1,20 @@ 'use strict'; import { Iterables } from '../system'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; +import { CommitNode } from './commitNode'; import { ExplorerNode, ResourceType } from './explorerNode'; import { GitService, GitStatus, GitUri } from '../gitService'; -import { CommitNode } from './commitNode'; export class StatusUpstreamNode extends ExplorerNode { readonly resourceType: ResourceType = 'gitlens:status-upstream'; - constructor(public readonly status: GitStatus, public readonly direction: 'ahead' | 'behind', private readonly template: string, protected readonly context: ExtensionContext, protected readonly git: GitService) { + constructor( + public readonly status: GitStatus, + public readonly direction: 'ahead' | 'behind', + protected readonly context: ExtensionContext, + protected readonly git: GitService + ) { super(new GitUri(Uri.file(status.repoPath), { repoPath: status.repoPath, fileName: status.repoPath })); } @@ -17,10 +22,11 @@ export class StatusUpstreamNode extends ExplorerNode { const range = this.direction === 'ahead' ? `${this.status.upstream}..${this.status.branch}` : `${this.status.branch}..${this.status.upstream}`; + let log = await this.git.getLogForRepo(this.uri.repoPath!, range, 0); if (log === undefined) return []; - if (this.direction !== 'ahead') return [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.template, this.context, this.git))]; + if (this.direction !== 'ahead') return [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.context, this.git))]; // Since the last commit when we are looking 'ahead' can have no previous (because of the range given) -- look it up const commits = Array.from(log.commits.values()); @@ -32,8 +38,8 @@ export class StatusUpstreamNode extends ExplorerNode { } } - return [...Iterables.map(commits, c => new CommitNode(c, this.template, this.context, this.git))]; -} + return [...Iterables.map(commits, c => new CommitNode(c, this.context, this.git))]; + } async getTreeItem(): Promise { const label = this.direction === 'ahead'