mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 12:19:20 +00:00
git extension api: registerCredentialsProvider
This commit is contained in:
@@ -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) { }
|
||||
}
|
||||
|
||||
|
||||
11
extensions/git/src/api/git.d.ts
vendored
11
extensions/git/src/api/git.d.ts
vendored
@@ -204,6 +204,15 @@ export interface RemoteSourceProvider {
|
||||
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
|
||||
}
|
||||
|
||||
export interface Credentials {
|
||||
readonly username: string;
|
||||
readonly password: string;
|
||||
}
|
||||
|
||||
export interface CredentialsProvider {
|
||||
getCredentials(host: Uri): ProviderResult<Credentials>;
|
||||
}
|
||||
|
||||
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<Repository | null>;
|
||||
|
||||
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
|
||||
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
|
||||
}
|
||||
|
||||
export interface GitExtension {
|
||||
|
||||
@@ -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<string, Credentials>();
|
||||
private credentialsProviders = new Set<CredentialsProvider>();
|
||||
|
||||
static getDisabledEnv(): AskpassEnvironment {
|
||||
return {
|
||||
GIT_ASKPASS: path.join(__dirname, 'askpass-empty.sh')
|
||||
};
|
||||
static async create(outputChannel: OutputChannel): Promise<Askpass> {
|
||||
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<string> {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function createIPCServer(): Promise<IIPCServer> {
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string>('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}`);
|
||||
|
||||
@@ -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()];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user