Open branches on vscode.dev from ref picker (#181549)

This commit is contained in:
Joyce Er 2023-05-24 02:23:40 -07:00 committed by GitHub
parent edcad3ab53
commit 9f081fd11a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 179 additions and 16 deletions

View file

@ -5,9 +5,9 @@
import { Disposable, commands } from 'vscode';
import { Model } from '../model';
import { pickRemoteSource } from '../remoteSource';
import { getRemoteSourceActions, pickRemoteSource } from '../remoteSource';
import { GitBaseExtensionImpl } from './extension';
import { API, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceProvider } from './git-base';
import { API, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction, RemoteSourceProvider } from './git-base';
export class ApiImpl implements API {
@ -17,6 +17,10 @@ export class ApiImpl implements API {
return pickRemoteSource(this._model, options as any);
}
getRemoteSourceActions(url: string): Promise<RemoteSourceAction[]> {
return getRemoteSourceActions(this._model, url);
}
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable {
return this._model.registerRemoteSourceProvider(provider);
}

View file

@ -44,6 +44,15 @@ export interface PickRemoteSourceResult {
readonly branch?: string;
}
export interface RemoteSourceAction {
readonly label: string;
/**
* Codicon name
*/
readonly icon: string;
run(branch: string): void;
}
export interface RemoteSource {
readonly name: string;
readonly description?: string;
@ -70,6 +79,7 @@ export interface RemoteSourceProvider {
readonly supportsQuery?: boolean;
getBranches?(url: string): ProviderResult<string[]>;
getRemoteSourceActions?(url: string): ProviderResult<RemoteSourceAction[]>;
getRecentRemoteSources?(query?: string): ProviderResult<RecentRemoteSource[]>;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
}

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { QuickPickItem, window, QuickPick, QuickPickItemKind, l10n } from 'vscode';
import { RemoteSourceProvider, RemoteSource, PickRemoteSourceOptions, PickRemoteSourceResult } from './api/git-base';
import { RemoteSourceProvider, RemoteSource, PickRemoteSourceOptions, PickRemoteSourceResult, RemoteSourceAction } from './api/git-base';
import { Model } from './model';
import { throttle, debounce } from './decorators';
@ -81,6 +81,20 @@ class RemoteSourceProviderQuickPick {
}
}
export async function getRemoteSourceActions(model: Model, url: string): Promise<RemoteSourceAction[]> {
const providers = model.getRemoteProviders();
const remoteSourceActions = [];
for (const provider of providers) {
const providerActions = await provider.getRemoteSourceActions?.(url);
if (providerActions?.length) {
remoteSourceActions.push(...providerActions);
}
}
return remoteSourceActions;
}
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise<string | undefined>;
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch: true }): Promise<PickRemoteSourceResult | undefined>;
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {

View file

@ -8,6 +8,7 @@ export { ProviderResult } from 'vscode';
export interface API {
pickRemoteSource(options: PickRemoteSourceOptions): Promise<string | PickRemoteSourceResult | undefined>;
getRemoteSourceActions(url: string): Promise<RemoteSourceAction[]>;
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
}
@ -31,9 +32,12 @@ export interface GitBaseExtension {
export interface PickRemoteSourceOptions {
readonly providerLabel?: (provider: RemoteSourceProvider) => string;
readonly urlLabel?: string;
readonly urlLabel?: string | ((url: string) => string);
readonly providerName?: string;
readonly title?: string;
readonly placeholder?: string;
readonly branch?: boolean; // then result is PickRemoteSourceResult
readonly showRecentSources?: boolean;
}
export interface PickRemoteSourceResult {
@ -41,20 +45,42 @@ export interface PickRemoteSourceResult {
readonly branch?: string;
}
export interface RemoteSourceAction {
readonly label: string;
/**
* Codicon name
*/
readonly icon: string;
run(branch: string): void;
}
export interface RemoteSource {
readonly name: string;
readonly description?: string;
readonly detail?: string;
/**
* Codicon name
*/
readonly icon?: string;
readonly url: string | string[];
}
export interface RecentRemoteSource extends RemoteSource {
readonly timestamp: number;
}
export interface RemoteSourceProvider {
readonly name: string;
/**
* Codicon name
*/
readonly icon?: string;
readonly label?: string;
readonly placeholder?: string;
readonly supportsQuery?: boolean;
getBranches?(url: string): ProviderResult<string[]>;
getRemoteSourceActions?(url: string): ProviderResult<RemoteSourceAction[]>;
getRecentRemoteSources?(query?: string): ProviderResult<RecentRemoteSource[]>;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
}

View file

@ -5,7 +5,7 @@
import * as os from 'os';
import * as path from 'path';
import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind } from 'vscode';
import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon } from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator';
import { Branch, ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote } from './api/git';
@ -17,7 +17,8 @@ import { fromGitUri, toGitUri, isGitUri, toMergeUris } from './uri';
import { grep, isDescendant, pathEquals, relativePath } from './util';
import { GitTimelineItem } from './timelineProvider';
import { ApiRepository } from './api/api1';
import { pickRemoteSource } from './remoteSource';
import { getRemoteSourceActions, pickRemoteSource } from './remoteSource';
import { RemoteSourceAction } from './api/git-base';
class CheckoutItem implements QuickPickItem {
@ -25,8 +26,11 @@ class CheckoutItem implements QuickPickItem {
get label(): string { return `${this.repository.isBranchProtected(this.ref) ? '$(lock)' : '$(git-branch)'} ${this.ref.name || this.shortCommit}`; }
get description(): string { return this.shortCommit; }
get refName(): string | undefined { return this.ref.name; }
get refRemote(): string | undefined { return this.ref.remote; }
get buttons(): QuickInputButton[] | undefined { return this._buttons; }
set buttons(newButtons: QuickInputButton[] | undefined) { this._buttons = newButtons; }
constructor(protected repository: Repository, protected ref: Ref) { }
constructor(protected repository: Repository, protected ref: Ref, protected _buttons?: QuickInputButton[]) { }
async run(opts?: { detached?: boolean }): Promise<void> {
if (!this.ref.name) {
@ -278,7 +282,54 @@ async function createCheckoutItems(repository: Repository, detached = false): Pr
}
}
return processors.reduce<CheckoutItem[]>((r, p) => r.concat(...p.items), []);
const buttons = await getRemoteRefItemButtons(repository);
let fallbackRemoteButtons: RemoteSourceActionButton[] | undefined = [];
const remote = repository.remotes.find(r => r.pushUrl === repository.HEAD?.remote || r.fetchUrl === repository.HEAD?.remote) ?? repository.remotes[0];
const remoteUrl = remote.pushUrl ?? remote.fetchUrl;
if (remoteUrl) {
fallbackRemoteButtons = buttons.get(remoteUrl);
}
return processors.reduce<CheckoutItem[]>((r, p) => r.concat(...p.items.map((item) => {
if (item.refRemote) {
const matchingRemote = repository.remotes.find((remote) => remote.name === item.refRemote);
const remoteUrl = matchingRemote?.pushUrl ?? matchingRemote?.fetchUrl;
if (remoteUrl) {
item.buttons = buttons.get(item.refRemote);
}
}
item.buttons = fallbackRemoteButtons;
return item;
})), []);
}
type RemoteSourceActionButton = {
iconPath: ThemeIcon;
tooltip: string;
actual: RemoteSourceAction;
};
async function getRemoteRefItemButtons(repository: Repository) {
// Compute actions for all known remotes
const remoteUrlsToActions = new Map<string, RemoteSourceActionButton[]>();
const getButtons = async (remoteUrl: string) => (await getRemoteSourceActions(remoteUrl)).map((action) => ({ iconPath: new ThemeIcon(action.icon), tooltip: action.label, actual: action }));
for (const remote of repository.remotes) {
if (remote.fetchUrl) {
const actions = remoteUrlsToActions.get(remote.fetchUrl) ?? [];
actions.push(...await getButtons(remote.fetchUrl));
remoteUrlsToActions.set(remote.fetchUrl, actions);
}
if (remote.pushUrl && remote.pushUrl !== remote.fetchUrl) {
const actions = remoteUrlsToActions.get(remote.pushUrl) ?? [];
actions.push(...await getButtons(remote.pushUrl));
remoteUrlsToActions.set(remote.pushUrl, actions);
}
}
return remoteUrlsToActions;
}
class CheckoutProcessor {
@ -2084,7 +2135,17 @@ export class CommandCenter {
quickpick.items = picks;
quickpick.busy = false;
const choice = await new Promise<QuickPickItem | undefined>(c => quickpick.onDidAccept(() => c(quickpick.activeItems[0])));
const choice = await new Promise<QuickPickItem | undefined>(c => {
quickpick.onDidAccept(() => c(quickpick.activeItems[0]));
quickpick.onDidTriggerItemButton((e) => {
quickpick.hide();
const button = e.button as QuickInputButton & { actual: RemoteSourceAction };
const item = e.item as CheckoutItem;
if (button.actual && item.refName) {
button.actual.run(item.refRemote ? item.refName.substring(item.refRemote.length + 1) : item.refName);
}
});
});
quickpick.hide();
if (!choice) {

View file

@ -11,3 +11,7 @@ export async function pickRemoteSource(options: PickRemoteSourceOptions & { bran
export async function pickRemoteSource(options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {
return GitBaseApi.getAPI().pickRemoteSource(options);
}
export async function getRemoteSourceActions(url: string) {
return GitBaseApi.getAPI().getRemoteSourceActions(url);
}

View file

@ -7,11 +7,7 @@ import * as vscode from 'vscode';
import { API as GitAPI } from './typings/git';
import { publishRepository } from './publish';
import { DisposableStore } from './util';
import { LinkContext, getLink } from './links';
function getVscodeDevHost(): string {
return `https://${vscode.env.appName.toLowerCase().includes('insiders') ? 'insiders.' : ''}vscode.dev/github`;
}
import { LinkContext, getLink, getVscodeDevHost } from './links';
async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean, context: LinkContext, includeRange = true) {
try {

View file

@ -168,3 +168,17 @@ export function getLink(gitAPI: GitAPI, useSelection: boolean, hostPrefix?: stri
return `${hostPrefix}/${repo.owner}/${repo.repo}${blobSegment
}${fileSegments}`;
}
export function getBranchLink(url: string, branch: string, hostPrefix: string = 'https://github.com') {
const repo = getRepositoryFromUrl(url);
if (!repo) {
throw new Error('Invalid repository URL provided');
}
branch = encodeURIComponentExceptSlashes(branch);
return `${hostPrefix}/${repo.owner}/${repo.repo}/tree/${branch}`;
}
export function getVscodeDevHost(): string {
return `https://${vscode.env.appName.toLowerCase().includes('insiders') ? 'insiders.' : ''}vscode.dev/github`;
}

View file

@ -3,11 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workspace } from 'vscode';
import { RemoteSourceProvider, RemoteSource } from './typings/git-base';
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 } from './util';
import { getBranchLink, getVscodeDevHost } from './links';
function asRemoteSource(raw: any): RemoteSource {
const protocol = workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol');
@ -112,4 +113,27 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider {
return branches.sort((a, b) => a === defaultBranch ? -1 : b === defaultBranch ? 1 : 0);
}
async getRemoteSourceActions(url: string): Promise<RemoteSourceAction[]> {
const repository = getRepositoryFromUrl(url);
if (!repository) {
return [];
}
return [{
label: l10n.t('Open on GitHub'),
icon: 'github',
run(branch: string) {
const link = getBranchLink(url, branch);
env.openExternal(Uri.parse(link));
}
}, {
label: l10n.t('Checkout on vscode.dev'),
icon: 'globe',
run(branch: string) {
const link = getBranchLink(url, branch, getVscodeDevHost());
env.openExternal(Uri.parse(link));
}
}];
}
}

View file

@ -44,6 +44,15 @@ export interface PickRemoteSourceResult {
readonly branch?: string;
}
export interface RemoteSourceAction {
readonly label: string;
/**
* Codicon name
*/
readonly icon: string;
run(branch: string): void;
}
export interface RemoteSource {
readonly name: string;
readonly description?: string;
@ -70,6 +79,7 @@ export interface RemoteSourceProvider {
readonly supportsQuery?: boolean;
getBranches?(url: string): ProviderResult<string[]>;
getRemoteSourceActions?(url: string): ProviderResult<RemoteSourceAction[]>;
getRecentRemoteSources?(query?: string): ProviderResult<RecentRemoteSource[]>;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
}