Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/polite-carrots-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@changesets/ghcommit": minor
---

Support `branch` and `tag` format for `commitChangesFromRepo` `base` option. This aligns with the `commitFilesFromBase64` `base` option.
90 changes: 32 additions & 58 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Comment on lines -19 to -21

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This part is refactored away as we can avoid accessing info.baseRef in this state.

If base: { commit: "123" } is passed, info.baseRef would be null as it doesn't support direct SHAs. This is fine because we're not going to access it anyways in

const baseSha = "commit" in base ? base.commit : getBaseRefSha(info.baseRef);

}
};
function getBaseRefSha(
baseRef: NonNullable<GetRepositoryMetadataQuery["repository"]>["baseRef"],
) {
if (!baseRef?.target) return null;

const getOidFromRef = (
base: GitBase,
ref: (GetRepositoryMetadataQuery["repository"] &
Record<never, never>)["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" &&
Expand All @@ -55,20 +34,20 @@ const isAlreadyExistingRefError = (error: unknown) =>
const createCommit = async ({
octokit,
refId,
baseOid,
baseSha,
message,
fileChanges,
}: Pick<CommitFilesFromBase64Args, "octokit" | "message" | "fileChanges"> & {
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, {
input: {
branch: {
id: refId,
},
expectedHeadOid: baseOid,
expectedHeadOid: baseSha,
message: normalizeCommitMessage(message),
fileChanges,
},
Expand All @@ -85,8 +64,7 @@ export const commitFilesFromBase64 = async ({
message,
fileChanges,
}: CommitFilesFromBase64Args): Promise<CommitFilesResult> => {
const repositoryNameWithOwner = `${owner}/${repo}`;
const baseRef = getBaseRef(base);
const baseRef = resolveGitRef(base);
const targetRef = `refs/heads/${branch}`;

const info = await getRepositoryMetadata(octokit, {
Expand All @@ -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`,
);
}

Expand All @@ -146,7 +120,7 @@ export const commitFilesFromBase64 = async ({
owner,
repo,
ref: `refs/heads/${tempBranch}`,
sha: baseOid,
sha: baseSha,
});

const refIdStr = createdTempRef.data.node_id;
Expand All @@ -165,7 +139,7 @@ export const commitFilesFromBase64 = async ({
owner,
repo,
ref: `heads/${tempBranch}`,
sha: baseOid,
sha: baseSha,
force: true,
});

Expand All @@ -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}`,
);
Expand All @@ -198,7 +172,7 @@ export const commitFilesFromBase64 = async ({
owner,
repo,
ref: `heads/${branch}`,
sha: tempHeadOid,
sha: tempHeadSha,
force: true,
});

Expand Down Expand Up @@ -226,7 +200,7 @@ export const commitFilesFromBase64 = async ({
owner,
repo,
ref: `refs/heads/${branch}`,
sha: baseOid,
sha: baseSha,
});

const refIdStr = createdRef.data.node_id;
Expand All @@ -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,
});
Expand Down
16 changes: 8 additions & 8 deletions src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,33 @@ 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<CommitFilesResult> => {
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({
...otherArgs,
fileChanges: await getFileChanges(
workingDirectory,
repoRoot,
refOid,
refSha,
filterFiles,
),
base: {
commit: refOid,
commit: refSha,
},
});
};
Expand Down Expand Up @@ -142,7 +142,7 @@ export async function getFileChanges(
return { additions, deletions };
}

async function getOidForRef(cwd: string, ref: string): Promise<string | null> {
async function getShaForRef(cwd: string, ref: string): Promise<string | null> {
try {
const { stdout } = await exec("git", ["rev-parse", ref], {
throwOnError: true,
Expand Down
8 changes: 3 additions & 5 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type CommitFilesResult = {
refId: string | null;
};

export type GitBase =
export type GitRef =
| {
branch: string;
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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`
Expand Down
11 changes: 11 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
}
}
80 changes: 80 additions & 0 deletions tests/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!",
Expand Down