From 27afc6b47725bae244f266388cdd998b6e42aac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Wed, 15 Apr 2020 21:30:32 +0200 Subject: [PATCH] wip: git remote providers --- extensions/git/src/commands.ts | 90 ++++++++++++++++++++++++++++++++-- extensions/git/src/model.ts | 24 ++++++++- 2 files changed, 108 insertions(+), 6 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index e8a696735fc..cd2984cb908 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -6,18 +6,19 @@ import { lstat, Stats } from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env } from 'vscode'; +import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, QuickPick } from 'vscode'; import TelemetryReporter from 'vscode-extension-telemetry'; import * as nls from 'vscode-nls'; import { Branch, GitErrorCodes, Ref, RefType, Status, CommitOptions } from './api/git'; import { ForcePushMode, Git, Stash } from './git'; -import { Model } from './model'; +import { Model, RemoteProvider, Remote } from './model'; import { Repository, Resource, ResourceGroupType } from './repository'; import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging'; import { fromGitUri, toGitUri, isGitUri } from './uri'; import { grep, isDescendant, pathEquals } from './util'; import { Log, LogLevel } from './log'; import { GitTimelineItem } from './timelineProvider'; +import { throttle, debounce } from './decorators'; const localize = nls.loadMessageBundle(); @@ -233,6 +234,59 @@ interface PushOptions { silent?: boolean; } +async function getQuickPickResult(quickpick: QuickPick): Promise { + const result = await new Promise(c => { + quickpick.onDidAccept(() => c(quickpick.selectedItems[0])); + quickpick.onDidHide(() => c(undefined)); + quickpick.show(); + }); + + quickpick.hide(); + return result; +} + +class RemoteProviderQuickPick { + + private quickpick: QuickPick; + + constructor(private provider: RemoteProvider) { + this.quickpick = window.createQuickPick(); + this.quickpick.ignoreFocusOut = true; + + if (provider.searchSupport) { + this.quickpick.placeholder = localize('type to search', "Repository name (type to search)"); + this.quickpick.onDidChangeValue(this.onDidChangeValue, this); + } else { + this.quickpick.placeholder = localize('type to filter', "Repository name"); + } + } + + @debounce(300) + onDidChangeValue(): void { + this.query(); + } + + @throttle + async query(): Promise { + this.quickpick.busy = true; + const remotes = await this.provider.getRemotes(this.quickpick.value); + this.quickpick.busy = false; + + this.quickpick.items = remotes.map(remote => ({ + label: remote.name, + description: remote.url, + remote + })); + } + + async pick(): Promise { + this.query(); + + const result = await getQuickPickResult(this.quickpick); + return result?.remote; + } +} + export class CommandCenter { private disposables: Disposable[]; @@ -454,10 +508,36 @@ export class CommandCenter { @command('git.clone') async clone(url?: string, parentPath?: string): Promise { if (!url) { - url = await window.showInputBox({ - prompt: localize('repourl', "Repository URL"), - ignoreFocusOut: true + const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteProvider })>(); + quickpick.ignoreFocusOut = true; + + const providers = this.model.getRemoteProviders() + .map(provider => ({ label: localize('clonefrom', "Clone from {0}", provider.name), alwaysShow: true, provider })); + + quickpick.items = providers; + quickpick.placeholder = providers.length === 0 + ? localize('provide url', "Provide repository URL to clone from") + : localize('provide url or pick', "Provide repository URL or pick a repository source"); + + quickpick.onDidChangeValue(value => { + if (value) { + quickpick.items = [{ label: value, description: localize('repourl', "Clone from URL"), alwaysShow: true }, ...providers]; + } else { + quickpick.items = providers; + } }); + + const result = await getQuickPickResult(quickpick); + + if (result) { + if (result.provider) { + const quickpick = new RemoteProviderQuickPick(result.provider); + const remote = await quickpick.pick(); + url = remote?.url; + } else { + url = result.label; + } + } } if (!url) { diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index bb664235103..df097912903 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -6,7 +6,7 @@ import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, OutputChannel } from 'vscode'; import { Repository, RepositoryState } from './repository'; import { memoize, sequentialize, debounce } from './decorators'; -import { dispose, anyEvent, filterEvent, isDescendant, firstIndex, pathEquals } from './util'; +import { dispose, anyEvent, filterEvent, isDescendant, firstIndex, pathEquals, toDisposable } from './util'; import { Git } from './git'; import * as path from 'path'; import * as fs from 'fs'; @@ -44,6 +44,17 @@ interface OpenRepository extends Disposable { repository: Repository; } +export interface Remote { + readonly name: string; + readonly url: string; +} + +export interface RemoteProvider { + readonly name: string; + readonly searchSupport?: boolean; + getRemotes(query?: string): Remote[] | Promise; +} + export class Model { private _onDidOpenRepository = new EventEmitter(); @@ -74,6 +85,8 @@ export class Model { this._onDidChangeState.fire(state); } + private remoteProviders = new Set(); + private disposables: Disposable[] = []; constructor(readonly git: Git, private globalState: Memento, private outputChannel: OutputChannel) { @@ -447,6 +460,15 @@ export class Model { return undefined; } + registerRemoteProvider(provider: RemoteProvider): Disposable { + this.remoteProviders.add(provider); + return toDisposable(() => this.remoteProviders.delete(provider)); + } + + getRemoteProviders(): RemoteProvider[] { + return [...this.remoteProviders.values()]; + } + dispose(): void { const openRepositories = [...this.openRepositories]; openRepositories.forEach(r => r.dispose());