未验证 提交 c7fec928 编写于 作者: J João Moreno 提交者: GitHub

Merge pull request #96069 from microsoft/joao/github-auth

Provide automatic git authentication to GitHub
......@@ -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) { }
}
......
......@@ -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;
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { CredentialsProvider, Credentials } from './api/git';
export class GitHubCredentialProvider implements CredentialsProvider {
async getCredentials(host: vscode.Uri): Promise<Credentials | undefined> {
if (!/github\.com/i.test(host.authority)) {
return;
}
const session = await this.getSession();
return { username: session.account.id, password: await session.getAccessToken() };
}
private async getSession(): Promise<vscode.AuthenticationSession> {
const authenticationSessions = await vscode.authentication.getSessions('github', ['repo']);
if (authenticationSessions.length) {
return await authenticationSessions[0];
} else {
return await vscode.authentication.login('github', ['repo']);
}
}
}
......@@ -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,9 +20,9 @@ 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';
import { GitHubCredentialProvider } from './github';
const deactivateTasks: { (): Promise<any>; }[] = [];
......@@ -36,27 +36,12 @@ 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);
context.subscriptions.push(askpass.registerCredentialsProvider(new GitHubCredentialProvider()));
try {
ipc = await createIPCServer();
disposables.push(ipc);
env = { ...env, ...ipc.getEnv() };
} catch {
// noop
}
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()];
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册