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

Commit 12ead05

Browse files
committed
feat: add merge button (currently no squash and rebase)
1 parent fc8fed4 commit 12ead05

File tree

5 files changed

+188
-62
lines changed

5 files changed

+188
-62
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
{
3737
"command": "extension.browserPullRequest",
3838
"title": "GitHub: Browse open pull request..."
39+
},
40+
{
41+
"command": "extension.mergePullRequest",
42+
"title": "GitHub: Merge open pull request..."
3943
}
4044
]
4145
},

src/extension.ts

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {join} from 'path';
22
import * as sander from 'sander';
33
import * as vscode from 'vscode';
44
import * as git from './git';
5-
import {GitHubError, PullRequest} from './github';
5+
import {GitHubError, PullRequest, MergeMethod} from './github';
66
import {StatusBarManager} from './status-bar-manager';
77
import {GitHubManager} from './github-manager';
88

@@ -31,22 +31,20 @@ export function activate(context: vscode.ExtensionContext): void {
3131
vscode.commands.registerCommand('extension.setGitHubToken', createGithubTokenCommand(context)),
3232
vscode.commands.registerCommand('extension.createPullRequest', wrapCommand(createPullRequest)),
3333
vscode.commands.registerCommand('extension.checkoutPullRequests', wrapCommand(checkoutPullRequests)),
34-
vscode.commands.registerCommand('extension.browserPullRequest', wrapCommand(browserPullRequest))
34+
vscode.commands.registerCommand('extension.browserPullRequest', wrapCommand(browserPullRequest)),
35+
vscode.commands.registerCommand('extension.mergePullRequest', wrapCommand(mergePullRequest))
3536
);
3637
}
3738

