Browse Source

Adds Stashes view

main
Eric Amodio 4 years ago
parent
commit
24c9081e59
10 changed files with 376 additions and 28 deletions
  1. +144
    -6
      package.json
  2. +3
    -3
      src/commands/git/stash.ts
  3. +11
    -0
      src/container.ts
  4. +1
    -1
      src/git/gitService.ts
  5. +2
    -2
      src/git/models/repository.ts
  6. +5
    -4
      src/views/nodes/commitNode.ts
  7. +6
    -4
      src/views/nodes/stashNode.ts
  8. +8
    -7
      src/views/nodes/stashesNode.ts
  9. +192
    -0
      src/views/stashesView.ts
  10. +4
    -1
      src/views/viewBase.ts

+ 144
- 6
package.json View File

@ -2017,6 +2017,40 @@
"markdownDescription": "Specifies the description format of stashes in the views. See [_Commit Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs",
"scope": "window"
},
"gitlens.views.stashes.avatars": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to show avatar images instead of commit (or status) icons in the _Stashes_ view",
"scope": "window"
},
"gitlens.views.stashes.files.compact": {
"type": "boolean",
"default": true,
"markdownDescription": "Specifies whether to compact (flatten) unnecessary file nesting in the _Stashes_ view. Only applies when `#gitlens.views.stashes.files.layout#` is set to `tree` or `auto`",
"scope": "window"
},
"gitlens.views.stashes.files.layout": {
"type": "string",
"default": "auto",
"enum": [
"auto",
"list",
"tree"
],
"enumDescriptions": [
"Automatically switches between displaying files as a `tree` or `list` based on the `#gitlens.views.stashes.files.threshold#` value and the number of files at each nesting level",
"Displays files as a list",
"Displays files as a tree"
],
"markdownDescription": "Specifies how the _Stashes_ view will display files",
"scope": "window"
},
"gitlens.views.stashes.files.threshold": {
"type": "number",
"default": 5,
"markdownDescription": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the _Stashes_ view. Only applies when `#gitlens.views.stashes.files.layout#` is set to `auto`",
"scope": "window"
},
"gitlens.views.statusFileFormat": {
"type": "string",
"default": "${working }${file}",
@ -3837,6 +3871,48 @@
"category": "GitLens"
},
{
"command": "gitlens.views.stashes.copy",
"title": "Copy",
"category": "GitLens"
},
{
"command": "gitlens.views.stashes.refresh",
"title": "Refresh",
"category": "GitLens",
"icon": "$(refresh)"
},
{
"command": "gitlens.views.stashes.setFilesLayoutToAuto",
"title": "Toggle File Layout (Tree)",
"category": "GitLens",
"icon": "$(list-tree)"
},
{
"command": "gitlens.views.stashes.setFilesLayoutToList",
"title": "Toggle File Layout (Auto)",
"category": "GitLens",
"icon": {
"dark": "images/dark/icon-view-auto.svg",
"light": "images/light/icon-view-auto.svg"
}
},
{
"command": "gitlens.views.stashes.setFilesLayoutToTree",
"title": "Toggle File Layout (List)",
"category": "GitLens",
"icon": "$(list-flat)"
},
{
"command": "gitlens.views.stashes.setShowAvatarsOn",
"title": "Show Avatars",
"category": "GitLens"
},
{
"command": "gitlens.views.stashes.setShowAvatarsOff",
"title": "Hide Avatars",
"category": "GitLens"
},
{
"command": "gitlens.views.tags.copy",
"title": "Copy",
"category": "GitLens"
@ -4869,6 +4945,34 @@
"when": "false"
},
{
"command": "gitlens.views.stashes.copy",
"when": "false"
},
{
"command": "gitlens.views.stashes.refresh",
"when": "false"
},
{
"command": "gitlens.views.stashes.setFilesLayoutToAuto",
"when": "false"
},
{
"command": "gitlens.views.stashes.setFilesLayoutToList",
"when": "false"
},
{
"command": "gitlens.views.stashes.setFilesLayoutToTree",
"when": "false"
},
{
"command": "gitlens.views.stashes.setShowAvatarsOn",
"when": "false"
},
{
"command": "gitlens.views.stashes.setShowAvatarsOff",
"when": "false"
},
{
"command": "gitlens.views.tags.copy",
"when": "false"
},
@ -5677,6 +5781,36 @@
"group": "1_gitlens@0"
},
{
"command": "gitlens.views.stashes.setFilesLayoutToList",
"when": "view =~ /^gitlens\\.views\\.stashes/ && config.gitlens.views.stashes.files.layout == auto",
"group": "navigation@13"
},
{
"command": "gitlens.views.stashes.setFilesLayoutToTree",
"when": "view =~ /^gitlens\\.views\\.stashes/ && config.gitlens.views.stashes.files.layout == list",
"group": "navigation@13"
},
{
"command": "gitlens.views.stashes.setFilesLayoutToAuto",
"when": "view =~ /^gitlens\\.views\\.stashes/ && config.gitlens.views.stashes.files.layout == tree",
"group": "navigation@13"
},
{
"command": "gitlens.views.stashes.refresh",
"when": "view =~ /^gitlens\\.views\\.stashes/",
"group": "navigation@99"
},
{
"command": "gitlens.views.stashes.setShowAvatarsOn",
"when": "view =~ /^gitlens\\.views\\.stashes/ && !config.gitlens.views.stashes.avatars",
"group": "1_gitlens@0"
},
{
"command": "gitlens.views.stashes.setShowAvatarsOff",
"when": "view =~ /^gitlens\\.views\\.stashes/ && config.gitlens.views.stashes.avatars",
"group": "1_gitlens@0"
},
{
"command": "gitlens.views.tags.setLayoutToList",
"when": "view =~ /gitlens\\.views\\.tags/ && config.gitlens.views.tags.branches.layout == tree",
"group": "navigation@1"
@ -6955,20 +7089,24 @@
"name": "Branches"
},
{
"id": "gitlens.views.history",
"name": "History"
"id": "gitlens.views.contributors",
"name": "Contributors"
},
{
"id": "gitlens.views.tags",
"name": "Tags"
"id": "gitlens.views.history",
"name": "History"
},
{
"id": "gitlens.views.remotes",
"name": "Remotes"
},
{
"id": "gitlens.views.contributors",
"name": "Contributors"
"id": "gitlens.views.stashes",
"name": "Stashes"
},
{
"id": "gitlens.views.tags",
"name": "Tags"
}
],
"explorer": [

+ 3
- 3
src/commands/git/stash.ts View File

@ -254,7 +254,7 @@ export class StashGitCommand extends QuickCommand {
while (this.canStepsContinue(state)) {
if (state.counter < 3 || state.reference == null) {
const result: StepResult<GitStashReference> = yield* pickStashStep(state, context, {
stash: await Container.git.getStashList(state.repo.path),
stash: await Container.git.getStash(state.repo.path),
placeholder: (context, stash) =>
stash == null
? `No stashes found in ${state.repo.formattedName}`
@ -369,7 +369,7 @@ export class StashGitCommand extends QuickCommand {
while (this.canStepsContinue(state)) {
if (state.counter < 3 || state.reference == null) {
const result: StepResult<GitStashReference> = yield* pickStashStep(state, context, {
stash: await Container.git.getStashList(state.repo.path),
stash: await Container.git.getStash(state.repo.path),
placeholder: (context, stash) =>
stash == null ? `No stashes found in ${state.repo.formattedName}` : 'Choose a stash to delete',
picked: state.reference?.ref,
@ -430,7 +430,7 @@ export class StashGitCommand extends QuickCommand {
while (this.canStepsContinue(state)) {
if (state.counter < 3 || state.reference == null) {
const result: StepResult<GitStashCommit> = yield* pickStashStep(state, context, {
stash: await Container.git.getStashList(state.repo.path),
stash: await Container.git.getStash(state.repo.path),
placeholder: (context, stash) =>
stash == null ? `No stashes found in ${state.repo.formattedName}` : 'Choose a stash',
picked: state.reference?.ref,

+ 11
- 0
src/container.ts View File

@ -32,6 +32,7 @@ import { LineHistoryView } from './views/lineHistoryView';
import { RemotesView } from './views/remotesView';
import { RepositoriesView } from './views/repositoriesView';
import { SearchView } from './views/searchView';
import { StashesView } from './views/stashesView';
import { TagsView } from './views/tagsView';
import { ViewCommands } from './views/viewCommands';
import { VslsController } from './vsls/vsls';
@ -71,6 +72,7 @@ export class Container {
context.subscriptions.push((this._contributorsView = new ContributorsView()));
context.subscriptions.push((this._historyView = new HistoryView()));
context.subscriptions.push((this._remotesView = new RemotesView()));
context.subscriptions.push((this._stashesView = new StashesView()));
context.subscriptions.push((this._tagsView = new TagsView()));
if (config.views.compare.enabled) {
@ -331,6 +333,15 @@ export class Container {
return this._settingsWebview;
}
private static _stashesView: StashesView | undefined;
static get stashesView() {
if (this._stashesView === undefined) {
this._context.subscriptions.push((this._stashesView = new StashesView()));
}
return this._stashesView;
}
private static _statusBarController: StatusBarController;
static get statusBar() {
return this._statusBarController;

+ 1
- 1
src/git/gitService.ts View File

@ -2875,7 +2875,7 @@ export class GitService implements Disposable {
}
@log()
async getStashList(repoPath: string | undefined): Promise<GitStash | undefined> {
async getStash(repoPath: string | undefined): Promise<GitStash | undefined> {
if (repoPath == null) return undefined;
const data = await Git.stash__list(repoPath, {

+ 2
- 2
src/git/models/repository.ts View File

@ -420,8 +420,8 @@ export class Repository implements Disposable {
);
}
getStashList(): Promise<GitStash | undefined> {
return Container.git.getStashList(this.path);
getStash(): Promise<GitStash | undefined> {
return Container.git.getStash(this.path);
}
getStatus(): Promise<GitStatus | undefined> {

+ 5
- 4
src/views/nodes/commitNode.ts View File

@ -6,7 +6,7 @@ import { ViewFilesLayout } from '../../configuration';
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import { CommitFormatter, GitBranch, GitLogCommit, GitRevisionReference } from '../../git/git';
import { Arrays, Iterables, Strings } from '../../system';
import { Arrays, Strings } from '../../system';
import { ViewsWithFiles } from '../viewBase';
import { CommitFileNode } from './commitFileNode';
import { FileNode, FolderNode } from './folderNode';
@ -34,9 +34,10 @@ export class CommitNode extends ViewRefNode
getChildren(): ViewNode[] {
const commit = this.commit;
let children: FileNode[] = [
...Iterables.map(commit.files, s => new CommitFileNode(this.view, this, s, commit.toFileCommit(s)!)),
];
let children: FileNode[] = commit.files.map(
s => new CommitFileNode(this.view, this, s, commit.toFileCommit(s)!),
);
if (this.view.config.files.layout !== ViewFilesLayout.List) {
const hierarchy = Arrays.makeHierarchical(

+ 6
- 4
src/views/nodes/stashNode.ts View File

@ -34,7 +34,7 @@ export class StashNode extends ViewRefNode {
}
async getChildren(): Promise<ViewNode[]> {
const files = this.commit.files;
let files = this.commit.files;
// Check for any untracked files -- since git doesn't return them via `git stash list` :(
// See https://stackoverflow.com/questions/12681529/
@ -42,16 +42,18 @@ export class StashNode extends ViewRefNode {
limit: 1,
ref: `${this.commit.stashName}^3`,
});
if (log !== undefined) {
if (log != null) {
const commit = Iterables.first(log.commits.values());
if (commit !== undefined && commit.files.length !== 0) {
if (commit != null && commit.files.length !== 0) {
// Since these files are untracked -- make them look that way
commit.files.forEach(s => (s.status = '?'));
files.splice(files.length, 0, ...commit.files);
files = { ...files, ...commit.files };
}
}
let children: FileNode[] = files.map(s => new StashFileNode(this.view, this, s, this.commit.toFileCommit(s)!));
if (this.view.config.files.layout !== ViewFilesLayout.List) {
const hierarchy = Arrays.makeHierarchical(
children,

+ 8
- 7
src/views/nodes/stashesNode.ts View File

@ -1,22 +1,23 @@
'use strict';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { MessageNode } from './common';
import { Container } from '../../container';
import { Repository } from '../../git/git';
import { GitUri } from '../../git/gitUri';
import { Iterables } from '../../system';
import { ViewsWithFiles } from '../viewBase';
import { MessageNode } from './common';
import { RepositoriesView } from '../repositoriesView';
import { RepositoryNode } from './repositoryNode';
import { StashesView } from '../stashesView';
import { StashNode } from './stashNode';
import { Iterables } from '../../system';
import { ContextValues, ViewNode } from './viewNode';
import { RepositoryNode } from './repositoryNode';
export class StashesNode extends ViewNode<ViewsWithFiles> {
export class StashesNode extends ViewNode<StashesView | RepositoriesView> {
static key = ':stashes';
static getId(repoPath: string): string {
return `${RepositoryNode.getId(repoPath)}${this.key}`;
}
constructor(uri: GitUri, view: ViewsWithFiles, parent: ViewNode, public readonly repo: Repository) {
constructor(uri: GitUri, view: StashesView | RepositoriesView, parent: ViewNode, public readonly repo: Repository) {
super(uri, view, parent);
}
@ -25,7 +26,7 @@ export class StashesNode extends ViewNode {
}
async getChildren(): Promise<ViewNode[]> {
const stash = await this.repo.getStashList();
const stash = await this.repo.getStash();
if (stash === undefined) return [new MessageNode(this.view, this, 'No stashes could be found.')];
return [...Iterables.map(stash.commits.values(), c => new StashNode(this.view, this, c))];

+ 192
- 0
src/views/stashesView.ts View File

@ -0,0 +1,192 @@
'use strict';
import { commands, ConfigurationChangeEvent, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { configuration, StashesViewConfig, ViewFilesLayout } from '../configuration';
import { Container } from '../container';
import { Repository, RepositoryChange, RepositoryChangeEvent } from '../git/git';
import { GitUri } from '../git/gitUri';
import { ContextValues, MessageNode, StashesNode, SubscribeableViewNode, unknownGitUri, ViewNode } from './nodes';
import { debug, gate } from '../system';
import { ViewBase } from './viewBase';
export class StashesRepositoryNode extends SubscribeableViewNode<StashesView> {
private child: StashesNode | undefined;
constructor(
uri: GitUri,
view: StashesView,
parent: ViewNode,
public readonly repo: Repository,
private readonly root: boolean,
) {
super(uri, view, parent);
}
async getChildren(): Promise<ViewNode[]> {
if (this.child == null) {
this.child = new StashesNode(this.uri, this.view, this, this.repo);
void this.ensureSubscription();
}
return this.child.getChildren();
}
getTreeItem(): TreeItem {
const item = new TreeItem(
this.repo.formattedName ?? this.uri.repoPath ?? '',
TreeItemCollapsibleState.Expanded,
);
item.contextValue = ContextValues.RepositoryFolder;
void this.ensureSubscription();
return item;
}
@gate()
@debug()
async refresh(reset: boolean = false) {
await this.child?.triggerChange(reset);
await this.ensureSubscription();
}
@debug()
protected subscribe() {
return this.repo.onDidChange(this.onRepositoryChanged, this);
}
@debug({
args: {
0: (e: RepositoryChangeEvent) =>
`{ repository: ${e.repository ? e.repository.name : ''}, changes: ${e.changes.join()} }`,
},
})
private onRepositoryChanged(e: RepositoryChangeEvent) {
if (e.changed(RepositoryChange.Closed)) {
this.dispose();
void this.parent?.triggerChange(true);
return;
}
if (e.changed(RepositoryChange.Heads)) {
void this.triggerChange(true);
if (this.root) {
void this.parent?.triggerChange(true);
}
}
}
}
export class StashesViewNode extends ViewNode<StashesView> {
private children: StashesRepositoryNode[] | undefined;
constructor(view: StashesView) {
super(unknownGitUri, view);
}
async getChildren(): Promise<ViewNode[]> {
if (this.children != null) {
for (const child of this.children) {
child.dispose?.();
}
this.children = undefined;
}
const repositories = await Container.git.getOrderedRepositories();
if (repositories.length === 0) return [new MessageNode(this.view, this, 'No stashes could be found.')];
const root = repositories.length === 1;
this.children = repositories.map(
r => new StashesRepositoryNode(GitUri.fromRepoPath(r.path), this.view, this, r, root),
);
if (root) {
const [child] = this.children;
const stash = await child.repo.getStash();
this.view.description = stash == null ? undefined : `(${stash.commits.size})`;
return child.getChildren();
}
return this.children;
}
getTreeItem(): TreeItem {
const item = new TreeItem('Stashes', TreeItemCollapsibleState.Expanded);
return item;
}
}
export class StashesView extends ViewBase<StashesViewNode, StashesViewConfig> {
protected readonly configKey = 'stashes';
constructor() {
super('gitlens.views.stashes', 'Stashes');
}
getRoot() {
return new StashesViewNode(this);
}
protected registerCommands() {
void Container.viewCommands;
commands.registerCommand(
this.getQualifiedCommand('copy'),
() => commands.executeCommand('gitlens.views.copy', this.selection),
this,
);
commands.registerCommand(this.getQualifiedCommand('refresh'), () => this.refresh(true), this);
commands.registerCommand(
this.getQualifiedCommand('setFilesLayoutToAuto'),
() => this.setFilesLayout(ViewFilesLayout.Auto),
this,
);
commands.registerCommand(
this.getQualifiedCommand('setFilesLayoutToList'),
() => this.setFilesLayout(ViewFilesLayout.List),
this,
);
commands.registerCommand(
this.getQualifiedCommand('setFilesLayoutToTree'),
() => this.setFilesLayout(ViewFilesLayout.Tree),
this,
);
commands.registerCommand(this.getQualifiedCommand('setShowAvatarsOn'), () => this.setShowAvatars(true), this);
commands.registerCommand(this.getQualifiedCommand('setShowAvatarsOff'), () => this.setShowAvatars(false), this);
}
protected filterConfigurationChanged(e: ConfigurationChangeEvent) {
const changed = super.filterConfigurationChanged(e);
if (
!changed &&
!configuration.changed(e, 'defaultDateFormat') &&
!configuration.changed(e, 'defaultDateSource') &&
!configuration.changed(e, 'defaultDateStyle') &&
!configuration.changed(e, 'defaultGravatarsStyle')
) {
return false;
}
return true;
}
protected onConfigurationChanged(e: ConfigurationChangeEvent) {
if (configuration.initializing(e)) {
this.initialize(undefined, { showCollapseAll: true });
}
if (!configuration.initializing(e) && this._root != null) {
void this.refresh(true);
}
}
private setFilesLayout(layout: ViewFilesLayout) {
return configuration.updateEffective('views', this.configKey, 'files', 'layout', layout);
}
private setShowAvatars(enabled: boolean) {
return configuration.updateEffective('views', this.configKey, 'avatars', enabled);
}
}

+ 4
- 1
src/views/viewBase.ts View File

@ -39,6 +39,7 @@ import {
} from '../configuration';
import { GlyphChars } from '../constants';
import { Container } from '../container';
import { ContributorsView } from './contributorsView';
import { FileHistoryView } from './fileHistoryView';
import { LineHistoryView } from './lineHistoryView';
import { Logger } from '../logger';
@ -46,9 +47,9 @@ import { PageableViewNode, ViewNode } from './nodes';
import { RemotesView } from './remotesView';
import { RepositoriesView } from './repositoriesView';
import { SearchView } from './searchView';
import { StashesView } from './stashesView';
import { debug, Functions, log, Promises, Strings } from '../system';
import { TagsView } from './tagsView';
import { ContributorsView } from './contributorsView';
export type View =
| BranchesView
@ -60,6 +61,7 @@ export type View =
| RemotesView
| RepositoriesView
| SearchView
| StashesView
| TagsView;
export type ViewsWithFiles =
| BranchesView
@ -69,6 +71,7 @@ export type ViewsWithFiles =
| RemotesView
| RepositoriesView
| SearchView
| StashesView
| TagsView;
export interface TreeViewNodeStateChangeEvent<T> extends TreeViewExpansionEvent<T> {

Loading…
Cancel
Save