@@ -233,6 +233,181 @@ describe('ref', () => {
233233
234234 expect ( ref ) . toEqual ( 'refs/heads/test' ) ;
235235 } ) ;
236+
237+ it ( 'returns mocked detached branch ref checked out by SHA' , async ( ) => {
238+ jest . spyOn ( Exec , 'getExecOutput' ) . mockImplementation ( ( cmd , args ) : Promise < ExecOutput > => {
239+ const fullCmd = `${ cmd } ${ args ?. join ( ' ' ) } ` ;
240+ let result = '' ;
241+ switch ( fullCmd ) {
242+ case 'git branch --show-current' :
243+ result = '' ;
244+ break ;
245+ case 'git show -s --pretty=%D' :
246+ result = 'HEAD, origin/feature-branch' ;
247+ break ;
248+ }
249+ return Promise . resolve ( {
250+ stdout : result ,
251+ stderr : '' ,
252+ exitCode : 0
253+ } ) ;
254+ } ) ;
255+
256+ const ref = await Git . ref ( ) ;
257+
258+ expect ( ref ) . toEqual ( 'refs/heads/feature-branch' ) ;
259+ } ) ;
260+
261+ it ( 'infers ref from local branch when detached HEAD returns only "HEAD"' , async ( ) => {
262+ jest . spyOn ( Exec , 'getExecOutput' ) . mockImplementation ( ( cmd , args ) : Promise < ExecOutput > => {
263+ const fullCmd = `${ cmd } ${ args ?. join ( ' ' ) } ` ;
264+ let result = '' ;
265+ switch ( fullCmd ) {
266+ case 'git branch --show-current' :
267+ result = '' ;
268+ break ;
269+ case 'git show -s --pretty=%D' :
270+ result = 'HEAD' ;
271+ break ;
272+ case 'git for-each-ref --format=%(refname) --contains HEAD --sort=-committerdate refs/heads/' :
273+ result = 'refs/heads/main\nrefs/heads/develop' ;
274+ break ;
275+ }
276+ return Promise . resolve ( {
277+ stdout : result ,
278+ stderr : '' ,
279+ exitCode : 0
280+ } ) ;
281+ } ) ;
282+
283+ const ref = await Git . ref ( ) ;
284+
285+ expect ( ref ) . toEqual ( 'refs/heads/main' ) ;
286+ } ) ;
287+
288+ it ( 'infers ref from remote branch when no local branch contains HEAD' , async ( ) => {
289+ jest . spyOn ( Exec , 'getExecOutput' ) . mockImplementation ( ( cmd , args ) : Promise < ExecOutput > => {
290+ const fullCmd = `${ cmd } ${ args ?. join ( ' ' ) } ` ;
291+ let result = '' ;
292+ switch ( fullCmd ) {
293+ case 'git branch --show-current' :
294+ result = '' ;
295+ break ;
296+ case 'git show -s --pretty=%D' :
297+ result = 'HEAD' ;
298+ break ;
299+ case 'git for-each-ref --format=%(refname) --contains HEAD --sort=-committerdate refs/heads/' :
300+ result = '' ;
301+ break ;
302+ case 'git for-each-ref --format=%(refname) --contains HEAD --sort=-committerdate refs/remotes/' :
303+ result = 'refs/remotes/origin/feature' ;
304+ break ;
305+ }
306+ return Promise . resolve ( {
307+ stdout : result ,
308+ stderr : '' ,
309+ exitCode : 0
310+ } ) ;
311+ } ) ;
312+
313+ const ref = await Git . ref ( ) ;
314+
315+ expect ( ref ) . toEqual ( 'refs/heads/feature' ) ;
316+ } ) ;
317+
318+ it ( 'infers ref from tag when no branch contains HEAD' , async ( ) => {
319+ jest . spyOn ( Exec , 'getExecOutput' ) . mockImplementation ( ( cmd , args ) : Promise < ExecOutput > => {
320+ const fullCmd = `${ cmd } ${ args ?. join ( ' ' ) } ` ;
321+ let result = '' ;
322+ switch ( fullCmd ) {
323+ case 'git branch --show-current' :
324+ result = '' ;
325+ break ;
326+ case 'git show -s --pretty=%D' :
327+ result = 'HEAD' ;
328+ break ;
329+ case 'git for-each-ref --format=%(refname) --contains HEAD --sort=-committerdate refs/heads/' :
330+ result = '' ;
331+ break ;
332+ case 'git for-each-ref --format=%(refname) --contains HEAD --sort=-committerdate refs/remotes/' :
333+ result = '' ;
334+ break ;
335+ case 'git tag --contains HEAD' :
336+ result = 'v1.0.0\nv0.9.0' ;
337+ break ;
338+ }
339+ return Promise . resolve ( {
340+ stdout : result ,
341+ stderr : '' ,
342+ exitCode : 0
343+ } ) ;
344+ } ) ;
345+
346+ const ref = await Git . ref ( ) ;
347+
348+ expect ( ref ) . toEqual ( 'refs/tags/v1.0.0' ) ;
349+ } ) ;
350+
351+ it ( 'throws error when cannot infer ref from detached HEAD' , async ( ) => {
352+ jest . spyOn ( Exec , 'getExecOutput' ) . mockImplementation ( ( cmd , args ) : Promise < ExecOutput > => {
353+ const fullCmd = `${ cmd } ${ args ?. join ( ' ' ) } ` ;
354+ let result = '' ;
355+ switch ( fullCmd ) {
356+ case 'git branch --show-current' :
357+ result = '' ;
358+ break ;
359+ case 'git show -s --pretty=%D' :
360+ result = 'HEAD' ;
361+ break ;
362+ case 'git for-each-ref --format=%(refname) --contains HEAD --sort=-committerdate refs/heads/' :
363+ result = '' ;
364+ break ;
365+ case 'git for-each-ref --format=%(refname) --contains HEAD --sort=-committerdate refs/remotes/' :
366+ result = '' ;
367+ break ;
368+ case 'git tag --contains HEAD' :
369+ result = '' ;
370+ break ;
371+ }
372+ return Promise . resolve ( {
373+ stdout : result ,
374+ stderr : '' ,
375+ exitCode : 0
376+ } ) ;
377+ } ) ;
378+
379+ await expect ( Git . ref ( ) ) . rejects . toThrow ( 'Cannot infer ref from detached HEAD' ) ;
380+ } ) ;
381+
382+ it ( 'handles remote ref without branch pattern when inferring from remote' , async ( ) => {
383+ jest . spyOn ( Exec , 'getExecOutput' ) . mockImplementation ( ( cmd , args ) : Promise < ExecOutput > => {
384+ const fullCmd = `${ cmd } ${ args ?. join ( ' ' ) } ` ;
385+ let result = '' ;
386+ switch ( fullCmd ) {
387+ case 'git branch --show-current' :
388+ result = '' ;
389+ break ;
390+ case 'git show -s --pretty=%D' :
391+ result = 'HEAD' ;
392+ break ;
393+ case 'git for-each-ref --format=%(refname) --contains HEAD --sort=-committerdate refs/heads/' :
394+ result = '' ;
395+ break ;
396+ case 'git for-each-ref --format=%(refname) --contains HEAD --sort=-committerdate refs/remotes/' :
397+ result = 'refs/remotes/unusual-format' ;
398+ break ;
399+ }
400+ return Promise . resolve ( {
401+ stdout : result ,
402+ stderr : '' ,
403+ exitCode : 0
404+ } ) ;
405+ } ) ;
406+
407+ const ref = await Git . ref ( ) ;
408+
409+ expect ( ref ) . toEqual ( 'refs/remotes/unusual-format' ) ;
410+ } ) ;
236411} ) ;
237412
238413describe ( 'fullCommit' , ( ) => {
0 commit comments