Browse Source

Closes #358 - adds tree layout to tags

main
Eric Amodio 6 years ago
parent
commit
e3e816c20f
8 changed files with 337 additions and 299 deletions
  1. +3
    -0
      CHANGELOG.md
  2. +20
    -8
      src/git/models/tag.ts
  3. +49
    -48
      src/views/branchOrTagFolderNode.ts
  4. +74
    -74
      src/views/branchesNode.ts
  5. +86
    -86
      src/views/remoteNode.ts
  6. +49
    -41
      src/views/tagNode.ts
  7. +56
    -42
      src/views/tagsNode.ts

+ 3
- 0
CHANGELOG.md View File

@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
### Added
- Adds a tree layout option to tags in the *GitLens* explorer — closes [#358](https://github.com/eamodio/vscode-gitlens/issues/358)
### Fixed
- Fixes issue where comparing previous revision during a merge/rebase conflict failed to show the correct contents

+ 20
- 8
src/git/models/tag.ts View File

@ -1,9 +1,21 @@
'use strict';
export class GitTag {
constructor(
public readonly repoPath: string,
public readonly name: string
) { }
'use strict';
export class GitTag {
constructor(
public readonly repoPath: string,
public readonly name: string
) { }
private _basename: string | undefined;
getBasename(): string {
if (this._basename === undefined) {
const index = this.name.lastIndexOf('/');
this._basename = index !== -1
? this.name.substring(index + 1)
: this.name;
}
return this._basename;
}
}

src/views/branchFolderNode.ts → src/views/branchOrTagFolderNode.ts View File

@ -1,48 +1,49 @@
'use strict';
import { Arrays, Objects } from '../system';
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchNode } from './branchNode';
// import { Container } from '../container';
import { Explorer, ExplorerNode, ResourceType } from './explorerNode';
import { GitUri } from '../gitService';
export class BranchFolderNode extends ExplorerNode {
constructor(
public readonly repoPath: string,
public readonly branchFolderName: string,
public readonly relativePath: string | undefined,
public readonly root: Arrays.IHierarchicalItem<BranchNode>,
private readonly explorer: Explorer
) {
super(GitUri.fromRepoPath(repoPath));
}
async getChildren(): Promise<ExplorerNode[]> {
if (this.root.descendants === undefined || this.root.children === undefined) return [];
const children: (BranchFolderNode | BranchNode)[] = [];
for (const folder of Objects.values(this.root.children)) {
if (folder.value === undefined) {
children.push(new BranchFolderNode(this.repoPath, folder.name, folder.relativePath, folder, this.explorer));
continue;
}
children.push(folder.value);
}
return children;
}
async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(this.label, TreeItemCollapsibleState.Collapsed);
item.contextValue = ResourceType.Folder;
item.iconPath = ThemeIcon.Folder;
item.tooltip = this.label;
return item;
}
get label(): string {
return this.branchFolderName;
}
}
'use strict';
import { Arrays, Objects } from '../system';
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchNode } from './branchNode';
// import { Container } from '../container';
import { Explorer, ExplorerNode, ResourceType } from './explorerNode';
import { GitUri } from '../gitService';
import { TagNode } from './tagNode';
export class BranchOrTagFolderNode extends ExplorerNode {
constructor(
public readonly repoPath: string,
public readonly folderName: string,
public readonly relativePath: string | undefined,
public readonly root: Arrays.IHierarchicalItem<BranchNode | TagNode>,
private readonly explorer: Explorer
) {
super(GitUri.fromRepoPath(repoPath));
}
async getChildren(): Promise<ExplorerNode[]> {
if (this.root.descendants === undefined || this.root.children === undefined) return [];
const children: (BranchOrTagFolderNode | BranchNode | TagNode)[] = [];
for (const folder of Objects.values(this.root.children)) {
if (folder.value === undefined) {
children.push(new BranchOrTagFolderNode(this.repoPath, folder.name, folder.relativePath, folder, this.explorer));
continue;
}
children.push(folder.value);
}
return children;
}
async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(this.label, TreeItemCollapsibleState.Collapsed);
item.contextValue = ResourceType.Folder;
item.iconPath = ThemeIcon.Folder;
item.tooltip = this.label;
return item;
}
get label(): string {
return this.folderName;
}
}

+ 74
- 74
src/views/branchesNode.ts View File

