Closes #354 - Adds line history explorer (wip) Closes #456 - Adds repository status to the repository nodesmain
@ -0,0 +1,5 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<svg width="16" height="22" version="1.1" xmlns="http://www.w3.org/2000/svg"> | |||
<path fill="#C5C5C5" d="m6,12l-1,0l0,-1l1,0l0,1l0,0zm0,-3l-1,0l0,1l1,0l0,-1l0,0zm0,-2l-1,0l0,1l1,0l0,-1l0,0zm0,-2l-1,0l0,1l1,0l0,-1l0,0zm8,-1l0,12c0,0.55 -0.45,1 -1,1l-5,0l0,2l-1.5,-1.5l-1.5,1.5l0,-2l-2,0c-0.55,0 -1,-0.45 -1,-1l0,-12c0,-0.55 0.45,-1 1,-1l10,0c0.55,0 1,0.45 1,1l0,0zm-1,10l-10,0l0,2l2,0l0,-1l3,0l0,1l5,0l0,-2l0,0zm0,-10l-9,0l0,9l9,0l0,-9l0,0z" /> | |||
<ellipse fill="#0366d6" stroke="#C5C5C5" stroke-width="0.5" rx="3" ry="3" cx="13" cy="4" /> | |||
</svg> |
@ -0,0 +1,5 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<svg width="16" height="22" version="1.1" xmlns="http://www.w3.org/2000/svg"> | |||
<path fill="#424242" d="m6,12l-1,0l0,-1l1,0l0,1l0,0zm0,-3l-1,0l0,1l1,0l0,-1l0,0zm0,-2l-1,0l0,1l1,0l0,-1l0,0zm0,-2l-1,0l0,1l1,0l0,-1l0,0zm8,-1l0,12c0,0.55 -0.45,1 -1,1l-5,0l0,2l-1.5,-1.5l-1.5,1.5l0,-2l-2,0c-0.55,0 -1,-0.45 -1,-1l0,-12c0,-0.55 0.45,-1 1,-1l10,0c0.55,0 1,0.45 1,1l0,0zm-1,10l-10,0l0,2l2,0l0,-1l3,0l0,1l5,0l0,-2l0,0zm0,-10l-9,0l0,9l9,0l0,-9l0,0z" /> | |||
<ellipse fill="#0366d6" stroke="#424242" stroke-width="0.5" rx="3" ry="3" cx="13" cy="4" /> | |||
</svg> |
@ -0,0 +1,33 @@ | |||
'use strict'; | |||
import { Container } from '../container'; | |||
import { Command, CommandContext, Commands } from './common'; | |||
export class ShowExplorerCommand extends Command { | |||
constructor() { | |||
super([ | |||
Commands.ShowGitExplorer, | |||
Commands.ShowFileHistoryExplorer, | |||
Commands.ShowLineHistoryExplorer, | |||
Commands.ShowResultsExplorer | |||
]); | |||
} | |||
protected async preExecute(context: CommandContext): Promise<any> { | |||
return this.execute(context.command as Commands); | |||
} | |||
execute(command: Commands) { | |||
switch (command) { | |||
case Commands.ShowGitExplorer: | |||
return Container.gitExplorer.show(); | |||
case Commands.ShowFileHistoryExplorer: | |||
return Container.fileHistoryExplorer.show(); | |||
case Commands.ShowLineHistoryExplorer: | |||
return Container.lineHistoryExplorer.show(); | |||
case Commands.ShowResultsExplorer: | |||
return Container.resultsExplorer.show(); | |||
} | |||
return undefined; | |||
} | |||
} |
@ -1,13 +0,0 @@ | |||
'use strict'; | |||
import { Container } from '../container'; | |||
import { Command, Commands } from './common'; | |||
export class ShowGitExplorerCommand extends Command { | |||
constructor() { | |||
super(Commands.ShowGitExplorer); | |||
} | |||
execute() { | |||
return Container.gitExplorer.show(); | |||
} | |||
} |
@ -1,13 +0,0 @@ | |||
'use strict'; | |||
import { Container } from '../container'; | |||
import { Command, Commands } from './common'; | |||
export class ShowHistoryExplorerCommand extends Command { | |||
constructor() { | |||
super(Commands.ShowHistoryExplorer); | |||
} | |||
execute() { | |||
return Container.historyExplorer.show(); | |||
} | |||
} |
@ -1,13 +0,0 @@ | |||
'use strict'; | |||
import { Container } from '../container'; | |||
import { Command, Commands } from './common'; | |||
export class ShowResultsExplorerCommand extends Command { | |||
constructor() { | |||
super(Commands.ShowResultsExplorer); | |||
} | |||
execute() { | |||
return Container.resultsExplorer.show(); | |||
} | |||
} |
@ -0,0 +1,80 @@ | |||
'use strict'; | |||
import { commands, ConfigurationChangeEvent } from 'vscode'; | |||
import { configuration, IExplorersConfig, IFileHistoryExplorerConfig } from '../configuration'; | |||
import { CommandContext, setCommandContext } from '../constants'; | |||
import { Container } from '../container'; | |||
import { ExplorerBase, RefreshReason } from './explorer'; | |||
import { RefreshNodeCommandArgs } from './explorerCommands'; | |||
import { ActiveFileHistoryNode, ExplorerNode } from './nodes'; | |||
export class FileHistoryExplorer extends ExplorerBase<ActiveFileHistoryNode> { | |||
constructor() { | |||
super('gitlens.fileHistoryExplorer'); | |||
} | |||
getRoot() { | |||
return new ActiveFileHistoryNode(this); | |||
} | |||
protected registerCommands() { | |||
Container.explorerCommands; | |||
commands.registerCommand(this.getQualifiedCommand('refresh'), () => this.refresh(), this); | |||
commands.registerCommand( | |||
this.getQualifiedCommand('refreshNode'), | |||
(node: ExplorerNode, args?: RefreshNodeCommandArgs) => this.refreshNode(node, args), | |||
this | |||
); | |||
commands.registerCommand( | |||
this.getQualifiedCommand('setRenameFollowingOn'), | |||
() => this.setRenameFollowing(true), | |||
this | |||
); | |||
commands.registerCommand( | |||
this.getQualifiedCommand('setRenameFollowingOff'), | |||
() => this.setRenameFollowing(false), | |||
this | |||
); | |||
} | |||
protected onConfigurationChanged(e: ConfigurationChangeEvent) { | |||
const initializing = configuration.initializing(e); | |||
if ( | |||
!initializing && | |||
!configuration.changed(e, configuration.name('fileHistoryExplorer').value) && | |||
!configuration.changed(e, configuration.name('explorers').value) && | |||
!configuration.changed(e, configuration.name('defaultGravatarsStyle').value) && | |||
!configuration.changed(e, configuration.name('advanced')('fileHistoryFollowsRenames').value) | |||
) { | |||
return; | |||
} | |||
if ( | |||
initializing || | |||
configuration.changed(e, configuration.name('fileHistoryExplorer')('enabled').value) || | |||
configuration.changed(e, configuration.name('fileHistoryExplorer')('location').value) | |||
) { | |||
setCommandContext(CommandContext.FileHistoryExplorer, this.config.enabled ? this.config.location : false); | |||
} | |||
if (initializing || configuration.changed(e, configuration.name('fileHistoryExplorer')('location').value)) { | |||
this.initialize(this.config.location); | |||
} | |||
if (!initializing && this._root !== undefined) { | |||
void this.refresh(RefreshReason.ConfigurationChanged); | |||
} | |||
} | |||
get config(): IExplorersConfig & IFileHistoryExplorerConfig { | |||
return { ...Container.config.explorers, ...Container.config.fileHistoryExplorer }; | |||
} | |||
private setRenameFollowing(enabled: boolean) { | |||
return configuration.updateEffective( | |||
configuration.name('advanced')('fileHistoryFollowsRenames').value, | |||
enabled | |||
); | |||
} | |||
} |
@ -1,251 +0,0 @@ | |||
'use strict'; | |||
import * as path from 'path'; | |||
import { | |||
commands, | |||
ConfigurationChangeEvent, | |||
Disposable, | |||
Event, | |||
EventEmitter, | |||
TextEditor, | |||
TreeDataProvider, | |||
TreeItem, | |||
TreeView, | |||
Uri, | |||
window | |||
} from 'vscode'; | |||
import { UriComparer } from '../comparers'; | |||
import { configuration, IExplorersConfig, IHistoryExplorerConfig } from '../configuration'; | |||
import { CommandContext, GlyphChars, setCommandContext } from '../constants'; | |||
import { Container } from '../container'; | |||
import { GitUri } from '../git/gitUri'; | |||
import { Logger } from '../logger'; | |||
import { Functions } from '../system'; | |||
import { RefreshNodeCommandArgs } from '../views/explorerCommands'; | |||
import { ExplorerNode, HistoryNode, MessageNode, RefreshReason } from './nodes'; | |||
export * from './nodes'; | |||
export class HistoryExplorer implements TreeDataProvider<ExplorerNode>, Disposable { | |||
private _disposable: Disposable | undefined; | |||
private _root?: ExplorerNode; | |||
private _tree: TreeView<ExplorerNode> | undefined; | |||
private _onDidChangeTreeData = new EventEmitter<ExplorerNode>(); | |||
public get onDidChangeTreeData(): Event<ExplorerNode> { | |||
return this._onDidChangeTreeData.event; | |||
} | |||
constructor() { | |||
Container.explorerCommands; | |||
commands.registerCommand('gitlens.historyExplorer.refresh', this.refresh, this); | |||
commands.registerCommand('gitlens.historyExplorer.refreshNode', this.refreshNode, this); | |||
commands.registerCommand( | |||
'gitlens.historyExplorer.setRenameFollowingOn', | |||
() => this.setRenameFollowing(true), | |||
this | |||
); | |||
commands.registerCommand( | |||
'gitlens.historyExplorer.setRenameFollowingOff', | |||
() => this.setRenameFollowing(false), | |||
this | |||
); | |||
Container.context.subscriptions.push( | |||
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this), | |||
window.onDidChangeVisibleTextEditors(Functions.debounce(this.onVisibleEditorsChanged, 500), this), | |||
configuration.onDidChange(this.onConfigurationChanged, this) | |||
); | |||
void this.onConfigurationChanged(configuration.initializingChangeEvent); | |||
} | |||
dispose() { | |||
this._disposable && this._disposable.dispose(); | |||
} | |||
private async onConfigurationChanged(e: ConfigurationChangeEvent) { | |||
const initializing = configuration.initializing(e); | |||
if ( | |||
!initializing && | |||
!configuration.changed(e, configuration.name('historyExplorer').value) && | |||
!configuration.changed(e, configuration.name('explorers').value) && | |||
!configuration.changed(e, configuration.name('defaultGravatarsStyle').value) && | |||
!configuration.changed(e, configuration.name('advanced')('fileHistoryFollowsRenames').value) | |||
) { | |||
return; | |||
} | |||
if ( | |||
initializing || | |||
configuration.changed(e, configuration.name('historyExplorer')('enabled').value) || | |||
configuration.changed(e, configuration.name('historyExplorer')('location').value) | |||
) { | |||
setCommandContext(CommandContext.HistoryExplorer, this.config.enabled ? this.config.location : false); | |||
} | |||
if (initializing) { | |||
this.setRoot(await this.getRootNode(window.activeTextEditor)); | |||
} | |||
if (initializing || configuration.changed(e, configuration.name('historyExplorer')('location').value)) { | |||
if (this._disposable) { | |||
this._disposable.dispose(); | |||
this._onDidChangeTreeData = new EventEmitter<ExplorerNode>(); | |||
} | |||
this._tree = window.createTreeView(`gitlens.historyExplorer:${this.config.location}`, { | |||
treeDataProvider: this | |||
}); | |||
this._disposable = this._tree; | |||
} | |||
if (!initializing && this._root !== undefined) { | |||
void this.refresh(RefreshReason.ConfigurationChanged); | |||
} | |||
} | |||
private async onActiveEditorChanged(editor: TextEditor | undefined) { | |||
const root = await this.getRootNode(editor); | |||
if (!this.setRoot(root)) return; | |||
void this.refresh(RefreshReason.ActiveEditorChanged, root); | |||
} | |||
private onVisibleEditorsChanged(editors: TextEditor[]) { | |||
if (this._root === undefined) return; | |||
// If we have no visible editors, or no trackable visible editors reset the view | |||
if (editors.length === 0 || !editors.some(e => e.document && Container.git.isTrackable(e.document.uri))) { | |||
this.clearRoot(); | |||
void this.refresh(RefreshReason.VisibleEditorsChanged); | |||
} | |||
} | |||
get config(): IExplorersConfig & IHistoryExplorerConfig { | |||
return { ...Container.config.explorers, ...Container.config.historyExplorer }; | |||
} | |||
getParent(element: ExplorerNode): ExplorerNode | undefined { | |||
return undefined; | |||
} | |||
async getChildren(node?: ExplorerNode): Promise<ExplorerNode[]> { | |||
if (this._root === undefined) return [new MessageNode(`No active file ${GlyphChars.Dash} no history to show`)]; | |||
if (node === undefined) return this._root.getChildren(); | |||
return node.getChildren(); | |||
} | |||
async getTreeItem(node: ExplorerNode): Promise<TreeItem> { | |||
return node.getTreeItem(); | |||
} | |||
getQualifiedCommand(command: string) { | |||
return `gitlens.historyExplorer.${command}`; | |||
} | |||
async refresh(reason?: RefreshReason, root?: ExplorerNode) { | |||
if (reason === undefined) { | |||
reason = RefreshReason.Command; | |||
} | |||
Logger.log(`HistoryExplorer.refresh`, `reason='${reason}'`); | |||
if (this._root === undefined || root === undefined) { | |||
this.clearRoot(); | |||
this.setRoot(await this.getRootNode(window.activeTextEditor)); | |||
} | |||
this._onDidChangeTreeData.fire(); | |||
} | |||
refreshNode(node: ExplorerNode, args?: RefreshNodeCommandArgs) { | |||
Logger.log(`HistoryExplorer.refreshNode(${(node as { id?: string }).id || ''})`); | |||
if (args !== undefined && node.supportsPaging) { | |||
node.maxCount = args.maxCount; | |||
} | |||
node.refresh(); | |||
// Since a root node won't actually refresh, force everything | |||
this._onDidChangeTreeData.fire(this._root === node ? undefined : node); | |||
} | |||
async show() { | |||
if (this._root === undefined || this._tree === undefined) return; | |||
try { | |||
await this._tree.reveal(this._root, { select: false }); | |||
} | |||
catch (ex) { | |||
Logger.error(ex); | |||
} | |||
} | |||
private clearRoot() { | |||
if (this._root === undefined) return; | |||
this._root.dispose(); | |||
this._root = undefined; | |||
} | |||
private async getRootNode(editor: TextEditor | undefined): Promise<ExplorerNode | undefined> { | |||
// If we have no active editor, or no visible editors, or no trackable visible editors reset the view | |||
if ( | |||
editor == null || | |||
window.visibleTextEditors.length === 0 || | |||
!window.visibleTextEditors.some(e => e.document && Container.git.isTrackable(e.document.uri)) | |||
) { | |||
return undefined; | |||
} | |||
// If we do have a visible trackable editor, don't change from the last state (avoids issues when focus switches to the problems/output/debug console panes) | |||
if (editor.document === undefined || !Container.git.isTrackable(editor.document.uri)) return this._root; | |||
let gitUri = await GitUri.fromUri(editor.document.uri); | |||
const repo = await Container.git.getRepository(gitUri); | |||
if (repo === undefined) return undefined; | |||
let uri; | |||
if (gitUri.sha !== undefined) { | |||
// If we have a sha, normalize the history to the working file (so we get a full history all the time) | |||
const [fileName, repoPath] = await Container.git.findWorkingFileName( | |||
gitUri.fsPath, | |||
gitUri.repoPath, | |||
gitUri.sha | |||
); | |||
if (fileName !== undefined) { | |||
uri = Uri.file(repoPath !== undefined ? path.join(repoPath, fileName) : fileName); | |||
} | |||
} | |||
if (UriComparer.equals(uri || gitUri, this._root && this._root.uri)) return this._root; | |||
if (uri !== undefined) { | |||
gitUri = await GitUri.fromUri(uri); | |||
} | |||
return new HistoryNode(gitUri, repo, this); | |||
} | |||
private setRenameFollowing(enabled: boolean) { | |||
return configuration.updateEffective( | |||
configuration.name('advanced')('fileHistoryFollowsRenames').value, | |||
enabled | |||
); | |||
} | |||
private setRoot(root: ExplorerNode | undefined): boolean { | |||
if (this._root === root) return false; | |||
if (this._root !== undefined) { | |||
this._root.dispose(); | |||
} | |||
this._root = root; | |||
return true; | |||
} | |||
} |
@ -0,0 +1,80 @@ | |||
'use strict'; | |||
import { commands, ConfigurationChangeEvent } from 'vscode'; | |||
import { configuration, IExplorersConfig, ILineHistoryExplorerConfig } from '../configuration'; | |||
import { CommandContext, setCommandContext } from '../constants'; | |||
import { Container } from '../container'; | |||
import { ExplorerBase, RefreshReason } from './explorer'; | |||
import { RefreshNodeCommandArgs } from './explorerCommands'; | |||
import { ExplorerNode } from './nodes'; | |||
import { ActiveLineHistoryNode } from './nodes/activeLineHistoryNode'; | |||
export class LineHistoryExplorer extends ExplorerBase<ActiveLineHistoryNode> { | |||
constructor() { | |||
super('gitlens.lineHistoryExplorer'); | |||
} | |||
getRoot() { | |||
return new ActiveLineHistoryNode(this); | |||
} | |||
protected registerCommands() { | |||
Container.explorerCommands; | |||
commands.registerCommand(this.getQualifiedCommand('refresh'), () => this.refresh(), this); | |||
commands.registerCommand( | |||
this.getQualifiedCommand('refreshNode'), | |||
(node: ExplorerNode, args?: RefreshNodeCommandArgs) => this.refreshNode(node, args), | |||
this | |||
); | |||
commands.registerCommand( | |||
this.getQualifiedCommand('setRenameFollowingOn'), | |||
() => this.setRenameFollowing(true), | |||
this | |||
); | |||
commands.registerCommand( | |||
this.getQualifiedCommand('setRenameFollowingOff'), | |||
() => this.setRenameFollowing(false), | |||
this | |||
); | |||
} | |||
protected onConfigurationChanged(e: ConfigurationChangeEvent) { | |||
const initializing = configuration.initializing(e); | |||
if ( | |||
!initializing && | |||
!configuration.changed(e, configuration.name('lineHistoryExplorer').value) && | |||
!configuration.changed(e, configuration.name('explorers').value) && | |||
!configuration.changed(e, configuration.name('defaultGravatarsStyle').value) && | |||
!configuration.changed(e, configuration.name('advanced')('fileHistoryFollowsRenames').value) | |||
) { | |||
return; | |||
} | |||
if ( | |||
initializing || | |||
configuration.changed(e, configuration.name('lineHistoryExplorer')('enabled').value) || | |||
configuration.changed(e, configuration.name('lineHistoryExplorer')('location').value) | |||
) { | |||
setCommandContext(CommandContext.LineHistoryExplorer, this.config.enabled ? this.config.location : false); | |||
} | |||
if (initializing || configuration.changed(e, configuration.name('lineHistoryExplorer')('location').value)) { | |||
this.initialize(this.config.location); | |||
} | |||
if (!initializing && this._root !== undefined) { | |||
void this.refresh(RefreshReason.ConfigurationChanged); | |||
} | |||
} | |||
get config(): IExplorersConfig & ILineHistoryExplorerConfig { | |||
return { ...Container.config.explorers, ...Container.config.lineHistoryExplorer }; | |||
} | |||
private setRenameFollowing(enabled: boolean) { | |||
return configuration.updateEffective( | |||
configuration.name('advanced')('fileHistoryFollowsRenames').value, | |||
enabled | |||
); | |||
} | |||
} |
@ -0,0 +1,108 @@ | |||
'use strict'; | |||
import * as path from 'path'; | |||
import { Disposable, TextEditor, TreeItem, TreeItemCollapsibleState, Uri, window } from 'vscode'; | |||
import { UriComparer } from '../../comparers'; | |||
import { Container } from '../../container'; | |||
import { GitUri } from '../../git/gitService'; | |||
import { Functions } from '../../system'; | |||
import { FileHistoryExplorer } from '../fileHistoryExplorer'; | |||
import { MessageNode } from './common'; | |||
import { ExplorerNode, ResourceType, SubscribeableExplorerNode, unknownGitUri } from './explorerNode'; | |||
import { FileHistoryNode } from './fileHistoryNode'; | |||
export class ActiveFileHistoryNode extends SubscribeableExplorerNode<FileHistoryExplorer> { | |||
private _child: FileHistoryNode | undefined; | |||
constructor(explorer: FileHistoryExplorer) { | |||
super(unknownGitUri, explorer); | |||
} | |||
dispose() { | |||
super.dispose(); | |||
this.resetChild(); | |||
} | |||
resetChild() { | |||
if (this._child !== undefined) { | |||
this._child.dispose(); | |||
this._child = undefined; | |||
} | |||
} | |||
async getChildren(): Promise<ExplorerNode[]> { | |||
if (this._child === undefined) { | |||
if (this.uri === unknownGitUri) { | |||
return [new MessageNode('There are no editors open that can provide file history')]; | |||
} | |||
this._child = new FileHistoryNode(this.uri, this.explorer); | |||
} | |||
return [this._child]; | |||
} | |||
getTreeItem(): TreeItem { | |||
const item = new TreeItem('File History', TreeItemCollapsibleState.Expanded); | |||
item.contextValue = ResourceType.ActiveFileHistory; | |||
void this.ensureSubscription(); | |||
return item; | |||
} | |||
async refresh() { | |||
const editor = window.activeTextEditor; | |||
if (editor == null || !Container.git.isTrackable(editor.document.uri)) { | |||
if ( | |||
this.uri === unknownGitUri || | |||
(Container.git.isTrackable(this.uri) && | |||
window.visibleTextEditors.some(e => e.document && UriComparer.equals(e.document.uri, this.uri))) | |||
) { | |||
return; | |||
} | |||
this._uri = unknownGitUri; | |||
this.resetChild(); | |||
return; | |||
} | |||
if (UriComparer.equals(editor!.document.uri, this.uri)) return; | |||
let gitUri = await GitUri.fromUri(editor!.document.uri); | |||
let uri; | |||
if (gitUri.sha !== undefined) { | |||
// If we have a sha, normalize the history to the working file (so we get a full history all the time) | |||
const [fileName, repoPath] = await Container.git.findWorkingFileName( | |||
gitUri.fsPath, | |||
gitUri.repoPath, | |||
gitUri.sha | |||
); | |||
if (fileName !== undefined) { | |||
uri = Uri.file(repoPath !== undefined ? path.join(repoPath, fileName) : fileName); | |||
} | |||
} | |||
if (this.uri !== unknownGitUri && UriComparer.equals(uri || gitUri, this.uri)) return; | |||
if (uri !== undefined) { | |||
gitUri = await GitUri.fromUri(uri); | |||
} | |||
this._uri = gitUri; | |||
this.resetChild(); | |||
} | |||
protected async subscribe() { | |||
return Disposable.from( | |||
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this) | |||
); | |||
} | |||
private onActiveEditorChanged(editor: TextEditor | undefined) { | |||
void this.explorer.refreshNode(this); | |||
} | |||
} |
@ -0,0 +1,116 @@ | |||
'use strict'; | |||
import { | |||
Disposable, | |||
Selection, | |||
TextEditor, | |||
TextEditorSelectionChangeEvent, | |||
TreeItem, | |||
TreeItemCollapsibleState, | |||
window | |||
} from 'vscode'; | |||
import { UriComparer } from '../../comparers'; | |||
import { Container } from '../../container'; | |||
import { GitUri } from '../../git/gitService'; | |||
import { Functions } from '../../system'; | |||
import { LineHistoryExplorer } from '../lineHistoryExplorer'; | |||
import { MessageNode } from './common'; | |||
import { ExplorerNode, ResourceType, SubscribeableExplorerNode, unknownGitUri } from './explorerNode'; | |||
import { LineHistoryNode } from './lineHistoryNode'; | |||
export class ActiveLineHistoryNode extends SubscribeableExplorerNode<LineHistoryExplorer> { | |||
private _child: LineHistoryNode | undefined; | |||
private _selection: Selection | undefined; | |||
constructor(explorer: LineHistoryExplorer) { | |||
super(unknownGitUri, explorer); | |||
} | |||
dispose() { | |||
super.dispose(); | |||
this.resetChild(); | |||
} | |||
resetChild() { | |||
if (this._child !== undefined) { | |||
this._child.dispose(); | |||
this._child = undefined; | |||
} | |||
} | |||
async getChildren(): Promise<ExplorerNode[]> { | |||
if (this._child === undefined) { | |||
if (this.uri === unknownGitUri) { | |||
return [new MessageNode('There are no editors open that can provide line history')]; | |||
} | |||
this._child = new LineHistoryNode(this.uri, this._selection!, this.explorer); | |||
} | |||
return [this._child]; | |||
} | |||
getTreeItem(): TreeItem { | |||
const item = new TreeItem('Line History', TreeItemCollapsibleState.Expanded); | |||
item.contextValue = ResourceType.ActiveLineHistory; | |||
void this.ensureSubscription(); | |||
return item; | |||
} | |||
async refresh() { | |||
const editor = window.activeTextEditor; | |||
if (editor == null || !Container.git.isTrackable(editor.document.uri)) { | |||
if ( | |||
this.uri === unknownGitUri || | |||
(Container.git.isTrackable(this.uri) && | |||
window.visibleTextEditors.some(e => e.document && UriComparer.equals(e.document.uri, this.uri))) | |||
) { | |||
return; | |||
} | |||
this._uri = unknownGitUri; | |||
this._selection = undefined; | |||
this.resetChild(); | |||
return; | |||
} | |||
if ( | |||
UriComparer.equals(editor!.document.uri, this.uri) && | |||
(this._selection !== undefined && editor.selection.isEqual(this._selection)) | |||
) { | |||
return; | |||
} | |||
const gitUri = await GitUri.fromUri(editor!.document.uri); | |||
if ( | |||
this.uri !== unknownGitUri && | |||
UriComparer.equals(gitUri, this.uri) && | |||
(this._selection !== undefined && editor.selection.isEqual(this._selection)) | |||
) { | |||
return; | |||
} | |||
this._uri = gitUri; | |||
this._selection = editor.selection; | |||
this.resetChild(); | |||
} | |||
protected async subscribe() { | |||
return Disposable.from( | |||
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this), | |||
window.onDidChangeTextEditorSelection(Functions.debounce(this.onSelectionChanged, 500), this) | |||
); | |||
} | |||
private onActiveEditorChanged(editor: TextEditor | undefined) { | |||
void this.explorer.refreshNode(this); | |||
} | |||
private onSelectionChanged(e: TextEditorSelectionChangeEvent) { | |||
void this.explorer.refreshNode(this); | |||
} | |||
} |
@ -1,97 +0,0 @@ | |||
'use strict'; | |||
import { TextEditor, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; | |||
import { isTextEditor } from '../../constants'; | |||
import { Container } from '../../container'; | |||
import { GitUri } from '../../git/gitService'; | |||
import { Functions } from '../../system'; | |||
import { GitExplorer } from '../gitExplorer'; | |||
import { ExplorerNode } from './explorerNode'; | |||
import { RepositoryNode } from './repositoryNode'; | |||
export class ActiveRepositoryNode extends ExplorerNode { | |||
private _repositoryNode: RepositoryNode | undefined; | |||
constructor( | |||
private readonly explorer: GitExplorer | |||
) { | |||
super(undefined!); | |||
Container.context.subscriptions.push( | |||
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this) | |||
); | |||
void this.onActiveEditorChanged(window.activeTextEditor); | |||
} | |||
dispose() { | |||
super.dispose(); | |||
if (this._repositoryNode !== undefined) { | |||
this._repositoryNode.dispose(); | |||
this._repositoryNode = undefined; | |||
} | |||
} | |||
get id(): string { | |||
return 'gitlens:repository:active'; | |||
} | |||
private async onActiveEditorChanged(editor: TextEditor | undefined) { | |||
if (editor !== undefined && !isTextEditor(editor)) return; | |||
let changed = false; | |||
try { | |||
const repoPath = await Container.git.getActiveRepoPath(editor); | |||
if (repoPath === undefined) { | |||
if (this._repositoryNode !== undefined) { | |||
changed = true; | |||
this._repositoryNode.dispose(); | |||
this._repositoryNode = undefined; | |||
} | |||
return; | |||
} | |||
if (this._repositoryNode !== undefined && this._repositoryNode.repo.path === repoPath) return; | |||
const repo = await Container.git.getRepository(repoPath); | |||
if (repo === undefined || repo.closed) { | |||
if (this._repositoryNode !== undefined) { | |||
changed = true; | |||
this._repositoryNode.dispose(); | |||
this._repositoryNode = undefined; | |||
} | |||
return; | |||
} | |||
changed = true; | |||
if (this._repositoryNode !== undefined) { | |||
this._repositoryNode.dispose(); | |||
} | |||
this._repositoryNode = new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this.explorer, true, this); | |||
} | |||
finally { | |||
if (changed) { | |||
this.explorer.refreshNode(this); | |||
} | |||
} | |||
} | |||
async getChildren(): Promise<ExplorerNode[]> { | |||
return this._repositoryNode !== undefined ? this._repositoryNode.getChildren() : []; | |||
} | |||
getTreeItem(): TreeItem { | |||
const item = | |||
this._repositoryNode !== undefined | |||
? this._repositoryNode.getTreeItem() | |||
: new TreeItem('No active repository', TreeItemCollapsibleState.None); | |||
item.id = this.id; | |||
return item; | |||
} | |||
} |
@ -1,38 +0,0 @@ | |||
'use strict'; | |||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||
import { GitLog, GitUri } from '../../git/gitService'; | |||
import { Iterables } from '../../system'; | |||
import { CommitNode } from './commitNode'; | |||
import { Explorer, ExplorerNode, ResourceType, ShowAllNode } from './explorerNode'; | |||
export class CommitsNode extends ExplorerNode { | |||
readonly supportsPaging: boolean = true; | |||
constructor( | |||
public readonly repoPath: string, | |||
private readonly logFn: (maxCount: number | undefined) => Promise<GitLog | undefined>, | |||
private readonly explorer: Explorer | |||
) { | |||
super(GitUri.fromRepoPath(repoPath)); | |||
} | |||
async getChildren(): Promise<ExplorerNode[]> { | |||
const log = await this.logFn(this.maxCount); | |||
if (log === undefined) return []; | |||
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('Commits', TreeItemCollapsibleState.Collapsed); | |||
item.contextValue = ResourceType.Commits; | |||
return item; | |||
} | |||
} |
@ -1,74 +0,0 @@ | |||
'use strict'; | |||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||
import { GitLog, GitUri } from '../../git/gitService'; | |||
import { Iterables } from '../../system'; | |||
import { CommitNode } from './commitNode'; | |||
import { Explorer, ExplorerNode, ResourceType, ShowAllNode } from './explorerNode'; | |||
export class CommitsResultsNode extends ExplorerNode { | |||
readonly supportsPaging: boolean = true; | |||
private _cache: { label: string; log: GitLog | undefined } | undefined; | |||
constructor( | |||
public readonly repoPath: string, | |||
private readonly labelFn: (log: GitLog | undefined) => Promise<string>, | |||
private readonly logFn: (maxCount: number | undefined) => Promise<GitLog | undefined>, | |||
private readonly explorer: Explorer, | |||
private readonly contextValue: ResourceType = ResourceType.ResultsCommits | |||
) { | |||
super(GitUri.fromRepoPath(repoPath)); | |||
} | |||
async getChildren(): Promise<ExplorerNode[]> { | |||
const log = await this.getLog(); | |||
if (log === undefined) return []; | |||
const children: (CommitNode | ShowAllNode)[] = [ | |||
...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer)) | |||
]; | |||
if (log.truncated) { | |||
children.push(new ShowAllNode('Show All Results', this, this.explorer)); | |||
} | |||
return children; | |||
} | |||
async getTreeItem(): Promise<TreeItem> { | |||
const log = await this.getLog(); | |||
const item = new TreeItem( | |||
await this.getLabel(), | |||
log && log.count > 0 ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None | |||
); | |||
item.contextValue = this.contextValue; | |||
return item; | |||
} | |||
refresh() { | |||
this._cache = undefined; | |||
} | |||
private async ensureCache() { | |||
if (this._cache === undefined) { | |||
const log = await this.logFn(this.maxCount); | |||
this._cache = { | |||
label: await this.labelFn(log), | |||
log: log | |||
}; | |||
} | |||
return this._cache; | |||
} | |||
private async getLabel() { | |||
const cache = await this.ensureCache(); | |||
return cache.label; | |||
} | |||
private async getLog() { | |||
const cache = await this.ensureCache(); | |||
return cache.log; | |||
} | |||
} |
@ -0,0 +1,94 @@ | |||
import { Command, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; | |||
import { GlyphChars } from '../../constants'; | |||
import { Container } from '../../container'; | |||
import { Explorer } from '../explorer'; | |||
import { RefreshNodeCommandArgs } from '../explorerCommands'; | |||
import { ExplorerNode, ResourceType, unknownGitUri } from '../nodes/explorerNode'; | |||
export class MessageNode extends ExplorerNode { | |||
constructor( | |||
private readonly message: string, | |||
private readonly tooltip?: string, | |||
private readonly iconPath?: | |||
| string | |||
| Uri | |||
| { | |||
light: string | Uri; | |||
dark: string | Uri; | |||
} | |||
| ThemeIcon | |||
) { | |||
super(unknownGitUri); | |||
} | |||
getChildren(): ExplorerNode[] | Promise<ExplorerNode[]> { | |||
return []; | |||
} | |||
getTreeItem(): TreeItem | Promise<TreeItem> { | |||
const item = new TreeItem(this.message, TreeItemCollapsibleState.None); | |||
item.contextValue = ResourceType.Message; | |||
item.tooltip = this.tooltip; | |||
item.iconPath = this.iconPath; | |||
return item; | |||
} | |||
} | |||
export abstract class PagerNode extends ExplorerNode { | |||
protected _args: RefreshNodeCommandArgs = {}; | |||
constructor( | |||
protected readonly message: string, | |||
protected readonly node: ExplorerNode, | |||
protected readonly explorer: Explorer | |||
) { | |||
super(unknownGitUri); | |||
} | |||
getChildren(): ExplorerNode[] | Promise<ExplorerNode[]> { | |||
return []; | |||
} | |||
getTreeItem(): TreeItem | Promise<TreeItem> { | |||
const item = new TreeItem(this.message, TreeItemCollapsibleState.None); | |||
item.contextValue = ResourceType.Pager; | |||
item.command = this.getCommand(); | |||
item.iconPath = { | |||
dark: Container.context.asAbsolutePath('images/dark/icon-unfold.svg'), | |||
light: Container.context.asAbsolutePath('images/light/icon-unfold.svg') | |||
}; | |||
return item; | |||
} | |||
getCommand(): Command | undefined { | |||
return { | |||
title: 'Refresh', | |||
command: this.explorer.getQualifiedCommand('refreshNode'), | |||
arguments: [this.node, this._args] | |||
} as Command; | |||
} | |||
} | |||
export class ShowMoreNode extends PagerNode { | |||
constructor( | |||
type: string, | |||
node: ExplorerNode, | |||
explorer: Explorer, | |||
maxCount: number = Container.config.advanced.maxListItems | |||
) { | |||
super( | |||
maxCount === 0 | |||
? `Show All ${type} ${GlyphChars.Space}${GlyphChars.Dash}${GlyphChars.Space} this may take a while` | |||
: `Show More ${type}`, | |||
node, | |||
explorer | |||
); | |||
this._args.maxCount = maxCount; | |||
} | |||
} | |||
export class ShowAllNode extends ShowMoreNode { | |||
constructor(type: string, node: ExplorerNode, explorer: Explorer) { | |||
super(type, node, explorer, 0); | |||
} | |||
} |
@ -1,59 +0,0 @@ | |||
'use strict'; | |||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||
import { GlyphChars } from '../../constants'; | |||
import { Container } from '../../container'; | |||
import { GitLog, GitService, GitUri } from '../../git/gitService'; | |||
import { Strings } from '../../system'; | |||
import { CommitsResultsNode } from './commitsResultsNode'; | |||
import { Explorer, ExplorerNode, NamedRef, ResourceType } from './explorerNode'; | |||
import { StatusFilesResultsNode } from './statusFilesResultsNode'; | |||
export class ComparisonResultsNode extends ExplorerNode { | |||
constructor( | |||
public readonly repoPath: string, | |||
public readonly ref1: NamedRef, | |||
public readonly ref2: NamedRef, | |||
private readonly explorer: Explorer | |||
) { | |||
super(GitUri.fromRepoPath(repoPath)); | |||
} | |||
async getChildren(): Promise<ExplorerNode[]> { | |||
this.resetChildren(); | |||
const commitsQueryFn = (maxCount: number | undefined) => | |||
Container.git.getLog(this.uri.repoPath!, { | |||
maxCount: maxCount, | |||
ref: `${this.ref1.ref}...${this.ref2.ref || 'HEAD'}` | |||
}); | |||
const commitsLabelFn = async (log: GitLog | undefined) => { | |||
const count = log !== undefined ? log.count : 0; | |||
const truncated = log !== undefined ? log.truncated : false; | |||
return Strings.pluralize('commit', count, { number: truncated ? `${count}+` : undefined, zero: 'No' }); | |||
}; | |||
this.children = [ | |||
new CommitsResultsNode(this.uri.repoPath!, commitsLabelFn, commitsQueryFn, this.explorer), | |||
new StatusFilesResultsNode(this.uri.repoPath!, this.ref1.ref, this.ref2.ref, this.explorer) | |||
]; | |||
return this.children; | |||
} | |||
async getTreeItem(): Promise<TreeItem> { | |||
let repository = ''; | |||
if ((await Container.git.getRepositoryCount()) > 1) { | |||
const repo = await Container.git.getRepository(this.uri.repoPath!); | |||
repository = ` ${Strings.pad(GlyphChars.Dash, 1, 1)} ${(repo && repo.formattedName) || this.uri.repoPath}`; | |||
} | |||
const item = new TreeItem( | |||
`Comparing ${this.ref1.label || GitService.shortenSha(this.ref1.ref, { working: 'Working Tree' })} to ${this | |||
.ref2.label || GitService.shortenSha(this.ref2.ref, { working: 'Working Tree' })}${repository}`, | |||
TreeItemCollapsibleState.Expanded | |||
); | |||
item.contextValue = ResourceType.ComparisonResults; | |||
return item; | |||
} | |||
} |
@ -1,35 +0,0 @@ | |||
'use strict'; | |||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||
import { Container } from '../../container'; | |||
import { GitUri, Repository } from '../../git/gitService'; | |||
import { Explorer, ExplorerNode, ResourceType } from './explorerNode'; | |||
import { FileHistoryNode } from './fileHistoryNode'; | |||
export class HistoryNode extends ExplorerNode { | |||
constructor( | |||
uri: GitUri, | |||
private readonly repo: Repository, | |||
private readonly explorer: Explorer | |||
) { | |||
super(uri); | |||
} | |||
async getChildren(): Promise<ExplorerNode[]> { | |||
this.resetChildren(); | |||
this.children = [new FileHistoryNode(this.uri, this.repo, this.explorer)]; | |||
return this.children; | |||
} | |||
getTreeItem(): TreeItem { | |||
const item = new TreeItem(`${this.uri.getFormattedPath()}`, TreeItemCollapsibleState.Expanded); | |||
item.contextValue = ResourceType.History; | |||
item.iconPath = { | |||
dark: Container.context.asAbsolutePath('images/dark/icon-history.svg'), | |||
light: Container.context.asAbsolutePath('images/light/icon-history.svg') | |||
}; | |||
return item; | |||
} | |||
} |
@ -0,0 +1,131 @@ | |||
'use strict'; | |||
import { Disposable, Selection, TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||
import { Container } from '../../container'; | |||
import { GitCommitType, GitLogCommit, IGitStatusFile } from '../../git/git'; | |||
import { GitUri, RepositoryChange, RepositoryChangeEvent, RepositoryFileSystemChangeEvent } from '../../git/gitService'; | |||
import { Logger } from '../../logger'; | |||
import { Iterables } from '../../system'; | |||
import { LineHistoryExplorer } from '../lineHistoryExplorer'; | |||
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode'; | |||
import { MessageNode } from './common'; | |||
import { ExplorerNode, ResourceType, SubscribeableExplorerNode } from './explorerNode'; | |||
export class LineHistoryNode extends SubscribeableExplorerNode<LineHistoryExplorer> { | |||
constructor( | |||
uri: GitUri, | |||
public readonly selection: Selection, | |||
explorer: LineHistoryExplorer | |||
) { | |||
super(uri, explorer); | |||
} | |||
async getChildren(): Promise<ExplorerNode[]> { | |||
const children: ExplorerNode[] = []; | |||
const displayAs = | |||
CommitFileNodeDisplayAs.CommitLabel | | |||
(this.explorer.config.avatars ? CommitFileNodeDisplayAs.Gravatar : CommitFileNodeDisplayAs.StatusIcon); | |||
const log = await Container.git.getLogForFile(this.uri.repoPath, this.uri.fsPath, { | |||
ref: this.uri.sha, | |||
range: this.selection | |||
}); | |||
if (log !== undefined) { | |||
children.push( | |||
...Iterables.filterMap( | |||
log.commits.values(), | |||
c => new CommitFileNode(c.fileStatuses[0], c, this.explorer, displayAs, this.selection) | |||
) | |||
); | |||
} | |||
const blame = await Container.git.getBlameForLine(this.uri, this.selection.active.line); | |||
if (blame !== undefined) { | |||
const first = children[0] as CommitFileNode | undefined; | |||
if (first === undefined || first.commit.sha !== blame.commit.sha) { | |||
const status: IGitStatusFile = { | |||
fileName: blame.commit.fileName, | |||
indexStatus: '?', | |||
originalFileName: blame.commit.originalFileName, | |||
repoPath: this.uri.repoPath!, | |||
status: 'M', | |||
workTreeStatus: '?' | |||
}; | |||
const commit = new GitLogCommit( | |||
GitCommitType.File, | |||
this.uri.repoPath!, | |||
blame.commit.sha, | |||
'You', | |||
blame.commit.email, | |||
blame.commit.date, | |||
blame.commit.message, | |||
blame.commit.fileName, | |||
[status], | |||
'M', | |||
blame.commit.originalFileName, | |||
blame.commit.previousSha, | |||
blame.commit.originalFileName || blame.commit.fileName | |||
); | |||
children.splice(0, 0, new CommitFileNode(status, commit, this.explorer, displayAs, this.selection)); | |||
} | |||
} | |||
if (children.length === 0) return [new MessageNode('No line history')]; | |||
return children; | |||
} | |||
getTreeItem(): TreeItem { | |||
const lines = this.selection.isSingleLine | |||
? ` #${this.selection.start.line + 1}` | |||
: ` #${this.selection.start.line + 1}-${this.selection.end.line + 1}`; | |||
const item = new TreeItem( | |||
`${this.uri.getFormattedPath({ suffix: `${lines}${this.uri.sha ? ` (${this.uri.shortSha})` : ''}` })}`, | |||
TreeItemCollapsibleState.Expanded | |||
); | |||
item.contextValue = ResourceType.FileHistory; | |||
item.tooltip = `History of ${this.uri.getFilename()}${lines}\n${this.uri.getDirectory()}/`; | |||
item.iconPath = { | |||
dark: Container.context.asAbsolutePath('images/dark/icon-history.svg'), | |||
light: Container.context.asAbsolutePath('images/light/icon-history.svg') | |||
}; | |||
void this.ensureSubscription(); | |||
return item; | |||
} | |||
protected async subscribe() { | |||
const repo = await Container.git.getRepository(this.uri); | |||
if (repo === undefined) return undefined; | |||
const subscription = Disposable.from( | |||
repo.onDidChange(this.onRepoChanged, this), | |||
repo.onDidChangeFileSystem(this.onRepoFileSystemChanged, this), | |||
{ dispose: () => repo.stopWatchingFileSystem() } | |||
); | |||
repo.startWatchingFileSystem(); | |||
return subscription; | |||
} | |||
private onRepoChanged(e: RepositoryChangeEvent) { | |||
if (!e.changed(RepositoryChange.Repository)) return; | |||
Logger.log(`LineHistoryNode.onRepoChanged(${e.changes.join()}); triggering node refresh`); | |||
void this.explorer.refreshNode(this); | |||
} | |||
private onRepoFileSystemChanged(e: RepositoryFileSystemChangeEvent) { | |||
if (!e.uris.some(uri => uri.toString(true) === this.uri.toString(true))) return; | |||
Logger.log(`LineHistoryNode.onRepoFileSystemChanged; triggering node refresh`); | |||
void this.explorer.refreshNode(this); | |||
} | |||
} |
@ -1,41 +1,127 @@ | |||
'use strict'; | |||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||
import { GitUri, Repository } from '../../git/gitService'; | |||
import { Disposable, TextEditor, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; | |||
import { Container } from '../../container'; | |||
import { GitUri } from '../../git/gitService'; | |||
import { Logger } from '../../logger'; | |||
import { Functions } from '../../system'; | |||
import { GitExplorer } from '../gitExplorer'; | |||
import { ActiveRepositoryNode } from './activeRepositoryNode'; | |||
import { ExplorerNode, ResourceType } from './explorerNode'; | |||
import { MessageNode } from './common'; | |||
import { ExplorerNode, ResourceType, SubscribeableExplorerNode, unknownGitUri } from './explorerNode'; | |||
import { RepositoryNode } from './repositoryNode'; | |||
export class RepositoriesNode extends ExplorerNode { | |||
constructor( | |||
private readonly repositories: Repository[], | |||
private readonly explorer: GitExplorer | |||
) { | |||
super(undefined!); | |||
export class RepositoriesNode extends SubscribeableExplorerNode<GitExplorer> { | |||
private _children: (RepositoryNode | MessageNode)[] | undefined; | |||
constructor(explorer: GitExplorer) { | |||
super(unknownGitUri, explorer); | |||
} | |||
async getChildren(): Promise<ExplorerNode[]> { | |||
if (this.children === undefined) { | |||
this.children = this.repositories | |||
.sort((a, b) => a.index - b.index) | |||
.filter(repo => !repo.closed) | |||
.map(repo => new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this.explorer)); | |||
if (this.children.length > 1) { | |||
this.children.splice(0, 0, new ActiveRepositoryNode(this.explorer)); | |||
dispose() { | |||
super.dispose(); | |||
if (this._children !== undefined) { | |||
for (const child of this._children) { | |||
if (child instanceof RepositoryNode) { | |||
child.dispose(); | |||
} | |||
} | |||
this._children = undefined; | |||
} | |||
return this.children; | |||
} | |||
refresh() { | |||
this.resetChildren(); | |||
async getChildren(): Promise<ExplorerNode[]> { | |||
if (this._children === undefined) { | |||
const repositories = [...(await Container.git.getRepositories())]; | |||
if (repositories.length === 0) return [new MessageNode('No repositories found')]; | |||
const children = []; | |||
for (const repo of repositories.sort((a, b) => a.index - b.index)) { | |||
if (repo.closed) continue; | |||
children.push(new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this.explorer)); | |||
} | |||
this._children = children; | |||
} | |||
return this._children; | |||
} | |||
getTreeItem(): TreeItem { | |||
const item = new TreeItem(`Repositories`, TreeItemCollapsibleState.Expanded); | |||
item.contextValue = ResourceType.Repositories; | |||
void this.ensureSubscription(); | |||
return item; | |||
} | |||
async refresh() { | |||
if (this._children === undefined) return; | |||
const repositories = [...(await Container.git.getRepositories())]; | |||
if (repositories.length === 0 && (this._children === undefined || this._children.length === 0)) return; | |||
if (repositories.length === 0) { | |||
this._children = [new MessageNode('No repositories found')]; | |||
return; | |||
} | |||
const children = []; | |||
for (const repo of repositories.sort((a, b) => a.index - b.index)) { | |||
const normalizedPath = repo.normalizedPath; | |||
const child = (this._children as RepositoryNode[]).find(c => c.repo.normalizedPath === normalizedPath); | |||
if (child !== undefined) { | |||
children.push(child); | |||
child.refresh(); | |||
} | |||
else { | |||
children.push(new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this.explorer)); | |||
} | |||
} | |||
for (const child of this._children as RepositoryNode[]) { | |||
if (children.includes(child)) continue; | |||
child.dispose(); | |||
} | |||
this._children = children; | |||
void this.ensureSubscription(); | |||
} | |||
protected async subscribe() { | |||
return Disposable.from( | |||
window.onDidChangeActiveTextEditor(Functions.debounce(this.onActiveEditorChanged, 500), this), | |||
Container.git.onDidChangeRepositories(this.onRepositoriesChanged, this) | |||
); | |||
} | |||
private async onActiveEditorChanged(editor: TextEditor | undefined) { | |||
if (editor == null || this._children === undefined || this._children.length === 1) { | |||
return; | |||
} | |||
try { | |||
const uri = editor.document.uri; | |||
const gitUri = await Container.git.getVersionedUri(uri); | |||
const node = this._children.find(n => n instanceof RepositoryNode && n.repo.containsUri(gitUri || uri)) as | |||
| RepositoryNode | |||
| undefined; | |||
if (node === undefined) return; | |||
// HACK: Since we have no expand/collapse api, reveal the first child to force an expand | |||
// See https://github.com/Microsoft/vscode/issues/55879 | |||
const children = await node.getChildren(); | |||
await this.explorer.reveal(children !== undefined && children.length !== 0 ? children[0] : node); | |||
} | |||
catch (ex) { | |||
Logger.error(ex); | |||
} | |||
} | |||
private onRepositoriesChanged() { | |||
void this.explorer.refreshNode(this); | |||
} | |||
} |
@ -1,119 +1,222 @@ | |||
'use strict'; | |||
import { Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||
import { GlyphChars } from '../../constants'; | |||
import { GitUri, Repository, RepositoryChange, RepositoryChangeEvent } from '../../git/gitService'; | |||
import { Container } from '../../container'; | |||
import { | |||
GitBranch, | |||
GitStatus, | |||
GitUri, | |||
Repository, | |||
RepositoryChange, | |||
RepositoryChangeEvent, | |||
RepositoryFileSystemChangeEvent | |||
} from '../../git/gitService'; | |||
import { Logger } from '../../logger'; | |||
import { Strings } from '../../system'; | |||
import { GitExplorer } from '../gitExplorer'; | |||
import { BranchesNode } from './branchesNode'; | |||
import { ExplorerNode, ResourceType } from './explorerNode'; | |||
import { BranchNode } from './branchNode'; | |||
import { MessageNode } from './common'; | |||
import { ExplorerNode, ResourceType, SubscribeableExplorerNode } from './explorerNode'; | |||
import { RemotesNode } from './remotesNode'; | |||
import { StashesNode } from './stashesNode'; | |||
import { StatusNode } from './statusNode'; | |||
import { StatusFilesNode } from './statusFilesNode'; | |||
import { StatusUpstreamNode } from './statusUpstreamNode'; | |||
import { TagsNode } from './tagsNode'; | |||
export class RepositoryNode extends ExplorerNode { | |||
export class RepositoryNode extends SubscribeableExplorerNode<GitExplorer> { | |||
private _children: ExplorerNode[] | undefined; | |||
private _status: Promise<GitStatus | undefined>; | |||
constructor( | |||
uri: GitUri, | |||
public readonly repo: Repository, | |||
private readonly explorer: GitExplorer, | |||
private readonly active: boolean = false, | |||
private readonly activeParent?: ExplorerNode | |||
explorer: GitExplorer | |||
) { | |||
super(uri); | |||
super(uri, explorer); | |||
this._status = this.repo.getStatus(); | |||
} | |||
get id(): string { | |||
return `gitlens:repository(${this.repo.path})${this.active ? ':active' : ''}`; | |||
return `gitlens:repository(${this.repo.path})`; | |||
} | |||
async getChildren(): Promise<ExplorerNode[]> { | |||
if (this.children === undefined) { | |||
this.updateSubscription(); | |||
this.children = [ | |||
new StatusNode(this.uri, this.repo, this.explorer, this.active), | |||
new BranchesNode(this.uri, this.repo, this.explorer, this.active), | |||
new RemotesNode(this.uri, this.repo, this.explorer, this.active), | |||
new StashesNode(this.uri, this.repo, this.explorer, this.active), | |||
new TagsNode(this.uri, this.repo, this.explorer, this.active) | |||
]; | |||
if (this._children === undefined) { | |||
const children = []; | |||
const status = await this._status; | |||
if (status !== undefined) { | |||
const branch = new GitBranch( | |||
status.repoPath, | |||
status.branch, | |||
true, | |||
status.sha, | |||
status.upstream, | |||
status.state.ahead, | |||
status.state.behind, | |||
status.detached | |||
); | |||
children.push(new BranchNode(branch, this.uri, this.explorer, false)); | |||
if (status.state.behind) { | |||
children.push(new StatusUpstreamNode(status, 'behind', this.explorer)); | |||
} | |||
if (status.state.ahead) { | |||
children.push(new StatusUpstreamNode(status, 'ahead', this.explorer)); | |||
} | |||
if (status.state.ahead || (status.files.length !== 0 && this.includeWorkingTree)) { | |||
const range = status.upstream ? `${status.upstream}..${branch.ref}` : undefined; | |||
children.push(new StatusFilesNode(status, range, this.explorer)); | |||
} | |||
children.push(new MessageNode(GlyphChars.Dash.repeat(2), '')); | |||
} | |||
children.push( | |||
new BranchesNode(this.uri, this.repo, this.explorer), | |||
new RemotesNode(this.uri, this.repo, this.explorer), | |||
new StashesNode(this.uri, this.repo, this.explorer), | |||
new TagsNode(this.uri, this.repo, this.explorer) | |||
); | |||
this._children = children; | |||
} | |||
return this.children; | |||
return this._children; | |||
} | |||
getTreeItem(): TreeItem { | |||
this.updateSubscription(); | |||
async getTreeItem(): Promise<TreeItem> { | |||
let label = this.repo.formattedName || this.uri.repoPath || ''; | |||
let tooltip = this.repo.formattedName ? `${this.repo.formattedName}\n${this.uri.repoPath}` : this.uri.repoPath; | |||
let iconSuffix = ''; | |||
let workingStatus = ''; | |||
const status = await this._status; | |||
if (status !== undefined) { | |||
tooltip += `\n\n${status.branch}`; | |||
if (status.files.length !== 0 && this.includeWorkingTree) { | |||
workingStatus = status.getFormattedDiffStatus({ | |||
compact: true, | |||
prefix: Strings.pad(GlyphChars.Dot, 2, 2) | |||
}); | |||
} | |||
const label = this.active | |||
? `Active Repository ${Strings.pad(GlyphChars.Dash, 1, 1)} ${this.repo.formattedName || this.uri.repoPath}` | |||
: `${this.repo.formattedName || this.uri.repoPath}`; | |||
const upstreamStatus = status.getUpstreamStatus({ | |||
prefix: `${GlyphChars.Space} ` | |||
}); | |||
label += ` ${Strings.pad(GlyphChars.Dash, 2, 3)}${status.branch}${upstreamStatus}${workingStatus}`; | |||
iconSuffix = workingStatus ? '-blue' : ''; | |||
if (status.upstream !== undefined) { | |||
tooltip += ` is tracking ${status.upstream}\n${status.getUpstreamStatus({ | |||
empty: 'up-to-date', | |||
expand: true, | |||
separator: '\n', | |||
suffix: '\n' | |||
})}`; | |||
if (status.state.behind) { | |||
iconSuffix = '-red'; | |||
} | |||
if (status.state.ahead) { | |||
iconSuffix = status.state.behind ? '-yellow' : '-green'; | |||
} | |||
} | |||
const item = new TreeItem( | |||
label, | |||
this.active ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed | |||
); | |||
if (workingStatus) { | |||
tooltip += `\nWorking tree has uncommitted changes${status.getFormattedDiffStatus({ | |||
expand: true, | |||
prefix: `\n`, | |||
separator: '\n' | |||
})}`; | |||
} | |||
} | |||
const item = new TreeItem(label, TreeItemCollapsibleState.Expanded); | |||
item.id = this.id; | |||
item.contextValue = ResourceType.Repository; | |||
item.tooltip = tooltip; | |||
item.iconPath = { | |||
dark: Container.context.asAbsolutePath(`images/dark/icon-repo${iconSuffix}.svg`), | |||
light: Container.context.asAbsolutePath(`images/light/icon-repo${iconSuffix}.svg`) | |||
}; | |||
void this.ensureSubscription(); | |||
return item; | |||
} | |||
refresh() { | |||
this.resetChildren(); | |||
this.updateSubscription(); | |||
this._status = this.repo.getStatus(); | |||
this._children = undefined; | |||
void this.ensureSubscription(); | |||
} | |||
private updateSubscription() { | |||
// We only need to subscribe if auto-refresh is enabled, because if it becomes enabled we will be refreshed | |||
if (this.explorer.autoRefresh) { | |||
this.disposable = | |||
this.disposable || | |||
Disposable.from( | |||
this.explorer.onDidChangeAutoRefresh(this.onAutoRefreshChanged, this), | |||
this.repo.onDidChange(this.onRepoChanged, this) | |||
); | |||
} | |||
else if (this.disposable !== undefined) { | |||
this.disposable.dispose(); | |||
this.disposable = undefined; | |||
protected async subscribe() { | |||
const disposables = [this.repo.onDidChange(this.onRepoChanged, this)]; | |||
if (this.includeWorkingTree) { | |||
disposables.push(this.repo.onDidChangeFileSystem(this.onFileSystemChanged, this), { | |||
dispose: () => this.repo.stopWatchingFileSystem() | |||
}); | |||
this.repo.startWatchingFileSystem(); | |||
} | |||
return Disposable.from(...disposables); | |||
} | |||
private get includeWorkingTree(): boolean { | |||
return this.explorer.config.includeWorkingTree; | |||
} | |||
private onAutoRefreshChanged() { | |||
this.updateSubscription(); | |||
private onFileSystemChanged(e: RepositoryFileSystemChangeEvent) { | |||
void this.explorer.refreshNode(this); | |||
} | |||
private onRepoChanged(e: RepositoryChangeEvent) { | |||
Logger.log(`RepositoryNode.onRepoChanged(${e.changes.join()}); triggering node refresh`); | |||
if (e.changed(RepositoryChange.Closed)) { | |||
this.dispose(); | |||
return; | |||
} | |||
if ( | |||
this.children === undefined || | |||
this._children === undefined || | |||
e.changed(RepositoryChange.Repository) || | |||
e.changed(RepositoryChange.Config) | |||
) { | |||
this.explorer.refreshNode(this.active && this.activeParent !== undefined ? this.activeParent : this); | |||
void this.explorer.refreshNode(this); | |||
return; | |||
} | |||
if (e.changed(RepositoryChange.Stashes)) { | |||
const node = this.children.find(c => c instanceof StashesNode); | |||
const node = this._children.find(c => c instanceof StashesNode); | |||
if (node !== undefined) { | |||
this.explorer.refreshNode(node); | |||
void this.explorer.refreshNode(node); | |||
} | |||
} | |||
if (e.changed(RepositoryChange.Remotes)) { | |||
const node = this.children.find(c => c instanceof RemotesNode); | |||
const node = this._children.find(c => c instanceof RemotesNode); | |||
if (node !== undefined) { | |||
this.explorer.refreshNode(node); | |||
void this.explorer.refreshNode(node); | |||
} | |||
} | |||
if (e.changed(RepositoryChange.Tags)) { | |||
const node = this.children.find(c => c instanceof TagsNode); | |||
const node = this._children.find(c => c instanceof TagsNode); | |||
if (node !== undefined) { | |||
this.explorer.refreshNode(node); | |||
void this.explorer.refreshNode(node); | |||
} | |||
} | |||
} | |||
@ -0,0 +1,68 @@ | |||
'use strict'; | |||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||
import { GitLog, GitUri } from '../../git/gitService'; | |||
import { Iterables } from '../../system'; | |||
import { ResultsExplorer } from '../resultsExplorer'; | |||
import { CommitNode } from './commitNode'; | |||
import { ShowAllNode } from './common'; | |||
import { ExplorerNode, PageableExplorerNode, ResourceType } from './explorerNode'; | |||
export interface CommitsQueryResults { | |||
label: string; | |||
log: GitLog | undefined; | |||
} | |||
export class ResultsCommitsNode extends ExplorerNode implements PageableExplorerNode { | |||
readonly supportsPaging: boolean = true; | |||
maxCount: number | undefined; | |||
constructor( | |||
public readonly repoPath: string, | |||
private readonly commitsQuery: (maxCount: number | undefined) => Promise<CommitsQueryResults>, | |||
private readonly explorer: ResultsExplorer, | |||
private readonly contextValue: ResourceType = ResourceType.ResultsCommits | |||
) { | |||
super(GitUri.fromRepoPath(repoPath)); | |||
} | |||
async getChildren(): Promise<ExplorerNode[]> { | |||
const { log } = await this.getCommitsQueryResults(); | |||
if (log === undefined) return []; | |||
const children: (CommitNode | ShowAllNode)[] = [ | |||
...Iterables.map(log.commits.values(), c => new CommitNode(c, this.explorer)) | |||
]; | |||
if (log.truncated) { | |||
children.push(new ShowAllNode('Results', this, this.explorer)); | |||
} | |||
return children; | |||
} | |||
async getTreeItem(): Promise<TreeItem> { | |||
const { label, log } = await this.getCommitsQueryResults(); | |||
const item = new TreeItem( | |||
label, | |||
log && log.count > 0 ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.None | |||
); | |||
item.contextValue = this.contextValue; | |||
return item; | |||
} | |||
async refresh() { | |||
this._commitsQueryResults = this.commitsQuery(this.maxCount); | |||
} | |||
private _commitsQueryResults: Promise<CommitsQueryResults> | undefined; | |||
private getCommitsQueryResults() { | |||
if (this._commitsQueryResults === undefined) { | |||
this._commitsQueryResults = this.commitsQuery(this.maxCount); | |||
} | |||
return this._commitsQueryResults; | |||
} | |||
} |
@ -0,0 +1,84 @@ | |||
'use strict'; | |||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||
import { GlyphChars } from '../../constants'; | |||
import { Container } from '../../container'; | |||
import { GitService, GitUri } from '../../git/gitService'; | |||
import { Strings } from '../../system'; | |||
import { ResultsExplorer } from '../resultsExplorer'; | |||
import { ExplorerNode, NamedRef, ResourceType } from './explorerNode'; | |||
import { CommitsQueryResults, ResultsCommitsNode } from './resultsCommitsNode'; | |||
import { StatusFilesResultsNode } from './statusFilesResultsNode'; | |||
export class ResultsComparisonNode extends ExplorerNode { | |||
constructor( | |||
public readonly repoPath: string, | |||
ref1: NamedRef, | |||
ref2: NamedRef, | |||
private readonly explorer: ResultsExplorer | |||
) { | |||
super(GitUri.fromRepoPath(repoPath)); | |||
this._ref1 = ref1; | |||
this._ref2 = ref2; | |||
} | |||
private _ref1: NamedRef; | |||
get ref1(): NamedRef { | |||
return this._ref1; | |||
} | |||
private _ref2: NamedRef; | |||
get ref2(): NamedRef { | |||
return this._ref2; | |||
} | |||
async getChildren(): Promise<ExplorerNode[]> { | |||
return [ | |||
new ResultsCommitsNode(this.uri.repoPath!, this.getCommitsQuery.bind(this), this.explorer), | |||
new StatusFilesResultsNode(this.uri.repoPath!, this._ref1.ref, this._ref2.ref, this.explorer) | |||
]; | |||
} | |||
async getTreeItem(): Promise<TreeItem> { | |||
let repository = ''; | |||
if ((await Container.git.getRepositoryCount()) > 1) { | |||
const repo = await Container.git.getRepository(this.uri.repoPath!); | |||
repository = ` ${Strings.pad(GlyphChars.Dash, 1, 1)} ${(repo && repo.formattedName) || this.uri.repoPath}`; | |||
} | |||
const item = new TreeItem( | |||
`Comparing ${this._ref1.label || | |||
GitService.shortenSha(this._ref1.ref, { working: 'Working Tree' })} to ${this._ref2.label || | |||
GitService.shortenSha(this._ref2.ref, { working: 'Working Tree' })}${repository}`, | |||
TreeItemCollapsibleState.Expanded | |||
); | |||
item.contextValue = ResourceType.ComparisonResults; | |||
return item; | |||
} | |||
swap() { | |||
const ref1 = this._ref1; | |||
this._ref1 = this._ref2; | |||
this._ref2 = ref1; | |||
this.explorer.triggerNodeUpdate(this); | |||
} | |||
private async getCommitsQuery(maxCount: number | undefined): Promise<CommitsQueryResults> { | |||
const log = await Container.git.getLog(this.uri.repoPath!, { | |||
maxCount: maxCount, | |||
ref: `${this._ref1.ref}...${this._ref2.ref || 'HEAD'}` | |||
}); | |||
const count = log !== undefined ? log.count : 0; | |||
const truncated = log !== undefined ? log.truncated : false; | |||
const label = Strings.pluralize('commit', count, { number: truncated ? `${count}+` : undefined, zero: 'No' }); | |||
return { | |||
label: label, | |||
log: log | |||
}; | |||
} | |||
} |
@ -0,0 +1,64 @@ | |||
'use strict'; | |||
import { TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||
import { ResultsExplorer } from '../resultsExplorer'; | |||
import { MessageNode } from './common'; | |||
import { ExplorerNode, ResourceType, unknownGitUri } from './explorerNode'; | |||
export class ResultsNode extends ExplorerNode { | |||
private _children: (ExplorerNode | MessageNode)[] = []; | |||
constructor( | |||
public readonly explorer: ResultsExplorer | |||
) { | |||
super(unknownGitUri); | |||
} | |||
async getChildren(): Promise<ExplorerNode[]> { | |||
if (this._children.length === 0) return [new MessageNode('No results')]; | |||
return this._children; | |||
} | |||
getTreeItem(): TreeItem { | |||
const item = new TreeItem(`Results`, TreeItemCollapsibleState.Expanded); | |||
item.contextValue = ResourceType.Results; | |||
return item; | |||
} | |||
addOrReplace(results: ExplorerNode, replace: boolean) { | |||
if (this._children.includes(results)) return; | |||
if (this._children.length !== 0 && replace) { | |||
this._children.length = 0; | |||
this._children.push(results); | |||
} | |||
else { | |||
this._children.splice(0, 0, results); | |||
} | |||
this.explorer.triggerNodeUpdate(); | |||
} | |||
clear() { | |||
if (this._children.length === 0) return; | |||
this._children.length = 0; | |||
this.explorer.triggerNodeUpdate(); | |||
} | |||
dismiss(node: ExplorerNode) { | |||
if (this._children.length === 0) return; | |||
const index = this._children.findIndex(n => n === node); | |||
if (index === -1) return; | |||
this._children.splice(index, 1); | |||
this.explorer.triggerNodeUpdate(); | |||
} | |||
async refresh() { | |||
if (this._children.length === 0) return; | |||
this._children.forEach(c => c.refresh()); | |||
} | |||
} |
@ -1,180 +0,0 @@ | |||
import { Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode'; | |||
import { GlyphChars } from '../../constants'; | |||
import { Container } from '../../container'; | |||
import { GitBranch, GitUri, Repository, RepositoryFileSystemChangeEvent } from '../../git/gitService'; | |||
import { GitExplorer } from '../gitExplorer'; | |||
import { BranchNode } from './branchNode'; | |||
import { ExplorerNode, ResourceType } from './explorerNode'; | |||
import { StatusFilesNode } from './statusFilesNode'; | |||
import { StatusUpstreamNode } from './statusUpstreamNode'; | |||
export class StatusNode extends ExplorerNode { | |||
constructor( | |||
uri: GitUri, | |||
public 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' : ''}:status`; | |||
} | |||
async getChildren(): Promise<ExplorerNode[]> { | |||
this.resetChildren(); | |||
const children = []; | |||
const status = await this.repo.getStatus(); | |||
if (status !== undefined) { | |||
if (status.state.behind) { | |||
children.push(new StatusUpstreamNode(status, 'behind', this.explorer, this.active)); | |||
} | |||
if (status.state.ahead) { | |||
children.push(new StatusUpstreamNode(status, 'ahead', this.explorer, this.active)); | |||
} | |||
if (status.state.ahead || (status.files.length !== 0 && this.includeWorkingTree)) { | |||
const range = status.upstream ? `${status.upstream}..${status.ref}` : undefined; | |||
children.push(new StatusFilesNode(status, range, this.explorer, this.active)); | |||
} | |||
} | |||
let branch = await this.repo.getBranch(); | |||
if (branch !== undefined) { | |||
if (status !== undefined) { | |||
branch = new GitBranch( | |||
branch.repoPath, | |||
branch.name, | |||
branch.current, | |||
branch.sha, | |||
branch.tracking, | |||
status.state.ahead, | |||
status.state.behind, | |||
branch.detached | |||
); | |||
} | |||
children.push(new StatusBranchNode(branch, this.uri, this.explorer)); | |||
} | |||
this.children = children; | |||
return this.children; | |||
} | |||
async getTreeItem(): Promise<TreeItem> { | |||
if (this.disposable !== undefined) { | |||
this.disposable.dispose(); | |||
this.disposable = undefined; | |||
} | |||
const status = await this.repo.getStatus(); | |||
if (status === undefined) return new TreeItem('No repo status'); | |||
if (this.explorer.autoRefresh && this.includeWorkingTree) { | |||
this.disposable = Disposable.from( | |||
this.explorer.onDidChangeAutoRefresh(this.onAutoRefreshChanged, this), | |||
this.repo.onDidChangeFileSystem(this.onFileSystemChanged, this), | |||
{ dispose: () => this.repo.stopWatchingFileSystem() } | |||
); | |||
this.repo.startWatchingFileSystem(); | |||
} | |||
let hasChildren = false; | |||
const hasWorkingChanges = status.files.length !== 0 && this.includeWorkingTree; | |||
let label = `${status.getUpstreamStatus({ prefix: `${GlyphChars.Space} ` })}${ | |||
hasWorkingChanges ? status.getFormattedDiffStatus({ prefix: `${GlyphChars.Space} ` }) : '' | |||
}`; | |||
let tooltip = `${status.branch} (current)`; | |||
let iconSuffix = ''; | |||
if (status.upstream) { | |||
if (this.explorer.config.showTrackingBranch) { | |||
label += `${GlyphChars.Space} ${GlyphChars.ArrowLeftRightLong}${GlyphChars.Space} ${status.upstream}`; | |||
} | |||
tooltip += `\n\nTracking ${GlyphChars.Dash} ${status.upstream} | |||
${status.getUpstreamStatus({ empty: 'up-to-date', expand: true, separator: '\n' })}`; | |||
if (status.state.ahead || status.state.behind) { | |||
hasChildren = true; | |||
if (status.state.behind) { | |||
iconSuffix = '-red'; | |||
} | |||
if (status.state.ahead) { | |||
iconSuffix = status.state.behind ? '-yellow' : '-green'; | |||
} | |||
} | |||
} | |||
if (hasWorkingChanges) { | |||
tooltip += `\n\nHas uncommitted changes${status.getFormattedDiffStatus({ | |||
expand: true, | |||
prefix: `\n`, | |||
separator: '\n' | |||
})}`; | |||
} | |||
let state: TreeItemCollapsibleState; | |||
if (hasChildren || hasWorkingChanges) { | |||
// HACK: Until https://github.com/Microsoft/vscode/issues/30918 is fixed | |||
state = this.active ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed; | |||
} | |||
else { | |||
state = TreeItemCollapsibleState.Collapsed; | |||
} | |||
const item = new TreeItem(`${status.branch}${label}`, state); | |||
item.id = this.id; | |||
item.contextValue = ResourceType.Status; | |||
item.tooltip = tooltip; | |||
item.iconPath = { | |||
dark: Container.context.asAbsolutePath(`images/dark/icon-repo${iconSuffix}.svg`), | |||
light: Container.context.asAbsolutePath(`images/light/icon-repo${iconSuffix}.svg`) | |||
}; | |||
return item; | |||
} | |||
private get includeWorkingTree(): boolean { | |||
return this.explorer.config.includeWorkingTree; | |||
} | |||
private onAutoRefreshChanged() { | |||
if (this.disposable === undefined) return; | |||
// If auto-refresh changes, just kill the subscriptions | |||
// (if it was enabled -- we will get refreshed so we don't have to worry about re-hooking it up here) | |||
this.disposable.dispose(); | |||
this.disposable = undefined; | |||
} | |||
private async onFileSystemChanged(e: RepositoryFileSystemChangeEvent) { | |||
this.explorer.refreshNode(this); | |||
} | |||
} | |||
export class StatusBranchNode extends BranchNode { | |||
constructor(branch: GitBranch, uri: GitUri, explorer: GitExplorer) { | |||
super(branch, uri, explorer); | |||
} | |||
get markCurrent() { | |||
return false; | |||
} | |||
async getTreeItem(): Promise<TreeItem> { | |||
const item = await super.getTreeItem(); | |||
if (item.label!.startsWith('(') && item.label!.endsWith(')')) { | |||
item.label = `History ${item.label}`; | |||
} | |||
else { | |||
item.label = `History (${item.label})`; | |||
} | |||
return item; | |||
} | |||
} |