Merge from vscode 1b314ab317fbff7d799b21754326b7d849889ceb

This commit is contained in:
ADS Merger
2020-07-15 23:51:18 +00:00
parent aae013d498
commit 9d3f12d0b7
554 changed files with 15159 additions and 8223 deletions

View File

@@ -6,9 +6,10 @@
import * as vscode from 'vscode';
import { API as GitAPI } from './typings/git';
import { publishRepository } from './publish';
import { combinedDisposable } from './util';
export function registerCommands(gitAPI: GitAPI): vscode.Disposable[] {
const disposables = [];
export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
const disposables: vscode.Disposable[] = [];
disposables.push(vscode.commands.registerCommand('github.publish', async () => {
try {
@@ -18,5 +19,5 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable[] {
}
}));
return disposables;
return combinedDisposable(disposables);
}

View File

@@ -3,23 +3,43 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { Disposable, ExtensionContext, extensions } from 'vscode';
import { GithubRemoteSourceProvider } from './remoteSourceProvider';
import { GitExtension } from './typings/git';
import { registerCommands } from './commands';
import { GithubCredentialProviderManager } from './credentialProvider';
import { dispose, combinedDisposable } from './util';
import { GithubPushErrorHandler } from './pushErrorHandler';
export async function activate(context: vscode.ExtensionContext) {
const gitExtension = vscode.extensions.getExtension<GitExtension>('vscode.git')!.exports;
export function activate(context: ExtensionContext): void {
const disposables = new Set<Disposable>();
context.subscriptions.push(combinedDisposable(disposables));
try {
const gitAPI = gitExtension.getAPI(1);
const init = () => {
try {
const gitAPI = gitExtension.getAPI(1);
context.subscriptions.push(...registerCommands(gitAPI));
context.subscriptions.push(gitAPI.registerRemoteSourceProvider(new GithubRemoteSourceProvider(gitAPI)));
context.subscriptions.push(new GithubCredentialProviderManager(gitAPI));
} catch (err) {
console.error('Could not initialize GitHub extension');
console.warn(err);
}
disposables.add(registerCommands(gitAPI));
disposables.add(gitAPI.registerRemoteSourceProvider(new GithubRemoteSourceProvider(gitAPI)));
disposables.add(new GithubCredentialProviderManager(gitAPI));
disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler()));
} catch (err) {
console.error('Could not initialize GitHub extension');
console.warn(err);
}
};
const onDidChangeGitExtensionEnablement = (enabled: boolean) => {
if (!enabled) {
dispose(disposables);
disposables.clear();
} else {
init();
}
};
const gitExtension = extensions.getExtension<GitExtension>('vscode.git')!.exports;
context.subscriptions.push(gitExtension.onDidChangeEnablement(onDidChangeGitExtensionEnablement));
onDidChangeGitExtensionEnablement(gitExtension.enabled);
}

View File

