diff --git a/.changeset/polite-carrots-exist.md b/.changeset/polite-carrots-exist.md new file mode 100644 index 0000000..3f02016 --- /dev/null +++ b/.changeset/polite-carrots-exist.md @@ -0,0 +1,5 @@ +--- +"@changesets/ghcommit": minor +--- + +Support `branch` and `tag` format for `commitChangesFromRepo` `base` option. This aligns with the `commitFilesFromBase64` `base` option. diff --git a/src/core.ts b/src/core.ts index 7eb185c..667e505 100644 --- a/src/core.ts +++ b/src/core.ts @@ -6,41 +6,20 @@ import { import type { CommitFilesFromBase64Args, CommitFilesResult, - GitBase, } from "./interface.ts"; -import { normalizeCommitMessage } from "./utils.ts"; +import { normalizeCommitMessage, resolveGitRef } from "./utils.ts"; -const getBaseRef = (base: GitBase): string => { - if ("branch" in base) { - return `refs/heads/${base.branch}`; - } else if ("tag" in base) { - return `refs/tags/${base.tag}`; - } else { - // For explicit commit bases we don't resolve the base oid from a ref, - // but the shared metadata query still expects a valid qualified ref name. - return "HEAD"; - } -}; +function getBaseRefSha( + baseRef: NonNullable["baseRef"], +) { + if (!baseRef?.target) return null; -const getOidFromRef = ( - base: GitBase, - ref: (GetRepositoryMetadataQuery["repository"] & - Record)["baseRef"], -) => { - if ("commit" in base) { - return base.commit; + if ("target" in baseRef.target) { + return baseRef.target.target.oid; } - if (!ref?.target) { - throw new Error(`Could not determine oid from ref: ${JSON.stringify(ref)}`); - } - - if ("target" in ref.target) { - return ref.target.target.oid; - } - - return ref.target.oid; -}; + return baseRef.target.oid; +} const isAlreadyExistingRefError = (error: unknown) => typeof error === "object" && @@ -55,12 +34,12 @@ const isAlreadyExistingRefError = (error: unknown) => const createCommit = async ({ octokit, refId, - baseOid, + baseSha, message, fileChanges, }: Pick & { refId: string; - baseOid: string; + baseSha: string; }) => { // we have to stick to GraphQL here as with REST, each file change would become a separate API call return createCommitOnBranchQuery(octokit, { @@ -68,7 +47,7 @@ const createCommit = async ({ branch: { id: refId, }, - expectedHeadOid: baseOid, + expectedHeadOid: baseSha, message: normalizeCommitMessage(message), fileChanges, }, @@ -85,8 +64,7 @@ export const commitFilesFromBase64 = async ({ message, fileChanges, }: CommitFilesFromBase64Args): Promise => { - const repositoryNameWithOwner = `${owner}/${repo}`; - const baseRef = getBaseRef(base); + const baseRef = resolveGitRef(base); const targetRef = `refs/heads/${branch}`; const info = await getRepositoryMetadata(octokit, { @@ -97,40 +75,36 @@ export const commitFilesFromBase64 = async ({ }); if (!info) { - throw new Error( - `Repository ${JSON.stringify(repositoryNameWithOwner)} not found`, - ); + throw new Error(`Repository "${owner}/${repo}" not found`); } - if (!("commit" in base) && !info.baseRef) { - throw new Error(`Ref ${JSON.stringify(baseRef)} not found`); - } - - const resolvedBaseRef = info.baseRef; /** - * The commit oid to base the new commit on. + * The commit sha to base the new commit on. * * Used both to create the new commit, * and to determine whether an existing branch can be updated. */ - const baseOid = getOidFromRef(base, info.baseRef); - const targetOid = info.targetBranch?.target?.oid ?? null; + const baseSha = "commit" in base ? base.commit : getBaseRefSha(info.baseRef); + if (!baseSha) { + throw new Error(`Could not determine sha for base ref "${baseRef}"`); + } + const targetSha = info.targetBranch?.target?.oid ?? null; const sameBranchBase = "branch" in base && base.branch === branch; let mode: "create" | "update" | "force-update"; if (sameBranchBase) { mode = force ? "force-update" : "update"; - } else if (targetOid === null) { + } else if (targetSha === null) { // TODO: legit *creation* failure should be retried if `force === true` mode = "create"; } else if (force) { mode = "force-update"; - } else if (targetOid === baseOid) { + } else if (targetSha === baseSha) { mode = "update"; } else { throw new Error( - `Branch ${branch} exists already and does not match base ${baseOid}, force is set to false`, + `Branch ${branch} exists already and does not match base ${baseSha}, force is set to false`, ); } @@ -146,7 +120,7 @@ export const commitFilesFromBase64 = async ({ owner, repo, ref: `refs/heads/${tempBranch}`, - sha: baseOid, + sha: baseSha, }); const refIdStr = createdTempRef.data.node_id; @@ -165,7 +139,7 @@ export const commitFilesFromBase64 = async ({ owner, repo, ref: `heads/${tempBranch}`, - sha: baseOid, + sha: baseSha, force: true, }); @@ -181,14 +155,14 @@ export const commitFilesFromBase64 = async ({ const tempCommit = await createCommit({ octokit, refId: tempRefId, - baseOid, + baseSha, message, fileChanges, }); - const tempHeadOid = tempCommit.createCommitOnBranch?.commit?.oid; + const tempHeadSha = tempCommit.createCommitOnBranch?.commit?.oid; - if (!tempHeadOid) { + if (!tempHeadSha) { throw new Error( `Failed to determine head commit of temporary branch ${tempBranch}`, ); @@ -198,7 +172,7 @@ export const commitFilesFromBase64 = async ({ owner, repo, ref: `heads/${branch}`, - sha: tempHeadOid, + sha: tempHeadSha, force: true, }); @@ -226,7 +200,7 @@ export const commitFilesFromBase64 = async ({ owner, repo, ref: `refs/heads/${branch}`, - sha: baseOid, + sha: baseSha, }); const refIdStr = createdRef.data.node_id; @@ -237,13 +211,13 @@ export const commitFilesFromBase64 = async ({ refId = refIdStr; } else { - refId = sameBranchBase ? resolvedBaseRef!.id : info.targetBranch!.id; + refId = sameBranchBase ? info.baseRef!.id : info.targetBranch!.id; } const newCommit = await createCommit({ octokit, refId, - baseOid, + baseSha, message, fileChanges, }); diff --git a/src/git.ts b/src/git.ts index b9f9eea..8ea4cb8 100644 --- a/src/git.ts +++ b/src/git.ts @@ -7,21 +7,21 @@ import type { CommitFilesFromBase64Args, CommitFilesResult, } from "./interface.ts"; +import { resolveGitRef } from "./utils.ts"; export const commitChangesFromRepo = async ({ - base, cwd: workingDirectory, recursivelyFindRoot = true, filterFiles, ...otherArgs }: CommitChangesFromRepoArgs): Promise => { - const ref = base?.commit ?? "HEAD"; + const ref = resolveGitRef(otherArgs.base ?? { commit: "HEAD" }); const cwd = path.resolve(workingDirectory); const repoRoot = recursivelyFindRoot ? await findGitRoot(cwd) : cwd; - const refOid = await getOidForRef(repoRoot, ref); - if (!refOid) { - throw new Error(`Could not determine oid for ref ${ref}`); + const refSha = await getShaForRef(repoRoot, ref); + if (!refSha) { + throw new Error(`Could not determine sha for ref ${ref}`); } return await commitFilesFromBase64({ @@ -29,11 +29,11 @@ export const commitChangesFromRepo = async ({ fileChanges: await getFileChanges( workingDirectory, repoRoot, - refOid, + refSha, filterFiles, ), base: { - commit: refOid, + commit: refSha, }, }); }; @@ -142,7 +142,7 @@ export async function getFileChanges( return { additions, deletions }; } -async function getOidForRef(cwd: string, ref: string): Promise { +async function getShaForRef(cwd: string, ref: string): Promise { try { const { stdout } = await exec("git", ["rev-parse", ref], { throwOnError: true, diff --git a/src/interface.ts b/src/interface.ts index 1d8365e..a5c20d7 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -8,7 +8,7 @@ export type CommitFilesResult = { refId: string | null; }; -export type GitBase = +export type GitRef = | { branch: string; } @@ -39,7 +39,7 @@ export interface CommitFilesSharedArgsWithBase extends CommitFilesBasedArgs { /** * The current branch, tag or commit that the new branch should be based on. */ - base: GitBase; + base: GitRef; } export interface CommitFilesFromBase64Args extends CommitFilesSharedArgsWithBase { @@ -69,9 +69,7 @@ export interface CommitChangesFromRepoArgs extends CommitFilesBasedArgs { * * @default HEAD */ - base?: { - commit: string; - }; + base?: GitRef; /** * Don't require {@link cwd} to be the root of the repository, * and use it as a starting point to recursively search for the `.git` diff --git a/src/utils.ts b/src/utils.ts index 71bf7f6..55f31a8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import type { CommitMessage } from "./github/graphql/generated/types.ts"; +import type { GitRef } from "./interface.ts"; export function normalizeCommitMessage( message: string | CommitMessage, @@ -20,3 +21,13 @@ export function normalizeCommitMessage( body: bodyLines.join("\n").trim(), }; } + +export function resolveGitRef(ref: GitRef): string { + if ("branch" in ref) { + return `refs/heads/${ref.branch}`; + } else if ("tag" in ref) { + return `refs/tags/${ref.tag}`; + } else { + return ref.commit; + } +} diff --git a/tests/git.test.ts b/tests/git.test.ts index f50e666..2896dab 100644 --- a/tests/git.test.ts +++ b/tests/git.test.ts @@ -64,6 +64,86 @@ describe("getFileChanges", () => { }); }); + it("should support branch refs", async () => { + await using fixture = await createFixture({ + "a.txt": "Hello, world!", + }); + await setupGit(fixture.path); + + await exec("git", ["checkout", "-b", "new-branch"], { + nodeOptions: { cwd: fixture.path }, + }); + await fixture.writeFile("b.txt", "This is a new file!"); + + const result = await getFileChanges( + fixture.path, + fixture.path, + "refs/heads/new-branch", + ); + expect(result).toEqual({ + additions: [ + { + path: "b.txt", + contents: await fixture.readFile("b.txt", "base64"), + }, + ], + deletions: [], + }); + }); + + it("should support tag refs", async () => { + await using fixture = await createFixture({ + "a.txt": "Hello, world!", + }); + await setupGit(fixture.path); + + await exec("git", ["tag", "v1.0.0"], { + nodeOptions: { cwd: fixture.path }, + }); + await fixture.writeFile("b.txt", "This is a new file!"); + + const result = await getFileChanges( + fixture.path, + fixture.path, + "refs/tags/v1.0.0", + ); + expect(result).toEqual({ + additions: [ + { + path: "b.txt", + contents: await fixture.readFile("b.txt", "base64"), + }, + ], + deletions: [], + }); + }); + + it("should support commit refs", async () => { + await using fixture = await createFixture({ + "a.txt": "Hello, world!", + }); + await setupGit(fixture.path); + + const commitSha = ( + await exec("git", ["rev-parse", "HEAD"], { + nodeOptions: { cwd: fixture.path }, + }) + ).stdout.trim(); + + await fixture.writeFile("b.txt", "This is a new file!"); + + const result = await getFileChanges(fixture.path, fixture.path, commitSha); + expect(result).toEqual({ + additions: [ + { + path: "b.txt", + contents: await fixture.readFile("b.txt", "base64"), + }, + ], + deletions: [], + }); + }); + it("should filter files with filterFiles", async () => { await using fixture = await createFixture({ "foo.txt": "Hello, world!",