38-
function checkVersionAndToken(context: vscode.ExtensionContext, token: string|undefined): void {
39-
sander.readFile(join(context.extensionPath, 'package.json'))
40-
.then(content => JSON.parse(content))
41-
.then(json => json.version as string)
42-
.then((version) => {
43-
const storedVersion = context.globalState.get('version-test');
44-
if (version !== storedVersion && !Boolean(token)) {
45-
context.globalState.update('version-test', version);
46-
vscode.window.showInformationMessage(
47-
'To enable the Visual Studio Code GitHub Support, please set a Personal Access Token');
48-
}
49-
});
39+
async function checkVersionAndToken(context: vscode.ExtensionContext, token: string|undefined): Promise<void> {
40+
const content = await sander.readFile(join(context.extensionPath, 'package.json'));
41+
const version = JSON.parse(content).version as string;
42+
const storedVersion = context.globalState.get<string|undefined>('version-test');
43+
if (version !== storedVersion && !Boolean(token)) {
44+
context.globalState.update('version-test', version);
45+
vscode.window.showInformationMessage(
46+
'To enable the Visual Studio Code GitHub Support, please set a Personal Access Token');
47+
}
5048
}
5149

5250
function wrapCommand<T>(command: T): T {
@@ -75,40 +73,38 @@ function logAndShowError(e: Error): void {
7573
}
7674
}
7775

78-
function createGithubTokenCommand(context: vscode.ExtensionContext): () => PromiseLike<void> {
79-
return () => {
76+
function createGithubTokenCommand(context: vscode.ExtensionContext): () => void {
77+
return async () => {
8078
const options = {
8179
ignoreFocusOut: true,
8280
password: true,
8381
placeHolder: 'GitHub Personal Access Token'
8482
};
85-
return vscode.window.showInputBox(options)
86-
.then(input => {
87-
context.globalState.update('token', input);
88-
githubManager.connect(input);
89-
});
83+
const input = await vscode.window.showInputBox(options);
84+
context.globalState.update('token', input);
85+
githubManager.connect(input);
9086
};
9187
}
9288

9389
async function createPullRequest(): Promise<void> {
9490
const pullRequest = await githubManager.createPullRequest();
9591
if (pullRequest) {
96-
statusBarManager.updatePullRequestStatus(true);
92+
statusBarManager.updatePullRequestStatus();
9793
vscode.window.showInformationMessage(`Successfully created #${pullRequest.number}`);
9894
}
9995
}
10096

10197
async function selectPullRequest(doSomething: (pullRequest: PullRequest) => void): Promise<void> {
10298
const pullRequests = await githubManager.listPullRequests();
103-
vscode.window.showQuickPick(pullRequests.map(pullRequest => ({
99+
const items = pullRequests.map(pullRequest => ({
104100
label: pullRequest.title,
105101
description: `#${pullRequest.number}`,
106102
pullRequest
107-
}))).then(selected => {
108-
if (selected) {
109-
doSomething(selected.pullRequest);
110-
}
111-
});
103+
}));
104+
const selected = await vscode.window.showQuickPick(items);
105+
if (selected) {
106+
doSomething(selected.pullRequest);
107+
}
112108
}
113109

114110
async function checkoutPullRequests(): Promise<void> {
@@ -123,3 +119,33 @@ async function browserPullRequest(): Promise<void> {
123119
vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(pullRequest.html_url));
124120
});
125121
}
122+
123+
type MergeOptionItems = { label: string; description: string; method: MergeMethod; };
124+
async function mergePullRequest(): Promise<void> {
125+
const items: MergeOptionItems[] = [
126+
{
127+
label: 'Create merge commit',
128+
description: '',
129+
method: 'merge'
130+
},
131+
{
132+
label: 'Squash and merge',
133+
description: '',
134+
method: 'squash'
135+
},
136+
{
137+
label: 'Rebase and merge',
138+
description: '',
139+
method: 'rebase'
140+
}
141+
];
142+
const selected = await vscode.window.showQuickPick(items);
143+
if (selected) {
144+
if (await githubManager.mergePullRequest(selected.method)) {
145+
statusBarManager.updatePullRequestStatus();
146+
vscode.window.showInformationMessage(`Successfully merged`);
147+
} else {
148+
vscode.window.showInformationMessage(`Merge failed for unknown reason`);
149+
}
150+
}
151+
}

src/github-manager.ts

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as vscode from 'vscode';
22
import * as git from './git';
3-
import {getClient, GitHub, PullRequest, ListPullRequestsParameters, CreatePullRequestBody} from './github';
3+
import {getClient, GitHub, GitHubError, PullRequest, ListPullRequestsParameters, CreatePullRequestBody,
4+
PullRequestStatus, Merge, MergeMethod} from './github';
45

56
export class GitHubManager {
67

@@ -23,25 +24,32 @@ export class GitHubManager {
2324
this.github = getClient(token);
2425
}
2526

26-
public async hasPullRequestForCurrentBranch(): Promise<boolean> {
27+
public async getPullRequestForCurrentBranch(): Promise<PullRequest|undefined> {
2728
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd);
2829
const branch = await git.getCurrentBranch(this.cwd);
2930
const parameters: ListPullRequestsParameters = {
3031
state: 'open',
3132
head: `${owner}:${branch}`
3233
};
33-
const response = await this.github.listPullRequests(owner, repository, parameters);
34-
return response.length > 0;
34+
const list = await this.github.listPullRequests(owner, repository, parameters);
35+
if (list.body.length === 0) {
36+
return undefined;
37+
}
38+
return (await this.github.getPullRequest(owner, repository, list.body[0].number)).body;
39+
}
40+
41+
public async hasPullRequestForCurrentBranch(): Promise<boolean> {
42+
return Boolean(await this.getPullRequestForCurrentBranch());
3543
}
3644

37-
public async getCombinedStatusForPullRequest(): Promise<'failure' | 'pending' | 'success' |undefined> {
45+
public async getCombinedStatusForPullRequest(): Promise<PullRequestStatus |undefined> {
3846
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd);
3947
const branch = await git.getCurrentBranch(this.cwd);
4048
if (!branch) {
4149
return undefined;
4250
}
4351
const response = await this.github.getStatusForRef(owner, repository, branch);
44-
return response.state;
52+
return response.body.total_count > 0 ? response.body.state : undefined;
4553
}
4654

4755
public async createPullRequest(): Promise<PullRequest|undefined> {
@@ -57,15 +65,43 @@ export class GitHubManager {
5765
};
5866
this.channel.appendLine('Create pull request:');
5967
this.channel.appendLine(JSON.stringify(body, undefined, ' '));
60-
return this.github.createPullRequest(owner, repository, body);
68+
69+
const result = await this.github.createPullRequest(owner, repository, body);
70+
// TODO: Pretend should optionally redirect
71+
const number = result.headers['location'][0]
72+
.match(/https:\/\/api.github.com\/repos\/[^\/]+\/[^\/]+\/pulls\/([0-9]+)/) as RegExpMatchArray;
73+
return (await this.github.getPullRequest(owner, repository, parseInt(number[1] as string, 10))).body;
6174
}
6275

6376
public async listPullRequests(): Promise<PullRequest[]> {
6477
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd);
6578
const parameters: ListPullRequestsParameters = {
6679
state: 'open'
6780
};
68-
return await this.github.listPullRequests(owner, repository, parameters);
81+
return (await this.github.listPullRequests(owner, repository, parameters)).body;
82+
}
83+
84+
public async mergePullRequest(method: MergeMethod): Promise<boolean|undefined> {
85+
try {
86+
if (await this.hasPullRequestForCurrentBranch()) {
87+
const [owner, repository] = await git.getGitHubOwnerAndRepository(this.cwd);
88+
const pullRequest = await this.getPullRequestForCurrentBranch();
89+
if (pullRequest) {
90+
const body: Merge = {
91+
merge_method: method
92+
};
93+
const result = await this.github.mergePullRequest(owner, repository, pullRequest.number, body);
94+
return result.body.merged;
95+
}
96+
}
97+
return undefined;
98+
} catch (e) {
99+
if (!(e instanceof GitHubError)) {
100+
throw e;
101+
}
102+
// TODO...
103+
return false;
104+
}
69105
}
70106

71107
}

