Skip to content

Commit

Permalink
Introduce commitChangesFromRepo (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
s0 authored Aug 23, 2024
1 parent 70f219a commit 0f8c582
Show file tree
Hide file tree
Showing 5 changed files with 284 additions and 5 deletions.
4 changes: 0 additions & 4 deletions src/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as path from "path";
import type { FileAddition } from "./github/graphql/generated/types.js";
import { CommitFilesFromBase64Args, CommitFilesResult } from "./core.js";
import { commitFilesFromBuffers } from "./node.js";
import git from "isomorphic-git";

export type CommitFilesFromDirectoryArgs = Omit<
CommitFilesFromBase64Args,
Expand Down Expand Up @@ -42,6 +41,3 @@ export const commitFilesFromDirectory = async ({
},
});
};

// TODO: remove
export { git };
100 changes: 100 additions & 0 deletions src/git.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { promises as fs } from "fs";
import git from "isomorphic-git";
import { CommitFilesFromBase64Args } from "./core";
import { commitFilesFromBuffers, CommitFilesFromBuffersArgs } from "./node";

export type CommitChangesFromRepoArgs = Omit<
CommitFilesFromBase64Args,
"fileChanges" | "base"
> & {
/**
* The root of the repository.
*
* @default process.cwd()
*/
repoDirectory?: string;
};

export const commitChangesFromRepo = async ({
repoDirectory = process.cwd(),
log,
...otherArgs
}: CommitChangesFromRepoArgs) => {
const gitLog = await git.log({
fs,
dir: repoDirectory,
ref: "HEAD",
depth: 1,
});

const oid = gitLog[0]?.oid;

if (!oid) {
throw new Error("Could not determine oid for current branch");
}

// Determine changed files
const trees = [git.TREE({ ref: oid }), git.WORKDIR()];
const additions: CommitFilesFromBuffersArgs["fileChanges"]["additions"] = [];
const deletions: CommitFilesFromBuffersArgs["fileChanges"]["deletions"] = [];
const fileChanges = {
additions,
deletions,
};
await git.walk({
fs,
dir: repoDirectory,
trees,
map: async (filepath, [commit, workdir]) => {
const prevOid = await commit?.oid();
const currentOid = await workdir?.oid();
// Don't include files that haven't changed, and exist in both trees
if (prevOid === currentOid && !commit === !workdir) {
return null;
}
// Don't include ignored files
if (
await git.isIgnored({
fs,
dir: repoDirectory,
filepath,
})
) {
return null;
}
// Iterate through anything that may be a directory in either the
// current commit or the working directory
if (
(await commit?.type()) === "tree" ||
(await workdir?.type()) === "tree"
) {
// Iterate through these directories
return true;
}
if (!workdir) {
// File was deleted
deletions.push(filepath);
return null;
} else {
// File was added / updated
const arr = await workdir.content();
if (!arr) {
throw new Error(`Could not determine content of file ${filepath}`);
}
additions.push({
path: filepath,
contents: Buffer.from(arr),
});
}
return true;
},
});

return commitFilesFromBuffers({
...otherArgs,
fileChanges,
base: {
commit: oid,
},
});
};
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * as queries from "./github/graphql/queries.js";
export { commitFilesFromBase64 } from "./core.js";
export { commitChangesFromRepo } from "./git.js";
export { commitFilesFromDirectory } from "./fs.js";
175 changes: 175 additions & 0 deletions src/test/integration/git.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import fs from "fs";
import path from "path";
import {
ENV,
REPO,
ROOT_TEMP_DIRECTORY,
ROOT_TEST_BRANCH_PREFIX,
log,
} from "./env";
import { exec } from "child_process";
import { getOctokit } from "@actions/github/lib/github.js";
import { commitChangesFromRepo } from "../../git";
import { getRefTreeQuery } from "../../github/graphql/queries";
import { deleteBranches } from "./util";

const octokit = getOctokit(ENV.GITHUB_TOKEN);

const TEST_BRANCH_PREFIX = `${ROOT_TEST_BRANCH_PREFIX}-git`;

