diff --git a/images/icons/graph.svg b/images/icons/graph.svg index e591f38..bba53e9 100644 --- a/images/icons/graph.svg +++ b/images/icons/graph.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/package.json b/package.json index 6cd1d3e..ba73776 100644 --- a/package.json +++ b/package.json @@ -2092,19 +2092,26 @@ "title": "Commit Graph", "order": 105, "properties": { + "gitlens.graph.statusBar.enabled": { + "type": "boolean", + "default": true, + "markdownDescription": "Specifies whether to show the _Commit Graph_ in the status bar", + "scope": "window", + "order": 10 + }, "gitlens.graph.defaultItemLimit": { "type": "number", "default": 500, "markdownDescription": "Specifies the default number of items to show in the _Commit Graph_. Use 0 to specify no limit", "scope": "window", - "order": 10 + "order": 50 }, "gitlens.graph.pageItemLimit": { "type": "number", "default": 200, "markdownDescription": "Specifies the number of additional items to fetch when paginating in the _Commit Graph_. Use 0 to specify no limit", "scope": "window", - "order": 20 + "order": 60 }, "gitlens.graph.columnColors": { "type": "array", @@ -2126,7 +2133,7 @@ ], "markdownDescription": "Specifies the colors used for the different columns in the _Commit Graph_", "scope": "window", - "order": 30, + "order": 100, "maxItems": 10, "minItems": 10 } @@ -2719,8 +2726,45 @@ { "type": "object", "properties": { + "graph": { + "type": "boolean" + } + } + } + ] + }, + "scmRepositoryInline": { + "anyOf": [ + { + "enum": [ + false + ] + }, + { + "type": "object", + "properties": { + "graph": { + "type": "boolean" + } + } + } + ] + }, + "scmRepository": { + "anyOf": [ + { + "enum": [ + false + ] + }, + { + "type": "object", + "properties": { "authors": { "type": "boolean" + }, + "graph": { + "type": "boolean" } } } @@ -2824,7 +2868,14 @@ "remote": true }, "scm": { - "authors": true + "graph": true + }, + "scmRepositoryInline": { + "graph": true + }, + "scmRepository": { + "authors": true, + "graph": false }, "scmGroupInline": { "stash": true @@ -3827,7 +3878,8 @@ { "command": "gitlens.showGraphPage", "title": "Show Commit Graph", - "category": "GitLens" + "category": "GitLens+", + "icon": "$(gitlens-graph)" }, { "command": "gitlens.showSettingsPage", @@ -8036,7 +8088,7 @@ "git.commit": [ { "command": "gitlens.addAuthors", - "when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && config.gitlens.menus.scm.authors", + "when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && config.gitlens.menus.scmRepository.authors", "group": "4_gitlens@1" } ], @@ -8052,6 +8104,30 @@ "group": "1_gitlens@2" } ], + "scm/sourceControl": [ + { + "command": "gitlens.showGraphPage", + "when": "gitlens:enabled && config.gitlens.menus.scm.graph", + "group": "6_gitlens@1" + } + ], + "scm/title": [ + { + "command": "gitlens.showGraphPage", + "when": "gitlens:enabled && config.gitlens.menus.scmRepositoryInline.graph && config.gitlens.plusFeatures.enabled", + "group": "navigation@-1000" + }, + { + "command": "gitlens.addAuthors", + "when": "gitlens:enabled && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && config.gitlens.menus.scmRepository.authors", + "group": "2_z_gitlens@1" + }, + { + "command": "gitlens.showGraphPage", + "when": "gitlens:enabled && config.gitlens.menus.scmRepository.graph", + "group": "2_z_gitlens@2" + } + ], "scm/resourceGroup/context": [ { "command": "gitlens.stashSave", @@ -8218,6 +8294,11 @@ "group": "navigation@10" }, { + "command": "gitlens.showGraphPage", + "when": "view =~ /^gitlens\\.views\\.commits/ && config.gitlens.plusFeatures.enabled", + "group": "navigation@11" + }, + { "command": "gitlens.views.commits.setMyCommitsOnlyOff", "when": "view =~ /^gitlens\\.views\\.commits/ && gitlens:views:commits:myCommitsOnly", "group": "navigation@50" @@ -8283,9 +8364,14 @@ "group": "5_gitlens@2" }, { + "command": "gitlens.showGraphPage", + "when": "view =~ /^gitlens\\.views\\.commits/", + "group": "8_gitlens_toggles@0" + }, + { "command": "gitlens.showRepositoriesView", "when": "!gitlens:hasVirtualFolders && view =~ /^gitlens\\.views\\.commits/", - "group": "8_gitlens_toggles@0" + "group": "8_gitlens_toggles@1" }, { "command": "gitlens.views.addAuthors", @@ -9633,6 +9719,11 @@ "group": "inline@99" }, { + "command": "gitlens.showGraphPage", + "when": "viewItem =~ /gitlens:repo-folder\\b/ && config.gitlens.plusFeatures.enabled", + "group": "inline@100" + }, + { "command": "gitlens.views.fetch", "when": "gitlens:hasRemotes && !gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:repo-folder\\b/", "group": "1_gitlens_actions@1" @@ -9664,11 +9755,16 @@ "alt": "gitlens.copyRemoteRepositoryUrl" }, { - "command": "gitlens.showCommitSearch", + "command": "gitlens.showGraphPage", "when": "viewItem =~ /gitlens:repo-folder\\b/", "group": "3_gitlens_explore@1" }, { + "command": "gitlens.showCommitSearch", + "when": "viewItem =~ /gitlens:repo-folder\\b/", + "group": "3_gitlens_explore@2" + }, + { "command": "gitlens.stashSave", "when": "!gitlens:readonly && !gitlens:untrusted && !gitlens:hasVirtualFolders && viewItem =~ /gitlens:repo-folder\\b/", "group": "1_gitlens_actions_1@1" diff --git a/src/commands/base.ts b/src/commands/base.ts index 01b20f1..eb2dc96 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -1,5 +1,6 @@ import type { GitTimelineItem, + SourceControl, SourceControlResourceGroup, SourceControlResourceState, TextEditor, @@ -45,6 +46,11 @@ export interface CommandGitTimelineItemContext extends CommandBaseContext { readonly uri: Uri; } +export interface CommandScmContext extends CommandBaseContext { + readonly type: 'scm'; + readonly scm: SourceControl; +} + export interface CommandScmGroupsContext extends CommandBaseContext { readonly type: 'scm-groups'; readonly scmResourceGroups: SourceControlResourceGroup[]; @@ -183,6 +189,7 @@ export function isCommandContextViewNodeHasTag( export type CommandContext = | CommandGitTimelineItemContext + | CommandScmContext | CommandScmGroupsContext | CommandScmStatesContext | CommandUnknownContext @@ -192,6 +199,17 @@ export type CommandContext = | CommandViewNodeContext | CommandViewNodesContext; +function isScm(scm: any): scm is SourceControl { + if (scm == null) return false; + + return ( + (scm as SourceControl).id != null && + (scm as SourceControl).rootUri != null && + (scm as SourceControl).inputBox != null && + (scm as SourceControl).statusBarCommands != null + ); +} + function isScmResourceGroup(group: any): group is SourceControlResourceGroup { if (group == null) return false; @@ -278,16 +296,16 @@ export abstract class Command implements Disposable { } } -function parseCommandContext( +export function parseCommandContext( command: string, - options: CommandContextParsingOptions, + options?: CommandContextParsingOptions, ...args: any[] ): [CommandContext | CommandContext[], any[]] { let editor: TextEditor | undefined = undefined; let firstArg = args[0]; - if (options.expectsEditor) { + if (options?.expectsEditor) { if (firstArg == null || (firstArg.id != null && firstArg.document?.uri != null)) { editor = firstArg; args = args.slice(1); @@ -376,6 +394,11 @@ function parseCommandContext( return [{ command: command, type: 'timeline-item:git', item: item, uri: uri }, rest]; } + if (isScm(firstArg)) { + const [scm, ...rest] = args as [SourceControl, any]; + return [{ command: command, type: 'scm', scm: scm }, rest]; + } + return [{ command: command, type: 'unknown', editor: editor, uri: editor?.document.uri }, args]; } diff --git a/src/config.ts b/src/config.ts index 8146c36..debc4eb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -378,9 +378,12 @@ export interface GraphColumnConfig { } export interface GraphConfig { + columnColors: string[]; defaultItemLimit: number; pageItemLimit: number; - columnColors: string[]; + statusBar: { + enabled: boolean; + }; } export interface CodeLensConfig { @@ -441,7 +444,18 @@ export interface MenuConfig { scm: | false | { + graph: boolean; + }; + scmTitleInline: + | false + | { + graph: boolean; + }; + scmTitle: + | false + | { authors: boolean; + graph: boolean; }; scmGroupInline: | false diff --git a/src/plus/webviews/graph/graphWebview.ts b/src/plus/webviews/graph/graphWebview.ts index 9de54ab..fb0f263 100644 --- a/src/plus/webviews/graph/graphWebview.ts +++ b/src/plus/webviews/graph/graphWebview.ts @@ -1,7 +1,8 @@ import type { CommitType } from '@gitkraken/gitkraken-components'; import { commitNodeType, mergeNodeType, stashNodeType } from '@gitkraken/gitkraken-components'; -import type { ColorTheme, Disposable, Event } from 'vscode'; -import { ColorThemeKind, EventEmitter, Uri, ViewColumn, window } from 'vscode'; +import type { ColorTheme, ConfigurationChangeEvent, Disposable, Event, StatusBarItem } from 'vscode'; +import { ColorThemeKind, EventEmitter, MarkdownString, StatusBarAlignment, Uri, ViewColumn, window } from 'vscode'; +import { parseCommandContext } from '../../../commands/base'; import type { GraphColumnConfig } from '../../../configuration'; import { configuration } from '../../../configuration'; import { Commands } from '../../../constants'; @@ -14,9 +15,10 @@ import type { GitLog } from '../../../git/models/log'; import type { GitRemote } from '../../../git/models/remote'; import type { Repository, RepositoryChangeEvent } from '../../../git/models/repository'; import type { GitTag } from '../../../git/models/tag'; +import { RepositoryFolderNode } from '../../../views/nodes/viewNode'; import type { IpcMessage } from '../../../webviews/protocol'; import { onIpc } from '../../../webviews/protocol'; -import { WebviewWithConfigBase } from '../../../webviews/webviewWithConfigBase'; +import { WebviewBase } from '../../../webviews/webviewBase'; import { ensurePlusFeaturesEnabled } from '../../subscription/utils'; import type { GraphCommit, GraphCompositeConfig, GraphRemote, GraphRepository, State } from './protocol'; import { @@ -34,15 +36,17 @@ export interface GraphSelectionChangeEvent { readonly selection: GitCommit[]; } -export class GraphWebview extends WebviewWithConfigBase { +export class GraphWebview extends WebviewBase { private _onDidChangeSelection = new EventEmitter(); get onDidChangeSelection(): Event { return this._onDidChangeSelection.event; } + private _repositoryEventsDisposable: Disposable | undefined; + private _statusBarItem: StatusBarItem | undefined; + private selectedRepository?: Repository; private currentLog?: GitLog; - private repoDisposable: Disposable | undefined; private previewBanner?: boolean; constructor(container: Container) { @@ -54,13 +58,39 @@ export class GraphWebview extends WebviewWithConfigBase { 'Commit Graph', Commands.ShowGraphPage, ); - this.disposables.push({ dispose: () => void this.repoDisposable?.dispose() }); + this.disposables.push(configuration.onDidChange(this.onConfigurationChanged, this), { + dispose: () => { + this._statusBarItem?.dispose(); + void this._repositoryEventsDisposable?.dispose(); + }, + }); + + this.onConfigurationChanged(); } override async show(column: ViewColumn = ViewColumn.Active, ...args: any[]): Promise { if (!(await ensurePlusFeaturesEnabled())) return; void this.container.usage.track('graphWebview:shown'); + + if (this.container.git.repositoryCount > 1) { + const [contexts] = parseCommandContext(Commands.ShowGraphPage, undefined, ...args); + const context = Array.isArray(contexts) ? contexts[0] : contexts; + + if (context.type === 'scm' && context.scm.rootUri != null) { + const repository = this.container.git.getRepository(context.scm.rootUri); + if (repository != null) { + this.selectedRepository = repository; + } + } else if (context.type === 'viewItem' && context.node instanceof RepositoryFolderNode) { + this.selectedRepository = context.node.repo; + } + + if (this.selectedRepository != null) { + void this.refresh(); + } + } + return super.show(column, ...args); } @@ -104,6 +134,38 @@ export class GraphWebview extends WebviewWithConfigBase { } } + private onConfigurationChanged(e?: ConfigurationChangeEvent) { + if (configuration.changed(e, 'graph.statusBar.enabled') || configuration.changed(e, 'plusFeatures.enabled')) { + const enabled = configuration.get('graph.statusBar.enabled') && configuration.get('plusFeatures.enabled'); + if (enabled) { + if (this._statusBarItem == null) { + this._statusBarItem = window.createStatusBarItem( + 'gitlens.graph', + StatusBarAlignment.Left, + 10000 - 3, + ); + this._statusBarItem.name = 'GitLens Commit Graph'; + this._statusBarItem.command = Commands.ShowGraphPage; + this._statusBarItem.text = '$(gitlens-graph)'; + this._statusBarItem.tooltip = new MarkdownString( + 'Visualize commits on the all-new Commit Graph ✨', + ); + this._statusBarItem.accessibilityInformation = { + label: `Show the GitLens Commit Graph`, + }; + } + this._statusBarItem.show(); + } else { + this._statusBarItem?.dispose(); + this._statusBarItem = undefined; + } + } + + if (e != null && configuration.changed(e, 'graph')) { + void this.notifyDidChangeConfig(); + } + } + private dismissPreview() { this.previewBanner = false; void this.container.storage.storeWorkspace('graph:preview', false); @@ -316,9 +378,9 @@ export class GraphWebview extends WebviewWithConfigBase { if (this.selectedRepository === undefined) { const idealRepo = this.pickRepository(repositories); this.selectedRepository = idealRepo; - this.repoDisposable?.dispose(); + this._repositoryEventsDisposable?.dispose(); if (this.selectedRepository != null) { - this.repoDisposable = this.selectedRepository.onDidChange(this.onRepositoryChanged, this); + this._repositoryEventsDisposable = this.selectedRepository.onDidChange(this.onRepositoryChanged, this); } } diff --git a/src/views/commitsView.ts b/src/views/commitsView.ts index b1282fe..28c7b78 100644 --- a/src/views/commitsView.ts +++ b/src/views/commitsView.ts @@ -1,5 +1,5 @@ import type { CancellationToken, ConfigurationChangeEvent } from 'vscode'; -import { Disposable, ProgressLocation, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; +import { Disposable, ProgressLocation, ThemeIcon, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; import type { CommitsViewConfig } from '../configuration'; import { configuration, ViewFilesLayout, ViewShowBranchComparison } from '../configuration'; import { Commands, ContextKeys, GlyphChars } from '../constants'; @@ -16,8 +16,10 @@ import { executeCommand } from '../system/command'; import { gate } from '../system/decorators/gate'; import { debug } from '../system/decorators/log'; import { disposableInterval } from '../system/function'; +import type { UsageChangeEvent } from '../usageTracker'; import { BranchNode } from './nodes/branchNode'; import { BranchTrackingStatusNode } from './nodes/branchTrackingStatusNode'; +import { CommandMessageNode } from './nodes/common'; import { RepositoryNode } from './nodes/repositoryNode'; import type { ViewNode } from './nodes/viewNode'; import { RepositoriesSubscribeableNode, RepositoryFolderNode } from './nodes/viewNode'; @@ -130,6 +132,19 @@ export class CommitsViewNode extends RepositoriesSubscribeableNode { constructor(container: Container) { super('gitlens.views.commits', 'Commits', container); + this.disposables.push(container.usage.onDidChange(this.onUsageChanged, this)); + } + + private onUsageChanged(e: UsageChangeEvent | void) { + // Refresh the view if the graph usage state has changed, since we render a node for it before the first use + if (e == null || e.key === 'graphWebview:shown') { + void this.refresh(); + } } override get canReveal(): boolean { @@ -256,7 +279,8 @@ export class CommitsView extends ViewBase { !configuration.changed(e, 'defaultDateSource') && !configuration.changed(e, 'defaultDateStyle') && !configuration.changed(e, 'defaultGravatarsStyle') && - !configuration.changed(e, 'defaultTimeFormat') + !configuration.changed(e, 'defaultTimeFormat') && + !configuration.changed(e, 'plusFeatures.enabled') ) { return false; } diff --git a/src/webviews/apps/settings/partials/menus.html b/src/webviews/apps/settings/partials/menus.html index e1dffbb..af4a6a4 100644 --- a/src/webviews/apps/settings/partials/menus.html +++ b/src/webviews/apps/settings/partials/menus.html @@ -333,14 +333,91 @@ - Add Add Co-authors command + Add Show Commit Graph command + + + + + + + + + Add to the Source Control repository toolbar + + + + + + + + Add Show Commit Graph command + + + + + + + + + Add to the Source Control repository context menu + + + + + + + + Add Add Co-authors command + + + + + + Add Show Commit Graph command diff --git a/src/webviews/apps/shared/glicons.scss b/src/webviews/apps/shared/glicons.scss index 5a173b6..5bbe26e 100644 --- a/src/webviews/apps/shared/glicons.scss +++ b/src/webviews/apps/shared/glicons.scss @@ -1,7 +1,7 @@ @font-face { font-family: 'glicons'; font-display: block; - src: url("./glicons.woff2?7c1fcbaed5f09c54558dcca7d79320a6") format("woff2"); + src: url("./glicons.woff2?649cd5c77eb3632ea56d58730da557f0") format("woff2"); } .glicon {