Closes #139 - adds changed files node to repository status

Reworks commit-file nodes
This commit is contained in:
Eric Amodio
2017-09-17 02:06:18 -04:00
parent a69afdb6ef
commit 71d17bcc2f
23 changed files with 365 additions and 79 deletions

View File

@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
## [Unreleased] ## [Unreleased]
### Added ### 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) - 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 ## [5.1.0] - 2017-09-15

View File

@@ -1742,9 +1742,54 @@
"when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:status", "when": "gitlens:hasRemotes && view == gitlens.gitExplorer && viewItem == gitlens:status",
"group": "1_gitlens@1" "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", "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" "group": "9_gitlens@1"
} }
] ]

View File

@@ -39,7 +39,7 @@ export class CommitFormatter extends Formatter<GitCommit, ICommitFormatOptions>
} }
get id() { get id() {
return this._item.shortSha; return this._item.isUncommitted ? 'index' : this._item.shortSha;
} }
get message() { get message() {

View File

@@ -74,6 +74,7 @@ export const RepoChangedReasons = {
export class GitService extends Disposable { export class GitService extends Disposable {
static fakeSha = 'ffffffffffffffffffffffffffffffffffffffff'; static fakeSha = 'ffffffffffffffffffffffffffffffffffffffff';
static uncommittedSha = '0000000000000000000000000000000000000000';
private _onDidBlameFail = new EventEmitter<string>(); private _onDidBlameFail = new EventEmitter<string>();
get onDidBlameFail(): Event<string> { get onDidBlameFail(): Event<string> {

View File

@@ -1,7 +1,7 @@
'use strict'; 'use strict';
export namespace Arrays { export namespace Arrays {
export function groupBy<T>(array: T[], accessor: (item: T) => any): T[] { export function groupBy<T>(array: T[], accessor: (item: T) => string): { [key: string]: T[] } {
return array.reduce((previous, current) => { return array.reduce((previous, current) => {
const value = accessor(current); const value = accessor(current);
previous[value] = previous[value] || []; previous[value] = previous[value] || [];

View File

@@ -3,7 +3,7 @@ import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { CommitNode } from './commitNode'; import { CommitNode } from './commitNode';
import { GlyphChars } from '../constants'; import { GlyphChars } from '../constants';
import { ExplorerNode, ResourceType, ShowAllCommitsNode } from './explorerNode'; import { ExplorerNode, ResourceType, ShowAllNode } from './explorerNode';
import { GitBranch, GitService, GitUri } from '../gitService'; import { GitBranch, GitService, GitUri } from '../gitService';
export class BranchHistoryNode extends ExplorerNode { export class BranchHistoryNode extends ExplorerNode {
@@ -12,7 +12,12 @@ export class BranchHistoryNode extends ExplorerNode {
maxCount: number | undefined = undefined; 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); 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); const log = await this.git.getLogForRepo(this.uri.repoPath!, this.branch.name, this.maxCount);
if (log === undefined) return []; if (log === undefined) return [];
const children = Iterables.map(log.commits.values(), c => new CommitNode(c, this.template, this.context, this.git, this.branch)); const children: (CommitNode | ShowAllNode)[] = [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.context, this.git, this.branch))];
if (!log.truncated) return [...children]; if (log.truncated) {
children.push(new ShowAllNode('Show All Commits', this, this.context));
return [...children, new ShowAllCommitsNode(this, this.context)]; }
return children;
} }
async getTreeItem(): Promise<TreeItem> { async getTreeItem(): Promise<TreeItem> {

View File

@@ -9,7 +9,11 @@ export class BranchesNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:branches'; 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); super(uri);
} }
@@ -18,7 +22,7 @@ export class BranchesNode extends ExplorerNode {
if (branches === undefined) return []; if (branches === undefined) return [];
branches.sort((a, b) => (a.current ? -1 : 1) - (b.current ? -1 : 1) || a.name.localeCompare(b.name)); 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<TreeItem> { async getTreeItem(): Promise<TreeItem> {

View File

@@ -2,19 +2,36 @@
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../commands'; import { Commands, DiffWithPreviousCommandArgs } from '../commands';
import { ExplorerNode, ResourceType } from './explorerNode'; 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'; 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 { export class CommitFileNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:commit-file'; 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 })); super(new GitUri(Uri.file(path.resolve(commit.repoPath, status.fileName)), { repoPath: commit.repoPath, fileName: status.fileName, sha: commit.sha }));
} }
getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
return Promise.resolve([]); return [];
} }
async getTreeItem(): Promise<TreeItem> { async getTreeItem(): Promise<TreeItem> {
@@ -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; 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 = { item.iconPath = {
dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)), dark: this.context.asAbsolutePath(path.join('images', 'dark', icon)),
light: this.context.asAbsolutePath(path.join('images', 'light', icon)) light: this.context.asAbsolutePath(path.join('images', 'light', icon))
@@ -39,6 +66,14 @@ export class CommitFileNode extends ExplorerNode {
return item; return item;
} }
protected getCommitTemplate() {
return this.git.config.gitExplorer.commitFormat;
}
protected getCommitFileTemplate() {
return this.git.config.gitExplorer.commitFileFormat;
}
getCommand(): Command | undefined { getCommand(): Command | undefined {
return { return {
title: 'Compare File with Previous Revision', title: 'Compare File with Previous Revision',

View File

@@ -2,58 +2,44 @@
import { Iterables } from '../system'; import { Iterables } from '../system';
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Commands, DiffWithPreviousCommandArgs } from '../commands'; import { Commands, DiffWithPreviousCommandArgs } from '../commands';
import { CommitFileNode } from './commitFileNode'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { ExplorerNode, ResourceType } from './explorerNode'; import { ExplorerNode, ResourceType } from './explorerNode';
import { CommitFormatter, getGitStatusIcon, GitBranch, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService'; import { CommitFormatter, GitBranch, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService';
import * as path from 'path';
export class CommitNode extends ExplorerNode { export class CommitNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:commit'; 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)); super(new GitUri(commit.uri, commit));
} }
async getChildren(): Promise<ExplorerNode[]> { async getChildren(): Promise<ExplorerNode[]> {
if (this.commit.type === 'file') Promise.resolve([]);
const log = await this.git.getLogForRepo(this.commit.repoPath, this.commit.sha, 1); const log = await this.git.getLogForRepo(this.commit.repoPath, this.commit.sha, 1);
if (log === undefined) return []; if (log === undefined) return [];
const commit = Iterables.first(log.commits.values()); const commit = Iterables.first(log.commits.values());
if (commit === undefined) return []; 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 { 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, truncateMessageAtNewLine: true,
dataFormat: this.git.config.defaultDateFormat dataFormat: this.git.config.defaultDateFormat
} as ICommitFormatOptions)); } as ICommitFormatOptions), TreeItemCollapsibleState.Collapsed);
if (this.commit.type === 'file') {
item.collapsibleState = TreeItemCollapsibleState.None;
item.command = this.getCommand();
const resourceType: ResourceType = 'gitlens:commit-file';
item.contextValue = resourceType;
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.contextValue = this.resourceType;
item.iconPath = { item.iconPath = {
dark: this.context.asAbsolutePath('images/dark/icon-commit.svg'), dark: this.context.asAbsolutePath('images/dark/icon-commit.svg'),
light: this.context.asAbsolutePath('images/light/icon-commit.svg') light: this.context.asAbsolutePath('images/light/icon-commit.svg')
}; };
}
return item; return item;
} }

View File

@@ -20,6 +20,9 @@ export declare type ResourceType =
'gitlens:stash-file' | 'gitlens:stash-file' |
'gitlens:stashes' | 'gitlens:stashes' |
'gitlens:status' | 'gitlens:status' |
'gitlens:status-file' |
'gitlens:status-files' |
'gitlens:status-file-commits' |
'gitlens:status-upstream'; 'gitlens:status-upstream';
export abstract class ExplorerNode { 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 }; args: RefreshNodeCommandArgs = { maxCount: 0 };
constructor(node: ExplorerNode, context: ExtensionContext) { constructor(message: string, node: ExplorerNode, context: ExtensionContext) {
super(`Show All Commits ${GlyphChars.Space}${GlyphChars.Dash}${GlyphChars.Space} this may take a while`, node, context); super(`${message} ${GlyphChars.Space}${GlyphChars.Dash}${GlyphChars.Space} this may take a while`, node, context);
} }
} }

View File

@@ -10,8 +10,10 @@ export * from './historyNode';
export * from './remoteNode'; export * from './remoteNode';
export * from './remotesNode'; export * from './remotesNode';
export * from './repositoryNode'; export * from './repositoryNode';
export * from './stashesNode';
export * from './stashFileNode'; export * from './stashFileNode';
export * from './stashNode'; export * from './stashNode';
export * from './stashesNode'; export * from './statusFileCommitsNode';
export * from './statusFilesNode';
export * from './statusNode'; export * from './statusNode';
export * from './statusUpstreamNode'; export * from './statusUpstreamNode';

View File

@@ -1,7 +1,7 @@
'use strict'; 'use strict';
import { Iterables } from '../system'; import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { CommitNode } from './commitNode'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
import { ExplorerNode, MessageNode, ResourceType } from './explorerNode'; import { ExplorerNode, MessageNode, ResourceType } from './explorerNode';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri } from '../gitService';
@@ -9,7 +9,11 @@ export class FileHistoryNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:file-history'; 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); super(uri);
} }
@@ -17,7 +21,7 @@ export class FileHistoryNode extends ExplorerNode {
const log = await this.git.getLogForFile(this.uri.repoPath, this.uri.fsPath, this.uri.sha); const log = await this.git.getLogForFile(this.uri.repoPath, this.uri.fsPath, this.uri.sha);
if (log === undefined) return [new MessageNode('No file history')]; 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 { getTreeItem(): TreeItem {

View File

@@ -8,7 +8,11 @@ export class HistoryNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:history'; 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); super(uri);
} }

View File

@@ -10,7 +10,12 @@ export class RemoteNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:remote'; 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); super(uri);
} }
@@ -19,7 +24,7 @@ export class RemoteNode extends ExplorerNode {
if (branches === undefined) return []; if (branches === undefined) return [];
branches.sort((a, b) => a.name.localeCompare(b.name)); 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 { getTreeItem(): TreeItem {

View File

@@ -9,7 +9,11 @@ export class RemotesNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:remotes'; 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); super(uri);
} }

View File

@@ -12,7 +12,11 @@ export class RepositoryNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:repository'; 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); super(uri);
} }