@ -1,74 +1,74 @@
'use strict';
import { Arrays, Iterables } from '../system';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchFolderNode } from './branchFolderNode';
import { BranchNode } from './branchNode';
import { ExplorerBranchesLayout } from '../configuration';
import { Container } from '../container';
import { ExplorerNode, ResourceType } from './explorerNode';
import { GitExplorer } from './gitExplorer';
import { GitUri, Repository } from '../gitService';
export class BranchesNode extends ExplorerNode {
constructor(
uri: GitUri,
private readonly repo: Repository,
private readonly explorer: GitExplorer,
private readonly active: boolean = false
) {
super(uri);
}
get id(): string {
return `gitlens:repository(${this.repo.path})${this.active ? ':active' : ''}:branches`;
}
async getChildren(): Promise<ExplorerNode[]> {
const branches = await this.repo.getBranches();
if (branches === undefined) return [];
branches.sort((a, b) => (a.current ? -1 : 1) - (b.current ? -1 : 1) || a.name.localeCompare(b.name));
// filter local branches
const branchNodes = [...Iterables.filterMap(branches, b => b.remote ? undefined : new BranchNode(b, this.uri, this.explorer))];
if (this.explorer.config.branches.layout === ExplorerBranchesLayout.List) return branchNodes;
// Take out the current branch, since that should always be first and un-nested
const current = (branchNodes.length > 0 && branchNodes[0].current)
? branchNodes.splice(0, 1)[0]
: undefined;
const hierarchy = Arrays.makeHierarchical(branchNodes,
n => n.branch.isValid() ? n.branch.getName().split('/') : [n.branch.name],
(...paths: string[]) => paths.join('/'),
this.explorer.config.files.compact);
const root = new BranchFolderNode(this.repo.path, '', undefined, hierarchy, this.explorer);
const children = await root.getChildren() as (BranchFolderNode | BranchNode)[];
// If we found a current branch, insert it at the start
if (current !== undefined) {
children.splice(0, 0, current);
}
return children;
}
async getTreeItem(): Promise<TreeItem> {
// HACK: Until https://github.com/Microsoft/vscode/issues/30918 is fixed
const item = new TreeItem(`Branches`, this.active ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed);
const remotes = await this.repo.getRemotes();
item.contextValue = (remotes !== undefined && remotes.length > 0)
? ResourceType.BranchesWithRemotes
: ResourceType.Branches;
item.iconPath = {
dark: Container.context.asAbsolutePath('images/dark/icon-branch.svg'),
light: Container.context.asAbsolutePath('images/light/icon-branch.svg')
};
return item;
}
}
'use strict';
import { Arrays, Iterables } from '../system';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchOrTagFolderNode } from './branchOrTagFolderNode';
import { BranchNode } from './branchNode';
import { ExplorerBranchesLayout } from '../configuration';
import { Container } from '../container';
import { ExplorerNode, ResourceType } from './explorerNode';
import { GitExplorer } from './gitExplorer';
import { GitUri, Repository } from '../gitService';
export class BranchesNode extends ExplorerNode {
constructor(
uri: GitUri,
private readonly repo: Repository,
private readonly explorer: GitExplorer,
private readonly active: boolean = false
) {
super(uri);
}
get id(): string {
return `gitlens:repository(${this.repo.path})${this.active ? ':active' : ''}:branches`;
}
async getChildren(): Promise<ExplorerNode[]> {
const branches = await this.repo.getBranches();
if (branches === undefined) return [];
branches.sort((a, b) => (a.current ? -1 : 1) - (b.current ? -1 : 1) || a.name.localeCompare(b.name));
// filter local branches
const branchNodes = [...Iterables.filterMap(branches, b => b.remote ? undefined : new BranchNode(b, this.uri, this.explorer))];
if (this.explorer.config.branches.layout === ExplorerBranchesLayout.List) return branchNodes;
// Take out the current branch, since that should always be first and un-nested
const current = (branchNodes.length > 0 && branchNodes[0].current)
? branchNodes.splice(0, 1)[0]
: undefined;
const hierarchy = Arrays.makeHierarchical(branchNodes,
n => n.branch.isValid() ? n.branch.getName().split('/') : [n.branch.name],
(...paths: string[]) => paths.join('/'),
this.explorer.config.files.compact);
const root = new BranchOrTagFolderNode(this.repo.path, '', undefined, hierarchy, this.explorer);
const children = await root.getChildren() as (BranchOrTagFolderNode | BranchNode)[];
// If we found a current branch, insert it at the start
if (current !== undefined) {
children.splice(0, 0, current);
}
return children;
}
async getTreeItem(): Promise<TreeItem> {
// HACK: Until https://github.com/Microsoft/vscode/issues/30918 is fixed
const item = new TreeItem(`Branches`, this.active ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed);
const remotes = await this.repo.getRemotes();
item.contextValue = (remotes !== undefined && remotes.length > 0)
? ResourceType.BranchesWithRemotes
: ResourceType.Branches;
item.iconPath = {
dark: Container.context.asAbsolutePath('images/dark/icon-branch.svg'),
light: Container.context.asAbsolutePath('images/light/icon-branch.svg')
};
return item;
}
}

