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 { Model } from '../model';
|
||||||
import { Repository as BaseRepository, Resource } from '../repository';
|
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 { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands } from 'vscode';
|
||||||
import { mapEvent } from '../util';
|
import { mapEvent } from '../util';
|
||||||
import { toGitUri } from '../uri';
|
import { toGitUri } from '../uri';
|
||||||
@@ -263,6 +263,10 @@ export class ApiImpl implements API {
|
|||||||
return this._model.registerRemoteSourceProvider(provider);
|
return this._model.registerRemoteSourceProvider(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerCredentialsProvider(provider: CredentialsProvider): Disposable {
|
||||||
|
return this._model.registerCredentialsProvider(provider);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private _model: Model) { }
|
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[]>;
|
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 type APIState = 'uninitialized' | 'initialized';
|
||||||
|
|
||||||
export interface API {
|
export interface API {
|
||||||
@@ -217,7 +226,9 @@ export interface API {
|
|||||||
toGitUri(uri: Uri, ref: string): Uri;
|
toGitUri(uri: Uri, ref: string): Uri;
|
||||||
getRepository(uri: Uri): Repository | null;
|
getRepository(uri: Uri): Repository | null;
|
||||||
init(root: Uri): Promise<Repository | null>;
|
init(root: Uri): Promise<Repository | null>;
|
||||||
|
|
||||||
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
|
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
|
||||||
|
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GitExtension {
|
export interface GitExtension {
|
||||||
|
|||||||
@@ -3,36 +3,60 @@
|
|||||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { window, InputBoxOptions } from 'vscode';
|
import { window, InputBoxOptions, Uri, OutputChannel, Disposable } from 'vscode';
|
||||||
import { IDisposable } from './util';
|
import { IDisposable, EmptyDisposable, toDisposable } from './util';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { IIPCHandler, IIPCServer } from './ipc/ipcServer';
|
import { IIPCHandler, IIPCServer, createIPCServer } from './ipc/ipcServer';
|
||||||
|
import { CredentialsProvider, Credentials } from './api/git';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Askpass implements IIPCHandler {
|
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 {
|
static async create(outputChannel: OutputChannel): Promise<Askpass> {
|
||||||
return {
|
try {
|
||||||
GIT_ASKPASS: path.join(__dirname, 'askpass-empty.sh')
|
return new Askpass(await createIPCServer());
|
||||||
};
|
} catch (err) {
|
||||||
|
outputChannel.appendLine(`[error] Failed to create git askpass IPC: ${err}`);
|
||||||
|
return new Askpass();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(ipc: IIPCServer) {
|
private constructor(private ipc?: IIPCServer) {
|
||||||
this.disposable = ipc.registerHandler('askpass', this);
|
if (ipc) {
|
||||||
|
this.disposable = ipc.registerHandler('askpass', this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handle({ request, host }: { request: string, host: string }): Promise<string> {
|
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 = {
|
const options: InputBoxOptions = {
|
||||||
password: /password/i.test(request),
|
password,
|
||||||
placeHolder: request,
|
placeHolder: request,
|
||||||
prompt: `Git: ${host}`,
|
prompt: `Git: ${host}`,
|
||||||
ignoreFocusOut: true
|
ignoreFocusOut: true
|
||||||
@@ -41,8 +65,15 @@ export class Askpass implements IIPCHandler {
|
|||||||
return await window.showInputBox(options) || '';
|
return await window.showInputBox(options) || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
getEnv(): AskpassEnvironment {
|
getEnv(): { [key: string]: string; } {
|
||||||
|
if (!this.ipc) {
|
||||||
|
return {
|
||||||
|
GIT_ASKPASS: path.join(__dirname, 'askpass-empty.sh')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
...this.ipc.getEnv(),
|
||||||
ELECTRON_RUN_AS_NODE: '1',
|
ELECTRON_RUN_AS_NODE: '1',
|
||||||
GIT_ASKPASS: path.join(__dirname, 'askpass.sh'),
|
GIT_ASKPASS: path.join(__dirname, 'askpass.sh'),
|
||||||
VSCODE_GIT_ASKPASS_NODE: process.execPath,
|
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 {
|
dispose(): void {
|
||||||
this.disposable.dispose();
|
this.disposable.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2560,6 +2560,14 @@ export class CommandCenter {
|
|||||||
type = 'warning';
|
type = 'warning';
|
||||||
options.modal = false;
|
options.modal = false;
|
||||||
break;
|
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.NoUserNameConfigured:
|
||||||
case GitErrorCodes.NoUserEmailConfigured:
|
case GitErrorCodes.NoUserEmailConfigured:
|
||||||
message = localize('missing user info', "Make sure you configure your 'user.name' and 'user.email' in git.");
|
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 {
|
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)) {
|
if (/Another git process seems to be running in this repository|If no other git process is currently running/.test(stderr)) {
|
||||||
return GitErrorCodes.RepositoryIsLocked;
|
return GitErrorCodes.RepositoryIsLocked;
|
||||||
} else if (/Authentication failed/.test(stderr)) {
|
} else if (/Authentication failed/i.test(stderr)) {
|
||||||
return GitErrorCodes.AuthenticationFailed;
|
return GitErrorCodes.AuthenticationFailed;
|
||||||
} else if (/Not a git repository/i.test(stderr)) {
|
} else if (/Not a git repository/i.test(stderr)) {
|
||||||
return GitErrorCodes.NotAGitRepository;
|
return GitErrorCodes.NotAGitRepository;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export async function createIPCServer(): Promise<IIPCServer> {
|
|||||||
|
|
||||||
export interface IIPCServer extends Disposable {
|
export interface IIPCServer extends Disposable {
|
||||||
readonly ipcHandlePath: string | undefined;
|
readonly ipcHandlePath: string | undefined;
|
||||||
getEnv(): any;
|
getEnv(): { [key: string]: string; };
|
||||||
registerHandler(name: string, handler: IIPCHandler): Disposable;
|
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 };
|
return { VSCODE_GIT_IPC_HANDLE: this.ipcHandlePath };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { GitProtocolHandler } from './protocolHandler';
|
|||||||
import { GitExtensionImpl } from './api/extension';
|
import { GitExtensionImpl } from './api/extension';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { createIPCServer, IIPCServer } from './ipc/ipcServer';
|
|
||||||
import { GitTimelineProvider } from './timelineProvider';
|
import { GitTimelineProvider } from './timelineProvider';
|
||||||
import { registerAPICommands } from './api/api1';
|
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 pathHint = workspace.getConfiguration('git').get<string>('path');
|
||||||
const info = await findGit(pathHint, path => outputChannel.appendLine(localize('looking', "Looking for git in: {0}", path)));
|
const info = await findGit(pathHint, path => outputChannel.appendLine(localize('looking', "Looking for git in: {0}", path)));
|
||||||
|
|
||||||
let env: any = {};
|
const askpass = await Askpass.create(outputChannel);
|
||||||
let ipc: IIPCServer | undefined;
|
disposables.push(askpass);
|
||||||
|
|
||||||
try {
|
const git = new Git({ gitPath: info.path, version: info.version, env: askpass.getEnv() });
|
||||||
ipc = await createIPCServer();
|
const model = new Model(git, askpass, context.globalState, outputChannel);
|
||||||
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);
|
|
||||||
disposables.push(model);
|
disposables.push(model);
|
||||||
|
|
||||||
const onRepository = () => commands.executeCommand('setContext', 'gitOpenRepositoryCount', `${model.repositories.length}`);
|
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 fs from 'fs';
|
||||||
import * as nls from 'vscode-nls';
|
import * as nls from 'vscode-nls';
|
||||||
import { fromGitUri } from './uri';
|
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();
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ export class Model {
|
|||||||
|
|
||||||
private disposables: Disposable[] = [];
|
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);
|
workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables);
|
||||||
window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
|
window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
|
||||||
workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables);
|
workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables);
|
||||||
@@ -454,6 +455,10 @@ export class Model {
|
|||||||
return toDisposable(() => this.remoteProviders.delete(provider));
|
return toDisposable(() => this.remoteProviders.delete(provider));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerCredentialsProvider(provider: CredentialsProvider): Disposable {
|
||||||
|
return this.askpass.registerCredentialsProvider(provider);
|
||||||
|
}
|
||||||
|
|
||||||
getRemoteProviders(): RemoteSourceProvider[] {
|
getRemoteProviders(): RemoteSourceProvider[] {
|
||||||
return [...this.remoteProviders.values()];
|
return [...this.remoteProviders.values()];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user