View File

@@ -2,13 +2,26 @@
import { ExtensionContext } from 'vscode'; import { ExtensionContext } from 'vscode';
import { ResourceType } from './explorerNode'; import { ResourceType } from './explorerNode';
import { GitService, GitStashCommit, IGitStatusFile } from '../gitService'; import { GitService, GitStashCommit, IGitStatusFile } from '../gitService';
import { CommitFileNode } from './commitFileNode'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
export class StashFileNode extends CommitFileNode { export class StashFileNode extends CommitFileNode {
readonly resourceType: ResourceType = 'gitlens:stash-file'; readonly resourceType: ResourceType = 'gitlens:stash-file';
constructor(readonly status: IGitStatusFile, readonly commit: GitStashCommit, readonly context: ExtensionContext, readonly git: GitService) { constructor(
super(status, commit, context, git); 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;
} }
} }

View File

@@ -9,7 +9,11 @@ export class StashNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:stash'; 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)); 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 { getTreeItem(): TreeItem {

View File

@@ -9,7 +9,11 @@ export class StashesNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:stashes'; 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); super(uri);
} }

View File

@@ -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<ExplorerNode[]> {
return this.commits.map(c => new CommitFileNode(this.status, c, this.context, this.git, CommitFileNodeDisplayAs.Commit, this.branch));
}
async getTreeItem(): Promise<TreeItem> {
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
]
};
}
}

