GitHub - link provider for various hovers (#237961)

* Initial implementation

* Refactor code, add link to blame decoration

* Add links to timeline hover

* Saving my work

* Update remote order for "Open on GitHub" action

* Bug fixes

* Add link provider for graph hover

* Rename method
This commit is contained in:
Ladislau Szomoru
2025-01-15 16:30:43 +01:00
committed by GitHub
parent 96e03e0d94
commit 57e8c28877
15 changed files with 142 additions and 46 deletions

View File

@@ -12,7 +12,7 @@ import { BlameInformation, Commit } from './git';
import { fromGitUri, isGitUri } from './uri';
import { emojify, ensureEmojis } from './emoji';
import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation } from './staging';
import { getRemoteSourceControlHistoryItemCommands } from './remoteSource';
import { getRemoteSourceControlHistoryItemCommands, provideRemoteSourceLinks } from './remoteSource';
function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean {
return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive);
@@ -205,6 +205,7 @@ export class GitBlameController {
async getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation, includeCommitDetails = false): Promise<MarkdownString> {
let commitInformation: Commit | undefined;
let commitMessageWithLinks: string | undefined;
const remoteSourceCommands: Command[] = [];
const repository = this._model.getRepository(documentUri);
@@ -217,12 +218,15 @@ export class GitBlameController {
}
// Remote commands
const defaultRemote = repository.getDefaultRemote();
const unpublishedCommits = await repository.getUnpublishedCommits();
if (defaultRemote?.fetchUrl && !unpublishedCommits.has(blameInformation.hash)) {
remoteSourceCommands.push(...await getRemoteSourceControlHistoryItemCommands(defaultRemote.fetchUrl));
if (!unpublishedCommits.has(blameInformation.hash)) {
remoteSourceCommands.push(...await getRemoteSourceControlHistoryItemCommands(repository));
}
// Link provider
commitMessageWithLinks = await provideRemoteSourceLinks(
repository,
commitInformation?.message ?? blameInformation.subject ?? '');
}
const markdownString = new MarkdownString();
@@ -254,7 +258,7 @@ export class GitBlameController {
}
// Subject | Message
markdownString.appendMarkdown(`${emojify(commitInformation?.message ?? blameInformation.subject ?? '')}\n\n`);
markdownString.appendMarkdown(`${emojify(commitMessageWithLinks ?? commitInformation?.message ?? blameInformation.subject ?? '')}\n\n`);
markdownString.appendMarkdown(`---\n\n`);
// Short stats

View File

@@ -12,6 +12,7 @@ import { Branch, LogOptions, Ref, RefType } from './api/git';
import { emojify, ensureEmojis } from './emoji';
import { Commit } from './git';
import { OperationKind, OperationResult } from './operation';
import { provideRemoteSourceLinks } from './remoteSource';
function toSourceControlHistoryItemRef(repository: Repository, ref: Ref): SourceControlHistoryItemRef {
const rootUri = Uri.file(repository.root);
@@ -264,22 +265,33 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec
await ensureEmojis();
return commits.map(commit => {
const historyItems: SourceControlHistoryItem[] = [];
for (const commit of commits) {
const message = emojify(commit.message);
const messageWithLinks = await provideRemoteSourceLinks(this.repository, message) ?? message;
const newLineIndex = message.indexOf('\n');
const subject = newLineIndex !== -1
? `${message.substring(0, newLineIndex)}\u2026`
: message;
const references = this._resolveHistoryItemRefs(commit);
return {
historyItems.push({
id: commit.hash,
parentIds: commit.parents,
message: emojify(commit.message),
subject,
message: messageWithLinks,
author: commit.authorName,
authorEmail: commit.authorEmail,
icon: new ThemeIcon('git-commit'),
displayId: getCommitShortHash(Uri.file(this.repository.root), commit.hash),
timestamp: commit.authorDate?.getTime(),
statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 },
references: references.length !== 0 ? references : undefined
};
});
} satisfies SourceControlHistoryItem);
}
return historyItems;
} catch (err) {
this.logger.error(`[GitHistoryProvider][provideHistoryItems] Failed to get history items with options '${JSON.stringify(options)}': ${err}`);
return [];

View File

@@ -3,8 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Command } from 'vscode';
import { PickRemoteSourceOptions, PickRemoteSourceResult } from './typings/git-base';
import { GitBaseApi } from './git-base';
import { Repository } from './repository';
export async function pickRemoteSource(options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise<string | undefined>;
export async function pickRemoteSource(options: PickRemoteSourceOptions & { branch: true }): Promise<PickRemoteSourceResult | undefined>;
@@ -16,6 +18,35 @@ export async function getRemoteSourceActions(url: string) {
return GitBaseApi.getAPI().getRemoteSourceActions(url);
}
export async function getRemoteSourceControlHistoryItemCommands(url: string) {
return GitBaseApi.getAPI().getRemoteSourceControlHistoryItemCommands(url);
export async function getRemoteSourceControlHistoryItemCommands(repository: Repository): Promise<Command[]> {
if (repository.remotes.length === 0) {
return [];
}
const getCommands = async (repository: Repository, remoteName: string): Promise<Command[] | undefined> => {
const remote = repository.remotes.find(r => r.name === remoteName && r.fetchUrl);
return remote ? GitBaseApi.getAPI().getRemoteSourceControlHistoryItemCommands(remote.fetchUrl!) : undefined;
};
// upstream -> origin -> first
return await getCommands(repository, 'upstream')
?? await getCommands(repository, 'origin')
?? await getCommands(repository, repository.remotes[0].name)
?? [];
}
export async function provideRemoteSourceLinks(repository: Repository, content: string): Promise<string | undefined> {
if (repository.remotes.length === 0) {
return undefined;
}
const getDocumentLinks = async (repository: Repository, remoteName: string): Promise<string | undefined> => {
const remote = repository.remotes.find(r => r.name === remoteName && r.fetchUrl);
return remote ? GitBaseApi.getAPI().provideRemoteSourceLinks(remote.fetchUrl!, content) : undefined;
};
// upstream -> origin -> first
return await getDocumentLinks(repository, 'upstream')
?? await getDocumentLinks(repository, 'origin')
?? await getDocumentLinks(repository, repository.remotes[0].name);
}

View File

@@ -12,7 +12,7 @@ import { CommandCenter } from './commands';
import { OperationKind, OperationResult } from './operation';
import { getCommitShortHash } from './util';
import { CommitShortStat } from './git';
import { getRemoteSourceControlHistoryItemCommands } from './remoteSource';
import { getRemoteSourceControlHistoryItemCommands, provideRemoteSourceLinks } from './remoteSource';
export class GitTimelineItem extends TimelineItem {
static is(item: TimelineItem): item is GitTimelineItem {
@@ -215,25 +215,27 @@ export class GitTimelineProvider implements TimelineProvider {
const openComparison = l10n.t('Open Comparison');
const defaultRemote = repo.getDefaultRemote();
const unpublishedCommits = await repo.getUnpublishedCommits();
const remoteSourceCommands: Command[] = defaultRemote?.fetchUrl
? await getRemoteSourceControlHistoryItemCommands(defaultRemote.fetchUrl)
: [];
const remoteSourceCommands = await getRemoteSourceControlHistoryItemCommands(repo);
const items: GitTimelineItem[] = [];
for (let index = 0; index < commits.length; index++) {
const c = commits[index];
const items = commits.map<GitTimelineItem>((c, i) => {
const date = dateType === 'authored' ? c.authorDate : c.commitDate;
const message = emojify(c.message);
const item = new GitTimelineItem(c.hash, commits[i + 1]?.hash ?? `${c.hash}^`, message, date?.getTime() ?? 0, c.hash, 'git:file:commit');
const item = new GitTimelineItem(c.hash, commits[index + 1]?.hash ?? `${c.hash}^`, message, date?.getTime() ?? 0, c.hash, 'git:file:commit');
item.iconPath = new ThemeIcon('git-commit');
if (showAuthor) {
item.description = c.authorName;
}
const commitRemoteSourceCommands = !unpublishedCommits.has(c.hash) ? remoteSourceCommands : [];
item.setItemDetails(uri, c.hash, c.authorName!, c.authorEmail, dateFormatter.format(date), message, c.shortStat, commitRemoteSourceCommands);
const messageWithLinks = await provideRemoteSourceLinks(repo, message) ?? message;
item.setItemDetails(uri, c.hash, c.authorName!, c.authorEmail, dateFormatter.format(date), messageWithLinks, c.shortStat, commitRemoteSourceCommands);
const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri);
if (cmd) {
@@ -244,8 +246,8 @@ export class GitTimelineProvider implements TimelineProvider {
};
}
return item;
});
items.push(item);
}
if (options.cursor === undefined) {
const you = l10n.t('You');

View File

@@ -9,8 +9,9 @@ export { ProviderResult } from 'vscode';
export interface API {
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
getRemoteSourceActions(url: string): Promise<RemoteSourceAction[]>;
getRemoteSourceControlHistoryItemCommands(url: string): Promise<Command[]>;
getRemoteSourceControlHistoryItemCommands(url: string): Promise<Command[] | undefined>;
pickRemoteSource(options: PickRemoteSourceOptions): Promise<string | PickRemoteSourceResult | undefined>;
provideRemoteSourceLinks(url: string, content: string): Promise<string | undefined>;
}
export interface GitBaseExtension {