@@ -20,6 +20,8 @@ export interface RepoClientConfig {
2020 token : string ;
2121 owner : string ;
2222 repo : string ;
23+ /** Branch to use for sync (auto-detected if not specified, created if missing) */
24+ branch ?: string ;
2325 maxRetries ?: number ;
2426 retryDelayMs ?: number ;
2527}
@@ -61,7 +63,8 @@ export class RepoStorageBackend implements StorageBackend {
6163 private readonly baseUrl : string ;
6264 private readonly maxRetries : number ;
6365 private readonly retryDelayMs : number ;
64- private detectedBranch : 'main' | 'master' | null = null ;
66+ private readonly configuredBranch : string | undefined ;
67+ private detectedBranch : string | null = null ;
6568
6669 constructor ( config : RepoClientConfig ) {
6770 this . token = config . token ;
@@ -70,6 +73,7 @@ export class RepoStorageBackend implements StorageBackend {
7073 this . baseUrl = `https://api.github.com/repos/${ config . owner } /${ config . repo } ` ;
7174 this . maxRetries = config . maxRetries ?? DEFAULT_MAX_RETRIES ;
7275 this . retryDelayMs = config . retryDelayMs ?? DEFAULT_RETRY_DELAY_MS ;
76+ this . configuredBranch = config . branch ;
7377 }
7478
7579 public async exists ( ) : Promise < boolean > {
@@ -94,7 +98,7 @@ export class RepoStorageBackend implements StorageBackend {
9498
9599 // For files >1MB, GitHub returns empty content and provides download_url
96100 if ( ! data . content && data . size > 1000000 ) {
97- const branch = await this . getDefaultBranch ( ) ;
101+ const branch = await this . getBranch ( ) ;
98102 const downloadUrl = `https://raw.githubusercontent.com/${ this . owner } /${ this . repo } /${ branch } /${ fullPath } ` ;
99103 const downloadRes = await fetchWithRetry (
100104 downloadUrl ,
@@ -187,9 +191,20 @@ export class RepoStorageBackend implements StorageBackend {
187191 }
188192 }
189193
190- private async getDefaultBranch ( ) : Promise < 'main' | 'master' > {
194+ private async getBranch ( ) : Promise < string > {
191195 if ( this . detectedBranch ) return this . detectedBranch ;
192196
197+ // If branch is configured, use it (create if missing)
198+ if ( this . configuredBranch ) {
199+ const branchExists = await this . branchExists ( this . configuredBranch ) ;
200+ if ( ! branchExists ) {
201+ await this . createBranch ( this . configuredBranch ) ;
202+ }
203+ this . detectedBranch = this . configuredBranch ;
204+ return this . configuredBranch ;
205+ }
206+
207+ // Auto-detect: try main first, then master
193208 const res = await this . fetch ( '/git/ref/heads/main' ) ;
194209 if ( res . ok ) {
195210 this . detectedBranch = 'main' ;
@@ -206,8 +221,54 @@ export class RepoStorageBackend implements StorageBackend {
206221 return 'main' ;
207222 }
208223
224+ private async branchExists ( branch : string ) : Promise < boolean > {
225+ const res = await this . fetch ( `/git/ref/heads/${ branch } ` ) ;
226+ return res . ok ;
227+ }
228+
229+ private async createBranch ( branch : string ) : Promise < void > {
230+ // Get SHA from default branch (main or master)
231+ const defaultBranch = await this . detectDefaultBranch ( ) ;
232+ const defaultRes = await this . fetch ( `/git/ref/heads/${ defaultBranch } ` ) ;
233+ if ( ! defaultRes . ok ) {
234+ throw new RepoApiError (
235+ `Cannot find default branch (${ defaultBranch } ) to create new branch from` ,
236+ 404
237+ ) ;
238+ }
239+ const defaultRef = ( await defaultRes . json ( ) ) as GitRef ;
240+ const baseSha = defaultRef . object . sha ;
241+
242+ // Create new branch ref
243+ const body = JSON . stringify ( {
244+ ref : `refs/heads/${ branch } ` ,
245+ sha : baseSha ,
246+ } ) ;
247+
248+ const res = await this . fetch ( '/git/refs' , { method : 'POST' , body } ) ;
249+ if ( ! res . ok ) {
250+ const errBody = ( await res . json ( ) . catch ( ( ) => ( { } ) ) ) as { message ?: string } ;
251+ throw new RepoApiError (
252+ errBody . message ?? `Failed to create branch: ${ branch } ` ,
253+ res . status ,
254+ errBody
255+ ) ;
256+ }
257+ }
258+
259+ /** Detect default branch without creating - used as base for new branches */
260+ private async detectDefaultBranch ( ) : Promise < 'main' | 'master' > {
261+ const res = await this . fetch ( '/git/ref/heads/main' ) ;
262+ if ( res . ok ) return 'main' ;
263+
264+ const masterRes = await this . fetch ( '/git/ref/heads/master' ) ;
265+ if ( masterRes . ok ) return 'master' ;
266+
267+ return 'main' ;
268+ }
269+
209270 private async getHeadSha ( ) : Promise < string > {
210- const branch = await this . getDefaultBranch ( ) ;
271+ const branch = await this . getBranch ( ) ;
211272 const res = await this . fetch ( `/git/ref/heads/${ branch } ` ) ;
212273 if ( ! res . ok ) {
213274 throw new RepoApiError ( `Cannot find ${ branch } branch` , 404 ) ;
@@ -314,7 +375,7 @@ export class RepoStorageBackend implements StorageBackend {
314375 private async updateRef ( commitSha : string ) : Promise < void > {
315376 // force: false ensures proper CAS - GitHub will reject if HEAD moved
316377 const body = JSON . stringify ( { sha : commitSha , force : false } ) ;
317- const branch = await this . getDefaultBranch ( ) ;
378+ const branch = await this . getBranch ( ) ;
318379
319380 const res = await this . fetch ( `/git/refs/heads/${ branch } ` , { method : 'PATCH' , body } ) ;
320381
0 commit comments