@@ -7,6 +7,7 @@ interface PendingGitRequest {
77 headers : Record < string , string > ;
88 bodyBase64 : string | null ;
99 incomingRefs : string [ ] ;
10+ allRelayRefs : string [ ] ;
1011 resolve : ( response : Response ) => void ;
1112 timeoutId : ReturnType < typeof setTimeout > ;
1213 createdAt : number ;
@@ -28,6 +29,10 @@ export interface GitServeSessionState {
2829const REQUEST_TIMEOUT_MS = 60_000 ;
2930const POLL_TIMEOUT_MS = 30_000 ;
3031export const DEFAULT_SESSION_TTL_MS = 24 * 60 * 60 * 1000 ;
32+ const RECEIVE_PACK_DISABLED_MESSAGE = 'receive-pack not enabled' ;
33+ const ZERO_OID = '0000000000000000000000000000000000000000' ;
34+ const textEncoder = new TextEncoder ( ) ;
35+ const textDecoder = new TextDecoder ( ) ;
3136
3237export interface GitServeSessionOptions {
3338 sessionTtlMs ?: number ;
@@ -38,6 +43,7 @@ export interface GitServeSessionOptions {
3843}
3944
4045const INCOMING_REF_PATTERN = / r e f s \/ r e l a y \/ i n c o m i n g \/ [ A - Z a - z 0 - 9 . _ / - ] { 1 , 255 } / g;
46+ const RELAY_REF_PATTERN = / r e f s \/ [ A - Z a - z 0 - 9 . _ / - ] { 1 , 255 } / g;
4147
4248function generateRequestId ( ) : string {
4349 return crypto . randomUUID ( ) ;
@@ -71,7 +77,7 @@ function fromBase64(b64: string): Uint8Array {
7177}
7278
7379function extractIncomingRefs ( bodyBytes : Uint8Array ) : string [ ] {
74- const decoded = new TextDecoder ( ) . decode ( bodyBytes ) ;
80+ const decoded = textDecoder . decode ( bodyBytes ) ;
7581 const matches = decoded . match ( INCOMING_REF_PATTERN ) ;
7682 if ( ! matches || matches . length === 0 ) return [ ] ;
7783 const seen = new Set < string > ( ) ;
@@ -84,10 +90,88 @@ function extractIncomingRefs(bodyBytes: Uint8Array): string[] {
8490 return refs ;
8591}
8692
93+ function extractAllRelayRefs ( bodyBytes : Uint8Array ) : string [ ] {
94+ const decoded = textDecoder . decode ( bodyBytes ) ;
95+ const matches = decoded . match ( RELAY_REF_PATTERN ) ;
96+ if ( ! matches || matches . length === 0 ) return [ ] ;
97+ const seen = new Set < string > ( ) ;
98+ const refs : string [ ] = [ ] ;
99+ for ( const ref of matches ) {
100+ if ( seen . has ( ref ) ) continue ;
101+ seen . add ( ref ) ;
102+ refs . push ( ref ) ;
103+ }
104+ return refs ;
105+ }
106+
87107function isSuccessfulHttpStatus ( status : number ) : boolean {
88108 return status >= 200 && status < 300 ;
89109}
90110
111+ function toArrayBuffer ( bytes : Uint8Array ) : ArrayBuffer {
112+ const copied = new Uint8Array ( bytes . byteLength ) ;
113+ copied . set ( bytes ) ;
114+ return copied . buffer ;
115+ }
116+
117+ function toPktLine ( payload : string ) : string {
118+ const length = textEncoder . encode ( payload ) . byteLength + 4 ;
119+ return length . toString ( 16 ) . padStart ( 4 , '0' ) + payload ;
120+ }
121+
122+ function buildReceivePackAdvertisementBody ( ) : ArrayBuffer {
123+ const capabilities = [
124+ 'report-status' ,
125+ 'report-status-v2' ,
126+ 'delete-refs' ,
127+ 'ofs-delta' ,
128+ 'atomic' ,
129+ 'quiet' ,
130+ ] ;
131+ const firstRef = `${ ZERO_OID } capabilities^{}\0${ capabilities . join ( ' ' ) } \n` ;
132+ const body = `${ toPktLine ( '# service=git-receive-pack\n' ) } 0000${ toPktLine ( firstRef ) } 0000` ;
133+ return toArrayBuffer ( textEncoder . encode ( body ) ) ;
134+ }
135+
136+ function buildReceivePackResultBody ( incomingRefs : string [ ] ) : ArrayBuffer {
137+ let body = toPktLine ( 'unpack ok\n' ) ;
138+ for ( const ref of incomingRefs ) {
139+ body += toPktLine ( `ok ${ ref } \n` ) ;
140+ }
141+ body += '0000' ;
142+ return toArrayBuffer ( textEncoder . encode ( body ) ) ;
143+ }
144+
145+ function splitPath ( pathWithQuery : string ) : { pathname : string ; search : string } {
146+ const [ pathname , search = '' ] = pathWithQuery . split ( '?' , 2 ) ;
147+ return { pathname, search } ;
148+ }
149+
150+ function isReceivePackInfoRefsPath ( pathWithQuery : string ) : boolean {
151+ const { pathname, search } = splitPath ( pathWithQuery ) ;
152+ if ( pathname !== '/info/refs' ) return false ;
153+ const params = new URLSearchParams ( search ) ;
154+ return params . get ( 'service' ) === 'git-receive-pack' ;
155+ }
156+
157+ function isReceivePackRpcPath ( pathWithQuery : string ) : boolean {
158+ const { pathname } = splitPath ( pathWithQuery ) ;
159+ return pathname === '/git-receive-pack' ;
160+ }
161+
162+ function shouldCompatAcceptIncomingOnlyPush ( pending : PendingGitRequest ) : boolean {
163+ if ( ! isReceivePackRpcPath ( pending . path ) ) return false ;
164+ if ( pending . incomingRefs . length === 0 ) return false ;
165+ if ( pending . allRelayRefs . length === 0 ) return false ;
166+ return pending . allRelayRefs . every ( ( ref ) => ref . startsWith ( 'refs/relay/incoming/' ) ) ;
167+ }
168+
169+ function isReceivePackDisabledBody ( responseBody : ArrayBuffer | null ) : boolean {
170+ if ( ! responseBody ) return false ;
171+ const bodyText = textDecoder . decode ( new Uint8Array ( responseBody ) ) . trim ( ) . toLowerCase ( ) ;
172+ return bodyText === RECEIVE_PACK_DISABLED_MESSAGE ;
173+ }
174+
91175export interface PersistableSessionState {
92176 active : boolean ;
93177 sessionToken : string ;
@@ -230,6 +314,10 @@ export function createGitServeSession(options?: GitServeSessionOptions): {
230314 bodyBytes !== null
231315 ? extractIncomingRefs ( bodyBytes )
232316 : [ ] ;
317+ const allRelayRefs = request . method === 'POST' && gitPath . endsWith ( '/git-receive-pack' ) &&
318+ bodyBytes !== null
319+ ? extractAllRelayRefs ( bodyBytes )
320+ : [ ] ;
233321
234322 const headers : Record < string , string > = { } ;
235323 for ( const [ key , value ] of request . headers . entries ( ) ) {
@@ -267,6 +355,7 @@ export function createGitServeSession(options?: GitServeSessionOptions): {
267355 headers,
268356 bodyBase64,
269357 incomingRefs,
358+ allRelayRefs,
270359 resolve,
271360 timeoutId,
272361 createdAt : Date . now ( ) ,
@@ -382,10 +471,28 @@ export function createGitServeSession(options?: GitServeSessionOptions): {
382471 }
383472 }
384473
385- if ( pending . incomingRefs . length > 0 && isSuccessfulHttpStatus ( status ) ) {
474+ let finalStatus = status ;
475+ let finalHeaders = httpHeaders ;
476+ let finalBody = responseBody ;
477+ const receivePackDisabled = status === 403 && isReceivePackDisabledBody ( responseBody ) ;
478+ if ( receivePackDisabled && isReceivePackInfoRefsPath ( pending . path ) ) {
479+ finalStatus = 200 ;
480+ finalHeaders = new Headers ( httpHeaders ) ;
481+ finalHeaders . set ( 'content-type' , 'application/x-git-receive-pack-advertisement' ) ;
482+ finalHeaders . set ( 'cache-control' , 'no-cache' ) ;
483+ finalBody = buildReceivePackAdvertisementBody ( ) ;
484+ } else if ( receivePackDisabled && shouldCompatAcceptIncomingOnlyPush ( pending ) ) {
485+ finalStatus = 200 ;
486+ finalHeaders = new Headers ( httpHeaders ) ;
487+ finalHeaders . set ( 'content-type' , 'application/x-git-receive-pack-result' ) ;
488+ finalHeaders . set ( 'cache-control' , 'no-cache' ) ;
489+ finalBody = buildReceivePackResultBody ( pending . incomingRefs ) ;
490+ }
491+
492+ if ( pending . incomingRefs . length > 0 && isSuccessfulHttpStatus ( finalStatus ) ) {
386493 emitIncomingRefEvents ( pending . incomingRefs ) ;
387494 }
388- pending . resolve ( new Response ( responseBody , { status, headers : httpHeaders } ) ) ;
495+ pending . resolve ( new Response ( finalBody , { status : finalStatus , headers : finalHeaders } ) ) ;
389496
390497 return Response . json ( { ok : true } ) ;
391498 }
0 commit comments