@@ -5,10 +5,10 @@
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as path from 'path';
import { promises as fs } from 'fs';
import { API as GitAPI, Repository } from './typings/git';
import { getOctokit } from './auth';
import { TextEncoder } from 'util';
import { basename } from 'path';
const localize = nls.loadMessageBundle();
@@ -28,10 +28,12 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
return;
}
let folder: vscode.WorkspaceFolder;
let folder: vscode.Uri;
if (vscode.workspace.workspaceFolders.length === 1) {
folder = vscode.workspace.workspaceFolders[0];
if (repository) {
folder = repository.rootUri;
} else if (vscode.workspace.workspaceFolders.length === 1) {
folder = vscode.workspace.workspaceFolders[0].uri;
} else {
const picks = vscode.workspace.workspaceFolders.map(folder => ({ label: folder.name, folder }));
const placeHolder = localize('pick folder', "Pick a folder to publish to GitHub");
@@ -41,14 +43,14 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
return;
}
folder = pick.folder;
folder = pick.folder.uri;
}
let quickpick = vscode.window.createQuickPick<vscode.QuickPickItem & { repo?: string, auth?: 'https' | 'ssh' }>();
quickpick.ignoreFocusOut = true;
quickpick.placeholder = 'Repository Name';
quickpick.value = folder.name;
quickpick.value = basename(folder.fsPath);
quickpick.show();
quickpick.busy = true;
@@ -97,37 +99,49 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
return;
}
quickpick = vscode.window.createQuickPick();
quickpick.placeholder = localize('ignore', "Select which files should be included in the repository.");
quickpick.canSelectMany = true;
quickpick.show();
if (!repository) {
const gitignore = vscode.Uri.joinPath(folder, '.gitignore');
let shouldGenerateGitignore = false;
try {
quickpick.busy = true;
const repositoryPath = folder.uri.fsPath;
const currentPath = path.join(repositoryPath);
const children = await fs.readdir(currentPath);
quickpick.items = children.map(name => ({ label: name }));
quickpick.selectedItems = quickpick.items;
quickpick.busy = false;
const result = await Promise.race([
new Promise<readonly vscode.QuickPickItem[]>(c => quickpick.onDidAccept(() => c(quickpick.selectedItems))),
new Promise<undefined>(c => quickpick.onDidHide(() => c(undefined)))
]);
if (!result) {
return;
try {
await vscode.workspace.fs.stat(gitignore);
} catch (err) {
shouldGenerateGitignore = true;
}
const ignored = new Set(children);
result.forEach(c => ignored.delete(c.label));
if (shouldGenerateGitignore) {
quickpick = vscode.window.createQuickPick();
quickpick.placeholder = localize('ignore', "Select which files should be included in the repository.");
quickpick.canSelectMany = true;
quickpick.show();
const raw = [...ignored].map(i => `/${i}`).join('\n');
await fs.writeFile(path.join(repositoryPath, '.gitignore'), raw, 'utf8');
} finally {
quickpick.dispose();
try {
quickpick.busy = true;
const children = (await vscode.workspace.fs.readDirectory(folder)).map(([name]) => name);
quickpick.items = children.map(name => ({ label: name }));
quickpick.selectedItems = quickpick.items;
quickpick.busy = false;
const result = await Promise.race([
new Promise<readonly vscode.QuickPickItem[]>(c => quickpick.onDidAccept(() => c(quickpick.selectedItems))),
new Promise<undefined>(c => quickpick.onDidHide(() => c(undefined)))
]);
if (!result) {
return;
}
const ignored = new Set(children);
result.forEach(c => ignored.delete(c.label));
const raw = [...ignored].map(i => `/${i}`).join('\n');
const encoder = new TextEncoder();
await vscode.workspace.fs.writeFile(gitignore, encoder.encode(raw));
} finally {
quickpick.dispose();
}
}
}
const githubRepository = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, cancellable: false, title: 'Publish to GitHub' }, async progress => {
@@ -143,7 +157,7 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
progress.report({ message: 'Creating first commit', increment: 25 });
if (!repository) {
repository = await gitAPI.init(folder.uri) || undefined;
repository = await gitAPI.init(folder) || undefined;
if (!repository) {
return;

View File

@@ -0,0 +1,120 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { PushErrorHandler, GitErrorCodes, Repository, Remote } from './typings/git';
import { window, ProgressLocation, commands, Uri } from 'vscode';
import * as nls from 'vscode-nls';
import { getOctokit } from './auth';
const localize = nls.loadMessageBundle();
async function handlePushError(repository: Repository, remote: Remote, refspec: string, owner: string, repo: string): Promise<void> {
const yes = localize('create a fork', "Create Fork");
const no = localize('no', "No");
const answer = await window.showInformationMessage(localize('fork', "You don't have permissions to push to '{0}/{1}' on GitHub. Would you like to create a fork and push to it instead?", owner, repo), yes, no);
if (answer === no) {
return;
}
const match = /^([^:]*):([^:]*)$/.exec(refspec);
const localName = match ? match[1] : refspec;
const remoteName = match ? match[2] : refspec;
const [octokit, ghRepository] = await window.withProgress({ location: ProgressLocation.Notification, cancellable: false, title: localize('create fork', 'Create GitHub fork') }, async progress => {
progress.report({ message: localize('forking', "Forking '{0}/{1}'...", owner, repo), increment: 33 });
const octokit = await getOctokit();
// Issue: what if the repo already exists?
const res = await octokit.repos.createFork({ owner, repo });
const ghRepository = res.data;
progress.report({ message: localize('pushing', "Pushing changes..."), increment: 33 });
// Issue: what if there's already an `upstream` repo?
await repository.renameRemote(remote.name, 'upstream');
// Issue: what if there's already another `origin` repo?
await repository.addRemote('origin', ghRepository.clone_url);
await repository.fetch('origin', remoteName);
await repository.setBranchUpstream(localName, `origin/${remoteName}`);
await repository.push('origin', localName, true);
return [octokit, ghRepository];
});
// yield
(async () => {
const openInGitHub = localize('openingithub', "Open In GitHub");
const createPR = localize('createpr', "Create PR");
const action = await window.showInformationMessage(localize('done', "The fork '{0}' was successfully created on GitHub.", ghRepository.full_name), openInGitHub, createPR);
if (action === openInGitHub) {
await commands.executeCommand('vscode.open', Uri.parse(ghRepository.html_url));
} else if (action === createPR) {
const pr = await window.withProgress({ location: ProgressLocation.Notification, cancellable: false, title: localize('createghpr', "Creating GitHub Pull Request...") }, async _ => {
let title = `Update ${remoteName}`;
const head = repository.state.HEAD?.name;
if (head) {
const commit = await repository.getCommit(head);
title = commit.message.replace(/\n.*$/m, '');
}
const res = await octokit.pulls.create({
owner,
repo,
title,
head: `${ghRepository.owner.login}:${remoteName}`,
base: remoteName
});
await repository.setConfig(`branch.${localName}.remote`, 'upstream');
await repository.setConfig(`branch.${localName}.merge`, `refs/heads/${remoteName}`);
await repository.setConfig(`branch.${localName}.github-pr-owner-number`, `${owner}#${repo}#${pr.number}`);
return res.data;
});
const openPR = localize('openpr', "Open PR");
const action = await window.showInformationMessage(localize('donepr', "The PR '{0}/{1}#{2}' was successfully created on GitHub.", owner, repo, pr.number), openPR);
if (action === openPR) {
await commands.executeCommand('vscode.open', Uri.parse(pr.html_url));
}
}
})();
}
export class GithubPushErrorHandler implements PushErrorHandler {
async handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean> {
if (error.gitErrorCode !== GitErrorCodes.PermissionDenied) {
return false;
}
if (!remote.pushUrl) {
return false;
}
const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\.git/i.exec(remote.pushUrl)
|| /^git@github\.com:([^/]+)\/([^/]+)\.git/i.exec(remote.pushUrl);
if (!match) {
return false;
}
if (/^:/.test(refspec)) {
return false;
}
const [, owner, repo] = match;
await handlePushError(repository, remote, refspec, owner, repo);
return true;
}
}

View File

@@ -134,6 +134,8 @@ export interface CommitOptions {
export interface BranchQuery {
readonly remote?: boolean;
readonly pattern?: string;
readonly count?: number;
readonly contains?: string;
}
@@ -221,6 +223,10 @@ export interface CredentialsProvider {
getCredentials(host: Uri): ProviderResult<Credentials>;
}
export interface PushErrorHandler {
handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean>;
}
export type APIState = 'uninitialized' | 'initialized';
export interface API {
@@ -237,6 +243,7 @@ export interface API {
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
registerPushErrorHandler(handler: PushErrorHandler): Disposable;
}
export interface GitExtension {
@@ -274,6 +281,7 @@ export const enum GitErrorCodes {
CantOpenResource = 'CantOpenResource',
GitNotFound = 'GitNotFound',
CantCreatePipe = 'CantCreatePipe',
PermissionDenied = 'PermissionDenied',
CantAccessRemote = 'CantAccessRemote',
RepositoryNotFound = 'RepositoryNotFound',
RepositoryIsLocked = 'RepositoryIsLocked',

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export function dispose(arg: vscode.Disposable | Iterable<vscode.Disposable>): void {
if (arg instanceof vscode.Disposable) {
arg.dispose();
} else {
for (const disposable of arg) {
disposable.dispose();
}
}
}
export function combinedDisposable(disposables: Iterable<vscode.Disposable>): vscode.Disposable {
return {
dispose() {
dispose(disposables);
}
};
}