Merge from vscode 27ada910e121e23a6d95ecca9cae595fb98ab568

This commit is contained in:
ADS Merger
2020-04-30 00:53:43 +00:00
parent 87e5239713
commit 93f35ca321
413 changed files with 7190 additions and 8756 deletions

View File

@@ -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';
@@ -183,6 +183,10 @@ export class ApiRepository implements Repository {
return this._repository.removeRemote(name);
}
renameRemote(name: string, newName: string): Promise<void> {
return this._repository.renameRemote(name, newName);
}
fetch(remote?: string | undefined, ref?: string | undefined, depth?: number | undefined): Promise<void> {
return this._repository.fetch(remote, ref, depth);
}
@@ -248,10 +252,21 @@ export class ApiImpl implements API {
return result ? new ApiRepository(result) : null;
}
async init(root: Uri): Promise<Repository | null> {
const path = root.fsPath;
await this._model.git.init(path);
await this._model.openRepository(path);
return this.getRepository(root) || null;
}
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable {
return this._model.registerRemoteSourceProvider(provider);
}
registerCredentialsProvider(provider: CredentialsProvider): Disposable {
return this._model.registerCredentialsProvider(provider);
}
constructor(private _model: Model) { }
}

View File

@@ -179,6 +179,7 @@ export interface Repository {
addRemote(name: string, url: string): Promise<void>;
removeRemote(name: string): Promise<void>;
renameRemote(name: string, newName: string): Promise<void>;
fetch(remote?: string, ref?: string, depth?: number): Promise<void>;
pull(unshallow?: boolean): Promise<void>;
@@ -203,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 {
@@ -215,7 +225,10 @@ 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 {

View File

@@ -1,5 +1,5 @@
#!/bin/sh
VSCODE_GIT_ASKPASS_PIPE=`mktemp`
VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $*
ELECTRON_RUN_AS_NODE="1" VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $*
cat $VSCODE_GIT_ASKPASS_PIPE
rm $VSCODE_GIT_ASKPASS_PIPE
rm $VSCODE_GIT_ASKPASS_PIPE

View File

@@ -3,36 +3,60 @@
* Licensed under the Source EULA. 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, context?: string): Promise<Askpass> {
try {
return new Askpass(await createIPCServer(context));
} 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,15 +65,26 @@ 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 {
ELECTRON_RUN_AS_NODE: '1',
...this.ipc.getEnv(),
GIT_ASKPASS: path.join(__dirname, 'askpass.sh'),
VSCODE_GIT_ASKPASS_NODE: process.execPath,
VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js')
};
}
registerCredentialsProvider(provider: CredentialsProvider): Disposable {
this.credentialsProviders.add(provider);
return toDisposable(() => this.credentialsProviders.delete(provider));
}
dispose(): void {
this.disposable.dispose();
}

View File

@@ -524,7 +524,7 @@ export class CommandCenter {
quickpick.ignoreFocusOut = true;
const providers = this.model.getRemoteProviders()
.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + localize('clonefrom', "Clone from {1}", provider.name), alwaysShow: true, provider }));
.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + localize('clonefrom', "Clone from {0}", provider.name), alwaysShow: true, provider }));
quickpick.placeholder = providers.length === 0
? localize('provide url', "Provide repository URL.")
@@ -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.");

View File

