Skip to content

Commit ac571dc

Browse files
authored
feat: allow update function to trigger file deletion (#130)
BREAKING CHANGE: The meaning of `null` has changed! Previously the following snippet was causing a file deletion. ```js files: { "file/to/delete.txt": null } ``` If you want to retain this behavior you must use the DELETE_FILE Symbol import { createPullRequest, DELETE_FILE } from 'octokit-plugin-create-pull-request'; ```js files: { "file/to/delete.txt": DELETE_FILE } ``` If you want to trigger a file deletion from an update function, you can now do so by returning the deleteFile Symbol. ```js import { createPullRequest, DELETE_FILE } from 'octokit-plugin-create-pull-request'; files: { "file/to/delete.txt": ({ exists, encoding, content }) => { const fileContent = Buffer.from(content, encoding).toString("utf-8") if (fileContent.includes('abc')) { // trigger file deletion return DELETE_FILE } // do not alter file content return null; } }
1 parent f961a6d commit ac571dc

File tree

8 files changed

+1769
-9
lines changed

8 files changed

+1769
-9
lines changed

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ Install with `npm install @octokit/core octokit-plugin-create-pull-request`. Opt
3838

3939
```js
4040
const { Octokit } = require("@octokit/core");
41-
const { createPullRequest } = require("octokit-plugin-create-pull-request");
41+
const {
42+
createPullRequest,
43+
DELETE_FILE,
44+
} = require("octokit-plugin-create-pull-request");
4245
```
4346

4447
</td></tr>
@@ -75,7 +78,7 @@ octokit
7578
encoding: "base64",
7679
},
7780
// deletes file if it exists,
78-
"path/to/file3.txt": null,
81+
"path/to/file3.txt": DELETE_FILE,
7982
// updates file based on current content
8083
"path/to/file4.txt": ({ exists, encoding, content }) => {
8184
// do not create the file if it does not exist
@@ -92,6 +95,22 @@ octokit
9295
// https://developer.github.com/v3/git/trees/#tree-object
9396
mode: "100755",
9497
},
98+
"path/to/file6.txt": ({ exists, encoding, content }) => {
99+
// do nothing if it does not exist
100+
if (!exists) return null;
101+
102+
const content = Buffer.from(content, encoding)
103+
.toString("utf-8")
104+
.toUpperCase();
105+
106+
if (content.includes("octomania")) {
107+
// delete file
108+
return DELETE_FILE;
109+
}
110+
111+
// keep file
112+
return null;
113+
},
95114
},
96115
commit:
97116
"creating file1.txt, file2.png, deleting file3.txt, updating file4.txt (if it exists), file5.sh",

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const DELETE_FILE: unique symbol = Symbol("DELETE_FILE");

src/create-tree.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Changes, State, TreeParameter, UpdateFunctionFile } from "./types";
22

33
import { valueToTreeObject } from "./value-to-tree-object";
4+
import { DELETE_FILE } from "./constants";
45

56
export async function createTree(
67
state: Required<State>,
@@ -20,7 +21,7 @@ export async function createTree(
2021
for (const path of Object.keys(changes.files)) {
2122
const value = changes.files[path];
2223

23-
if (value === null) {
24+
if (value === DELETE_FILE) {
2425
// Deleting a non-existent file from a tree leads to an "GitRPC::BadObjectState" error,
2526
// so we only attempt to delete the file if it exists.
2627
try {
@@ -62,6 +63,28 @@ export async function createTree(
6263
result = await value(
6364
Object.assign(file, { exists: true }) as UpdateFunctionFile
6465
);
66+
67+
if (result === DELETE_FILE) {
68+
try {
69+
// https://developer.github.com/v3/repos/contents/#get-contents
70+
await octokit.request("HEAD /repos/{owner}/{repo}/contents/:path", {
71+
owner: ownerOrFork,
72+
repo,
73+
ref: latestCommitSha,
74+
path,
75+
});
76+
77+
tree.push({
78+
path,
79+
mode: "100644",
80+
sha: null,
81+
});
82+
continue;
83+
} catch (error) {
84+
// istanbul ignore next
85+
continue;
86+
}
87+
}
6588
} catch (error) {
6689
// @ts-ignore
6790
// istanbul ignore if
@@ -71,13 +94,24 @@ export async function createTree(
7194
result = await value({ exists: false });
7295
}
7396

74-
if (result === null || typeof result === "undefined") continue;
97+
if (
98+
result === null ||
99+
typeof result === "undefined" ||
100+
typeof result === "symbol"
101+
) {
102+
continue;
103+
}
104+
75105
tree.push(
106+
// @ts-expect-error - Argument result can never be of type Symbol at this branch
107+
// because the above condition will catch it and move on to the next iteration cycle
76108
await valueToTreeObject(octokit, ownerOrFork, repo, path, result)
77109
);
78110
continue;
79111
}
80112

113+
// @ts-expect-error - Argument value can never be of type Symbol at this branch
114+
// because the above condition will catch it and initiate a file deletion operation
81115
tree.push(await valueToTreeObject(octokit, ownerOrFork, repo, path, value));
82116
continue;
83117
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Octokit } from "@octokit/core";
22

33
import { composeCreatePullRequest } from "./compose-create-pull-request";
44
import { VERSION } from "./version";
5+
export { DELETE_FILE } from "./constants";
56
import type * as Types from "./types";
67

78
/**

src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export type Options = {
2020

2121
export type Changes = {
2222
files?: {
23-
[path: string]: string | File | UpdateFunction | null;
23+
[path: string]: string | File | UpdateFunction | null | Symbol;
2424
};
2525
emptyCommit?: boolean | string;
2626
commit: string;
@@ -54,7 +54,7 @@ export type UpdateFunctionFile =
5454

5555
export type UpdateFunction = (
5656
file: UpdateFunctionFile
57-
) => string | File | null | Promise<string | File | null>;
57+
) => string | File | null | Symbol | Promise<string | File | null | Symbol>;
5858

5959
export type State = {
6060
octokit: Octokit;

test/delete-files-function.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Octokit as Core } from "@octokit/core";
2+
import { RequestError } from "@octokit/request-error";
3+
4+
import { createPullRequest, DELETE_FILE } from "../src";
5+
const Octokit = Core.plugin(createPullRequest);
6+
7+
test("delete files function", async () => {
8+
const fixtures = require("./fixtures/delete-files-function");
9+
const fixturePr = fixtures[fixtures.length - 1].response;
10+
const octokit = new Octokit();
11+
12+
octokit.hook.wrap("request", (_, options) => {
13+
const currentFixtures = fixtures.shift();
14+
const {
15+
baseUrl,
16+
method,
17+
url,
18+
request,
19+
headers,
20+
mediaType,
21+
draft,
22+
...params
23+
} = options;
24+
25+
expect(
26+
`${currentFixtures.request.method} ${currentFixtures.request.url}`
27+
).toEqual(`${options.method} ${options.url}`);
28+
29+
Object.keys(params).forEach((paramName) => {
30+
expect(currentFixtures.request[paramName]).toStrictEqual(
31+
params[paramName]
32+
);
33+
});
34+
35+
if (currentFixtures.response.status >= 400) {
36+
throw new RequestError("Error", currentFixtures.response.status, {
37+
request: currentFixtures.request,
38+
headers: currentFixtures.response.headers,
39+
});
40+
}
41+
return currentFixtures.response;
42+
});
43+
44+
const pr = await octokit.createPullRequest({
45+
owner: "gr2m",
46+
repo: "pull-request-test",
47+
title: "One comes, one goes",
48+
body: "because",
49+
head: "patch",
50+
changes: {
51+
files: {
52+
"path/to/file1.txt": "Content for file1",
53+
"path/to/file2.txt": () => DELETE_FILE,
54+
"path/to/file-does-not-exist.txt": () => DELETE_FILE,
55+
},
56+
commit: "why",
57+
},
58+
});
59+
60+
expect(pr).toStrictEqual(fixturePr);
61+
expect(fixtures.length).toEqual(0);
62+
});

test/delete-files.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Octokit as Core } from "@octokit/core";
22
import { RequestError } from "@octokit/request-error";
33

4-
import { createPullRequest } from "../src";
4+
import { createPullRequest, DELETE_FILE } from "../src";
55
const Octokit = Core.plugin(createPullRequest);
66

77
test("delete files", async () => {
@@ -50,8 +50,8 @@ test("delete files", async () => {
5050
changes: {
5151
files: {
5252
"path/to/file1.txt": "Content for file1",
53-
"path/to/file2.txt": null,
54-
"path/to/file-does-not-exist.txt": null,
53+
"path/to/file2.txt": DELETE_FILE,
54+
"path/to/file-does-not-exist.txt": DELETE_FILE,
5555
},
5656
commit: "why",
5757
},

0 commit comments

Comments
 (0)