+ 86
- 86
src/views/remoteNode.ts View File

@ -1,86 +1,86 @@
'use strict';
import { Arrays, Iterables } from '../system';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchFolderNode } from './branchFolderNode';
import { BranchNode } from './branchNode';
import { ExplorerBranchesLayout } from '../configuration';
import { GlyphChars } from '../constants';
import { ExplorerNode, ResourceType } from './explorerNode';
import { GitExplorer } from './gitExplorer';
import { GitRemote, GitRemoteType, GitUri, Repository } from '../gitService';
import { Container } from '../container';
export class RemoteNode extends ExplorerNode {
constructor(
public readonly remote: GitRemote,
uri: GitUri,
private readonly repo: Repository,
private readonly explorer: GitExplorer
) {
super(uri);
}
async getChildren(): Promise<ExplorerNode[]> {
const branches = await this.repo.getBranches();
if (branches === undefined) return [];
branches.sort((a, b) => a.name.localeCompare(b.name));
// filter remote branches
const branchNodes = [...Iterables.filterMap(branches, b => !b.remote || !b.name.startsWith(this.remote.name) ? undefined : new BranchNode(b, this.uri, this.explorer))];
if (this.explorer.config.branches.layout === ExplorerBranchesLayout.List) return branchNodes;
const hierarchy = Arrays.makeHierarchical(
branchNodes,
n => n.branch.isValid() ? n.branch.getName().split('/') : [n.branch.name],
(...paths: string[]) => paths.join('/'),
this.explorer.config.files.compact
);
const root = new BranchFolderNode(this.repo.path, '', undefined, hierarchy, this.explorer);
const children = await root.getChildren() as (BranchFolderNode | BranchNode)[];
return children;
}
getTreeItem(): TreeItem {
const fetch = this.remote.types.find(rt => rt.type === GitRemoteType.Fetch);
const push = this.remote.types.find(rt => rt.type === GitRemoteType.Push);
let separator;
if (fetch && push) {
separator = GlyphChars.ArrowLeftRightLong;
}
else if (fetch) {
separator = GlyphChars.ArrowLeft;
}
else if (push) {
separator = GlyphChars.ArrowRight;
}
else {
separator = GlyphChars.Dash;
}
const label = `${this.remote.name} ${GlyphChars.Space}${separator}${GlyphChars.Space} ${(this.remote.provider !== undefined) ? this.remote.provider.name : this.remote.domain} ${GlyphChars.Space}${GlyphChars.Dot}${GlyphChars.Space} ${this.remote.path}`;
const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed);
item.contextValue = ResourceType.Remote;
item.tooltip = `${this.remote.name}\n${this.remote.path} (${(this.remote.provider !== undefined) ? this.remote.provider.name : this.remote.domain})`;
if (this.remote.provider !== undefined) {
item.iconPath = {
dark: Container.context.asAbsolutePath(`images/dark/icon-${this.remote.provider.icon}.svg`),
light: Container.context.asAbsolutePath(`images/light/icon-${this.remote.provider.icon}.svg`)
};
}
else {
item.iconPath = {
dark: Container.context.asAbsolutePath('images/dark/icon-remote.svg'),
light: Container.context.asAbsolutePath('images/light/icon-remote.svg')
};
}
return item;
}
}
'use strict';
import { Arrays, Iterables } from '../system';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchOrTagFolderNode } from './branchOrTagFolderNode';
import { BranchNode } from './branchNode';
import { ExplorerBranchesLayout } from '../configuration';
import { GlyphChars } from '../constants';
import { ExplorerNode, ResourceType } from './explorerNode';
import { GitExplorer } from './gitExplorer';
import { GitRemote, GitRemoteType, GitUri, Repository } from '../gitService';
import { Container } from '../container';
export class RemoteNode extends ExplorerNode {
constructor(
public readonly remote: GitRemote,
uri: GitUri,
private readonly repo: Repository,
private readonly explorer: GitExplorer
) {
super(uri);
}
async getChildren(): Promise<ExplorerNode[]> {
const branches = await this.repo.getBranches();
if (branches === undefined) return [];
branches.sort((a, b) => a.name.localeCompare(b.name));
// filter remote branches
const branchNodes = [...Iterables.filterMap(branches, b => !b.remote || !b.name.startsWith(this.remote.name) ? undefined : new BranchNode(b, this.uri, this.explorer))];
if (this.explorer.config.branches.layout === ExplorerBranchesLayout.List) return branchNodes;
const hierarchy = Arrays.makeHierarchical(
branchNodes,
n => n.branch.isValid() ? n.branch.getName().split('/') : [n.branch.name],
(...paths: string[]) => paths.join('/'),
this.explorer.config.files.compact
);
const root = new BranchOrTagFolderNode(this.repo.path, '', undefined, hierarchy, this.explorer);
const children = await root.getChildren() as (BranchOrTagFolderNode | BranchNode)[];
return children;
}
getTreeItem(): TreeItem {
const fetch = this.remote.types.find(rt => rt.type === GitRemoteType.Fetch);
const push = this.remote.types.find(rt => rt.type === GitRemoteType.Push);
let separator;
if (fetch && push) {
separator = GlyphChars.ArrowLeftRightLong;
}
else if (fetch) {
separator = GlyphChars.ArrowLeft;
}
else if (push) {
separator = GlyphChars.ArrowRight;
}
else {
separator = GlyphChars.Dash;
}
const label = `${this.remote.name} ${GlyphChars.Space}${separator}${GlyphChars.Space} ${(this.remote.provider !== undefined) ? this.remote.provider.name : this.remote.domain} ${GlyphChars.Space}${GlyphChars.Dot}${GlyphChars.Space} ${this.remote.path}`;
const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed);
item.contextValue = ResourceType.Remote;
item.tooltip = `${this.remote.name}\n${this.remote.path} (${(this.remote.provider !== undefined) ? this.remote.provider.name : this.remote.domain})`;
if (this.remote.provider !== undefined) {
item.iconPath = {
dark: Container.context.asAbsolutePath(`images/dark/icon-${this.remote.provider.icon}.svg`),
light: Container.context.asAbsolutePath(`images/light/icon-${this.remote.provider.icon}.svg`)
};
}
else {
item.iconPath = {
dark: Container.context.asAbsolutePath('images/dark/icon-remote.svg'),
light: Container.context.asAbsolutePath('images/light/icon-remote.svg')
};
}
return item;
}
}

