mirror of https://github.com/microsoft/vscode.git
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:
parent
96e03e0d94
commit
57e8c28877
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { Command, Disposable, commands } from 'vscode';
|
||||
import { Model } from '../model';
|
||||
import { getRemoteSourceActions, getRemoteSourceControlHistoryItemCommands, pickRemoteSource } from '../remoteSource';
|
||||
import { getRemoteSourceActions, getRemoteSourceControlHistoryItemCommands, pickRemoteSource, provideRemoteSourceLinks } from '../remoteSource';
|
||||
import { GitBaseExtensionImpl } from './extension';
|
||||
import { API, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction, RemoteSourceProvider } from './git-base';
|
||||
|
||||
|
@ -21,10 +21,14 @@ export class ApiImpl implements API {
|
|||
return getRemoteSourceActions(this._model, url);
|
||||
}
|
||||
|
||||
getRemoteSourceControlHistoryItemCommands(url: string): Promise<Command[]> {
|
||||
getRemoteSourceControlHistoryItemCommands(url: string): Promise<Command[] | undefined> {
|
||||
return getRemoteSourceControlHistoryItemCommands(this._model, url);
|
||||
}
|
||||
|
||||
provideRemoteSourceLinks(url: string, content: string): Promise<string> {
|
||||
return provideRemoteSourceLinks(this._model, url, content);
|
||||
}
|
||||
|
||||
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable {
|
||||
return this._model.registerRemoteSourceProvider(provider);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ 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>;
|
||||
provideRemoteSourceLinks(url: string, content: string): Promise<string | undefined>;
|
||||
pickRemoteSource(options: PickRemoteSourceOptions): Promise<string | PickRemoteSourceResult | undefined>;
|
||||
}
|
||||
|
||||
|
@ -85,4 +86,5 @@ export interface RemoteSourceProvider {
|
|||
getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult<Command[]>;
|
||||
getRecentRemoteSources?(query?: string): ProviderResult<RecentRemoteSource[]>;
|
||||
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
|
||||
provideRemoteSourceLinks?(url: string, content: string): Promise<string | undefined>;
|
||||
}
|
||||
|
|
|
@ -123,18 +123,30 @@ export async function getRemoteSourceActions(model: Model, url: string): Promise
|
|||
return remoteSourceActions;
|
||||
}
|
||||
|
||||
export async function getRemoteSourceControlHistoryItemCommands(model: Model, url: string): Promise<Command[]> {
|
||||
export async function getRemoteSourceControlHistoryItemCommands(model: Model, url: string): Promise<Command[] | undefined> {
|
||||
const providers = model.getRemoteProviders();
|
||||
|
||||
const remoteSourceCommands = [];
|
||||
for (const provider of providers) {
|
||||
const providerCommands = await provider.getRemoteSourceControlHistoryItemCommands?.(url);
|
||||
if (providerCommands?.length) {
|
||||
remoteSourceCommands.push(...providerCommands);
|
||||
}
|
||||
remoteSourceCommands.push(...(await provider.getRemoteSourceControlHistoryItemCommands?.(url) ?? []));
|
||||
}
|
||||
|
||||
return remoteSourceCommands;
|
||||
return remoteSourceCommands.length > 0 ? remoteSourceCommands : undefined;
|
||||
}
|
||||
|
||||
export async function provideRemoteSourceLinks(model: Model, url: string, content: string): Promise<string> {
|
||||
const providers = model.getRemoteProviders();
|
||||
|
||||
for (const provider of providers) {
|
||||
const parsedContent = await provider.provideRemoteSourceLinks?.(url, content);
|
||||
if (!parsedContent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
content = parsedContent;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise<string | undefined>;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 [];
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -85,10 +85,12 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
|
|||
return;
|
||||
}
|
||||
|
||||
// Default remote (origin, or the first remote)
|
||||
const defaultRemote = remotes.find(r => r.name === 'origin') ?? remotes[0];
|
||||
// Default remote (upstream -> origin -> first)
|
||||
const remote = remotes.find(r => r.name === 'upstream')
|
||||
?? remotes.find(r => r.name === 'origin')
|
||||
?? remotes[0];
|
||||
|
||||
const link = getCommitLink(defaultRemote.fetchUrl!, historyItem.id);
|
||||
const link = getCommitLink(remote.fetchUrl!, historyItem.id);
|
||||
vscode.env.openExternal(vscode.Uri.parse(link));
|
||||
}));
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { Command, Uri, env, l10n, workspace } from 'vscode';
|
|||
import { RemoteSourceProvider, RemoteSource, RemoteSourceAction } from './typings/git-base';
|
||||
import { getOctokit } from './auth';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { getRepositoryFromQuery, getRepositoryFromUrl } from './util';
|
||||
import { getRepositoryFromQuery, getRepositoryFromUrl, ISSUE_EXPRESSION } from './util';
|
||||
import { getBranchLink, getVscodeDevHost } from './links';
|
||||
|
||||
function asRemoteSource(raw: any): RemoteSource {
|
||||
|
@ -137,10 +137,10 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider {
|
|||
}];
|
||||
}
|
||||
|
||||
async getRemoteSourceControlHistoryItemCommands(url: string): Promise<Command[]> {
|
||||
async getRemoteSourceControlHistoryItemCommands(url: string): Promise<Command[] | undefined> {
|
||||
const repository = getRepositoryFromUrl(url);
|
||||
if (!repository) {
|
||||
return [];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [{
|
||||
|
@ -150,4 +150,28 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider {
|
|||
arguments: [url]
|
||||
}];
|
||||
}
|
||||
|
||||
provideRemoteSourceLinks(url: string, content: string): string | undefined {
|
||||
const repository = getRepositoryFromUrl(url);
|
||||
if (!repository) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return content.replace(
|
||||
ISSUE_EXPRESSION,
|
||||
(match, _group1, owner: string | undefined, repo: string | undefined, _group2, number: string | undefined) => {
|
||||
if (!number || Number.isNaN(parseInt(number))) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const label = owner && repo
|
||||
? `${owner}/${repo}#${number}`
|
||||
: `#${number}`;
|
||||
|
||||
owner = owner ?? repository.owner;
|
||||
repo = repo ?? repository.repo;
|
||||
|
||||
return `[${label}](https://github.com/${owner}/${repo}/issues/${number})`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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): ProviderResult<string>;
|
||||
}
|
||||
|
||||
export interface GitBaseExtension {
|
||||
|
@ -85,4 +86,5 @@ export interface RemoteSourceProvider {
|
|||
getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult<Command[]>;
|
||||
getRecentRemoteSources?(query?: string): ProviderResult<RecentRemoteSource[]>;
|
||||
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
|
||||
provideRemoteSourceLinks?(url: string, content: string): ProviderResult<string>;
|
||||
}
|
||||
|
|
|
@ -37,3 +37,5 @@ export function getRepositoryFromQuery(query: string): { owner: string; repo: st
|
|||
export function repositoryHasGitHubRemote(repository: Repository) {
|
||||
return !!repository.state.remotes.find(remote => remote.fetchUrl ? getRepositoryFromUrl(remote.fetchUrl) : undefined);
|
||||
}
|
||||
|
||||
export const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/g;
|
||||
|
|
|
@ -49,11 +49,7 @@ function toISCMHistoryItem(historyItemDto: SCMHistoryItemDto): ISCMHistoryItem {
|
|||
...r, icon: getIconFromIconDto(r.icon)
|
||||
}));
|
||||
|
||||
const newLineIndex = historyItemDto.message.indexOf('\n');
|
||||
const subject = newLineIndex === -1 ?
|
||||
historyItemDto.message : `${historyItemDto.message.substring(0, newLineIndex)}\u2026`;
|
||||
|
||||
return { ...historyItemDto, subject, references };
|
||||
return { ...historyItemDto, references };
|
||||
}
|
||||
|
||||
function toISCMHistoryItemRef(historyItemRefDto?: SCMHistoryItemRefDto, color?: ColorIdentifier): ISCMHistoryItemRef | undefined {
|
||||
|
|
|
@ -1603,6 +1603,7 @@ export interface SCMHistoryItemRefsChangeEventDto {
|
|||
export interface SCMHistoryItemDto {
|
||||
readonly id: string;
|
||||
readonly parentIds: string[];
|
||||
readonly subject: string;
|
||||
readonly message: string;
|
||||
readonly displayId?: string;
|
||||
readonly author?: string;
|
||||
|
|
|
@ -48,6 +48,7 @@ declare module 'vscode' {
|
|||
export interface SourceControlHistoryItem {
|
||||
readonly id: string;
|
||||
readonly parentIds: string[];
|
||||
readonly subject: string;
|
||||
readonly message: string;
|
||||
readonly displayId?: string;
|
||||
readonly author?: string;
|
||||
|
|
Loading…
Reference in New Issue