11import {
22 createCommitOnBranchQuery ,
3- createRefMutation ,
43 getRepositoryMetadata ,
5- updateRefMutation ,
64} from "./github/graphql/queries.js" ;
7- import type {
8- CreateCommitOnBranchMutationVariables ,
9- GetRepositoryMetadataQuery ,
10- } from "./github/graphql/generated/operations.js" ;
5+ import type { GetRepositoryMetadataQuery } from "./github/graphql/generated/operations.js" ;
116import {
127 CommitFilesFromBase64Args ,
138 CommitFilesResult ,
@@ -21,6 +16,8 @@ const getBaseRef = (base: GitBase): string => {
2116 } else if ( "tag" in base ) {
2217 return `refs/tags/${ base . tag } ` ;
2318 } 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.
2421 return "HEAD" ;
2522 }
2623} ;
@@ -45,6 +42,47 @@ const getOidFromRef = (
4542 return ref . target . oid ;
4643} ;
4744
45+ const isAlreadyExistingRefError = ( error : unknown ) =>
46+ typeof error === "object" &&
47+ error !== null &&
48+ "status" in error &&
49+ "message" in error &&
50+ typeof error . status === "number" &&
51+ typeof error . message === "string" &&
52+ error . status === 422 &&
53+ error . message . includes ( "Reference already exists" ) ;
54+
55+ const createCommit = async ( {
56+ octokit,
57+ refId,
58+ baseOid,
59+ message,
60+ fileChanges,
61+ } : Pick < CommitFilesFromBase64Args , "octokit" | "message" | "fileChanges" > & {
62+ refId : string ;
63+ baseOid : string ;
64+ } ) => {
65+ const normalizedMessage : CommitMessage =
66+ typeof message === "string"
67+ ? {
68+ headline : message . split ( "\n" ) [ 0 ] ?. trim ( ) ?? "" ,
69+ body : message . split ( "\n" ) . slice ( 1 ) . join ( "\n" ) . trim ( ) ,
70+ }
71+ : message ;
72+
73+ // we have to stick to GraphQL here as with REST, each file change would become a separate API call
74+ return createCommitOnBranchQuery ( octokit , {
75+ input : {
76+ branch : {
77+ id : refId ,
78+ } ,
79+ expectedHeadOid : baseOid ,
80+ message : normalizedMessage ,
81+ fileChanges,
82+ } ,
83+ } ) ;
84+ } ;
85+
4886export const commitFilesFromBase64 = async ( {
4987 octokit,
5088 owner,
@@ -70,115 +108,160 @@ export const commitFilesFromBase64 = async ({
70108 log ?. debug ( `Repo info: ${ JSON . stringify ( info , null , 2 ) } ` ) ;
71109
72110 if ( ! info ) {
73- throw new Error ( `Repository ${ repositoryNameWithOwner } not found` ) ;
111+ throw new Error (
112+ `Repository ${ JSON . stringify ( repositoryNameWithOwner ) } not found` ,
113+ ) ;
114+ }
115+ if ( ! ( "commit" in base ) && ! info . baseRef ) {
116+ throw new Error ( `Ref ${ JSON . stringify ( baseRef ) } not found` ) ;
74117 }
75118
76- const repositoryId = info . id ;
119+ const resolvedBaseRef = info . baseRef ;
120+
77121 /**
78122 * The commit oid to base the new commit on.
79123 *
80- * Used both to create / update the new branch (if necessary) ,
81- * and to ensure no changes have been made as we push the new commit .
124+ * Used both to create the new commit ,
125+ * and to determine whether an existing branch can be updated .
82126 */
83127 const baseOid = getOidFromRef ( base , info . baseRef ) ;
128+ const targetOid = info . targetBranch ?. target ?. oid ?? null ;
129+ const sameBranchBase = "branch" in base && base . branch === branch ;
84130
85- let refId : string ;
86-
87- if ( "branch" in base && base . branch === branch ) {
88- log ?. debug ( `Committing to the same branch as base: ${ branch } (${ baseOid } )` ) ;
89- // Get existing branch refId
131+ let mode : "create" | "update" | "force-update" ;
90132
91- if ( ! info . baseRef ) {
92- throw new Error ( `Ref ${ baseRef } not found` ) ;
93- }
94- refId = info . baseRef . id ;
133+ if ( sameBranchBase ) {
134+ mode = force ? "force-update" : "update" ;
135+ } else if ( targetOid === null ) {
136+ // TODO: legit *creation* failure should be retried if `force === true`
137+ mode = "create" ;
138+ } else if ( force ) {
139+ mode = "force-update" ;
140+ } else if ( targetOid === baseOid ) {
141+ mode = "update" ;
95142 } else {
96- // Determine if the branch needs to be created or not
97- if ( info . targetBranch ?. target ?. oid ) {
98- // Branch already exists, check if it matches the base
99- if ( info . targetBranch . target . oid !== baseOid ) {
100- if ( force ) {
101- log ?. debug (
102- `Branch ${ branch } exists but does not match base ${ baseOid } , forcing update to base` ,
103- ) ;
104- const refIdUpdate = await updateRefMutation ( octokit , {
105- input : {
106- refId : info . targetBranch . id ,
107- oid : baseOid ,
108- force : true ,
109- } ,
110- } ) ;
111-
112- log ?. debug (
113- `Updated branch with refId ${ JSON . stringify ( refIdUpdate , null , 2 ) } ` ,
114- ) ;
115-
116- const refIdStr = refIdUpdate . updateRef ?. ref ?. id ;
117-
118- if ( ! refIdStr ) {
119- throw new Error ( `Failed to create branch ${ branch } ` ) ;
120- }
121-
122- refId = refIdStr ;
123- } else {
124- throw new Error (
125- `Branch ${ branch } exists already and does not match base ${ baseOid } , force is set to false` ,
126- ) ;
127- }
128- } else {
129- log ?. debug (
130- `Branch ${ branch } already exists and matches base ${ baseOid } ` ,
131- ) ;
132- refId = info . targetBranch . id ;
133- }
134- } else {
135- // Create branch as it does not exist yet
136- log ?. debug ( `Creating branch ${ branch } from commit ${ baseOid } }` ) ;
137- const refIdCreation = await createRefMutation ( octokit , {
138- input : {
139- repositoryId,
140- name : `refs/heads/${ branch } ` ,
141- oid : baseOid ,
142- } ,
143+ throw new Error (
144+ `Branch ${ branch } exists already and does not match base ${ baseOid } , force is set to false` ,
145+ ) ;
146+ }
147+
148+ if ( mode === "force-update" ) {
149+ // Use a stable temp branch name so a later run can recover and reuse it
150+ // if an earlier run failed before cleanup completed.
151+ const tempBranch = `changesets-ghcommit-temp/${ branch } ` ;
152+
153+ let tempRefId : string ;
154+
155+ try {
156+ const createdTempRef = await octokit . rest . git . createRef ( {
157+ owner,
158+ repo,
159+ ref : `refs/heads/${ tempBranch } ` ,
160+ sha : baseOid ,
143161 } ) ;
144162
145- log ?. debug (
146- `Created branch with refId ${ JSON . stringify ( refIdCreation , null , 2 ) } ` ,
147- ) ;
163+ const refIdStr = createdTempRef . data . node_id ;
148164
149- const refIdStr = refIdCreation . createRef ?. ref ?. id ;
165+ if ( ! refIdStr ) {
166+ throw new Error ( `Failed to create temporary branch ${ tempBranch } ` ) ;
167+ }
168+
169+ tempRefId = refIdStr ;
170+ } catch ( error ) {
171+ if ( ! isAlreadyExistingRefError ( error ) ) {
172+ throw error ;
173+ }
174+
175+ const updatedTempRef = await octokit . rest . git . updateRef ( {
176+ owner,
177+ repo,
178+ ref : `heads/${ tempBranch } ` ,
179+ sha : baseOid ,
180+ force : true ,
181+ } ) ;
182+
183+ const refIdStr = updatedTempRef . data . node_id ;
150184
151185 if ( ! refIdStr ) {
152- throw new Error ( `Failed to create branch ${ branch } ` ) ;
186+ throw new Error ( `Failed to update temporary branch ${ tempBranch } ` ) ;
153187 }
154188
155- refId = refIdStr ;
189+ tempRefId = refIdStr ;
190+ }
191+
192+ log ?. debug ( `Creating commit on branch ${ tempBranch } ` ) ;
193+ const tempCommit = await createCommit ( {
194+ octokit,
195+ refId : tempRefId ,
196+ baseOid,
197+ message,
198+ fileChanges,
199+ } ) ;
200+
201+ const tempHeadOid = tempCommit . createCommitOnBranch ?. commit ?. oid ;
202+
203+ if ( ! tempHeadOid ) {
204+ throw new Error (
205+ `Failed to determine head commit of temporary branch ${ tempBranch } ` ,
206+ ) ;
156207 }
208+
209+ const updatedTargetRef = await octokit . rest . git . updateRef ( {
210+ owner,
211+ repo,
212+ ref : `heads/${ branch } ` ,
213+ sha : tempHeadOid ,
214+ force : true ,
215+ } ) ;
216+
217+ const updatedTargetRefId = updatedTargetRef . data . node_id ;
218+
219+ if ( ! updatedTargetRefId ) {
220+ throw new Error ( `Failed to update branch ${ branch } ` ) ;
221+ }
222+
223+ await octokit . rest . git . deleteRef ( {
224+ owner,
225+ repo,
226+ ref : `heads/${ tempBranch } ` ,
227+ } ) ;
228+
229+ return {
230+ refId : updatedTargetRefId ,
231+ } ;
157232 }
158233
159- const finalMessage : CommitMessage =
160- typeof message === "string"
161- ? {
162- headline : message . split ( "\n" ) [ 0 ] ?. trim ( ) ?? "" ,
163- body : message . split ( "\n" ) . slice ( 1 ) . join ( "\n" ) . trim ( ) ,
164- }
165- : message ;
234+ let refId : string ;
235+
236+ if ( mode === "create" ) {
237+ const createdRef = await octokit . rest . git . createRef ( {
238+ owner,
239+ repo,
240+ ref : `refs/heads/${ branch } ` ,
241+ sha : baseOid ,
242+ } ) ;
243+
244+ const refIdStr = createdRef . data . node_id ;
245+
246+ if ( ! refIdStr ) {
247+ throw new Error ( `Failed to create branch ${ branch } ` ) ;
248+ }
249+
250+ refId = refIdStr ;
251+ } else {
252+ refId = sameBranchBase ? resolvedBaseRef ! . id : info . targetBranch ! . id ;
253+ }
166254
167255 log ?. debug ( `Creating commit on branch ${ branch } ` ) ;
168- const createCommitMutation : CreateCommitOnBranchMutationVariables = {
169- input : {
170- branch : {
171- id : refId ,
172- } ,
173- expectedHeadOid : baseOid ,
174- message : finalMessage ,
175- fileChanges,
176- } ,
177- } ;
178- log ?. debug ( JSON . stringify ( createCommitMutation , null , 2 ) ) ;
256+ const newCommit = await createCommit ( {
257+ octokit,
258+ refId,
259+ baseOid,
260+ message,
261+ fileChanges,
262+ } ) ;
179263
180- const result = await createCommitOnBranchQuery ( octokit , createCommitMutation ) ;
181264 return {
182- refId : result . createCommitOnBranch ?. ref ?. id ?? null ,
265+ refId : newCommit . createCommitOnBranch ?. ref ?. id ?? null ,
183266 } ;
184267} ;
0 commit comments