code-server/lib/vscode/extensions/github/src/pushErrorHandler.ts
2021-04-09 11:32:27 +05:30

186 lines
6.5 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { commands, env, ProgressLocation, UIKind, Uri, window } from 'vscode';
import * as nls from 'vscode-nls';
import { getOctokit } from './auth';
import { GitErrorCodes, PushErrorHandler, Remote, Repository } from './typings/git';
const localize = nls.loadMessageBundle();
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
export function isInCodespaces(): boolean {
return env.remoteName === 'codespaces';
}
async function handlePushError(repository: Repository, remote: Remote, refspec: string, owner: string, repo: string): Promise<void> {
const inCodespaces = isInCodespaces();
let codespace: string | undefined;
if (inCodespaces) {
if (env.uiKind === UIKind.Web) {
// TODO@eamodio Find a better way to get the codespace id
// HACK to get the codespace id
try {
const codespaceUrl = (await env.asExternalUri(Uri.parse(`${env.uriScheme}://codespace/`))).authority;
if (codespaceUrl.endsWith('.github.dev')) {
codespace = codespaceUrl.slice(0, -11);
} else {
[codespace] = codespaceUrl.split('.');
}
} catch { }
} else {
// Call into the codespaces extension to get the codespace id
const info = await commands.executeCommand<{ name: string } | undefined>('github.codespaces.getCurrentCodespace');
codespace = info?.name;
}
if (!codespace) {
const ok = localize('ok', "OK");
await window.showErrorMessage(localize('fork unable', "You don't have permissions to push to '{0}/{1}' on GitHub.", owner, repo), ok);
return;
}
}
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;
let 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();
type CreateForkResponseData = Awaited<ReturnType<typeof octokit.repos.createFork>>['data'];
// Issue: what if the repo already exists?
let ghRepository: CreateForkResponseData;
try {
if (inCodespaces) {
const userResp = await octokit.users.getAuthenticated();
const user = userResp.data.login;
const resp = await octokit.request<{ repository: CreateForkResponseData, ref: string }>({ method: 'POST', url: `/vscs_internal/user/${user}/codespaces/${codespace}/fork_repo` });
ghRepository = resp.data.repository;
if (resp.data.ref) {
let ref = resp.data.ref;
if (ref.startsWith('refs/heads/')) {
ref = ref.substr(11);
}
remoteName = ref;
}
} else {
const resp = await octokit.repos.createFork({ owner, repo });
ghRepository = resp.data;
}
} catch (ex) {
console.error(ex);
throw ex;
}
progress.report({ message: localize('forking_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);
try {
await repository.fetch('origin', remoteName);
await repository.setBranchUpstream(localName, `origin/${remoteName}`);
} catch {
// noop
}
await repository.push('origin', localName, true);
return [octokit, ghRepository];
});
// yield
(async () => {
const openOnGitHub = localize('openingithub', "Open on GitHub");
const createPR = localize('createpr', "Create PR");
const action = await window.showInformationMessage(localize('forking_done', "The fork '{0}' was successfully created on GitHub.", ghRepository.full_name), openOnGitHub, createPR);
if (action === openOnGitHub) {
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;
}
const remoteUrl = remote.pushUrl || (isInCodespaces() ? remote.fetchUrl : undefined);
if (!remoteUrl) {
return false;
}
const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+)(?:\.git)?/i.exec(remoteUrl)
|| /^git@github\.com:([^/]+)\/([^/]+)(?:\.git)?/i.exec(remoteUrl);
if (!match) {
return false;
}
if (/^:/.test(refspec)) {
return false;
}
const [, owner, repo] = match;
await handlePushError(repository, remote, refspec, owner, repo);
return true;
}
}