@@ -9,53 +9,10 @@ import type {
99} from "./interface.ts" ;
1010import { normalizeCommitMessage , resolveGitRef } from "./utils.ts" ;
1111
12- function getBaseRefSha (
13- baseRef : NonNullable < GetRepositoryMetadataQuery [ "repository" ] > [ "baseRef" ] ,
14- ) {
15- if ( ! baseRef ?. target ) return null ;
16-
17- if ( "target" in baseRef . target ) {
18- return baseRef . target . target . oid ;
19- }
20-
21- return baseRef . target . oid ;
22- }
23-
24- function isAlreadyExistingRefError ( error : unknown ) {
25- return (
26- typeof error === "object" &&
27- error !== null &&
28- "status" in error &&
29- "message" in error &&
30- typeof error . status === "number" &&
31- typeof error . message === "string" &&
32- error . status === 422 &&
33- error . message . includes ( "Reference already exists" )
34- ) ;
35- }
36-
37- async function createCommit ( {
38- octokit,
39- refId,
40- baseSha,
41- message,
42- fileChanges,
43- } : Pick < CommitFilesFromBase64Args , "octokit" | "message" | "fileChanges" > & {
44- refId : string ;
45- baseSha : string ;
46- } ) {
47- // we have to stick to GraphQL here as with REST, each file change would become a separate API call
48- return createCommitOnBranchQuery ( octokit , {
49- input : {
50- branch : {
51- id : refId ,
52- } ,
53- expectedHeadOid : baseSha ,
54- message : normalizeCommitMessage ( message ) ,
55- fileChanges,
56- } ,
57- } ) ;
58- }
12+ type CreateCommit = (
13+ refId : string ,
14+ branch : string ,
15+ ) => Promise < { commitSha : string } > ;
5916
6017export async function commitFilesFromBase64 ( {
6118 octokit,
@@ -68,164 +25,198 @@ export async function commitFilesFromBase64({
6825 fileChanges,
6926} : CommitFilesFromBase64Args ) : Promise < CommitFilesResult > {
7027 const baseRef = resolveGitRef ( base ) ;
71- const targetRef = `refs/heads/${ branch } ` ;
7228
7329 const info = await getRepositoryMetadata ( octokit , {
7430 owner,
7531 repo,
7632 baseRef,
77- targetRef,
33+ targetRef : `refs/heads/ ${ branch } ` ,
7834 } ) ;
79-
8035 if ( ! info ) {
8136 throw new Error ( `Repository "${ owner } /${ repo } " not found` ) ;
8237 }
8338
84- /**
85- * The commit sha to base the new commit on.
86- *
87- * Used both to create the new commit,
88- * and to determine whether an existing branch can be updated.
89- */
39+ // Commits will base off this sha as the parent. Also used to check if this
40+ // is the same as the target branch's HEAD so we can safely commit to it.
9041 const baseSha = "commit" in base ? base . commit : getBaseRefSha ( info . baseRef ) ;
9142 if ( ! baseSha ) {
9243 throw new Error ( `Could not determine sha for base ref "${ baseRef } "` ) ;
9344 }
9445 const targetSha = info . targetBranch ?. target ?. oid ?? null ;
95- const sameBranchBase = "branch" in base && base . branch === branch ;
96-
97- let mode : "create" | "update" | "force-update" ;
98-
99- if ( sameBranchBase ) {
100- mode = force ? "force-update" : "update" ;
101- } else if ( targetSha === null ) {
102- // TODO: legit *creation* failure should be retried if `force === true`
103- mode = "create" ;
104- } else if ( force ) {
105- mode = "force-update" ;
106- } else if ( targetSha === baseSha ) {
107- mode = "update" ;
108- } else {
109- throw new Error (
110- `Branch ${ branch } exists already and does not match base ${ baseSha } , force is set to false` ,
111- ) ;
46+
47+ const createCommit : CreateCommit = async ( refId , branch ) => {
48+ // Use GraphQL because REST would require each non-text file change to be a
49+ // separate `createBlob` call. While in most cases users would commit only
50+ // text files, it's hard to guarantee that and it's simpler to keep the API
51+ // as only accepting base64 content.
52+ const result = await createCommitOnBranchQuery ( octokit , {
53+ input : {
54+ branch : { id : refId } ,
55+ expectedHeadOid : baseSha ,
56+ message : normalizeCommitMessage ( message ) ,
57+ fileChanges,
58+ } ,
59+ } ) ;
60+ // `ref.id` is the same as `refId`. We only use `ref.id` to verify the commit succeeded
61+ if ( result . createCommitOnBranch ?. ref ?. id == null ) {
62+ throw new Error ( `Failed to create commit on branch "${ branch } "` ) ;
63+ }
64+ if ( result . createCommitOnBranch ?. commit ?. oid == null ) {
65+ throw new Error (
66+ `Failed to determine commit sha for commit on branch "${ branch } "` ,
67+ ) ;
68+ }
69+ return { commitSha : result . createCommitOnBranch . commit . oid } ;
70+ } ;
71+
72+ // [CREATE] If the branch does not exist, create and commit to it
73+ if ( targetSha == null ) {
74+ const createdRef = await octokit . rest . git . createRef ( {
75+ owner,
76+ repo,
77+ ref : `refs/heads/${ branch } ` ,
78+ sha : baseSha ,
79+ } ) ;
80+ const createdRefId = createdRef . data . node_id ;
81+ if ( ! createdRefId ) {
82+ throw new Error ( `Failed to create branch "${ branch } "` ) ;
83+ }
84+
85+ await createCommit ( createdRefId , branch ) ;
86+
87+ return { refId : createdRefId } ;
11288 }
89+ // [UPDATE] If the branch exists and its HEAD matches the base, we can safely
90+ // directly commit to it
91+ else if ( targetSha === baseSha ) {
92+ // Safety: `targetSha` already ensures that `targetBranch` exists
93+ const targetRefId = info . targetBranch ! . id ;
11394
114- if ( mode === "force-update" ) {
115- // Use a stable temp branch name so a later run can recover and reuse it
116- // if an earlier run failed before cleanup completed.
117- const tempBranch = `changesets-ghcommit-temp/${ branch } ` ;
95+ await createCommit ( targetRefId , branch ) ;
11896
119- let tempRefId : string ;
97+ return { refId : targetRefId } ;
98+ }
99+ // [FORCE UPDATE] If the branch exists but its HEAD does not match the base,
100+ // we can only update it if `force` is true, which works like a force push.
101+ //
102+ // FORCE UPDATE creates a temporary branch, commits to it and returns the
103+ // sha, and then force updates the existing branch with the new sha. We cannot
104+ // reset the branch and then commit because if the branch has an existing PR,
105+ // GitHub will auto-close as it sees there's no changes with the base.
106+ else if ( force ) {
107+ const tempBranch = `changesets-ghcommit-temp/${ branch } ` ;
120108
121109 try {
122- const createdTempRef = await octokit . rest . git . createRef ( {
110+ const { tempRefId } = await createOrForceUpdateTemporaryBranch ( {
111+ octokit,
123112 owner,
124113 repo,
125- ref : `refs/heads/ ${ tempBranch } ` ,
126- sha : baseSha ,
114+ tempBranch,
115+ baseSha,
127116 } ) ;
128117
129- const refIdStr = createdTempRef . data . node_id ;
130-
131- if ( ! refIdStr ) {
132- throw new Error ( `Failed to create temporary branch ${ tempBranch } ` ) ;
133- }
134-
135- tempRefId = refIdStr ;
136- } catch ( error ) {
137- if ( ! isAlreadyExistingRefError ( error ) ) {
138- throw error ;
139- }
118+ const { commitSha } = await createCommit ( tempRefId , tempBranch ) ;
140119
141- const updatedTempRef = await octokit . rest . git . updateRef ( {
120+ const updatedRef = await octokit . rest . git . updateRef ( {
142121 owner,
143122 repo,
144- ref : `heads/${ tempBranch } ` ,
145- sha : baseSha ,
123+ ref : `heads/${ branch } ` ,
124+ sha : commitSha ,
146125 force : true ,
147126 } ) ;
148-
149- const refIdStr = updatedTempRef . data . node_id ;
150-
151- if ( ! refIdStr ) {
152- throw new Error ( `Failed to update temporary branch ${ tempBranch } ` ) ;
127+ const updatedRefId = updatedRef . data . node_id ;
128+ if ( ! updatedRefId ) {
129+ throw new Error ( `Failed to force update branch "${ branch } "` ) ;
153130 }
154131
155- tempRefId = refIdStr ;
132+ return { refId : updatedRefId } ;
133+ } finally {
134+ // Clean up the temporary branch
135+ await octokit . rest . git . deleteRef ( {
136+ owner,
137+ repo,
138+ ref : `heads/${ tempBranch } ` ,
139+ } ) ;
156140 }
141+ }
142+ // [ERROR] If the branch exists but its HEAD does not match the base, and
143+ // `force` isn't true, we cannot commit to it. Throw an error.
144+ else {
145+ throw new Error (
146+ `Branch "${ branch } " exists but its HEAD does not match the base ${ baseSha } and \`force\` is set to false` ,
147+ ) ;
148+ }
149+ }
157150
158- const tempCommit = await createCommit ( {
159- octokit,
160- refId : tempRefId ,
161- baseSha,
162- message,
163- fileChanges,
164- } ) ;
151+ function getBaseRefSha (
152+ baseRef : NonNullable < GetRepositoryMetadataQuery [ "repository" ] > [ "baseRef" ] ,
153+ ) {
154+ if ( ! baseRef ?. target ) return null ;
165155
166- const tempHeadSha = tempCommit . createCommitOnBranch ?. commit ?. oid ;
156+ if ( "target" in baseRef . target ) {
157+ return baseRef . target . target . oid ;
158+ }
167159
168- if ( ! tempHeadSha ) {
169- throw new Error (
170- `Failed to determine head commit of temporary branch ${ tempBranch } ` ,
171- ) ;
172- }
160+ return baseRef . target . oid ;
161+ }
173162
174- const updatedTargetRef = await octokit . rest . git . updateRef ( {
163+ async function createOrForceUpdateTemporaryBranch ( {
164+ octokit,
165+ owner,
166+ repo,
167+ tempBranch,
168+ baseSha,
169+ } : Pick < CommitFilesFromBase64Args , "octokit" | "owner" | "repo" > & {
170+ tempBranch : string ;
171+ baseSha : string ;
172+ } ) {
173+ try {
174+ const createdTempRef = await octokit . rest . git . createRef ( {
175175 owner,
176176 repo,
177- ref : `heads/${ branch } ` ,
178- sha : tempHeadSha ,
179- force : true ,
177+ ref : `refs/heads/${ tempBranch } ` ,
178+ sha : baseSha ,
180179 } ) ;
181180
182- const updatedTargetRefId = updatedTargetRef . data . node_id ;
181+ const createdTempRefId = createdTempRef . data . node_id ;
182+ if ( ! createdTempRefId ) {
183+ throw new Error ( `Failed to create temporary branch "${ tempBranch } "` ) ;
184+ }
183185
184- if ( ! updatedTargetRefId ) {
185- throw new Error ( `Failed to update branch ${ branch } ` ) ;
186+ return { tempRefId : createdTempRefId } ;
187+ } catch ( error ) {
188+ if ( ! isAlreadyExistingRefError ( error ) ) {
189+ throw error ;
186190 }
187191
188- await octokit . rest . git . deleteRef ( {
192+ const updatedTempRef = await octokit . rest . git . updateRef ( {
189193 owner,
190194 repo,
191195 ref : `heads/${ tempBranch } ` ,
192- } ) ;
193-
194- return {
195- refId : updatedTargetRefId ,
196- } ;
197- }
198-
199- let refId : string ;
200-
201- if ( mode === "create" ) {
202- const createdRef = await octokit . rest . git . createRef ( {
203- owner,
204- repo,
205- ref : `refs/heads/${ branch } ` ,
206196 sha : baseSha ,
197+ force : true ,
207198 } ) ;
208199
209- const refIdStr = createdRef . data . node_id ;
210-
211- if ( ! refIdStr ) {
212- throw new Error ( `Failed to create branch ${ branch } ` ) ;
200+ const updatedTempRefId = updatedTempRef . data . node_id ;
201+ if ( ! updatedTempRefId ) {
202+ throw new Error (
203+ `Failed to force update temporary branch "${ tempBranch } "` ,
204+ ) ;
213205 }
214206
215- refId = refIdStr ;
216- } else {
217- refId = sameBranchBase ? info . baseRef ! . id : info . targetBranch ! . id ;
207+ return { tempRefId : updatedTempRefId } ;
218208 }
209+ }
219210
220- const newCommit = await createCommit ( {
221- octokit ,
222- refId ,
223- baseSha ,
224- message ,
225- fileChanges ,
226- } ) ;
227-
228- return {
229- refId : newCommit . createCommitOnBranch ?. ref ?. id ?? null ,
230- } ;
211+ function isAlreadyExistingRefError ( error : unknown ) {
212+ return (
213+ typeof error === "object" &&
214+ error !== null &&
215+ "status" in error &&
216+ "message" in error &&
217+ typeof error . status === "number" &&
218+ typeof error . message === "string" &&
219+ error . status === 422 &&
220+ error . message . includes ( "Reference already exists" )
221+ ) ;
231222}
0 commit comments