Skip to content

Commit 1cf711e

Browse files
authored
Support branch and tag format for commitChangesFromRepo base (#111)
1 parent 5c47edd commit 1cf711e

6 files changed

Lines changed: 139 additions & 71 deletions

File tree

.changeset/polite-carrots-exist.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@changesets/ghcommit": minor
3+
---
4+
5+
Support `branch` and `tag` format for `commitChangesFromRepo` `base` option. This aligns with the `commitFilesFromBase64` `base` option.

src/core.ts

Lines changed: 32 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,20 @@ import {
66
import type {
77
CommitFilesFromBase64Args,
88
CommitFilesResult,
9-
GitBase,
109
} from "./interface.ts";
11-
import { normalizeCommitMessage } from "./utils.ts";
10+
import { normalizeCommitMessage, resolveGitRef } from "./utils.ts";
1211

13-
const getBaseRef = (base: GitBase): string => {
14-
if ("branch" in base) {
15-
return `refs/heads/${base.branch}`;
16-
} else if ("tag" in base) {
17-
return `refs/tags/${base.tag}`;
18-
} else {
19-
// For explicit commit bases we don't resolve the base oid from a ref,
20-
// but the shared metadata query still expects a valid qualified ref name.
21-
return "HEAD";
22-
}
23-
};
12+
function getBaseRefSha(
13+
baseRef: NonNullable<GetRepositoryMetadataQuery["repository"]>["baseRef"],
14+
) {
15+
if (!baseRef?.target) return null;
2416

25-
const getOidFromRef = (
26-
base: GitBase,
27-
ref: (GetRepositoryMetadataQuery["repository"] &
28-
Record<never, never>)["baseRef"],
29-
) => {
30-
if ("commit" in base) {
31-
return base.commit;
17+
if ("target" in baseRef.target) {
18+
return baseRef.target.target.oid;
3219
}
3320

34-
if (!ref?.target) {
35-
throw new Error(`Could not determine oid from ref: ${JSON.stringify(ref)}`);
36-
}
37-
38-
if ("target" in ref.target) {
39-
return ref.target.target.oid;
40-
}
41-
42-
return ref.target.oid;
43-
};
21+
return baseRef.target.oid;
22+
}
4423

4524
const isAlreadyExistingRefError = (error: unknown) =>
4625
typeof error === "object" &&
@@ -55,20 +34,20 @@ const isAlreadyExistingRefError = (error: unknown) =>
5534
const createCommit = async ({
5635
octokit,
5736
refId,
58-
baseOid,
37+
baseSha,
5938
message,
6039
fileChanges,
6140
}: Pick<CommitFilesFromBase64Args, "octokit" | "message" | "fileChanges"> & {
6241
refId: string;
63-
baseOid: string;
42+
baseSha: string;
6443
}) => {
6544
// we have to stick to GraphQL here as with REST, each file change would become a separate API call
6645
return createCommitOnBranchQuery(octokit, {
6746
input: {
6847
branch: {
6948
id: refId,
7049
},
71-
expectedHeadOid: baseOid,
50+
expectedHeadOid: baseSha,
7251
message: normalizeCommitMessage(message),
7352
fileChanges,
7453
},
@@ -85,8 +64,7 @@ export const commitFilesFromBase64 = async ({
8564
message,
8665
fileChanges,
8766
}: CommitFilesFromBase64Args): Promise<CommitFilesResult> => {
88-
const repositoryNameWithOwner = `${owner}/${repo}`;
89-
const baseRef = getBaseRef(base);
67+
const baseRef = resolveGitRef(base);
9068
const targetRef = `refs/heads/${branch}`;
9169

9270
const info = await getRepositoryMetadata(octokit, {
@@ -97,40 +75,36 @@ export const commitFilesFromBase64 = async ({
9775
});
9876

9977
if (!info) {
100-
throw new Error(
101-
`Repository ${JSON.stringify(repositoryNameWithOwner)} not found`,
102-
);
78+
throw new Error(`Repository "${owner}/${repo}" not found`);
10379
}
104-
if (!("commit" in base) && !info.baseRef) {
105-
throw new Error(`Ref ${JSON.stringify(baseRef)} not found`);
106-
}
107-
108-
const resolvedBaseRef = info.baseRef;
10980

11081
/**
111-
* The commit oid to base the new commit on.
82+
* The commit sha to base the new commit on.
11283
*
11384
* Used both to create the new commit,
11485
* and to determine whether an existing branch can be updated.
11586
*/
116-
const baseOid = getOidFromRef(base, info.baseRef);
117-
const targetOid = info.targetBranch?.target?.oid ?? null;
87+
const baseSha = "commit" in base ? base.commit : getBaseRefSha(info.baseRef);
88+
if (!baseSha) {
89+
throw new Error(`Could not determine sha for base ref "${baseRef}"`);
90+
}
91+
const targetSha = info.targetBranch?.target?.oid ?? null;
11892
const sameBranchBase = "branch" in base && base.branch === branch;
11993

12094
let mode: "create" | "update" | "force-update";
12195

12296
if (sameBranchBase) {
12397
mode = force ? "force-update" : "update";
124-
} else if (targetOid === null) {
98+
} else if (targetSha === null) {
12599
// TODO: legit *creation* failure should be retried if `force === true`
126100
mode = "create";
127101
} else if (force) {
128102
mode = "force-update";
129-
} else if (targetOid === baseOid) {
103+
} else if (targetSha === baseSha) {
130104
mode = "update";
131105
} else {
132106
throw new Error(
133-
`Branch ${branch} exists already and does not match base ${baseOid}, force is set to false`,
107+
`Branch ${branch} exists already and does not match base ${baseSha}, force is set to false`,
134108
);
135109
}
136110

@@ -146,7 +120,7 @@ export const commitFilesFromBase64 = async ({
146120
owner,
147121
repo,
148122
ref: `refs/heads/${tempBranch}`,
149-
sha: baseOid,
123+
sha: baseSha,
150124
});
151125

152126
const refIdStr = createdTempRef.data.node_id;
@@ -165,7 +139,7 @@ export const commitFilesFromBase64 = async ({
165139
owner,
166140
repo,
167141
ref: `heads/${tempBranch}`,
168-
sha: baseOid,
142+
sha: baseSha,
169143
force: true,
170144
});
171145

@@ -181,14 +155,14 @@ export const commitFilesFromBase64 = async ({
181155
const tempCommit = await createCommit({
182156
octokit,
183157
refId: tempRefId,
184-
baseOid,
158+
baseSha,
185159
message,
186160
fileChanges,
187161
});
188162

189-
const tempHeadOid = tempCommit.createCommitOnBranch?.commit?.oid;
163+
const tempHeadSha = tempCommit.createCommitOnBranch?.commit?.oid;
190164

191-
if (!tempHeadOid) {
165+
if (!tempHeadSha) {
192166
throw new Error(
193167
`Failed to determine head commit of temporary branch ${tempBranch}`,
194168
);
@@ -198,7 +172,7 @@ export const commitFilesFromBase64 = async ({
198172
owner,
199173
repo,
200174
ref: `heads/${branch}`,
201-
sha: tempHeadOid,
175+
sha: tempHeadSha,
202176
force: true,
203177
});
204178

@@ -226,7 +200,7 @@ export const commitFilesFromBase64 = async ({
226200
owner,
227201
repo,
228202
ref: `refs/heads/${branch}`,
229-
sha: baseOid,
203+
sha: baseSha,
230204
});
231205

232206
const refIdStr = createdRef.data.node_id;
@@ -237,13 +211,13 @@ export const commitFilesFromBase64 = async ({
237211

238212
refId = refIdStr;
239213
} else {
240-
refId = sameBranchBase ? resolvedBaseRef!.id : info.targetBranch!.id;
214+
refId = sameBranchBase ? info.baseRef!.id : info.targetBranch!.id;
241215
}
242216

243217
const newCommit = await createCommit({
244218
octokit,
245219
refId,
246-
baseOid,
220+
baseSha,
247221
message,
248222
fileChanges,
249223
});

src/git.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,33 @@ import type {
77
CommitFilesFromBase64Args,
88
CommitFilesResult,
99
} from "./interface.ts";
10+
import { resolveGitRef } from "./utils.ts";
1011

1112
export const commitChangesFromRepo = async ({
12-
base,
1313
cwd: workingDirectory,
1414
recursivelyFindRoot = true,
1515
filterFiles,
1616
...otherArgs
1717
}: CommitChangesFromRepoArgs): Promise<CommitFilesResult> => {
18-
const ref = base?.commit ?? "HEAD";
18+
const ref = resolveGitRef(otherArgs.base ?? { commit: "HEAD" });
1919
const cwd = path.resolve(workingDirectory);
2020
const repoRoot = recursivelyFindRoot ? await findGitRoot(cwd) : cwd;
2121

22-
const refOid = await getOidForRef(repoRoot, ref);
23-
if (!refOid) {
24-
throw new Error(`Could not determine oid for ref ${ref}`);
22+
const refSha = await getShaForRef(repoRoot, ref);
23+
if (!refSha) {
24+
throw new Error(`Could not determine sha for ref ${ref}`);
2525
}
2626

2727
return await commitFilesFromBase64({
2828
...otherArgs,
2929
fileChanges: await getFileChanges(
3030
workingDirectory,
3131
repoRoot,
32-
refOid,
32+
refSha,
3333
filterFiles,
3434
),
3535
base: {
36-
commit: refOid,
36+
commit: refSha,
3737
},
3838
});
3939
};
@@ -142,7 +142,7 @@ export async function getFileChanges(
142142
return { additions, deletions };
143143
}
144144

145-
async function getOidForRef(cwd: string, ref: string): Promise<string | null> {
145+
async function getShaForRef(cwd: string, ref: string): Promise<string | null> {
146146
try {
147147
const { stdout } = await exec("git", ["rev-parse", ref], {
148148
throwOnError: true,

src/interface.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export type CommitFilesResult = {
88
refId: string | null;
99
};
1010

11-
export type GitBase =
11+
export type GitRef =
1212
| {
1313
branch: string;
1414
}
@@ -39,7 +39,7 @@ export interface CommitFilesSharedArgsWithBase extends CommitFilesBasedArgs {
3939
/**
4040
* The current branch, tag or commit that the new branch should be based on.
4141
*/
42-
base: GitBase;
42+
base: GitRef;
4343
}
4444

4545
export interface CommitFilesFromBase64Args extends CommitFilesSharedArgsWithBase {
@@ -69,9 +69,7 @@ export interface CommitChangesFromRepoArgs extends CommitFilesBasedArgs {
6969
*
7070
* @default HEAD
7171
*/
72-
base?: {
73-
commit: string;
74-
};
72+
base?: GitRef;
7573
/**
7674
* Don't require {@link cwd} to be the root of the repository,
7775
* and use it as a starting point to recursively search for the `.git`

src/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { CommitMessage } from "./github/graphql/generated/types.ts";
2+
import type { GitRef } from "./interface.ts";
23

34
export function normalizeCommitMessage(
45
message: string | CommitMessage,
@@ -20,3 +21,13 @@ export function normalizeCommitMessage(
2021
body: bodyLines.join("\n").trim(),
2122
};
2223
}
24+
25+
export function resolveGitRef(ref: GitRef): string {
26+
if ("branch" in ref) {
27+
return `refs/heads/${ref.branch}`;
28+
} else if ("tag" in ref) {
29+
return `refs/tags/${ref.tag}`;
30+
} else {
31+
return ref.commit;
32+
}
33+
}

tests/git.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,86 @@ describe("getFileChanges", () => {
6464
});
6565
});
6666

67+
it("should support branch refs", async () => {
68+
await using fixture = await createFixture({
69+
"a.txt": "Hello, world!",
70+
});
71+
await setupGit(fixture.path);
72+
73+
await exec("git", ["checkout", "-b", "new-branch"], {
74+
nodeOptions: { cwd: fixture.path },
75+
});
76+
await fixture.writeFile("b.txt", "This is a new file!");
77+
78+
const result = await getFileChanges(
79+
fixture.path,
80+
fixture.path,
81+
"refs/heads/new-branch",
82+
);
83+
expect(result).toEqual({
84+
additions: [
85+
{
86+
path: "b.txt",
87+
contents: await fixture.readFile("b.txt", "base64"),
88+
},
89+
],
90+
deletions: [],
91+
});
92+
});
93+
94+
it("should support tag refs", async () => {
95+
await using fixture = await createFixture({
96+
"a.txt": "Hello, world!",
97+
});
98+
await setupGit(fixture.path);
99+
100+
await exec("git", ["tag", "v1.0.0"], {
101+
nodeOptions: { cwd: fixture.path },
102+
});
103+
await fixture.writeFile("b.txt", "This is a new file!");
104+
105+
const result = await getFileChanges(
106+
fixture.path,
107+
fixture.path,
108+
"refs/tags/v1.0.0",
109+
);
110+
expect(result).toEqual({
111+
additions: [
112+
{
113+
path: "b.txt",
114+
contents: await fixture.readFile("b.txt", "base64"),
115+
},
116+
],
117+
deletions: [],
118+
});
119+
});
120+
121+
it("should support commit refs", async () => {
122+
await using fixture = await createFixture({
123+
"a.txt": "Hello, world!",
124+
});
125+
await setupGit(fixture.path);
126+
127+
const commitSha = (
128+
await exec("git", ["rev-parse", "HEAD"], {
129+
nodeOptions: { cwd: fixture.path },
130+
})
131+
).stdout.trim();
132+
133+
await fixture.writeFile("b.txt", "This is a new file!");
134+
135+
const result = await getFileChanges(fixture.path, fixture.path, commitSha);
136+
expect(result).toEqual({
137+
additions: [
138+
{
139+
path: "b.txt",
140+
contents: await fixture.readFile("b.txt", "base64"),
141+
},
142+
],
143+
deletions: [],
144+
});
145+
});
146+
67147
it("should filter files with filterFiles", async () => {
68148
await using fixture = await createFixture({
69149
"foo.txt": "Hello, world!",

0 commit comments

Comments
 (0)