View File

@@ -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<ExplorerNode[]> {
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<IGitStatusFileWithCommit[]>(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<TreeItem> {
const item = new TreeItem(`Changed Files`, TreeItemCollapsibleState.Collapsed);
item.contextValue = this.resourceType;
return item;
}
}

View File

@@ -1,13 +1,18 @@
import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { ExplorerNode, ResourceType } from './explorerNode'; import { ExplorerNode, ResourceType } from './explorerNode';
import { GitService, GitUri } from '../gitService'; import { GitService, GitUri } from '../gitService';
import { StatusFilesNode } from './statusFilesNode';
import { StatusUpstreamNode } from './statusUpstreamNode'; import { StatusUpstreamNode } from './statusUpstreamNode';
export class StatusNode extends ExplorerNode { export class StatusNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:status'; 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); super(uri);
} }
@@ -15,14 +20,21 @@ export class StatusNode extends ExplorerNode {
const status = await this.git.getStatusForRepo(this.uri.repoPath!); const status = await this.git.getStatusForRepo(this.uri.repoPath!);
if (status === undefined) return []; if (status === undefined) return [];
const children = []; const children: ExplorerNode[] = [];
if (status.state.behind) { 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) { 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; return children;
@@ -57,6 +69,10 @@ export class StatusNode extends ExplorerNode {
label = `${status.branch} is up-to-date`; 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); const item = new TreeItem(label, hasChildren ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None);
item.contextValue = this.resourceType; item.contextValue = this.resourceType;

View File

@@ -1,15 +1,20 @@
'use strict'; 'use strict';
import { Iterables } from '../system'; import { Iterables } from '../system';
import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
import { CommitNode } from './commitNode';
import { ExplorerNode, ResourceType } from './explorerNode'; import { ExplorerNode, ResourceType } from './explorerNode';
import { GitService, GitStatus, GitUri } from '../gitService'; import { GitService, GitStatus, GitUri } from '../gitService';
import { CommitNode } from './commitNode';
export class StatusUpstreamNode extends ExplorerNode { export class StatusUpstreamNode extends ExplorerNode {
readonly resourceType: ResourceType = 'gitlens:status-upstream'; 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 })); 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' const range = this.direction === 'ahead'
? `${this.status.upstream}..${this.status.branch}` ? `${this.status.upstream}..${this.status.branch}`
: `${this.status.branch}..${this.status.upstream}`; : `${this.status.branch}..${this.status.upstream}`;
let log = await this.git.getLogForRepo(this.uri.repoPath!, range, 0); let log = await this.git.getLogForRepo(this.uri.repoPath!, range, 0);
if (log === undefined) return []; 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 // 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()); const commits = Array.from(log.commits.values());
@@ -32,7 +38,7 @@ 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<TreeItem> { async getTreeItem(): Promise<TreeItem> {