Skip to content

Commit 0f8c582

Browse files
authored
Introduce commitChangesFromRepo (#11)
1 parent 70f219a commit 0f8c582

File tree

5 files changed

+284
-5
lines changed

5 files changed

+284
-5
lines changed

src/fs.ts

-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import * as path from "path";
33
import type { FileAddition } from "./github/graphql/generated/types.js";
44
import { CommitFilesFromBase64Args, CommitFilesResult } from "./core.js";
55
import { commitFilesFromBuffers } from "./node.js";
6-
import git from "isomorphic-git";
76

87
export type CommitFilesFromDirectoryArgs = Omit<
98
CommitFilesFromBase64Args,
@@ -42,6 +41,3 @@ export const commitFilesFromDirectory = async ({
4241
},
4342
});
4443
};
45-
46-
// TODO: remove
47-
export { git };

src/git.ts

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { promises as fs } from "fs";
2+
import git from "isomorphic-git";
3+
import { CommitFilesFromBase64Args } from "./core";
4+
import { commitFilesFromBuffers, CommitFilesFromBuffersArgs } from "./node";
5+
6+
export type CommitChangesFromRepoArgs = Omit<
7+
CommitFilesFromBase64Args,
8+
"fileChanges" | "base"
9+
> & {
10+
/**
11+
* The root of the repository.
12+
*
13+
* @default process.cwd()
14+
*/
15+
repoDirectory?: string;
16+
};
17+
18+
export const commitChangesFromRepo = async ({
19+
repoDirectory = process.cwd(),
20+
log,
21+
...otherArgs
22+
}: CommitChangesFromRepoArgs) => {
23+
const gitLog = await git.log({
24+
fs,
25+
dir: repoDirectory,
26+
ref: "HEAD",
27+
depth: 1,
28+
});
29+
30+
const oid = gitLog[0]?.oid;
31+
32+
if (!oid) {
33+
throw new Error("Could not determine oid for current branch");
34+
}
35+
36+
// Determine changed files
37+
const trees = [git.TREE({ ref: oid }), git.WORKDIR()];
38+
const additions: CommitFilesFromBuffersArgs["fileChanges"]["additions"] = [];
39+
const deletions: CommitFilesFromBuffersArgs["fileChanges"]["deletions"] = [];
40+
const fileChanges = {
41+
additions,
42+
deletions,
43+
};
44+
await git.walk({
45+
fs,
46+
dir: repoDirectory,
47+
trees,
48+
map: async (filepath, [commit, workdir]) => {
49+
const prevOid = await commit?.oid();
50+
const currentOid = await workdir?.oid();
51+
// Don't include files that haven't changed, and exist in both trees
52+
if (prevOid === currentOid && !commit === !workdir) {
53+
return null;
54+
}
55+
// Don't include ignored files
56+
if (
57+
await git.isIgnored({
58+
fs,
59+
dir: repoDirectory,
60+
filepath,
61+
})
62+
) {
63+
return null;
64+
}
65+
// Iterate through anything that may be a directory in either the
66+
// current commit or the working directory
67+
if (
68+
(await commit?.type()) === "tree" ||
69+
(await workdir?.type()) === "tree"
70+
) {
71+
// Iterate through these directories
72+
return true;
73+
}
74+
if (!workdir) {
75+
// File was deleted
76+
deletions.push(filepath);
77+
return null;
78+
} else {
79+
// File was added / updated
80+
const arr = await workdir.content();
81+
if (!arr) {
82+
throw new Error(`Could not determine content of file ${filepath}`);
83+
}
84+
additions.push({
85+
path: filepath,
86+
contents: Buffer.from(arr),
87+
});
88+
}
89+
return true;
90+
},
91+
});
92+
93+
return commitFilesFromBuffers({
94+
...otherArgs,
95+
fileChanges,
96+
base: {
97+
commit: oid,
98+
},
99+
});
100+
};

src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export * as queries from "./github/graphql/queries.js";
2+
export { commitFilesFromBase64 } from "./core.js";
3+
export { commitChangesFromRepo } from "./git.js";
24
export { commitFilesFromDirectory } from "./fs.js";

