Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow update function to trigger file deletion #130

Merged
merged 1 commit into from Jun 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 21 additions & 2 deletions README.md
Expand Up @@ -38,7 +38,10 @@ Install with `npm install @octokit/core octokit-plugin-create-pull-request`. Opt

```js
const { Octokit } = require("@octokit/core");
const { createPullRequest } = require("octokit-plugin-create-pull-request");
const {
createPullRequest,
DELETE_FILE,
} = require("octokit-plugin-create-pull-request");
```

</td></tr>
Expand Down Expand Up @@ -75,7 +78,7 @@ octokit
encoding: "base64",
},
// deletes file if it exists,
"path/to/file3.txt": null,
"path/to/file3.txt": DELETE_FILE,
// updates file based on current content
"path/to/file4.txt": ({ exists, encoding, content }) => {
// do not create the file if it does not exist
Expand All @@ -92,6 +95,22 @@ octokit
// https://developer.github.com/v3/git/trees/#tree-object
mode: "100755",
},
"path/to/file6.txt": ({ exists, encoding, content }) => {
// do nothing if it does not exist
if (!exists) return null;

const content = Buffer.from(content, encoding)
.toString("utf-8")
.toUpperCase();

if (content.includes("octomania")) {
// delete file
return DELETE_FILE;
}

// keep file
return null;
},
},
commit:
"creating file1.txt, file2.png, deleting file3.txt, updating file4.txt (if it exists), file5.sh",
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
@@ -0,0 +1 @@
export const DELETE_FILE: unique symbol = Symbol("DELETE_FILE");
38 changes: 36 additions & 2 deletions src/create-tree.ts
@@ -1,6 +1,7 @@
import { Changes, State, TreeParameter, UpdateFunctionFile } from "./types";

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

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

if (value === null) {
if (value === DELETE_FILE) {
// Deleting a non-existent file from a tree leads to an "GitRPC::BadObjectState" error,
// so we only attempt to delete the file if it exists.
try {
Expand Down Expand Up @@ -62,6 +63,28 @@ export async function createTree(
result = await value(
Object.assign(file, { exists: true }) as UpdateFunctionFile
);

if (result === DELETE_FILE) {
try {
// https://developer.github.com/v3/repos/contents/#get-contents
await octokit.request("HEAD /repos/{owner}/{repo}/contents/:path", {
owner: ownerOrFork,
repo,
ref: latestCommitSha,
path,
});

tree.push({
path,
mode: "100644",
sha: null,
});
continue;
} catch (error) {
// istanbul ignore next
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this is fine since I saw basically the same for other catch blocks, but please let me know if you would prefer a test case for this.

continue;
}
}
} catch (error) {
// @ts-ignore
// istanbul ignore if
Expand All @@ -71,13 +94,24 @@ export async function createTree(
result = await value({ exists: false });
}

if (result === null || typeof result === "undefined") continue;
if (
result === null ||
typeof result === "undefined" ||
typeof result === "symbol"
) {
continue;
}

tree.push(
// @ts-expect-error - Argument result can never be of type Symbol at this branch
// because the above condition will catch it and move on to the next iteration cycle
await valueToTreeObject(octokit, ownerOrFork, repo, path, result)
stefanbuck marked this conversation as resolved.
Show resolved Hide resolved
);
continue;
}

// @ts-expect-error - Argument value can never be of type Symbol at this branch
// because the above condition will catch it and initiate a file deletion operation
tree.push(await valueToTreeObject(octokit, ownerOrFork, repo, path, value));
continue;
}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -2,6 +2,7 @@ import type { Octokit } from "@octokit/core";

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

/**
Expand Down
4 changes: 2 additions & 2 deletions src/types.ts
Expand Up @@ -20,7 +20,7 @@ export type Options = {

export type Changes = {
files?: {
[path: string]: string | File | UpdateFunction | null;
[path: string]: string | File | UpdateFunction | null | Symbol;
};
emptyCommit?: boolean | string;
commit: string;
Expand Down Expand Up @@ -54,7 +54,7 @@ export type UpdateFunctionFile =

export type UpdateFunction = (
file: UpdateFunctionFile
) => string | File | null | Promise<string | File | null>;
) => string | File | null | Symbol | Promise<string | File | null | Symbol>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the update function returns null it means no file changes.
If the update function return a symbol it triggers a file deletion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to type the Symbol to ensure it is the actual deleteFile symbol?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know, I actually never worked with types for Symbols


export type State = {
octokit: Octokit;
Expand Down
62 changes: 62 additions & 0 deletions test/delete-files-function.test.ts
@@ -0,0 +1,62 @@
import { Octokit as Core } from "@octokit/core";
import { RequestError } from "@octokit/request-error";

import { createPullRequest, DELETE_FILE } from "../src";
const Octokit = Core.plugin(createPullRequest);

test("delete files function", async () => {
const fixtures = require("./fixtures/delete-files-function");
const fixturePr = fixtures[fixtures.length - 1].response;
const octokit = new Octokit();

octokit.hook.wrap("request", (_, options) => {
const currentFixtures = fixtures.shift();
const {
baseUrl,
method,
url,
request,
headers,
mediaType,
draft,
...params
} = options;

expect(
`${currentFixtures.request.method} ${currentFixtures.request.url}`
).toEqual(`${options.method} ${options.url}`);

Object.keys(params).forEach((paramName) => {
expect(currentFixtures.request[paramName]).toStrictEqual(
params[paramName]
);
});

if (currentFixtures.response.status >= 400) {
throw new RequestError("Error", currentFixtures.response.status, {
request: currentFixtures.request,
headers: currentFixtures.response.headers,
});
}
return currentFixtures.response;
});

const pr = await octokit.createPullRequest({
owner: "gr2m",
repo: "pull-request-test",
title: "One comes, one goes",
body: "because",
head: "patch",
changes: {
files: {
"path/to/file1.txt": "Content for file1",
"path/to/file2.txt": () => DELETE_FILE,
"path/to/file-does-not-exist.txt": () => DELETE_FILE,
},
commit: "why",
},
});

expect(pr).toStrictEqual(fixturePr);
expect(fixtures.length).toEqual(0);
});
6 changes: 3 additions & 3 deletions test/delete-files.test.ts
@@ -1,7 +1,7 @@
import { Octokit as Core } from "@octokit/core";
import { RequestError } from "@octokit/request-error";

import { createPullRequest } from "../src";
import { createPullRequest, DELETE_FILE } from "../src";
const Octokit = Core.plugin(createPullRequest);

test("delete files", async () => {
Expand Down Expand Up @@ -50,8 +50,8 @@ test("delete files", async () => {
changes: {
files: {
"path/to/file1.txt": "Content for file1",
"path/to/file2.txt": null,
"path/to/file-does-not-exist.txt": null,
"path/to/file2.txt": DELETE_FILE,
"path/to/file-does-not-exist.txt": DELETE_FILE,
},
commit: "why",
},
Expand Down