Skip to content

Commit

Permalink
feat: allow update function to trigger file deletion (#130)
Browse files Browse the repository at this point in the history
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;
  }
}
  • Loading branch information
stefanbuck committed Jun 4, 2023
1 parent f961a6d commit ac571dc
Show file tree
Hide file tree
Showing 8 changed files with 1,769 additions and 9 deletions.
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
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)
);
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>;

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

0 comments on commit ac571dc

Please sign in to comment.