+ 49
- 41
src/views/tagNode.ts View File

@ -1,41 +1,49 @@
'use strict';
import { Iterables } from '../system';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { CommitNode } from './commitNode';
import { Container } from '../container';
import { Explorer, ExplorerNode, ExplorerRefNode, MessageNode, ResourceType, ShowAllNode } from './explorerNode';
import { GitTag, GitUri } from '../gitService';
export class TagNode extends ExplorerRefNode {
readonly supportsPaging: boolean = true;
constructor(
public readonly tag: GitTag,
uri: GitUri,
private readonly explorer: Explorer
) {
super(uri);
}
get ref(): string {
return this.tag.name;
}
async getChildren(): Promise<ExplorerNode[]> {
const log = await Container.git.getLog(this.uri.repoPath!, { maxCount: this.maxCount, ref: this.tag.name });
if (log === undefined) return [new MessageNode('No commits yet')];
const children: (CommitNode | ShowAllNode)[] = [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer))];
if (log.truncated) {
children.push(new ShowAllNode('Show All Commits', this, this.explorer));
}
return children;
}
async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(this.tag.name, TreeItemCollapsibleState.Collapsed);
item.contextValue = ResourceType.Tag;
return item;
}
}
'use strict';
import { Iterables } from '../system';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { CommitNode } from './commitNode';
import { ExplorerBranchesLayout } from '../configuration';
import { Container } from '../container';
import { ExplorerNode, ExplorerRefNode, MessageNode, ResourceType, ShowAllNode } from './explorerNode';
import { GitExplorer } from './gitExplorer';
import { GitTag, GitUri } from '../gitService';
export class TagNode extends ExplorerRefNode {
readonly supportsPaging: boolean = true;
constructor(
public readonly tag: GitTag,
uri: GitUri,
private readonly explorer: GitExplorer
) {
super(uri);
}
get label(): string {
return this.explorer.config.branches.layout === ExplorerBranchesLayout.Tree
? this.tag.getBasename()
: this.tag.name;
}
get ref(): string {
return this.tag.name;
}
async getChildren(): Promise<ExplorerNode[]> {
const log = await Container.git.getLog(this.uri.repoPath!, { maxCount: this.maxCount, ref: this.tag.name });
if (log === undefined) return [new MessageNode('No commits yet')];
const children: (CommitNode | ShowAllNode)[] = [...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer))];
if (log.truncated) {
children.push(new ShowAllNode('Show All Commits', this, this.explorer));
}
return children;
}
async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(this.label, TreeItemCollapsibleState.Collapsed);
item.contextValue = ResourceType.Tag;
return item;
}
}

