|
| 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 | +} |
0 commit comments