diff --git a/extensions/git-base/src/api/api1.ts b/extensions/git-base/src/api/api1.ts index 2c668945211..005a7930356 100644 --- a/extensions/git-base/src/api/api1.ts +++ b/extensions/git-base/src/api/api1.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Command, Disposable, commands } from 'vscode'; +import { Disposable, commands } from 'vscode'; import { Model } from '../model'; -import { getRemoteSourceActions, getRemoteSourceControlHistoryItemCommands, pickRemoteSource, provideRemoteSourceLinks } from '../remoteSource'; +import { getRemoteSourceActions, pickRemoteSource } from '../remoteSource'; import { GitBaseExtensionImpl } from './extension'; import { API, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction, RemoteSourceProvider } from './git-base'; @@ -21,14 +21,6 @@ export class ApiImpl implements API { return getRemoteSourceActions(this._model, url); } - getRemoteSourceControlHistoryItemCommands(url: string): Promise { - return getRemoteSourceControlHistoryItemCommands(this._model, url); - } - - provideRemoteSourceLinks(url: string, content: string): Promise { - return provideRemoteSourceLinks(this._model, url, content); - } - registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable { return this._model.registerRemoteSourceProvider(provider); } diff --git a/extensions/git-base/src/api/git-base.d.ts b/extensions/git-base/src/api/git-base.d.ts index 540aa583146..d4ec49df47d 100644 --- a/extensions/git-base/src/api/git-base.d.ts +++ b/extensions/git-base/src/api/git-base.d.ts @@ -9,8 +9,6 @@ export { ProviderResult } from 'vscode'; export interface API { registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; getRemoteSourceActions(url: string): Promise; - getRemoteSourceControlHistoryItemCommands(url: string): Promise; - provideRemoteSourceLinks(url: string, content: string): Promise; pickRemoteSource(options: PickRemoteSourceOptions): Promise; } @@ -83,8 +81,6 @@ export interface RemoteSourceProvider { getBranches?(url: string): ProviderResult; getRemoteSourceActions?(url: string): ProviderResult; - getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; - provideRemoteSourceLinks?(url: string, content: string): Promise; } diff --git a/extensions/git-base/src/remoteSource.ts b/extensions/git-base/src/remoteSource.ts index cf570e3aa00..eb86b27367a 100644 --- a/extensions/git-base/src/remoteSource.ts +++ b/extensions/git-base/src/remoteSource.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { QuickPickItem, window, QuickPick, QuickPickItemKind, l10n, Disposable, Command } from 'vscode'; +import { QuickPickItem, window, QuickPick, QuickPickItemKind, l10n, Disposable } from 'vscode'; import { RemoteSourceProvider, RemoteSource, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction } from './api/git-base'; import { Model } from './model'; import { throttle, debounce } from './decorators'; @@ -123,32 +123,6 @@ export async function getRemoteSourceActions(model: Model, url: string): Promise return remoteSourceActions; } -export async function getRemoteSourceControlHistoryItemCommands(model: Model, url: string): Promise { - const providers = model.getRemoteProviders(); - - const remoteSourceCommands = []; - for (const provider of providers) { - remoteSourceCommands.push(...(await provider.getRemoteSourceControlHistoryItemCommands?.(url) ?? [])); - } - - return remoteSourceCommands.length > 0 ? remoteSourceCommands : undefined; -} - -export async function provideRemoteSourceLinks(model: Model, url: string, content: string): Promise { - 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; export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch: true }): Promise; export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise { diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 518269c4162..55cd31a1acf 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -7,7 +7,7 @@ import { Model } from '../model'; import { Repository as BaseRepository, Resource } from '../repository'; -import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions } from './git'; +import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailProvider } from './git'; import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode'; import { combinedDisposable, filterEvent, mapEvent } from '../util'; import { toGitUri } from '../uri'; @@ -414,6 +414,10 @@ export class ApiImpl implements API { return this.#model.registerPushErrorHandler(handler); } + registerSourceControlHistoryItemDetailProvider(provider: SourceControlHistoryItemDetailProvider): Disposable { + return this.#model.registerSourceControlHistoryItemDetailProvider(provider); + } + registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable { return this.#model.registerBranchProtectionProvider(root, provider); } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index ea78ac4d99a..622c8a80b16 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken } from 'vscode'; +import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken, SourceControlHistoryItem } from 'vscode'; export { ProviderResult } from 'vscode'; export interface Git { @@ -326,6 +326,11 @@ export interface BranchProtectionProvider { provideBranchProtection(): BranchProtection[]; } +export interface SourceControlHistoryItemDetailProvider { + provideHoverCommands(repository: Repository): ProviderResult; + provideMessageLinks(repository: Repository, message: string): ProviderResult; +} + export type APIState = 'uninitialized' | 'initialized'; export interface PublishEvent { @@ -353,6 +358,7 @@ export interface API { registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable; registerPushErrorHandler(handler: PushErrorHandler): Disposable; registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable; + registerSourceControlHistoryItemDetailProvider(provider: SourceControlHistoryItemDetailProvider): Disposable; } export interface GitExtension { diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 6f9d0cc88e4..7265a61d879 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -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, provideRemoteSourceLinks } from './remoteSource'; +import { provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailProvider'; function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); @@ -204,9 +204,9 @@ export class GitBlameController { } async getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation, includeCommitDetails = false): Promise { + const remoteHoverCommands: Command[] = []; let commitInformation: Commit | undefined; let commitMessageWithLinks: string | undefined; - const remoteSourceCommands: Command[] = []; const repository = this._model.getRepository(documentUri); if (repository) { @@ -217,16 +217,15 @@ export class GitBlameController { } catch { } } - // Remote commands + // Remote hover commands const unpublishedCommits = await repository.getUnpublishedCommits(); if (!unpublishedCommits.has(blameInformation.hash)) { - remoteSourceCommands.push(...await getRemoteSourceControlHistoryItemCommands(repository)); + remoteHoverCommands.push(...await provideSourceControlHistoryItemHoverCommands(this._model, repository) ?? []); } - // Link provider - commitMessageWithLinks = await provideRemoteSourceLinks( - repository, - commitInformation?.message ?? blameInformation.subject ?? ''); + // Message links + commitMessageWithLinks = await provideSourceControlHistoryItemMessageLinks( + this._model, repository, commitInformation?.message ?? blameInformation.subject ?? ''); } const markdownString = new MarkdownString(); @@ -289,11 +288,11 @@ export class GitBlameController { markdownString.appendMarkdown(' '); markdownString.appendMarkdown(`[$(copy)](command:git.copyContentToClipboard?${encodeURIComponent(JSON.stringify(hash))} "${l10n.t('Copy Commit Hash')}")`); - // Remote commands - if (remoteSourceCommands.length > 0) { + // Remote hover commands + if (remoteHoverCommands.length > 0) { markdownString.appendMarkdown('  |  '); - const remoteCommandsMarkdown = remoteSourceCommands + const remoteCommandsMarkdown = remoteHoverCommands .map(command => `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify([...command.arguments ?? [], hash]))} "${command.tooltip}")`); markdownString.appendMarkdown(remoteCommandsMarkdown.join(' ')); } diff --git a/extensions/git/src/historyItemDetailProvider.ts b/extensions/git/src/historyItemDetailProvider.ts new file mode 100644 index 00000000000..af717a703f1 --- /dev/null +++ b/extensions/git/src/historyItemDetailProvider.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Command, Disposable } from 'vscode'; +import { SourceControlHistoryItemDetailProvider } from './api/git'; +import { Repository } from './repository'; +import { ApiRepository } from './api/api1'; + +export interface ISourceControlHistoryItemDetailProviderRegistry { + registerSourceControlHistoryItemDetailProvider(provider: SourceControlHistoryItemDetailProvider): Disposable; + getSourceControlHistoryItemDetailProviders(): SourceControlHistoryItemDetailProvider[]; +} + +export async function provideSourceControlHistoryItemHoverCommands( + registry: ISourceControlHistoryItemDetailProviderRegistry, + repository: Repository +): Promise { + for (const provider of registry.getSourceControlHistoryItemDetailProviders()) { + const result = await provider.provideHoverCommands(new ApiRepository(repository)); + + if (result) { + return result; + } + } + + return undefined; +} + +export async function provideSourceControlHistoryItemMessageLinks( + registry: ISourceControlHistoryItemDetailProviderRegistry, + repository: Repository, + message: string +): Promise { + for (const provider of registry.getSourceControlHistoryItemDetailProviders()) { + const result = await provider.provideMessageLinks( + new ApiRepository(repository), message); + + if (result) { + return result; + } + } + + return undefined; +} diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index bda106f46d5..430819977b5 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -12,7 +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'; +import { ISourceControlHistoryItemDetailProviderRegistry, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailProvider'; function toSourceControlHistoryItemRef(repository: Repository, ref: Ref): SourceControlHistoryItemRef { const rootUri = Uri.file(repository.root); @@ -97,7 +97,11 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec private disposables: Disposable[] = []; - constructor(protected readonly repository: Repository, private readonly logger: LogOutputChannel) { + constructor( + private historyItemDetailProviderRegistry: ISourceControlHistoryItemDetailProviderRegistry, + private readonly repository: Repository, + private readonly logger: LogOutputChannel + ) { const onDidRunWriteOperation = filterEvent(repository.onDidRunOperation, e => !e.operation.readOnly); this.disposables.push(onDidRunWriteOperation(this.onDidRunWriteOperation, this)); @@ -268,7 +272,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec const historyItems: SourceControlHistoryItem[] = []; for (const commit of commits) { const message = emojify(commit.message); - const messageWithLinks = await provideRemoteSourceLinks(this.repository, message) ?? message; + const messageWithLinks = await provideSourceControlHistoryItemMessageLinks( + this.historyItemDetailProviderRegistry, this.repository, message) ?? message; const newLineIndex = message.indexOf('\n'); const subject = newLineIndex !== -1 diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index 142d073914f..c94df8d5a50 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -12,13 +12,14 @@ import { Git } from './git'; import * as path from 'path'; import * as fs from 'fs'; import { fromGitUri } from './uri'; -import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider, BranchProtectionProvider } from './api/git'; +import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider, BranchProtectionProvider, SourceControlHistoryItemDetailProvider } from './api/git'; import { Askpass } from './askpass'; import { IPushErrorHandlerRegistry } from './pushError'; import { ApiRepository } from './api/api1'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { IPostCommitCommandsProviderRegistry } from './postCommitCommands'; import { IBranchProtectionProviderRegistry } from './branchProtection'; +import { ISourceControlHistoryItemDetailProviderRegistry } from './historyItemDetailProvider'; class RepositoryPick implements QuickPickItem { @memoize get label(): string { @@ -170,7 +171,7 @@ class UnsafeRepositoriesManager { } } -export class Model implements IRepositoryResolver, IBranchProtectionProviderRegistry, IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry { +export class Model implements IRepositoryResolver, IBranchProtectionProviderRegistry, IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry, ISourceControlHistoryItemDetailProviderRegistry { private _onDidOpenRepository = new EventEmitter(); readonly onDidOpenRepository: Event = this._onDidOpenRepository.event; @@ -236,6 +237,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi readonly onDidChangeBranchProtectionProviders = this._onDidChangeBranchProtectionProviders.event; private pushErrorHandlers = new Set(); + private historyItemDetailProviders = new Set(); private _unsafeRepositoriesManager: UnsafeRepositoriesManager; get unsafeRepositories(): string[] { @@ -633,7 +635,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi // Open repository const [dotGit, repositoryRootRealPath] = await Promise.all([this.git.getRepositoryDotGit(repositoryRoot), this.getRepositoryRootRealPath(repositoryRoot)]); - const repository = new Repository(this.git.open(repositoryRoot, repositoryRootRealPath, dotGit, this.logger), this, this, this, this, this, this.globalState, this.logger, this.telemetryReporter); + const repository = new Repository(this.git.open(repositoryRoot, repositoryRootRealPath, dotGit, this.logger), this, this, this, this, this, this, this.globalState, this.logger, this.telemetryReporter); this.open(repository); this._closedRepositoriesManager.deleteRepository(repository.root); @@ -1002,6 +1004,15 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi return [...this.pushErrorHandlers]; } + registerSourceControlHistoryItemDetailProvider(provider: SourceControlHistoryItemDetailProvider): Disposable { + this.historyItemDetailProviders.add(provider); + return toDisposable(() => this.historyItemDetailProviders.delete(provider)); + } + + getSourceControlHistoryItemDetailProviders(): SourceControlHistoryItemDetailProvider[] { + return [...this.historyItemDetailProviders]; + } + getUnsafeRepositoryPath(repository: string): string | undefined { return this._unsafeRepositoriesManager.getRepositoryPath(repository); } diff --git a/extensions/git/src/remoteSource.ts b/extensions/git/src/remoteSource.ts index 86daccedf95..eb63e5db81f 100644 --- a/extensions/git/src/remoteSource.ts +++ b/extensions/git/src/remoteSource.ts @@ -3,10 +3,8 @@ * 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; export async function pickRemoteSource(options: PickRemoteSourceOptions & { branch: true }): Promise; @@ -17,36 +15,3 @@ export async function pickRemoteSource(options: PickRemoteSourceOptions = {}): P export async function getRemoteSourceActions(url: string) { return GitBaseApi.getAPI().getRemoteSourceActions(url); } - -export async function getRemoteSourceControlHistoryItemCommands(repository: Repository): Promise { - if (repository.remotes.length === 0) { - return []; - } - - const getCommands = async (repository: Repository, remoteName: string): Promise => { - 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 { - if (repository.remotes.length === 0) { - return undefined; - } - - const getDocumentLinks = async (repository: Repository, remoteName: string): Promise => { - 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); -} diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 9ad8c2916c2..d7c37189957 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -26,6 +26,7 @@ import { toGitUri } from './uri'; import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; import { detectEncoding } from './encoding'; +import { ISourceControlHistoryItemDetailProviderRegistry } from './historyItemDetailProvider'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -855,6 +856,7 @@ export class Repository implements Disposable { remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry, postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry, private readonly branchProtectionProviderRegistry: IBranchProtectionProviderRegistry, + historyItemDetailProviderRegistry: ISourceControlHistoryItemDetailProviderRegistry, globalState: Memento, private readonly logger: LogOutputChannel, private telemetryReporter: TelemetryReporter @@ -893,7 +895,7 @@ export class Repository implements Disposable { this._sourceControl.quickDiffProvider = this; - this._historyProvider = new GitHistoryProvider(this, logger); + this._historyProvider = new GitHistoryProvider(historyItemDetailProviderRegistry, this, logger); this._sourceControl.historyProvider = this._historyProvider; this.disposables.push(this._historyProvider); diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 90d202cc415..6b9085eb821 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -12,7 +12,7 @@ import { CommandCenter } from './commands'; import { OperationKind, OperationResult } from './operation'; import { getCommitShortHash } from './util'; import { CommitShortStat } from './git'; -import { getRemoteSourceControlHistoryItemCommands, provideRemoteSourceLinks } from './remoteSource'; +import { provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailProvider'; export class GitTimelineItem extends TimelineItem { static is(item: TimelineItem): item is GitTimelineItem { @@ -216,7 +216,7 @@ export class GitTimelineProvider implements TimelineProvider { const openComparison = l10n.t('Open Comparison'); const unpublishedCommits = await repo.getUnpublishedCommits(); - const remoteSourceCommands = await getRemoteSourceControlHistoryItemCommands(repo); + const remoteHoverCommands = await provideSourceControlHistoryItemHoverCommands(this.model, repo); const items: GitTimelineItem[] = []; for (let index = 0; index < commits.length; index++) { @@ -232,8 +232,8 @@ export class GitTimelineProvider implements TimelineProvider { item.description = c.authorName; } - const commitRemoteSourceCommands = !unpublishedCommits.has(c.hash) ? remoteSourceCommands : []; - const messageWithLinks = await provideRemoteSourceLinks(repo, message) ?? message; + const commitRemoteSourceCommands = !unpublishedCommits.has(c.hash) ? remoteHoverCommands : []; + const messageWithLinks = await provideSourceControlHistoryItemMessageLinks(this.model, repo, message) ?? message; item.setItemDetails(uri, c.hash, c.authorName!, c.authorEmail, dateFormatter.format(date), messageWithLinks, c.shortStat, commitRemoteSourceCommands); diff --git a/extensions/git/src/typings/git-base.d.ts b/extensions/git/src/typings/git-base.d.ts index 3b61341d806..d4ec49df47d 100644 --- a/extensions/git/src/typings/git-base.d.ts +++ b/extensions/git/src/typings/git-base.d.ts @@ -9,9 +9,7 @@ export { ProviderResult } from 'vscode'; export interface API { registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; getRemoteSourceActions(url: string): Promise; - getRemoteSourceControlHistoryItemCommands(url: string): Promise; pickRemoteSource(options: PickRemoteSourceOptions): Promise; - provideRemoteSourceLinks(url: string, content: string): Promise; } export interface GitBaseExtension { @@ -83,7 +81,6 @@ export interface RemoteSourceProvider { getBranches?(url: string): ProviderResult; getRemoteSourceActions?(url: string): ProviderResult; - getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; } diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 47b6aea454b..805d91c7296 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -85,7 +85,7 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { return; } - // Default remote (upstream -> origin -> first) + // upstream -> origin -> first const remote = remotes.find(r => r.name === 'upstream') ?? remotes.find(r => r.name === 'origin') ?? remotes[0]; diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index 1827768312e..968630e1852 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -16,6 +16,7 @@ import { GithubRemoteSourcePublisher } from './remoteSourcePublisher'; import { GithubBranchProtectionProviderManager } from './branchProtection'; import { GitHubCanonicalUriProvider } from './canonicalUriProvider'; import { VscodeDevShareProvider } from './shareProviders'; +import { GitHubSourceControlHistoryItemDetailProvider } from './historyItemDetailProvider'; export function activate(context: ExtensionContext): void { const disposables: Disposable[] = []; @@ -100,6 +101,7 @@ function initializeGitExtension(context: ExtensionContext, telemetryReporter: Te disposables.add(new GithubBranchProtectionProviderManager(gitAPI, context.globalState, logger, telemetryReporter)); disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler(telemetryReporter))); disposables.add(gitAPI.registerRemoteSourcePublisher(new GithubRemoteSourcePublisher(gitAPI))); + disposables.add(gitAPI.registerSourceControlHistoryItemDetailProvider(new GitHubSourceControlHistoryItemDetailProvider())); disposables.add(new GitHubCanonicalUriProvider(gitAPI)); disposables.add(new VscodeDevShareProvider(gitAPI)); setGitHubContext(gitAPI, disposables); diff --git a/extensions/github/src/historyItemDetailProvider.ts b/extensions/github/src/historyItemDetailProvider.ts new file mode 100644 index 00000000000..bf1bdded391 --- /dev/null +++ b/extensions/github/src/historyItemDetailProvider.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Command, l10n } from 'vscode'; +import { Repository, SourceControlHistoryItemDetailProvider } from './typings/git'; +import { getRepositoryDefaultRemote, getRepositoryDefaultRemoteUrl } from './util'; + +const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/g; + +export class GitHubSourceControlHistoryItemDetailProvider implements SourceControlHistoryItemDetailProvider { + async provideHoverCommands(repository: Repository): Promise { + const url = getRepositoryDefaultRemoteUrl(repository); + if (!url) { + return undefined; + } + + return [{ + title: l10n.t('{0} Open on GitHub', '$(github)'), + tooltip: l10n.t('Open on GitHub'), + command: 'github.openOnGitHub', + arguments: [url] + }]; + } + + async provideMessageLinks(repository: Repository, message: string): Promise { + const descriptor = getRepositoryDefaultRemote(repository); + if (!descriptor) { + return undefined; + } + + return message.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 ?? descriptor.owner; + repo = repo ?? descriptor.repo; + + return `[${label}](https://github.com/${owner}/${repo}/issues/${number})`; + }); + } +} diff --git a/extensions/github/src/remoteSourceProvider.ts b/extensions/github/src/remoteSourceProvider.ts index e79931c7415..0d8b9340695 100644 --- a/extensions/github/src/remoteSourceProvider.ts +++ b/extensions/github/src/remoteSourceProvider.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Command, Uri, env, l10n, workspace } from 'vscode'; +import { 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, ISSUE_EXPRESSION } from './util'; +import { getRepositoryFromQuery, getRepositoryFromUrl } from './util'; import { getBranchLink, getVscodeDevHost } from './links'; function asRemoteSource(raw: any): RemoteSource { @@ -136,42 +136,4 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider { } }]; } - - async getRemoteSourceControlHistoryItemCommands(url: string): Promise { - const repository = getRepositoryFromUrl(url); - if (!repository) { - return undefined; - } - - return [{ - title: l10n.t('{0} Open on GitHub', '$(github)'), - tooltip: l10n.t('Open on GitHub'), - command: 'github.openOnGitHub', - 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})`; - }); - } } diff --git a/extensions/github/src/typings/git-base.d.ts b/extensions/github/src/typings/git-base.d.ts index 548369b1f0f..d4ec49df47d 100644 --- a/extensions/github/src/typings/git-base.d.ts +++ b/extensions/github/src/typings/git-base.d.ts @@ -9,9 +9,7 @@ export { ProviderResult } from 'vscode'; export interface API { registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; getRemoteSourceActions(url: string): Promise; - getRemoteSourceControlHistoryItemCommands(url: string): Promise; pickRemoteSource(options: PickRemoteSourceOptions): Promise; - provideRemoteSourceLinks(url: string, content: string): ProviderResult; } export interface GitBaseExtension { @@ -83,8 +81,6 @@ export interface RemoteSourceProvider { getBranches?(url: string): ProviderResult; getRemoteSourceActions?(url: string): ProviderResult; - getRemoteSourceControlHistoryItemCommands?(url: string): ProviderResult; getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; - provideRemoteSourceLinks?(url: string, content: string): ProviderResult; } diff --git a/extensions/github/src/typings/git.d.ts b/extensions/github/src/typings/git.d.ts index 7ac67937a47..318152e82f6 100644 --- a/extensions/github/src/typings/git.d.ts +++ b/extensions/github/src/typings/git.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Uri, Event, Disposable, ProviderResult, Command } from 'vscode'; +import { Uri, Event, Disposable, ProviderResult, Command, SourceControlHistoryItem } from 'vscode'; export { ProviderResult } from 'vscode'; export interface Git { @@ -289,6 +289,11 @@ export interface BranchProtectionProvider { provideBranchProtection(): BranchProtection[]; } +export interface SourceControlHistoryItemDetailProvider { + provideHoverCommands(repository: Repository): Promise; + provideMessageLinks(repository: Repository, message: string): Promise; +} + export type APIState = 'uninitialized' | 'initialized'; export interface PublishEvent { @@ -316,6 +321,7 @@ export interface API { registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable; registerPushErrorHandler(handler: PushErrorHandler): Disposable; registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable; + registerSourceControlHistoryItemDetailProvider(provider: SourceControlHistoryItemDetailProvider): Disposable; } export interface GitExtension { diff --git a/extensions/github/src/util.ts b/extensions/github/src/util.ts index 5289bb93181..eba3bced698 100644 --- a/extensions/github/src/util.ts +++ b/extensions/github/src/util.ts @@ -38,4 +38,23 @@ 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; +export function getRepositoryDefaultRemoteUrl(repository: Repository): string | undefined { + const remotes = repository.state.remotes + .filter(remote => remote.fetchUrl && getRepositoryFromUrl(remote.fetchUrl)); + + if (remotes.length === 0) { + return undefined; + } + + // upstream -> origin -> first + const remote = remotes.find(remote => remote.name === 'upstream') + ?? remotes.find(remote => remote.name === 'origin') + ?? remotes[0]; + + return remote.fetchUrl; +} + +export function getRepositoryDefaultRemote(repository: Repository): { owner: string; repo: string } | undefined { + const fetchUrl = getRepositoryDefaultRemoteUrl(repository); + return fetchUrl ? getRepositoryFromUrl(fetchUrl) : undefined; +}