@@ -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;
@@ -1498,7 +1498,12 @@ export class Repository {
}
async removeRemote(name: string): Promise<void> {
const args = ['remote', 'rm', name];
const args = ['remote', 'remove', name];
await this.run(args);
}
async renameRemote(name: string, newName: string): Promise<void> {
const args = ['remote', 'rename', name, newName];
await this.run(args);
}

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CredentialsProvider, Credentials } from './api/git';
import { IDisposable, filterEvent, EmptyDisposable } from './util';
import { workspace, Uri, AuthenticationSession, authentication } from 'vscode';
import { Askpass } from './askpass';
export class GitHubCredentialProvider implements CredentialsProvider {
async getCredentials(host: 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<AuthenticationSession> {
const authenticationSessions = await authentication.getSessions('github', ['repo']);
if (authenticationSessions.length) {
return await authenticationSessions[0];
} else {
return await authentication.login('github', ['repo']);
}
}
}
export class GithubCredentialProviderManager {
private providerDisposable: IDisposable = EmptyDisposable;
private readonly disposable: IDisposable;
private _enabled = false;
private set enabled(enabled: boolean) {
if (this._enabled === enabled) {
return;
}
this._enabled = enabled;
if (enabled) {
this.providerDisposable = this.askpass.registerCredentialsProvider(new GitHubCredentialProvider());
} else {
this.providerDisposable.dispose();
}
}
constructor(private readonly askpass: Askpass) {
this.disposable = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git'))(this.refresh, this);
this.refresh();
}
private refresh(): void {
this.enabled = workspace.getConfiguration('git', null).get('githubAuthentication', true);
}
dispose(): void {
this.enabled = false;
this.disposable.dispose();
}
}

View File

@@ -11,27 +11,42 @@ import * as os from 'os';
import * as fs from 'fs';
import * as crypto from 'crypto';
function getIPCHandlePath(nonce: string): string {
function getIPCHandlePath(id: string): string {
if (process.platform === 'win32') {
return `\\\\.\\pipe\\vscode-git-ipc-${nonce}-sock`;
return `\\\\.\\pipe\\vscode-git-${id}-sock`;
}
if (process.env['XDG_RUNTIME_DIR']) {
return path.join(process.env['XDG_RUNTIME_DIR'] as string, `vscode-git-ipc-${nonce}.sock`);
return path.join(process.env['XDG_RUNTIME_DIR'] as string, `vscode-git-${id}.sock`);
}
return path.join(os.tmpdir(), `vscode-git-ipc-${nonce}.sock`);
return path.join(os.tmpdir(), `vscode-git-${id}.sock`);
}
export interface IIPCHandler {
handle(request: any): Promise<any>;
}
export async function createIPCServer(): Promise<IIPCServer> {
export async function createIPCServer(context?: string): Promise<IIPCServer> {
const server = http.createServer();
const buffer = await new Promise<Buffer>((c, e) => crypto.randomBytes(20, (err, buf) => err ? e(err) : c(buf)));
const nonce = buffer.toString('hex');
const ipcHandlePath = getIPCHandlePath(nonce);
const hash = crypto.createHash('sha1');
if (!context) {
const buffer = await new Promise<Buffer>((c, e) => crypto.randomBytes(20, (err, buf) => err ? e(err) : c(buf)));
hash.update(buffer);
} else {
hash.update(context);
}
const ipcHandlePath = getIPCHandlePath(hash.digest('hex').substr(0, 10));
if (process.platform !== 'win32') {
try {
await fs.promises.unlink(ipcHandlePath);
} catch {
// noop
}
}
return new Promise((c, e) => {
try {
@@ -46,7 +61,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 +106,7 @@ class IPCServer implements IIPCServer, Disposable {
});
}
getEnv(): any {
getEnv(): { [key: string]: string; } {
return { VSCODE_GIT_IPC_HANDLE: this.ipcHandlePath };
}

View File

@@ -20,9 +20,10 @@ 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 { GithubCredentialProviderManager } from './github';
import { TerminalEnvironmentManager } from './terminal';
const deactivateTasks: { (): Promise<any>; }[] = [];
@@ -36,27 +37,18 @@ 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, context.storagePath);
disposables.push(askpass);
try {
ipc = await createIPCServer();
disposables.push(ipc);
env = { ...env, ...ipc.getEnv() };
} catch {
// noop
}
const env = askpass.getEnv();
const terminalEnvironmentManager = new TerminalEnvironmentManager(context, env);
disposables.push(terminalEnvironmentManager);
if (ipc) {
const askpass = new Askpass(ipc);
disposables.push(askpass);
env = { ...env, ...askpass.getEnv() };
} else {
env = { ...env, ...Askpass.getDisabledEnv() };
}
const githubCredentialProviderManager = new GithubCredentialProviderManager(askpass);
context.subscriptions.push(githubCredentialProviderManager);
const git = new Git({ gitPath: info.path, version: info.version, env });
const model = new Model(git, context.globalState, outputChannel);
const model = new Model(git, askpass, context.globalState, outputChannel);
disposables.push(model);
const onRepository = () => commands.executeCommand('setContext', 'gitOpenRepositoryCount', `${model.repositories.length}`);

View File

@@ -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()];
}

View File

@@ -1096,6 +1096,10 @@ export class Repository implements Disposable {
await this.run(Operation.Remote, () => this.repository.removeRemote(name));
}
async renameRemote(name: string, newName: string): Promise<void> {
await this.run(Operation.Remote, () => this.repository.renameRemote(name, newName));
}
@throttle
async fetchDefault(options: { silent?: boolean } = {}): Promise<void> {
await this.run(Operation.Fetch, () => this.repository.fetch(options));

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExtensionContext, workspace } from 'vscode';
import { filterEvent, IDisposable } from './util';
export class TerminalEnvironmentManager {
private readonly disposable: IDisposable;
private _enabled = false;
private set enabled(enabled: boolean) {
if (this._enabled === enabled) {
return;
}
this._enabled = enabled;
this.context.environmentVariableCollection.clear();
if (enabled) {
for (const name of Object.keys(this.env)) {
this.context.environmentVariableCollection.replace(name, this.env[name]);
}
}
}
constructor(private readonly context: ExtensionContext, private readonly env: { [key: string]: string }) {
this.disposable = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git'))
(this.refresh, this);
this.refresh();
}
private refresh(): void {
this.enabled = workspace.getConfiguration('git', null).get('terminalAuthentication', true);
}
dispose(): void {
this.disposable.dispose();
}
}