+ 56
- 42
src/views/tagsNode.ts View File

@ -1,42 +1,56 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Container } from '../container';
import { Explorer, ExplorerNode, MessageNode, ResourceType } from './explorerNode';
import { GitUri, Repository } from '../gitService';
import { TagNode } from './tagNode';
export class TagsNode extends ExplorerNode {
constructor(
uri: GitUri,
private readonly repo: Repository,
private readonly explorer: Explorer,
private readonly active: boolean = false
) {
super(uri);
}
get id(): string {
return `gitlens:repository(${this.repo.path})${this.active ? ':active' : ''}:tags`;
}
async getChildren(): Promise<ExplorerNode[]> {
const tags = await this.repo.getTags();
if (tags.length === 0) return [new MessageNode('No tags yet')];
tags.sort((a, b) => a.name.localeCompare(b.name));
return [...tags.map(t => new TagNode(t, this.uri, this.explorer))];
}
async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(`Tags`, TreeItemCollapsibleState.Collapsed);
item.contextValue = ResourceType.Tags;
item.iconPath = {
dark: Container.context.asAbsolutePath('images/dark/icon-tag.svg'),
light: Container.context.asAbsolutePath('images/light/icon-tag.svg')
};
return item;
}
}
'use strict';
import { Arrays } from '../system';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchOrTagFolderNode } from './branchOrTagFolderNode';
import { ExplorerBranchesLayout } from '../configuration';
import { Container } from '../container';
import { ExplorerNode, MessageNode, ResourceType } from './explorerNode';
import { GitExplorer } from './gitExplorer';
import { GitUri, Repository } from '../gitService';
import { TagNode } from './tagNode';
export class TagsNode extends ExplorerNode {
constructor(
uri: GitUri,
private readonly repo: Repository,
private readonly explorer: GitExplorer,
private readonly active: boolean = false
) {
super(uri);
}
get id(): string {
return `gitlens:repository(${this.repo.path})${this.active ? ':active' : ''}:tags`;
}
async getChildren(): Promise<ExplorerNode[]> {
const tags = await this.repo.getTags();
if (tags.length === 0) return [new MessageNode('No tags yet')];
tags.sort((a, b) => a.name.localeCompare(b.name));
const tagNodes = [...tags.map(t => new TagNode(t, this.uri, this.explorer))];
if (this.explorer.config.branches.layout === ExplorerBranchesLayout.List) return tagNodes;
const hierarchy = Arrays.makeHierarchical(tagNodes,
n => n.tag.name.split('/'),
(...paths: string[]) => paths.join('/'),
this.explorer.config.files.compact);
const root = new BranchOrTagFolderNode(this.repo.path, '', undefined, hierarchy, this.explorer);
const children = await root.getChildren() as (BranchOrTagFolderNode | TagNode)[];
return children;
}
async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(`Tags`, TreeItemCollapsibleState.Collapsed);
item.contextValue = ResourceType.Tags;
item.iconPath = {
dark: Container.context.asAbsolutePath('images/dark/icon-tag.svg'),
light: Container.context.asAbsolutePath('images/light/icon-tag.svg')
};
return item;
}
}

Loading…
Cancel
Save