src/test/integration/git.test.ts

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import fs from "fs";
2+
import path from "path";
3+
import {
4+
ENV,
5+
REPO,
6+
ROOT_TEMP_DIRECTORY,
7+
ROOT_TEST_BRANCH_PREFIX,
8+
log,
9+
} from "./env";
10+
import { exec } from "child_process";
11+
import { getOctokit } from "@actions/github/lib/github.js";
12+
import { commitChangesFromRepo } from "../../git";
13+
import { getRefTreeQuery } from "../../github/graphql/queries";
14+
import { deleteBranches } from "./util";
15+
16+
const octokit = getOctokit(ENV.GITHUB_TOKEN);
17+
18+
const TEST_BRANCH_PREFIX = `${ROOT_TEST_BRANCH_PREFIX}-git`;
19+
20+
const expectBranchHasFile = async ({
21+
branch,
22+
path,
23+
oid,
24+
}: {
25+
branch: string;
26+
path: string;
27+
oid: string | null;
28+
}) => {
29+
if (oid === null) {
30+
expect(() =>
31+
getRefTreeQuery(octokit, {
32+
owner: REPO.owner,
33+
name: REPO.repository,
34+
ref: `refs/heads/${branch}`,
35+
path,
36+
}),
37+
).rejects.toThrow("Could not resolve file for path");
38+
return;
39+
}
40+
const ref = (
41+
await getRefTreeQuery(octokit, {
42+
owner: REPO.owner,
43+
name: REPO.repository,
44+
ref: `refs/heads/${branch}`,
45+
path,
46+
})
47+
).repository?.ref?.target;
48+
49+
if (!ref) {
50+
throw new Error("Unexpected missing ref");
51+
}
52+
53+
if ("tree" in ref) {
54+
expect(ref.file?.oid ?? null).toEqual(oid);
55+
} else {
56+
throw new Error("Expected ref to have a tree");
57+
}
58+
};
59+
60+
describe("git", () => {
61+
const branches: string[] = [];
62+
63+
// Set timeout to 1 minute
64+
jest.setTimeout(60 * 1000);
65+
66+
describe("commitChangesFromRepo", () => {
67+
const testDir = path.join(ROOT_TEMP_DIRECTORY, "commitChangesFromRepo");
68+
69+
it("should correctly commit all changes", async () => {
70+
const branch = `${TEST_BRANCH_PREFIX}-multiple-changes`;
71+
72+
await fs.promises.mkdir(testDir, { recursive: true });
73+
const repoDirectory = path.join(testDir, "repo-1");
74+
75+
// Clone the git repo locally usig the git cli and child-process
76+
await new Promise<void>((resolve, reject) => {
77+
const p = exec(
78+
`git clone ${process.cwd()} repo-1`,
79+
{ cwd: testDir },
80+
(error) => {
81+
if (error) {
82+
reject(error);
83+
} else {
84+
resolve();
85+
}
86+
},
87+
);
88+
p.stdout?.pipe(process.stdout);
89+
p.stderr?.pipe(process.stderr);
90+
});
91+
92+
// Update an existing file
93+
await fs.promises.writeFile(
94+
path.join(repoDirectory, "LICENSE"),
95+
"This is a new license",
96+
);
97+
// Remove a file
98+
await fs.promises.rm(path.join(repoDirectory, "package.json"));
99+
// Remove a file nested in a directory
100+
await fs.promises.rm(path.join(repoDirectory, "src", "index.ts"));
101+
// Add a new file
102+
await fs.promises.writeFile(
103+
path.join(repoDirectory, "new-file.txt"),
104+
"This is a new file",
105+
);
106+
// Add a new file nested in a directory
107+
await fs.promises.mkdir(path.join(repoDirectory, "nested"), {
108+
recursive: true,
109+
});
110+
await fs.promises.writeFile(
111+
path.join(repoDirectory, "nested", "nested-file.txt"),
112+
"This is a nested file",
113+
);
114+
// Add files that should be ignored
115+
await fs.promises.writeFile(
116+
path.join(repoDirectory, ".env"),
117+
"This file should be ignored",
118+
);
119+
await fs.promises.mkdir(path.join(repoDirectory, "coverage", "foo"), {
120+
recursive: true,
121+
});
122+
await fs.promises.writeFile(
123+
path.join(repoDirectory, "coverage", "foo", "bar"),
124+
"This file should be ignored",
125+
);
126+
127+
// Push the changes
128+
await commitChangesFromRepo({
129+
octokit,
130+
...REPO,
131+
branch,
132+
message: {
133+
headline: "Test commit",
134+
body: "This is a test commit",
135+
},
136+
repoDirectory,
137+
log,
138+
});
139+
140+
// Expect the deleted files to not exist
141+
await expectBranchHasFile({ branch, path: "package.json", oid: null });
142+
await expectBranchHasFile({ branch, path: "src/index.ts", oid: null });
143+
// Expect updated file to have new oid
144+
await expectBranchHasFile({
145+
branch,
146+
path: "LICENSE",
147+
oid: "8dd03bb8a1d83212f3667bd2eb8b92746120ab8f",
148+
});
149+
// Expect new files to have correct oid
150+
await expectBranchHasFile({
151+
branch,
152+
path: "new-file.txt",
153+
oid: "be5b944ff55ca7569cc2ae34c35b5bda8cd5d37e",
154+
});
155+
await expectBranchHasFile({
156+
branch,
157+
path: "nested/nested-file.txt",
158+
oid: "60eb5af9a0c03dc16dc6d0bd9a370c1aa4e095a3",
159+
});
160+
// Expect ignored files to not exist
161+
await expectBranchHasFile({ branch, path: ".env", oid: null });
162+
await expectBranchHasFile({
163+
branch,
164+
path: "coverage/foo/bar",
165+
oid: null,
166+
});
167+
});
168+
});
169+
170+
afterAll(async () => {
171+
console.info("Cleaning up test branches");
172+
173+
await deleteBranches(octokit, branches);
174+
});
175+
});

tsup.config.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { defineConfig } from "tsup";
22

33
export default defineConfig({
4-
entry: ["src/index.ts", "src/core.ts", "src/fs.ts", "src/node.ts"],
4+
entry: [
5+
"src/index.ts",
6+
"src/core.ts",
7+
"src/git.ts",
8+
"src/fs.ts",
9+
"src/node.ts",
10+
],
511
format: ["cjs", "esm"],
612
dts: true,
713
});

0 commit comments

Comments
 (0)