Skip to content

Commit aae09d8

Browse files
authored
TypeScript (gr2m#35)
BREAKING CHANGE: The default export is no longer supported. Use `import { createPullRequest } from "octokit-plugin-create-pull-request";` instead.
1 parent e44dc66 commit aae09d8

16 files changed

+14610
-7214
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.nyc_output
22
coverage
33
node_modules
4+
pkg

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,40 @@ Features
1515

1616
## Usage
1717

18+
## Usage
19+
20+
<table>
21+
<tbody valign=top align=left>
22+
<tr><th>
23+
Browsers
24+
</th><td width=100%>
25+
26+
Load `octokit-plugin-create-pull-request` and [`@octokit/core`](https://github.com/octokit/core.js) (or core-compatible module) directly from [cdn.pika.dev](https://cdn.pika.dev)
27+
28+
```html
29+
<script type="module">
30+
import { Octokit } from "https://cdn.pika.dev/@octokit/core";
31+
import { createPullRequest } from "https://cdn.pika.dev/octokit-plugin-create-pull-request";
32+
</script>
33+
```
34+
35+
</td></tr>
36+
<tr><th>
37+
Node
38+
</th><td>
39+
40+
Install with `npm install @octokit/core octokit-plugin-create-pull-request`. Optionally replace `@octokit/core` with a core-compatible module
41+
1842
```js
1943
const { Octokit } = require("@octokit/core");
2044
const { createPullRequest } = require("octokit-plugin-create-pull-request");
45+
```
46+
47+
</td></tr>
48+
</tbody>
49+
</table>
50+
51+
```js
2152
const MyOctokit = Octokit.plugin(createPullRequest);
2253

2354
const TOKEN = "secret123"; // token needs "repo" scope

package-lock.json

Lines changed: 14208 additions & 7133 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22
"name": "octokit-plugin-create-pull-request",
33
"version": "0.0.0-development",
44
"description": "Octokit plugin to create a pull request with multiple file changes",
5-
"main": "index.js",
65
"scripts": {
7-
"coverage": "nyc report --reporter=html && open coverage/index.html",
8-
"lint": "prettier --check '{lib,test}/**/*.{js,json}' README.md package.json",
9-
"lint:fix": "prettier --write '{lib,test}/**/*.{js,json}' README.md package.json",
6+
"build": "pika build",
7+
"lint": "prettier --check '{src,test}/**/*.{ts,md}' *.md package.json tsconfig.json",
8+
"lint:fix": "prettier --write '{src,test}/**/*.{ts,md}' *.md package.json tsconfig.json",
109
"pretest": "npm run -s lint",
11-
"test": "tap --100 --coverage test/*-test.js"
10+
"test": "jest --coverage"
1211
},
1312
"keywords": [
1413
"github",
@@ -19,17 +18,67 @@
1918
"author": "Gregor Martynus (https://github.com/gr2m)",
2019
"license": "MIT",
2120
"devDependencies": {
22-
"@octokit/core": "^2.4.1",
23-
"prettier": "^2.0.1",
21+
"@octokit/core": "^3.0.0",
22+
"@octokit/plugin-paginate-rest": "^2.0.1",
23+
"@pika/pack": "^0.5.0",
24+
"@pika/plugin-build-node": "^0.9.2",
25+
"@pika/plugin-build-web": "^0.9.2",
26+
"@pika/plugin-ts-standard-pkg": "^0.9.2",
27+
"@types/jest": "^26.0.0",
28+
"@types/node": "^14.0.12",
29+
"jest": "^26.0.1",
30+
"prettier": "^2.0.5",
2431
"semantic-release": "^17.0.0",
25-
"tap": "^14.0.0"
32+
"semantic-release-plugin-update-version-in-files": "^1.0.0",
33+
"ts-jest": "^26.1.0",
34+
"typescript": "^3.9.5"
2635
},
27-
"repository": {
28-
"type": "git",
29-
"url": "https://github.com/gr2m/octokit-plugin-create-pull-request.git"
36+
"repository": "https://github.com/gr2m/octokit-plugin-create-pull-request",
37+
"jest": {
38+
"preset": "ts-jest",
39+
"coverageThreshold": {
40+
"global": {
41+
"statements": 100,
42+
"branches": 100,
43+
"functions": 100,
44+
"lines": 100
45+
}
46+
}
3047
},
31-
"bugs": {
32-
"url": "https://github.com/gr2m/octokit-plugin-create-pull-request/issues"
48+
"@pika/pack": {
49+
"pipeline": [
50+
[
51+
"@pika/plugin-ts-standard-pkg"
52+
],
53+
[
54+
"@pika/plugin-build-node"
55+
],
56+
[
57+
"@pika/plugin-build-web"
58+
]
59+
]
3360
},
34-
"homepage": "https://github.com/gr2m/octokit-plugin-create-pull-request#readme"
61+
"release": {
62+
"plugins": [
63+
"@semantic-release/commit-analyzer",
64+
"@semantic-release/release-notes-generator",
65+
"@semantic-release/github",
66+
[
67+
"@semantic-release/npm",
68+
{
69+
"pkgRoot": "./pkg"
70+
}
71+
],
72+
[
73+
"semantic-release-plugin-update-version-in-files",
74+
{
75+
"files": [
76+
"pkg/dist-web/*",
77+
"pkg/dist-node/*",
78+
"pkg/*/version.*"
79+
]
80+
}
81+
]
82+
]
83+
}
3584
}

src/create-pull-request.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import type { Octokit } from "@octokit/core";
2+
import type { Endpoints } from "@octokit/types";
3+
4+
type TreeParameter = Endpoints["POST /repos/:owner/:repo/git/trees"]["parameters"]["tree"];
5+
6+
type Options = {
7+
owner: string;
8+
repo: string;
9+
title: string;
10+
body: string;
11+
head: string;
12+
base?: string;
13+
changes: Changes;
14+
};
15+
16+
type Changes = {
17+
files: {
18+
[path: string]: string | File;
19+
};
20+
commit: string;
21+
};
22+
23+
// https://developer.github.com/v3/git/blobs/#parameters
24+
type File = {
25+
content: string;
26+
encoding: "utf-8" | "base64";
27+
};
28+
29+
export async function octokitCreatePullRequest(
30+
octokit: Octokit,
31+
{ owner, repo, title, body, base, head, changes }: Options
32+
) {
33+
// https://developer.github.com/v3/repos/#get-a-repository
34+
const { data: repository } = await octokit.request(
35+
"GET /repos/:owner/:repo",
36+
{
37+
owner,
38+
repo,
39+
}
40+
);
41+
42+
if (!repository.permissions) {
43+
throw new Error(
44+
"[octokit-plugin-create-pull-request] Missing authentication"
45+
);
46+
}
47+
48+
if (!base) {
49+
base = repository.default_branch;
50+
}
51+
52+
let fork = owner;
53+
54+
if (!repository.permissions.push) {
55+
// https://developer.github.com/v3/users/#get-the-authenticated-user
56+
const user = await octokit.request("GET /user");
57+
58+
// https://developer.github.com/v3/repos/forks/#list-forks
59+
const forks = await octokit.request("GET /repos/:owner/:repo/forks", {
60+
owner,
61+
repo,
62+
});
63+
const hasFork = forks.data.find(
64+
(fork) => fork.owner.login === user.data.login
65+
);
66+
67+
if (!hasFork) {
68+
// https://developer.github.com/v3/repos/forks/#create-a-fork
69+
await octokit.request("POST /repos/:owner/:repo/forks", {
70+
owner,
71+
repo,
72+
});
73+
}
74+
75+
fork = user.data.login;
76+
}
77+
78+
// https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository
79+
const {
80+
data: [firstCommit],
81+
} = await octokit.request("GET /repos/:owner/:repo/commits", {
82+
owner,
83+
repo,
84+
sha: base,
85+
per_page: 1,
86+
});
87+
const treeSha = firstCommit.commit.tree.sha;
88+
89+
const tree = (
90+
await Promise.all(
91+
Object.keys(changes.files).map(async (path) => {
92+
const value = changes.files[path];
93+
94+
if (value === null) {
95+
// Deleting a non-existent file from a tree leads to an "GitRPC::BadObjectState" error,
96+
// so we only attempt to delete the file if it exists.
97+
try {
98+
// https://developer.github.com/v3/repos/contents/#get-contents
99+
await octokit.request("HEAD /repos/:owner/:repo/contents/:path", {
100+
owner: fork,
101+
repo,
102+
ref: firstCommit.sha,
103+
path,
104+
});
105+
106+
return {
107+
path,
108+
mode: "100644",
109+
sha: null,
110+
};
111+
} catch (error) {
112+
return;
113+
}
114+
}
115+
116+
// Text files can be changed through the .content key
117+
if (typeof value === "string") {
118+
return {
119+
path,
120+
mode: "100644",
121+
content: value,
122+
};
123+
}
124+
125+
// Binary files need to be created first using the git blob API,
126+
// then changed by referencing in the .sha key
127+
const { data } = await octokit.request(
128+
"POST /repos/:owner/:repo/git/blobs",
129+
{
130+
owner,
131+
repo,
132+
...value,
133+
}
134+
);
135+
const blobSha = data.sha;
136+
return {
137+
path,
138+
mode: "100644",
139+
sha: blobSha,
140+
};
141+
})
142+
)
143+
).filter(Boolean) as TreeParameter;
144+
145+
// https://developer.github.com/v3/git/trees/#create-a-tree
146+
const {
147+
data: { sha: newTreeSha },
148+
} = await octokit.request("POST /repos/:owner/:repo/git/trees", {
149+
owner: fork,
150+
repo,
151+
base_tree: treeSha,
152+
tree,
153+
});
154+
155+
// https://developer.github.com/v3/git/commits/#create-a-commit
156+
const {
157+
data: { sha: latestCommitSha },
158+
} = await octokit.request("POST /repos/:owner/:repo/git/commits", {
159+
owner: fork,
160+
repo,
161+
message: changes.commit,
162+
tree: newTreeSha,
163+
parents: [firstCommit.sha],
164+
});
165+
166+
// https://developer.github.com/v3/git/refs/#create-a-reference
167+
await octokit.request("POST /repos/:owner/:repo/git/refs", {
168+
owner: fork,
169+
repo,
170+
sha: latestCommitSha,
171+
ref: `refs/heads/${head}`,
172+
});
173+
174+
// https://developer.github.com/v3/pulls/#create-a-pull-request
175+
return await octokit.request("POST /repos/:owner/:repo/pulls", {
176+
owner,
177+
repo,
178+
head: `${fork}:${head}`,
179+
base,
180+
title,
181+
body,
182+
});
183+
}

src/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Octokit } from "@octokit/core";
2+
3+
import { octokitCreatePullRequest } from "./create-pull-request";
4+
import { VERSION } from "./version";
5+
6+
/**
7+
* @param octokit Octokit instance
8+
*/
9+
export function createPullRequest(octokit: Octokit) {
10+
return {
11+
createPullRequest: octokitCreatePullRequest.bind(null, octokit),
12+
};
13+
}
14+
15+
createPullRequest.VERSION = VERSION;

src/version.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const VERSION = "0.0.0-development";

test/create-binary-file-test.js renamed to test/create-binary-file.test.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
const { test } = require("tap");
2-
const { RequestError } = require("@octokit/request-error");
1+
import { Octokit as Core } from "@octokit/core";
2+
import { RequestError } from "@octokit/request-error";
33

4-
const { Octokit: Core } = require("@octokit/core");
5-
const { createPullRequest } = require("..");
4+
import { createPullRequest } from "../src";
65
const Octokit = Core.plugin(createPullRequest);
76

8-
test("happy path", async (t) => {
7+
test("happy path", async () => {
98
const fixtures = require("./fixtures/create-binary-file");
109
const fixturePr = fixtures[fixtures.length - 1].response;
1110
const octokit = new Octokit();
@@ -22,15 +21,20 @@ test("happy path", async (t) => {
2221
...params
2322
} = options;
2423

25-
t.equal(currentFixtures.request.method, options.method);
26-
t.equal(currentFixtures.request.url, options.url);
24+
expect(currentFixtures.request.method).toEqual(options.method);
25+
expect(currentFixtures.request.url).toEqual(options.url);
2726

2827
Object.keys(params).forEach((paramName) => {
29-
t.deepEqual(currentFixtures.request[paramName], params[paramName]);
28+
expect(currentFixtures.request[paramName]).toStrictEqual(
29+
params[paramName]
30+
);
3031
});
3132

3233
if (currentFixtures.response.status >= 400) {
33-
throw new RequestError("Error", currentFixtures.response.status);
34+
throw new RequestError("Error", currentFixtures.response.status, {
35+
request: currentFixtures.request,
36+
headers: currentFixtures.response.headers,
37+
});
3438
}
3539
return currentFixtures.response;
3640
});
@@ -53,6 +57,6 @@ test("happy path", async (t) => {
5357
},
5458
});
5559

56-
t.deepEqual(pr, fixturePr);
57-
t.equal(fixtures.length, 0);
60+
expect(pr).toStrictEqual(fixturePr);
61+
expect(fixtures.length).toEqual(0);
5862
});

0 commit comments

Comments
 (0)