Skip to content
This repository has been archived by the owner on Nov 27, 2023. It is now read-only.

Commit

Permalink
feat: github enterprise support
Browse files Browse the repository at this point in the history
Closes #59
  • Loading branch information
KnisterPeter committed May 6, 2017
1 parent ddc1733 commit 94d1cf9
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 40 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Current it is possible to do the following:
* Checkout one of the open pull requests
* Open github page for the current project in your default browser
* Browse one of the open pull requests in your default browser
* Browse the pull requests of your current branch
* Display pull request and current status (e.g. mergeable, travis build done, ...) in the StatusBar
* Create a new pull request based on the current branch and the last commit
The current branch will be requested to merge into master and the pull request title is the commit message summary, or a custom message if configured that way.
Expand All @@ -22,6 +23,7 @@ Current it is possible to do the following:
* Configure default branch, merge method and refresh interval.
* Allow to manage assignees for pull requests
* Allow to create and cancel pull request reviews
* Support for GitHub Enterprise (on-premise installations)

![Create pull request](images/create-pull-request.png)

Expand Down
14 changes: 5 additions & 9 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,18 @@ type MergeOptionItems = { label: string; description: string; method: MergeMetho

class Extension {

private githubHostname: string;

private channel: vscode.OutputChannel;

private githubManager: GitHubManager;

private statusBarManager: StatusBarManager;

constructor(context: vscode.ExtensionContext) {
this.githubHostname = 'github.com';

this.channel = vscode.window.createOutputChannel('github');
context.subscriptions.push(this.channel);
this.channel.appendLine('Visual Studio Code GitHub Extension');

this.githubManager = new GitHubManager(this.cwd, this.githubHostname, 'https://api.github.com', this.channel);
this.githubManager = new GitHubManager(this.cwd, this.channel);
this.statusBarManager = new StatusBarManager(context, this.cwd, this.githubManager, this.channel);

const token = context.globalState.get<string|undefined>('token');
Expand Down Expand Up @@ -121,7 +117,7 @@ class Extension {
const input = await vscode.window.showInputBox(options);
if (input) {
context.globalState.update('token', input);
this.githubManager.connect(input);
await this.githubManager.connect(input);
}
};
}
Expand Down Expand Up @@ -165,7 +161,7 @@ class Extension {
return;
}
progress.report(`Gather data`);
let [owner, repo] = await git.getGitHubOwnerAndRepository(this.cwd, this.githubHostname);
let [owner, repo] = await git.getGitHubOwnerAndRepository(this.cwd);
const repository = await this.githubManager.getRepository();
let pullRequest: PullRequest|undefined;
if (repository.parent) {
Expand Down Expand Up @@ -232,8 +228,8 @@ class Extension {

private async browseProject(): Promise<void> {
await this.withinProgressUI(async() => {
const slug = await this.githubManager.getGithubSlug();
await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(`https://${this.githubHostname}/${slug}`));
const url = await this.githubManager.getGithubUrl();
await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url));
});
}

Expand Down
28 changes: 19 additions & 9 deletions src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@ import * as vscode from 'vscode';
* @return {Promise<string[]>} A tuple of username and repository (e.g. KnisterPeter/vscode-github)
* @throws Throws if the could not be parsed as a github url
*/
export async function getGitHubOwnerAndRepository(cwd: string, githubHostname: string): Promise<string[]> {
export async function getGitHubOwnerAndRepository(cwd: string): Promise<string[]> {
const defaultUpstream = vscode.workspace.getConfiguration('github').get<string|undefined>('upstream', undefined);
if (defaultUpstream) {
return Promise.resolve(defaultUpstream.split('/'));
}
return getGitHubOwnerAndRepositoryFromGitConfig(cwd, githubHostname);
return (await getGitHubOwnerAndRepositoryFromGitConfig(cwd)).slice(1, 3);
}

async function getGitHubOwnerAndRepositoryFromGitConfig(cwd: string, githubHostname: string): Promise<string[]> {
// as we expect this function to throw on non-Github repos we can chain
// whatever calls and they will thrown on non-correct remotes
export async function getGitHubHostname(cwd: string): Promise<string> {
return (await getGitHubOwnerAndRepositoryFromGitConfig(cwd))[0];
}

async function getGitHubOwnerAndRepositoryFromGitConfig(cwd: string): Promise<string[]> {
const remote = (await execa('git', 'config --local --get remote.origin.url'.split(' '), {cwd})).stdout.trim();
if (!remote.length) {
throw new Error('Git remote is empty!');
Expand All @@ -31,17 +33,25 @@ async function getGitHubOwnerAndRepositoryFromGitConfig(cwd: string, githubHostn
// git protocol remotes, may be git@github:username/repo.git
// or git://github/user/repo.git, domain names are not case-sensetive
if (remote.startsWith('git')) {
const regexp = new RegExp(`^git(?:@|://)${githubHostname}[/:](.*?)/(.*?)(?:.git)?$`, 'i');
return regexp.exec(remote)!.slice(1, 3);
const regexp = new RegExp('^git(?:@|://)([^:/]+)[:/]([^/]+)/([^.]+)(?:.git)?$', 'i');
return regexp.exec(remote)!.slice(1, 4);
}

return getGitHubOwnerAndRepositoryFromHttpUrl(remote);
}

function getGitHubOwnerAndRepositoryFromHttpUrl(remote: string): string[] {
// it must be http or https based remote
const { hostname, pathname } = parse(remote);
// domain names are not case-sensetive
if (!pathname || !hostname || !new RegExp(`^${githubHostname}$`, 'i').test(hostname)) {
if (!hostname || !pathname) {
throw new Error('Not a Github remote!');
}
const match = pathname.match(/\/(.*?)\/(.*?)(?:.git)?$/);
if (!match) {
throw new Error('Not a Github remote!');
}
return pathname.match(/\/(.*?)\/(.*?)(?:.git)?$/)!.slice(1, 3);
return [hostname, ...match.slice(1, 3)];
}

export async function getCurrentBranch(cwd: string): Promise<string|undefined> {
Expand Down
52 changes: 30 additions & 22 deletions src/github-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,12 @@ export class GitHubManager {

private cwd: string;

private hostname: string;

private apiEndpoint: string;

private channel: vscode.OutputChannel;

private github: GitHub;

constructor(cwd: string, hostname: string, apiEndpoint: string, channel: vscode.OutputChannel) {
constructor(cwd: string, channel: vscode.OutputChannel) {
this.cwd = cwd;
this.hostname = hostname;
this.apiEndpoint = apiEndpoint;
this.channel = channel;
}

Expand All @@ -32,12 +26,20 @@ export class GitHubManager {
return Boolean(this.github);
}

public connect(token: string): void {
this.github = getClient(this.apiEndpoint, token);
public async connect(token: string): Promise<void> {
this.github = getClient(await this.getApiEndpoint(), token);
}

private async getApiEndpoint(): Promise<string> {
const hostname = await git.getGitHubHostname(this.cwd);
if (hostname === 'github.com') {
return 'https://api.github.com';
}
return `https://${hostname}/api/v3`;
}

public async getRepository(): Promise<Repository> {
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd, this.hostname);
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd);
return (await this.github.getRepository(owner, repository)).body;
}

Expand All @@ -61,7 +63,7 @@ export class GitHubManager {
}

public async getPullRequestForCurrentBranch(): Promise<PullRequest|undefined> {
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd, this.hostname);
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd);
const branch = await git.getCurrentBranch(this.cwd);
const parameters: ListPullRequestsParameters = {
state: 'open',
Expand All @@ -79,7 +81,7 @@ export class GitHubManager {
}

public async getCombinedStatusForPullRequest(): Promise<PullRequestStatus |undefined> {
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd, this.hostname);
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd);
const branch = await git.getCurrentBranch(this.cwd);
if (!branch) {
return undefined;
Expand All @@ -93,7 +95,7 @@ export class GitHubManager {
if (await this.hasPullRequestForCurrentBranch()) {
return undefined;
}
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd, this.hostname);
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd);
const branch = await git.getCurrentBranch(this.cwd);
if (!branch) {
throw new Error('No current branch');
Expand All @@ -102,7 +104,7 @@ export class GitHubManager {
const firstCommit = await git.getFirstCommitOnBranch(branch, this.cwd);
this.log(`First commit on branch ${firstCommit}`);
const requestBody = await git.getPullRequestBody(firstCommit, this.cwd);
if (!requestBody) {
if (requestBody === undefined) {
vscode.window.showWarningMessage(
`For some unknown reason no pull request body could be build; Aborting operation`);
return undefined;
Expand All @@ -128,7 +130,7 @@ export class GitHubManager {
const result = await this.github.createPullRequest(upstreamOwner, upstreamRepository, body);
// tslint:disable-next-line:comment-format
// TODO: Pretend should optionally redirect
const expr = new RegExp(`${this.apiEndpoint}/repos/[^/]+/[^/]+/pulls/([0-9]+)`);
const expr = new RegExp(`${await this.getApiEndpoint()}/repos/[^/]+/[^/]+/pulls/([0-9]+)`);
const number = expr.exec(result.headers['location'][0]) as RegExpMatchArray;
return (await this.github
.getPullRequest(upstreamOwner, upstreamRepository, parseInt(number[1], 10)))
Expand All @@ -144,7 +146,7 @@ export class GitHubManager {
}

public async listPullRequests(): Promise<PullRequest[]> {
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd, this.hostname);
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd);
const parameters: ListPullRequestsParameters = {
state: 'open'
};
Expand All @@ -154,7 +156,7 @@ export class GitHubManager {
public async mergePullRequest(pullRequest: PullRequest, method: MergeMethod): Promise<boolean|undefined> {
try {
if (pullRequest.mergeable) {
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd, this.hostname);
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd);
const body: Merge = {
merge_method: method
};
Expand All @@ -176,27 +178,33 @@ export class GitHubManager {
}

public async getGithubSlug(): Promise<string> {
const [owner, repo] = await git.getGitHubOwnerAndRepository(this.cwd, this.hostname);
const [owner, repo] = await git.getGitHubOwnerAndRepository(this.cwd);
return `${owner}/${repo}`;
}

public async getGithubUrl(): Promise<string> {
const hostname = await git.getGitHubHostname(this.cwd);
const [owner, repo] = await git.getGitHubOwnerAndRepository(this.cwd);
return `https://${hostname}/${owner}/${repo}`;
}

public async addAssignee(issue: number, name: string): Promise<void> {
const [owner, repo] = await git.getGitHubOwnerAndRepository(this.cwd, this.hostname);
const [owner, repo] = await git.getGitHubOwnerAndRepository(this.cwd);
await this.github.addAssignees(owner, repo, issue, {assignees: [name]});
}

public async removeAssignee(issue: number, name: string): Promise<void> {
const [owner, repo] = await git.getGitHubOwnerAndRepository(this.cwd, this.hostname);
const [owner, repo] = await git.getGitHubOwnerAndRepository(this.cwd);
await this.github.removeAssignees(owner, repo, issue, {assignees: [name]});
}

public async requestReview(issue: number, name: string): Promise<void> {
const [owner, repo] = await git.getGitHubOwnerAndRepository(this.cwd, this.hostname);
const [owner, repo] = await git.getGitHubOwnerAndRepository(this.cwd);
await this.github.requestReview(owner, repo, issue, {reviewers: [name]});
}

public async deleteReviewRequest(issue: number, name: string): Promise<void> {
const [owner, repo] = await git.getGitHubOwnerAndRepository(this.cwd, this.hostname);
const [owner, repo] = await git.getGitHubOwnerAndRepository(this.cwd);
await this.github.deleteReviewRequest(owner, repo, issue, {reviewers: [name]});
}

Expand Down

0 comments on commit 94d1cf9

Please sign in to comment.