diff --git a/README.md b/README.md index 348d72e..28fe8ec 100644 --- a/README.md +++ b/README.md @@ -719,14 +719,15 @@ GitLens is highly customizable and provides many configuration settings to allow ## Status Bar Settings [#](#status-bar-settings- 'Status Bar Settings') -| Name | Description | -| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `gitlens.statusBar.alignment` | Specifies the blame alignment in the status bar

`left` - aligns to the left
`right` - aligns to the right | -| `gitlens.statusBar.command` | Specifies the command to be executed when the blame status bar item is clicked

`gitlens.toggleFileBlame` - toggles file blame annotations
`gitlens.diffWithPrevious` - opens line changes with the previous revision
`gitlens.diffWithWorking` - opens line changes with the working file
`gitlens.revealCommitInView` - reveals the commit in the Side Bar
`gitlens.showCommitsInView` - searches for the commit
`gitlens.toggleCodeLens` - toggles the Git code lens
`gitlens.showQuickCommitDetails` - shows details of the commit
`gitlens.showQuickCommitFileDetails` - show file details of the commit
`gitlens.showQuickFileHistory` - shows the current file history
`gitlens.showQuickRepoHistory` - shows the current branch history | -| `gitlens.statusBar.dateFormat` | Specifies how to format absolute dates (e.g. using the `${date}` token) in the blame information in the status bar. See the [Moment.js docs](https://momentjs.com/docs/#/displaying/format/) for valid formats | -| `gitlens.statusBar.enabled` | Specifies whether to provide blame information in the status bar | -| `gitlens.statusBar.format` | Specifies the format of the blame information in the status bar. See [_Commit Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs. Date formatting is controlled by the `gitlens.statusBar.dateFormat` setting | -| `gitlens.statusBar.reduceFlicker` | Specifies whether to avoid clearing the previous blame information when changing lines to reduce status bar "flashing" | +| Name | Description | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `gitlens.statusBar.alignment` | Specifies the blame alignment in the status bar

`left` - aligns to the left
`right` - aligns to the right | +| `gitlens.statusBar.command` | Specifies the command to be executed when the blame status bar item is clicked

`gitlens.toggleFileBlame` - toggles file blame annotations
`gitlens.diffWithPrevious` - opens line changes with the previous revision
`gitlens.diffWithWorking` - opens line changes with the working file
`gitlens.revealCommitInView` - reveals the commit in the Side Bar
`gitlens.showCommitsInView` - searches for the commit
`gitlens.toggleCodeLens` - toggles the Git code lens
`gitlens.showQuickCommitDetails` - shows details of the commit
`gitlens.showQuickCommitFileDetails` - show file details of the commit
`gitlens.showQuickFileHistory` - shows the current file history
`gitlens.showQuickRepoHistory` - shows the current branch history | +| `gitlens.statusBar.dateFormat` | Specifies how to format absolute dates (e.g. using the `${date}` token) in the blame information in the status bar. See the [Moment.js docs](https://momentjs.com/docs/#/displaying/format/) for valid formats | +| `gitlens.statusBar.enabled` | Specifies whether to provide blame information in the status bar | +| `gitlens.statusBar.format` | Specifies the format of the blame information in the status bar. See [_Commit Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs. Date formatting is controlled by the `gitlens.statusBar.dateFormat` setting | +| `gitlens.statusBar.pullRequests.enabled` | Specifies whether to provide information about the Pull Request (if any) that introduced the commit in the status bar. Requires a connection to a supported remote service (e.g. GitHub) | +| `gitlens.statusBar.reduceFlicker` | Specifies whether to avoid clearing the previous blame information when changing lines to reduce status bar "flashing" | ## Hover Settings [#](#hover-settings- 'Hover Settings') diff --git a/package.json b/package.json index 02d0a47..4063f25 100644 --- a/package.json +++ b/package.json @@ -1571,10 +1571,16 @@ }, "gitlens.statusBar.format": { "type": "string", - "default": "${author}, ${agoOrDate}", + "default": "${author}, ${agoOrDate}${' via 'pullRequest}", "markdownDescription": "Specifies the format of the blame information in the status bar. See [_Commit Tokens_](https://github.com/eamodio/vscode-gitlens/wiki/Custom-Formatting#commit-tokens) in the GitLens docs. Date formatting is controlled by the `#gitlens.statusBar.dateFormat#` setting", "scope": "window" }, + "gitlens.statusBar.pullRequests.enabled": { + "type": "boolean", + "default": true, + "markdownDescription": "Specifies whether to provide information about the Pull Request (if any) that introduced the commit in the status bar. Requires a connection to a supported remote service (e.g. GitHub)", + "scope": "window" + }, "gitlens.statusBar.reduceFlicker": { "type": "boolean", "default": false, diff --git a/src/config.ts b/src/config.ts index 08efdee..d1d5a44 100644 --- a/src/config.ts +++ b/src/config.ts @@ -114,6 +114,9 @@ export interface Config { enabled: boolean; format: string; reduceFlicker: boolean; + pullRequests: { + enabled: boolean; + }; }; strings: { codeLens: { diff --git a/src/statusbar/statusBarController.ts b/src/statusbar/statusBarController.ts index 6c947f9..909a793 100644 --- a/src/statusbar/statusBarController.ts +++ b/src/statusbar/statusBarController.ts @@ -1,17 +1,28 @@ 'use strict'; -import { ConfigurationChangeEvent, Disposable, StatusBarAlignment, StatusBarItem, TextEditor, window } from 'vscode'; +import { + CancellationToken, + CancellationTokenSource, + ConfigurationChangeEvent, + Disposable, + StatusBarAlignment, + StatusBarItem, + TextEditor, + window, +} from 'vscode'; import { Commands } from '../commands'; import { configuration, StatusBarCommand } from '../configuration'; -import { isTextEditor } from '../constants'; +import { GlyphChars, isTextEditor } from '../constants'; import { Container } from '../container'; -import { CommitFormatter, GitCommit } from '../git/git'; +import { CommitFormatter, GitBlameCommit, PullRequest } from '../git/git'; import { LinesChangeEvent } from '../trackers/gitLineTracker'; -import { debug } from '../system'; +import { debug, Promises } from '../system'; +import { LogCorrelationContext, Logger } from '../logger'; export class StatusBarController implements Disposable { - private _blameStatusBarItem: StatusBarItem | undefined; + private _cancellation: CancellationTokenSource | undefined; private readonly _disposable: Disposable; - private _modeStatusBarItem: StatusBarItem | undefined; + private _statusBarBlame: StatusBarItem | undefined; + private _statusBarMode: StatusBarItem | undefined; constructor() { this._disposable = Disposable.from(configuration.onDidChange(this.onConfigurationChanged, this)); @@ -21,8 +32,8 @@ export class StatusBarController implements Disposable { dispose() { this.clearBlame(); - this._blameStatusBarItem?.dispose(); - this._modeStatusBarItem?.dispose(); + this._statusBarBlame?.dispose(); + this._statusBarMode?.dispose(); Container.lineTracker.stop(this); this._disposable.dispose(); @@ -41,22 +52,22 @@ export class StatusBarController implements Disposable { : StatusBarAlignment.Left; if (configuration.changed(e, 'mode', 'statusBar', 'alignment')) { - if (this._modeStatusBarItem?.alignment !== alignment) { - this._modeStatusBarItem?.dispose(); - this._modeStatusBarItem = undefined; + if (this._statusBarMode?.alignment !== alignment) { + this._statusBarMode?.dispose(); + this._statusBarMode = undefined; } } - this._modeStatusBarItem = - this._modeStatusBarItem ?? + this._statusBarMode = + this._statusBarMode ?? window.createStatusBarItem(alignment, alignment === StatusBarAlignment.Right ? 999 : 1); - this._modeStatusBarItem.command = Commands.SwitchMode; - this._modeStatusBarItem.text = mode.statusBarItemName; - this._modeStatusBarItem.tooltip = 'Switch GitLens Mode'; - this._modeStatusBarItem.show(); + this._statusBarMode.command = Commands.SwitchMode; + this._statusBarMode.text = mode.statusBarItemName; + this._statusBarMode.tooltip = 'Switch GitLens Mode'; + this._statusBarMode.show(); } else { - this._modeStatusBarItem?.dispose(); - this._modeStatusBarItem = undefined; + this._statusBarMode?.dispose(); + this._statusBarMode = undefined; } } @@ -67,16 +78,16 @@ export class StatusBarController implements Disposable { Container.config.statusBar.alignment !== 'left' ? StatusBarAlignment.Right : StatusBarAlignment.Left; if (configuration.changed(e, 'statusBar', 'alignment')) { - if (this._blameStatusBarItem?.alignment !== alignment) { - this._blameStatusBarItem?.dispose(); - this._blameStatusBarItem = undefined; + if (this._statusBarBlame?.alignment !== alignment) { + this._statusBarBlame?.dispose(); + this._statusBarBlame = undefined; } } - this._blameStatusBarItem = - this._blameStatusBarItem ?? + this._statusBarBlame = + this._statusBarBlame ?? window.createStatusBarItem(alignment, alignment === StatusBarAlignment.Right ? 1000 : 0); - this._blameStatusBarItem.command = Container.config.statusBar.command; + this._statusBarBlame.command = Container.config.statusBar.command; if (configuration.changed(e, 'statusBar', 'enabled')) { Container.lineTracker.start( @@ -87,8 +98,8 @@ export class StatusBarController implements Disposable { } else if (configuration.changed(e, 'statusBar', 'enabled')) { Container.lineTracker.stop(this); - this._blameStatusBarItem?.dispose(); - this._blameStatusBarItem = undefined; + this._statusBarBlame?.dispose(); + this._statusBarBlame = undefined; } } @@ -110,7 +121,7 @@ export class StatusBarController implements Disposable { if (!e.pending && e.selections != null) { const state = Container.lineTracker.getState(e.selections[0].active); if (state?.commit != null) { - this.updateBlame(state.commit, e.editor!); + void this.updateBlame(e.editor!, state.commit); return; } @@ -124,53 +135,118 @@ export class StatusBarController implements Disposable { } clearBlame() { - this._blameStatusBarItem?.hide(); + this._cancellation?.cancel(); + this._statusBarBlame?.hide(); } - private updateBlame(commit: GitCommit, editor: TextEditor) { + @debug({ args: false }) + private async updateBlame(editor: TextEditor, commit: GitBlameCommit, options?: { pr?: PullRequest | undefined }) { const cfg = Container.config.statusBar; - if (!cfg.enabled || this._blameStatusBarItem == null || !isTextEditor(editor)) return; + if (!cfg.enabled || this._statusBarBlame == null || !isTextEditor(editor)) return; + + const cc = Logger.getCorrelationContext(); + + // TODO: Make this configurable? + const timeout = 100; + const [getBranchAndTagTips, pr] = await Promise.all([ + CommitFormatter.has(cfg.format, 'tips') + ? Container.git.getBranchesAndTagsTipsFn(commit.repoPath) + : undefined, + cfg.pullRequests.enabled && + CommitFormatter.has( + cfg.format, + 'pullRequest', + 'pullRequestAgo', + 'pullRequestAgoOrDate', + 'pullRequestDate', + 'pullRequestState', + ) + ? options?.pr ?? this.getPullRequest(commit, { timeout: timeout }) + : undefined, + ]); + + if (pr != null) { + this._cancellation?.cancel(); + this._cancellation = new CancellationTokenSource(); + void this.waitForPendingPullRequest(editor, commit, pr, this._cancellation.token, timeout, cc); + } - this._blameStatusBarItem.text = `$(git-commit) ${CommitFormatter.fromTemplate(cfg.format, commit, { - messageTruncateAtNewLine: true, + this._statusBarBlame.text = `$(git-commit) ${CommitFormatter.fromTemplate(cfg.format, commit, { dateFormat: cfg.dateFormat === null ? Container.config.defaultDateFormat : cfg.dateFormat, + getBranchAndTagTips: getBranchAndTagTips, + messageTruncateAtNewLine: true, + pullRequestOrRemote: pr, })}`; switch (cfg.command) { case StatusBarCommand.ToggleFileBlame: - this._blameStatusBarItem.tooltip = 'Toggle File Blame Annotations'; + this._statusBarBlame.tooltip = 'Toggle File Blame Annotations'; break; case StatusBarCommand.DiffWithPrevious: - this._blameStatusBarItem.command = Commands.DiffLineWithPrevious; - this._blameStatusBarItem.tooltip = 'Open Line Changes with Previous Revision'; + this._statusBarBlame.command = Commands.DiffLineWithPrevious; + this._statusBarBlame.tooltip = 'Open Line Changes with Previous Revision'; break; case StatusBarCommand.DiffWithWorking: - this._blameStatusBarItem.command = Commands.DiffLineWithWorking; - this._blameStatusBarItem.tooltip = 'Open Line Changes with Working File'; + this._statusBarBlame.command = Commands.DiffLineWithWorking; + this._statusBarBlame.tooltip = 'Open Line Changes with Working File'; break; case StatusBarCommand.ToggleCodeLens: - this._blameStatusBarItem.tooltip = 'Toggle Git CodeLens'; + this._statusBarBlame.tooltip = 'Toggle Git CodeLens'; break; case StatusBarCommand.RevealCommitInView: - this._blameStatusBarItem.tooltip = 'Reveal Commit in the Side Bar'; + this._statusBarBlame.tooltip = 'Reveal Commit in the Side Bar'; break; case StatusBarCommand.ShowCommitsInView: - this._blameStatusBarItem.tooltip = 'Search for Commit'; + this._statusBarBlame.tooltip = 'Search for Commit'; break; case StatusBarCommand.ShowQuickCommitDetails: - this._blameStatusBarItem.tooltip = 'Show Commit'; + this._statusBarBlame.tooltip = 'Show Commit'; break; case StatusBarCommand.ShowQuickCommitFileDetails: - this._blameStatusBarItem.tooltip = 'Show Commit (file)'; + this._statusBarBlame.tooltip = 'Show Commit (file)'; break; case StatusBarCommand.ShowQuickFileHistory: - this._blameStatusBarItem.tooltip = 'Show File History'; + this._statusBarBlame.tooltip = 'Show File History'; break; case StatusBarCommand.ShowQuickCurrentBranchHistory: - this._blameStatusBarItem.tooltip = 'Show Branch History'; + this._statusBarBlame.tooltip = 'Show Branch History'; break; } - this._blameStatusBarItem.show(); + this._statusBarBlame.show(); + } + + private async getPullRequest(commit: GitBlameCommit, { timeout }: { timeout?: number } = {}) { + const remote = await Container.git.getRemoteWithApiProvider(commit.repoPath); + if (remote?.provider == null) return undefined; + + const { provider } = remote; + try { + return await Container.git.getPullRequestForCommit(commit.ref, provider, { timeout: timeout }); + } catch (ex) { + return ex; + } + } + + private async waitForPendingPullRequest( + editor: TextEditor, + commit: GitBlameCommit, + pr: PullRequest | Promises.CancellationError> | undefined, + cancellationToken: CancellationToken, + timeout: number, + cc: LogCorrelationContext | undefined, + ) { + if (cancellationToken.isCancellationRequested || !(pr instanceof Promises.CancellationError)) return; + + // If the PR timed out, refresh the status bar once it completes + Logger.debug(cc, `${GlyphChars.Dot} pull request query took too long (over ${timeout} ms)`); + + pr = await pr.promise; + + if (cancellationToken.isCancellationRequested) return; + + Logger.debug(cc, `${GlyphChars.Dot} pull request query completed; refreshing...`); + + void this.updateBlame(editor, commit, { pr: pr }); } } diff --git a/src/system/promise.ts b/src/system/promise.ts index 95c76c7..3b1b1a0 100644 --- a/src/system/promise.ts +++ b/src/system/promise.ts @@ -15,13 +15,15 @@ export class CancellationErrorWithId extends CancellationErro } export function cancellable( - promise: Thenable, - timeoutOrToken: number | CancellationToken, + promise: Promise, + timeoutOrToken?: number | CancellationToken, options: { cancelMessage?: string; onDidCancel?(resolve: (value?: T | PromiseLike | undefined) => void, reject: (reason?: any) => void): void; } = {}, ): Promise { + if (timeoutOrToken == null) return promise; + return new Promise((resolve, reject) => { let fulfilled = false; let timer: NodeJS.Timer | undefined; diff --git a/src/webviews/apps/settings/partials/blame.html b/src/webviews/apps/settings/partials/blame.html index c67930c..6a6354e 100644 --- a/src/webviews/apps/settings/partials/blame.html +++ b/src/webviews/apps/settings/partials/blame.html @@ -56,16 +56,16 @@ id="blame.format" name="blame.format" type="text" - placeholder="${message|40?} ${agoOrDate|14-}" + placeholder="${message|50?} ${agoOrDate|14-}" data-setting - data-default-value="${message|40?} ${agoOrDate|14-}" + data-default-value="${message|50?} ${agoOrDate|14-}" data-popup-trigger /> - +
diff --git a/src/webviews/apps/settings/partials/current-line.html b/src/webviews/apps/settings/partials/current-line.html index 079d901..69edbb2 100644 --- a/src/webviews/apps/settings/partials/current-line.html +++ b/src/webviews/apps/settings/partials/current-line.html @@ -64,9 +64,9 @@ id="currentLine.format" name="currentLine.format" type="text" - placeholder="${authorAgoOrDate} • ${message}" + placeholder="${author, }${agoOrDate}${' via 'pullRequest}${ • message|50?}" data-setting - data-default-value="${authorAgoOrDate} • ${message}" + data-default-value="${author, }${agoOrDate}${' via 'pullRequest}${ • message|50?}" data-popup-trigger disabled /> diff --git a/src/webviews/apps/settings/partials/status-bar.html b/src/webviews/apps/settings/partials/status-bar.html index 658b550..d011822 100644 --- a/src/webviews/apps/settings/partials/status-bar.html +++ b/src/webviews/apps/settings/partials/status-bar.html @@ -20,23 +20,43 @@
+
+ + +
+ + +
+ +
-
+