From cab6f958a23b9c5376b051960859e047036c9b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Fri, 24 Apr 2020 18:04:36 +0200 Subject: [PATCH] git extension api: registerCredentialsProvider --- extensions/git/src/api/api1.ts | 6 ++- extensions/git/src/api/git.d.ts | 11 +++++ extensions/git/src/askpass.ts | 76 +++++++++++++++++++++-------- extensions/git/src/commands.ts | 8 +++ extensions/git/src/git.ts | 2 +- extensions/git/src/ipc/ipcServer.ts | 4 +- extensions/git/src/main.ts | 25 ++-------- extensions/git/src/model.ts | 9 +++- 8 files changed, 94 insertions(+), 47 deletions(-) diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 56ff982df8c..84f503743eb 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -5,7 +5,7 @@ import { Model } from '../model'; import { Repository as BaseRepository, Resource } from '../repository'; -import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, GitExtension, RefType, RemoteSourceProvider } from './git'; +import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, GitExtension, RefType, RemoteSourceProvider, CredentialsProvider } from './git'; import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands } from 'vscode'; import { mapEvent } from '../util'; import { toGitUri } from '../uri'; @@ -263,6 +263,10 @@ export class ApiImpl implements API { return this._model.registerRemoteSourceProvider(provider); } + registerCredentialsProvider(provider: CredentialsProvider): Disposable { + return this._model.registerCredentialsProvider(provider); + } + constructor(private _model: Model) { } } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 866b9110038..46e8217ce6f 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -204,6 +204,15 @@ export interface RemoteSourceProvider { getRemoteSources(query?: string): ProviderResult; } +export interface Credentials { + readonly username: string; + readonly password: string; +} + +export interface CredentialsProvider { + getCredentials(host: Uri): ProviderResult; +} + export type APIState = 'uninitialized' | 'initialized'; export interface API { @@ -217,7 +226,9 @@ export interface API { toGitUri(uri: Uri, ref: string): Uri; getRepository(uri: Uri): Repository | null; init(root: Uri): Promise; + registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; + registerCredentialsProvider(provider: CredentialsProvider): Disposable; } export interface GitExtension { diff --git a/extensions/git/src/askpass.ts b/extensions/git/src/askpass.ts index dd7e84c2e03..29299af2810 100644 --- a/extensions/git/src/askpass.ts +++ b/extensions/git/src/askpass.ts @@ -3,36 +3,60 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { window, InputBoxOptions } from 'vscode'; -import { IDisposable } from './util'; +import { window, InputBoxOptions, Uri, OutputChannel, Disposable } from 'vscode'; +import { IDisposable, EmptyDisposable, toDisposable } from './util'; import * as path from 'path'; -import { IIPCHandler, IIPCServer } from './ipc/ipcServer'; - -export interface AskpassEnvironment { - GIT_ASKPASS: string; - ELECTRON_RUN_AS_NODE?: string; - VSCODE_GIT_ASKPASS_NODE?: string; - VSCODE_GIT_ASKPASS_MAIN?: string; - VSCODE_GIT_ASKPASS_HANDLE?: string; -} +import { IIPCHandler, IIPCServer, createIPCServer } from './ipc/ipcServer'; +import { CredentialsProvider, Credentials } from './api/git'; export class Askpass implements IIPCHandler { - private disposable: IDisposable; + private disposable: IDisposable = EmptyDisposable; + private cache = new Map(); + private credentialsProviders = new Set(); - static getDisabledEnv(): AskpassEnvironment { - return { - GIT_ASKPASS: path.join(__dirname, 'askpass-empty.sh') - }; + static async create(outputChannel: OutputChannel): Promise { + try { + return new Askpass(await createIPCServer()); + } catch (err) { + outputChannel.appendLine(`[error] Failed to create git askpass IPC: ${err}`); + return new Askpass(); + } } - constructor(ipc: IIPCServer) { - this.disposable = ipc.registerHandler('askpass', this); + private constructor(private ipc?: IIPCServer) { + if (ipc) { + this.disposable = ipc.registerHandler('askpass', this); + } } async handle({ request, host }: { request: string, host: string }): Promise { + const uri = Uri.parse(host); + const authority = uri.authority.replace(/^.*@/, ''); + const password = /password/i.test(request); + const cached = this.cache.get(authority); + + if (cached && password) { + this.cache.delete(authority); + return cached.password; + } + + if (!password) { + for (const credentialsProvider of this.credentialsProviders) { + try { + const credentials = await credentialsProvider.getCredentials(uri); + + if (credentials) { + this.cache.set(authority, credentials); + setTimeout(() => this.cache.delete(authority), 60_000); + return credentials.username; + } + } catch { } + } + } + const options: InputBoxOptions = { - password: /password/i.test(request), + password, placeHolder: request, prompt: `Git: ${host}`, ignoreFocusOut: true @@ -41,8 +65,15 @@ export class Askpass implements IIPCHandler { return await window.showInputBox(options) || ''; } - getEnv(): AskpassEnvironment { + getEnv(): { [key: string]: string; } { + if (!this.ipc) { + return { + GIT_ASKPASS: path.join(__dirname, 'askpass-empty.sh') + }; + } + return { + ...this.ipc.getEnv(), ELECTRON_RUN_AS_NODE: '1', GIT_ASKPASS: path.join(__dirname, 'askpass.sh'), VSCODE_GIT_ASKPASS_NODE: process.execPath, @@ -50,6 +81,11 @@ export class Askpass implements IIPCHandler { }; } + registerCredentialsProvider(provider: CredentialsProvider): Disposable { + this.credentialsProviders.add(provider); + return toDisposable(() => this.credentialsProviders.delete(provider)); + } + dispose(): void { this.disposable.dispose(); } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index d5fab6178bb..139753b1908 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2560,6 +2560,14 @@ export class CommandCenter { type = 'warning'; options.modal = false; break; + case GitErrorCodes.AuthenticationFailed: + const regex = /Authentication failed for '(.*)'/i; + const match = regex.exec(err.stderr || String(err)); + + message = match + ? localize('auth failed specific', "Failed to authenticate to git remote:\n\n{0}", match[1]) + : localize('auth failed', "Failed to authenticate to git remote."); + break; case GitErrorCodes.NoUserNameConfigured: case GitErrorCodes.NoUserEmailConfigured: message = localize('missing user info', "Make sure you configure your 'user.name' and 'user.email' in git."); diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 704e1528c8f..691d36e937a 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -306,7 +306,7 @@ export interface IGitOptions { function getGitErrorCode(stderr: string): string | undefined { if (/Another git process seems to be running in this repository|If no other git process is currently running/.test(stderr)) { return GitErrorCodes.RepositoryIsLocked; - } else if (/Authentication failed/.test(stderr)) { + } else if (/Authentication failed/i.test(stderr)) { return GitErrorCodes.AuthenticationFailed; } else if (/Not a git repository/i.test(stderr)) { return GitErrorCodes.NotAGitRepository; diff --git a/extensions/git/src/ipc/ipcServer.ts b/extensions/git/src/ipc/ipcServer.ts index 332490896cc..1339a9a838e 100644 --- a/extensions/git/src/ipc/ipcServer.ts +++ b/extensions/git/src/ipc/ipcServer.ts @@ -46,7 +46,7 @@ export async function createIPCServer(): Promise { export interface IIPCServer extends Disposable { readonly ipcHandlePath: string | undefined; - getEnv(): any; + getEnv(): { [key: string]: string; }; registerHandler(name: string, handler: IIPCHandler): Disposable; } @@ -91,7 +91,7 @@ class IPCServer implements IIPCServer, Disposable { }); } - getEnv(): any { + getEnv(): { [key: string]: string; } { return { VSCODE_GIT_IPC_HANDLE: this.ipcHandlePath }; } diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 5c904ee547b..dd50fa8e39a 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -20,7 +20,6 @@ import { GitProtocolHandler } from './protocolHandler'; import { GitExtensionImpl } from './api/extension'; import * as path from 'path'; import * as fs from 'fs'; -import { createIPCServer, IIPCServer } from './ipc/ipcServer'; import { GitTimelineProvider } from './timelineProvider'; import { registerAPICommands } from './api/api1'; @@ -36,27 +35,11 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann const pathHint = workspace.getConfiguration('git').get('path'); const info = await findGit(pathHint, path => outputChannel.appendLine(localize('looking', "Looking for git in: {0}", path))); - let env: any = {}; - let ipc: IIPCServer | undefined; + const askpass = await Askpass.create(outputChannel); + disposables.push(askpass); - try { - ipc = await createIPCServer(); - disposables.push(ipc); - env = { ...env, ...ipc.getEnv() }; - } catch (err) { - outputChannel.appendLine(`[error] Failed to create git askpass IPC: ${err}`); - } - - if (ipc) { - const askpass = new Askpass(ipc); - disposables.push(askpass); - env = { ...env, ...askpass.getEnv() }; - } else { - env = { ...env, ...Askpass.getDisabledEnv() }; - } - - const git = new Git({ gitPath: info.path, version: info.version, env }); - const model = new Model(git, context.globalState, outputChannel); + const git = new Git({ gitPath: info.path, version: info.version, env: askpass.getEnv() }); + const model = new Model(git, askpass, context.globalState, outputChannel); disposables.push(model); const onRepository = () => commands.executeCommand('setContext', 'gitOpenRepositoryCount', `${model.repositories.length}`); diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index f08b852c22f..a6cbc3df30c 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -12,7 +12,8 @@ import * as path from 'path'; import * as fs from 'fs'; import * as nls from 'vscode-nls'; import { fromGitUri } from './uri'; -import { GitErrorCodes, APIState as State, RemoteSourceProvider } from './api/git'; +import { GitErrorCodes, APIState as State, RemoteSourceProvider, CredentialsProvider } from './api/git'; +import { Askpass } from './askpass'; const localize = nls.loadMessageBundle(); @@ -78,7 +79,7 @@ export class Model { private disposables: Disposable[] = []; - constructor(readonly git: Git, private globalState: Memento, private outputChannel: OutputChannel) { + constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private outputChannel: OutputChannel) { workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables); window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables); workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); @@ -454,6 +455,10 @@ export class Model { return toDisposable(() => this.remoteProviders.delete(provider)); } + registerCredentialsProvider(provider: CredentialsProvider): Disposable { + return this.askpass.registerCredentialsProvider(provider); + } + getRemoteProviders(): RemoteSourceProvider[] { return [...this.remoteProviders.values()]; }