const expectBranchHasFile = async ({
branch,
path,
oid,
}: {
branch: string;
path: string;
oid: string | null;
}) => {
if (oid === null) {
expect(() =>
getRefTreeQuery(octokit, {
owner: REPO.owner,
name: REPO.repository,
ref: `refs/heads/${branch}`,
path,
}),
).rejects.toThrow("Could not resolve file for path");
return;
}
const ref = (
await getRefTreeQuery(octokit, {
owner: REPO.owner,
name: REPO.repository,
ref: `refs/heads/${branch}`,
path,
})
).repository?.ref?.target;

if (!ref) {
throw new Error("Unexpected missing ref");
}

if ("tree" in ref) {
expect(ref.file?.oid ?? null).toEqual(oid);
} else {
throw new Error("Expected ref to have a tree");
}
};

describe("git", () => {
const branches: string[] = [];

// Set timeout to 1 minute
jest.setTimeout(60 * 1000);

describe("commitChangesFromRepo", () => {
const testDir = path.join(ROOT_TEMP_DIRECTORY, "commitChangesFromRepo");

it("should correctly commit all changes", async () => {
const branch = `${TEST_BRANCH_PREFIX}-multiple-changes`;

await fs.promises.mkdir(testDir, { recursive: true });
const repoDirectory = path.join(testDir, "repo-1");

// Clone the git repo locally usig the git cli and child-process
await new Promise<void>((resolve, reject) => {
const p = exec(
`git clone ${process.cwd()} repo-1`,
{ cwd: testDir },
(error) => {
if (error) {
reject(error);
} else {
resolve();
}
},
);
p.stdout?.pipe(process.stdout);
p.stderr?.pipe(process.stderr);
});

// Update an existing file
await fs.promises.writeFile(
path.join(repoDirectory, "LICENSE"),
"This is a new license",
);
// Remove a file
await fs.promises.rm(path.join(repoDirectory, "package.json"));
// Remove a file nested in a directory
await fs.promises.rm(path.join(repoDirectory, "src", "index.ts"));
// Add a new file
await fs.promises.writeFile(
path.join(repoDirectory, "new-file.txt"),
"This is a new file",
);
// Add a new file nested in a directory
await fs.promises.mkdir(path.join(repoDirectory, "nested"), {
recursive: true,
});
await fs.promises.writeFile(
path.join(repoDirectory, "nested", "nested-file.txt"),
"This is a nested file",
);
// Add files that should be ignored
await fs.promises.writeFile(
path.join(repoDirectory, ".env"),
"This file should be ignored",
);
await fs.promises.mkdir(path.join(repoDirectory, "coverage", "foo"), {
recursive: true,
});
await fs.promises.writeFile(
path.join(repoDirectory, "coverage", "foo", "bar"),
"This file should be ignored",
);

// Push the changes
await commitChangesFromRepo({
octokit,
...REPO,
branch,
message: {
headline: "Test commit",
body: "This is a test commit",
},
repoDirectory,
log,
});

// Expect the deleted files to not exist
await expectBranchHasFile({ branch, path: "package.json", oid: null });
await expectBranchHasFile({ branch, path: "src/index.ts", oid: null });
// Expect updated file to have new oid
await expectBranchHasFile({
branch,
path: "LICENSE",
oid: "8dd03bb8a1d83212f3667bd2eb8b92746120ab8f",
});
// Expect new files to have correct oid
await expectBranchHasFile({
branch,
path: "new-file.txt",
oid: "be5b944ff55ca7569cc2ae34c35b5bda8cd5d37e",
});
await expectBranchHasFile({
branch,
path: "nested/nested-file.txt",
oid: "60eb5af9a0c03dc16dc6d0bd9a370c1aa4e095a3",
});
// Expect ignored files to not exist
await expectBranchHasFile({ branch, path: ".env", oid: null });
await expectBranchHasFile({
branch,
path: "coverage/foo/bar",
oid: null,
});
});
});

afterAll(async () => {
console.info("Cleaning up test branches");

await deleteBranches(octokit, branches);
});
});
8 changes: 7 additions & 1 deletion tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { defineConfig } from "tsup";

export default defineConfig({
entry: ["src/index.ts", "src/core.ts", "src/fs.ts", "src/node.ts"],
entry: [
"src/index.ts",
"src/core.ts",
"src/git.ts",
"src/fs.ts",
"src/node.ts",
],
format: ["cjs", "esm"],
dts: true,
});

0 comments on commit 0f8c582

Please sign in to comment.