Skip to content

Commit 413e660

Browse files
committed
github: PushErrorHandler, fork and pr
fixes microsoft#102393
1 parent 1ab3137 commit 413e660

3 files changed

Lines changed: 128 additions & 0 deletions

File tree

extensions/github/src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { GitExtension } from './typings/git';
99
import { registerCommands } from './commands';
1010
import { GithubCredentialProviderManager } from './credentialProvider';
1111
import { dispose, combinedDisposable } from './util';
12+
import { GithubPushErrorHandler } from './pushErrorHandler';
1213

1314
export function activate(context: ExtensionContext): void {
1415
const disposables = new Set<Disposable>();
@@ -21,6 +22,7 @@ export function activate(context: ExtensionContext): void {
2122
disposables.add(registerCommands(gitAPI));
2223
disposables.add(gitAPI.registerRemoteSourceProvider(new GithubRemoteSourceProvider(gitAPI)));
2324
disposables.add(new GithubCredentialProviderManager(gitAPI));
25+
disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler()));
2426
} catch (err) {
2527
console.error('Could not initialize GitHub extension');
2628
console.warn(err);
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { PushErrorHandler, GitErrorCodes, Repository, Remote } from './typings/git';
7+
import { window, ProgressLocation, commands, Uri } from 'vscode';
8+
import * as nls from 'vscode-nls';
9+
import { getOctokit } from './auth';
10+
11+
const localize = nls.loadMessageBundle();
12+
13+
async function handlePushError(repository: Repository, remote: Remote, refspec: string, owner: string, repo: string): Promise<void> {
14+
const yes = localize('create a fork', "Create Fork");
15+
const no = localize('no', "No");
16+
17+
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);
18+
19+
if (answer === no) {
20+
return;
21+
}
22+
23+
const match = /^([^:]*):([^:]*)$/.exec(refspec);
24+
const localName = match ? match[1] : refspec;
25+
const remoteName = match ? match[2] : refspec;
26+
27+
const [octokit, ghRepository] = await window.withProgress({ location: ProgressLocation.Notification, cancellable: false, title: localize('create fork', 'Create GitHub fork') }, async progress => {
28+
progress.report({ message: localize('forking', "Forking '{0}/{1}'...", owner, repo), increment: 33 });
29+
30+
const octokit = await getOctokit();
31+
32+
// Issue: what if the repo already exists?
33+
const res = await octokit.repos.createFork({ owner, repo });
34+
const ghRepository = res.data;
35+
36+
progress.report({ message: localize('pushing', "Pushing changes..."), increment: 33 });
37+
38+
// Issue: what if there's already an `upstream` repo?
39+
await repository.renameRemote(remote.name, 'upstream');
40+
41+
// Issue: what if there's already another `origin` repo?
42+
await repository.addRemote('origin', ghRepository.clone_url);
43+
await repository.fetch('origin', remoteName);
44+
await repository.setBranchUpstream(localName, `origin/${remoteName}`);
45+
await repository.push('origin', localName, true);
46+
47+
return [octokit, ghRepository];
48+
});
49+
50+
// yield
51+
(async () => {
52+
const openInGitHub = localize('openingithub', "Open In GitHub");
53+
const createPR = localize('createpr', "Create PR");
54+
const action = await window.showInformationMessage(localize('done', "The fork '{0}' was successfully created on GitHub.", ghRepository.full_name), openInGitHub, createPR);
55+
56+
if (action === openInGitHub) {
57+
await commands.executeCommand('vscode.open', Uri.parse(ghRepository.html_url));
58+
} else if (action === createPR) {
59+
const pr = await window.withProgress({ location: ProgressLocation.Notification, cancellable: false, title: localize('createghpr', "Creating GitHub Pull Request...") }, async _ => {
60+
let title = `Update ${remoteName}`;
61+
const head = repository.state.HEAD?.name;
62+
63+
if (head) {
64+
const commit = await repository.getCommit(head);
65+
title = commit.message.replace(/\n.*$/m, '');
66+
}
67+
68+
const res = await octokit.pulls.create({
69+
owner,
70+
repo,
71+
title,
72+
head: `${ghRepository.owner.login}:${remoteName}`,
73+
base: remoteName
74+
});
75+
76+
await repository.setConfig(`branch.${localName}.remote`, 'upstream');
77+
await repository.setConfig(`branch.${localName}.merge`, `refs/heads/${remoteName}`);
78+
await repository.setConfig(`branch.${localName}.github-pr-owner-number`, `${owner}#${repo}#${pr.number}`);
79+
80+
return res.data;
81+
});
82+
83+
const openPR = localize('openpr', "Open PR");
84+
const action = await window.showInformationMessage(localize('donepr', "The PR '{0}/{1}#{2}' was successfully created on GitHub.", owner, repo, pr.number), openPR);
85+
86+
if (action === openPR) {
87+
await commands.executeCommand('vscode.open', Uri.parse(pr.html_url));
88+
}
89+
}
90+
})();
91+
}
92+
93+
export class GithubPushErrorHandler implements PushErrorHandler {
94+
95+
async handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean> {
96+
if (error.gitErrorCode !== GitErrorCodes.PermissionDenied) {
97+
return false;
98+
}
99+
100+
if (!remote.pushUrl) {
101+
return false;
102+
}
103+
104+
const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\.git/i.exec(remote.pushUrl)
105+
|| /^git@github\.com:([^/]+)\/([^/]+)\.git/i.exec(remote.pushUrl);
106+
107+
if (!match) {
108+
return false;
109+
}
110+
111+
if (/^:/.test(refspec)) {
112+
return false;
113+
}
114+
115+
const [, owner, repo] = match;
116+
await handlePushError(repository, remote, refspec, owner, repo);
117+
118+
return true;
119+
}
120+
}

extensions/github/src/typings/git.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,10 @@ export interface CredentialsProvider {
223223
getCredentials(host: Uri): ProviderResult<Credentials>;
224224
}
225225

226+
export interface PushErrorHandler {
227+
handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean>;
228+
}
229+
226230
export type APIState = 'uninitialized' | 'initialized';
227231

228232
export interface API {
@@ -239,6 +243,7 @@ export interface API {
239243

240244
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
241245
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
246+
registerPushErrorHandler(handler: PushErrorHandler): Disposable;
242247
}
243248

244249
export interface GitExtension {
@@ -276,6 +281,7 @@ export const enum GitErrorCodes {
276281
CantOpenResource = 'CantOpenResource',
277282
GitNotFound = 'GitNotFound',
278283
CantCreatePipe = 'CantCreatePipe',
284+
PermissionDenied = 'PermissionDenied',
279285
CantAccessRemote = 'CantAccessRemote',
280286
RepositoryNotFound = 'RepositoryNotFound',
281287
RepositoryIsLocked = 'RepositoryIsLocked',

0 commit comments

Comments
 (0)