src/github.ts

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,48 @@
1-
import {Pretend, Get, Post, Interceptor, IPretendRequestInterceptor,
1+
import {Pretend, Get, Post, Put, Interceptor, IPretendRequestInterceptor,
22
IPretendDecoder} from 'pretend';
33
import * as LRUCache from 'lru-cache';
44

55
export interface GitHub {
6+
7+
getPullRequest(owner: string, repo: string, number: number): Promise<GitHubResponse<PullRequest>>;
8+
69
listPullRequests(owner: string, repo: string, parameters?: ListPullRequestsParameters):
7-
Promise<PullRequest[]>;
8-
createPullRequest(owner: string, repo: string, body: any): Promise<PullRequest>;
9-
getStatusForRef(owner: string, repo: string, ref: string): Promise<CombinedStatus>;
10+
Promise<GitHubResponse<PullRequest[]>>;
11+
12+
createPullRequest(owner: string, repo: string, body: CreatePullRequestBody): Promise<GitHubResponse<PullRequest>>;
13+
14+
getStatusForRef(owner: string, repo: string, ref: string): Promise<GitHubResponse<CombinedStatus>>;
15+
16+
mergePullRequest(owner: string, repo: string, number: number, body: Merge): Promise<GitHubResponse<MergeResult>>;
17+
18+
}
19+
20+
export interface GitHubResponse<T> {
21+
status: number;
22+
headers: {[name: string]: string[]};
23+
body: T;
24+
}
25+
26+
export type MergeMethod = 'merge' | 'squash' | 'rebase';
27+
28+
export interface Merge {
29+
commit_title?: string;
30+
commit_message?: string;
31+
sha?: string;
32+
merge_method?: MergeMethod;
1033
}
1134

35+
export interface MergeResult {
36+
sha?: string;
37+
merged?: boolean;
38+
message: string;
39+
documentation_url?: string;
40+
}
41+
42+
export type PullRequestStatus = 'failure' | 'pending' | 'success';
43+
1244
export interface CombinedStatus {
13-
state: 'failure' | 'pending' | 'success';
45+
state: PullRequestStatus;
1446
total_count: number;
1547
statuses: any[];
1648
}
@@ -45,13 +77,15 @@ export interface PullRequest {
4577
label: string;
4678
ref: string;
4779
};
80+
mergeable?: boolean|null;
4881
}
4982

5083
export function getClient(token: string): GitHub {
5184
return Pretend
5285
.builder()
5386
.interceptor(impl.githubCache())
5487
.requestInterceptor(impl.githubTokenAuthenticator(token))
88+
.interceptor(impl.logger())
5589
.decode(impl.githubDecoder())
5690
.target(impl.GitHubBlueprint, 'https://api.github.com');
5791
}
@@ -68,6 +102,15 @@ export class GitHubError extends Error {
68102

69103
namespace impl {
70104

105+
export function logger(): Interceptor {
106+
return async (chain, request) => {
107+
// console.log('github-request: ', request);
108+
const response = await chain(request);
109+
// console.log('response', response);
110+
return response;
111+
};
112+
}
113+
71114
export function githubCache(): Interceptor {
72115
// Cache at most 100 requests
73116
const cache = LRUCache<{etag: string, response: any}>(100);
@@ -87,10 +130,10 @@ namespace impl {
87130
etag: response.headers.etag,
88131
response
89132
});
90-
return response.body;
133+
return response;
91134
}
92135
// Respond from cache
93-
return entry.response.body;
136+
return entry.response;
94137
};
95138
}
96139

@@ -111,7 +154,7 @@ namespace impl {
111154
}
112155
let headers = {};
113156
response.headers.forEach((value, index) => {
114-
headers[index] = value;
157+
headers[index] = [...(headers[index] || []), value];
115158
});
116159
return {
117160
status: response.status,
@@ -123,20 +166,20 @@ namespace impl {
123166

124167
export class GitHubBlueprint implements GitHub {
125168

169+
@Get('/repos/:owner/:repo/pulls/:number')
170+
public getPullRequest(): any {/* */}
171+
126172
@Get('/repos/:owner/:repo/pulls', true)
127-
public listPullRequests(_owner: string, _repo: string, _parameters?: ListPullRequestsParameters): any {
128-
//
129-
}
173+
public listPullRequests(): any {/* */}
130174

131175
@Post('/repos/:owner/:repo/pulls')
132-
public createPullRequest(_owner: string, _repo: string, _body: any): any {
133-
//
134-
}
176+
public createPullRequest(): any {/* */}
135177

136178
@Get('/repos/:owner/:repo/commits/:ref/status')
137-
public getStatusForRef(_owner: string, _repo: string, _ref: string): any {
138-
//
139-
}
179+
public getStatusForRef(): any {/* */}
180+
181+
@Put('/repos/:owner/:repo/pulls/:number/merge')
182+
public mergePullRequest(): any {/* */}
140183

141184
}
142185
}

0 commit comments

Comments
 (0)