@@ -24,18 +24,27 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2424import { resetEnv , setEnv } from '@socketsecurity/lib/env/rewire'
2525
2626import {
27+ detectDefaultBranch ,
2728 getBaseBranch ,
29+ getRepoInfo ,
30+ getRepoName ,
31+ getRepoOwner ,
2832 gitBranch ,
2933 gitCheckoutBranch ,
3034 gitCleanFdx ,
3135 gitCommit ,
3236 gitCreateBranch ,
3337 gitDeleteBranch ,
38+ gitDeleteRemoteBranch ,
3439 gitEnsureIdentity ,
40+ gitLocalBranchExists ,
3541 gitPushBranch ,
42+ gitRemoteBranchExists ,
43+ gitResetAndClean ,
3644 gitResetHard ,
45+ gitUnstagedModifiedFiles ,
3746 parseGitRemoteUrl ,
38- } from '../../../../../ src/utils/git/operations.mts'
47+ } from '../../../../src/utils/git/operations.mts'
3948
4049// Mock spawn.
4150vi . mock ( '@socketsecurity/lib/spawn' , ( ) => ( {
@@ -48,12 +57,12 @@ vi.mock('@socketsecurity/lib/bin', () => ({
4857 whichReal : vi . fn ( ) . mockResolvedValue ( 'git' ) ,
4958} ) )
5059
51- vi . mock ( '../../../../../ src/constants/cli.mts' , ( ) => ( {
60+ vi . mock ( '../../../../src/constants/cli.mts' , ( ) => ( {
5261 FLAG_QUIET : '--quiet' ,
5362} ) )
5463
5564// Mock debug.
56- vi . mock ( '../../../../../ src/utils/debug.mts' , ( ) => ( {
65+ vi . mock ( '../../../../src/utils/debug.mts' , ( ) => ( {
5766 debugGit : vi . fn ( ) ,
5867} ) )
5968
@@ -331,4 +340,238 @@ describe('git utilities', () => {
331340 )
332341 } )
333342 } )
343+
344+ describe ( 'getRepoInfo' , ( ) => {
345+ it ( 'returns owner and repo from remote URL' , async ( ) => {
346+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
347+ spawn . mockResolvedValue ( {
348+ status : 0 ,
349+ stdout : 'git@github.com:socketdev/socket-cli.git' ,
350+ stderr : '' ,
351+ } as any )
352+
353+ const result = await getRepoInfo ( '/test/dir' )
354+ expect ( result ) . toEqual ( { owner : 'socketdev' , repo : 'socket-cli' } )
355+ } )
356+
357+ it ( 'returns undefined when spawn fails' , async ( ) => {
358+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
359+ spawn . mockRejectedValue ( new Error ( 'Not a git repo' ) )
360+
361+ const result = await getRepoInfo ( '/test/dir' )
362+ expect ( result ) . toBeUndefined ( )
363+ } )
364+
365+ it ( 'returns undefined when spawn returns null' , async ( ) => {
366+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
367+ spawn . mockResolvedValue ( null as any )
368+
369+ const result = await getRepoInfo ( '/test/dir' )
370+ expect ( result ) . toBeUndefined ( )
371+ } )
372+ } )
373+
374+ describe ( 'getRepoName' , ( ) => {
375+ it ( 'returns repo name from remote URL' , async ( ) => {
376+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
377+ spawn . mockResolvedValue ( {
378+ status : 0 ,
379+ stdout : 'git@github.com:socketdev/socket-cli.git' ,
380+ stderr : '' ,
381+ } as any )
382+
383+ const result = await getRepoName ( '/test/dir' )
384+ expect ( result ) . toBe ( 'socket-cli' )
385+ } )
386+
387+ it ( 'returns default when no repo info' , async ( ) => {
388+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
389+ spawn . mockRejectedValue ( new Error ( 'Not a git repo' ) )
390+
391+ const result = await getRepoName ( '/test/dir' )
392+ // Should return the default repository name.
393+ expect ( typeof result ) . toBe ( 'string' )
394+ } )
395+ } )
396+
397+ describe ( 'getRepoOwner' , ( ) => {
398+ it ( 'returns owner from remote URL' , async ( ) => {
399+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
400+ spawn . mockResolvedValue ( {
401+ status : 0 ,
402+ stdout : 'git@github.com:socketdev/socket-cli.git' ,
403+ stderr : '' ,
404+ } as any )
405+
406+ const result = await getRepoOwner ( '/test/dir' )
407+ expect ( result ) . toBe ( 'socketdev' )
408+ } )
409+
410+ it ( 'returns undefined when no repo info' , async ( ) => {
411+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
412+ spawn . mockRejectedValue ( new Error ( 'Not a git repo' ) )
413+
414+ const result = await getRepoOwner ( '/test/dir' )
415+ expect ( result ) . toBeUndefined ( )
416+ } )
417+ } )
418+
419+ describe ( 'detectDefaultBranch' , ( ) => {
420+ it ( 'returns main when it exists locally' , async ( ) => {
421+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
422+ spawn . mockResolvedValue ( { status : 0 , stdout : '' , stderr : '' } as any )
423+
424+ const result = await detectDefaultBranch ( '/test/dir' )
425+ expect ( result ) . toBe ( 'main' )
426+ } )
427+
428+ it ( 'checks common branch names in order' , async ( ) => {
429+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
430+ // All local branches fail.
431+ spawn
432+ . mockRejectedValueOnce ( new Error ( 'main not found' ) )
433+ . mockRejectedValueOnce ( new Error ( 'master not found' ) )
434+ . mockRejectedValueOnce ( new Error ( 'develop not found' ) )
435+ . mockRejectedValueOnce ( new Error ( 'trunk not found' ) )
436+ . mockRejectedValueOnce ( new Error ( 'default not found' ) )
437+ // First remote succeeds.
438+ . mockResolvedValueOnce ( { status : 0 , stdout : 'refs/heads/main' , stderr : '' } as any )
439+
440+ const result = await detectDefaultBranch ( '/test/dir' )
441+ expect ( result ) . toBe ( 'main' )
442+ } )
443+ } )
444+
445+ describe ( 'gitDeleteRemoteBranch' , ( ) => {
446+ it ( 'deletes a remote branch' , async ( ) => {
447+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
448+ spawn . mockResolvedValue ( { status : 0 , stdout : '' , stderr : '' } as any )
449+
450+ const result = await gitDeleteRemoteBranch ( 'old-feature' )
451+ expect ( result ) . toBe ( true )
452+ expect ( spawn ) . toHaveBeenCalledWith (
453+ 'git' ,
454+ [ 'push' , 'origin' , '--delete' , 'old-feature' ] ,
455+ expect . any ( Object ) ,
456+ )
457+ } )
458+
459+ it ( 'returns false when branch does not exist' , async ( ) => {
460+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
461+ spawn . mockRejectedValue ( new Error ( 'Branch not found' ) )
462+
463+ const result = await gitDeleteRemoteBranch ( 'nonexistent' )
464+ expect ( result ) . toBe ( false )
465+ } )
466+ } )
467+
468+ describe ( 'gitLocalBranchExists' , ( ) => {
469+ it ( 'returns true when branch exists' , async ( ) => {
470+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
471+ spawn . mockResolvedValue ( { status : 0 , stdout : '' , stderr : '' } as any )
472+
473+ const result = await gitLocalBranchExists ( 'main' )
474+ expect ( result ) . toBe ( true )
475+ expect ( spawn ) . toHaveBeenCalledWith (
476+ 'git' ,
477+ [ 'show-ref' , '--quiet' , 'refs/heads/main' ] ,
478+ expect . any ( Object ) ,
479+ )
480+ } )
481+
482+ it ( 'returns false when branch does not exist' , async ( ) => {
483+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
484+ spawn . mockRejectedValue ( new Error ( 'Branch not found' ) )
485+
486+ const result = await gitLocalBranchExists ( 'nonexistent' )
487+ expect ( result ) . toBe ( false )
488+ } )
489+ } )
490+
491+ describe ( 'gitRemoteBranchExists' , ( ) => {
492+ it ( 'returns true when remote branch exists' , async ( ) => {
493+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
494+ spawn . mockResolvedValue ( {
495+ status : 0 ,
496+ stdout : 'abc123\trefs/heads/main' ,
497+ stderr : '' ,
498+ } as any )
499+
500+ const result = await gitRemoteBranchExists ( 'main' )
501+ expect ( result ) . toBe ( true )
502+ expect ( spawn ) . toHaveBeenCalledWith (
503+ 'git' ,
504+ [ 'ls-remote' , '--heads' , 'origin' , 'main' ] ,
505+ expect . any ( Object ) ,
506+ )
507+ } )
508+
509+ it ( 'returns false when remote branch does not exist' , async ( ) => {
510+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
511+ spawn . mockResolvedValue ( {
512+ status : 0 ,
513+ stdout : '' ,
514+ stderr : '' ,
515+ } as any )
516+
517+ const result = await gitRemoteBranchExists ( 'nonexistent' )
518+ expect ( result ) . toBe ( false )
519+ } )
520+
521+ it ( 'returns false when spawn fails' , async ( ) => {
522+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
523+ spawn . mockRejectedValue ( new Error ( 'Network error' ) )
524+
525+ const result = await gitRemoteBranchExists ( 'main' )
526+ expect ( result ) . toBe ( false )
527+ } )
528+ } )
529+
530+ describe ( 'gitResetAndClean' , ( ) => {
531+ it ( 'calls gitResetHard and gitCleanFdx' , async ( ) => {
532+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
533+ spawn . mockResolvedValue ( { status : 0 , stdout : '' , stderr : '' } as any )
534+
535+ await gitResetAndClean ( 'main' , '/test/dir' )
536+ expect ( spawn ) . toHaveBeenCalledWith (
537+ 'git' ,
538+ [ 'reset' , '--hard' , 'main' ] ,
539+ expect . any ( Object ) ,
540+ )
541+ expect ( spawn ) . toHaveBeenCalledWith (
542+ 'git' ,
543+ [ 'clean' , '-fdx' ] ,
544+ expect . any ( Object ) ,
545+ )
546+ } )
547+ } )
548+
549+ describe ( 'gitUnstagedModifiedFiles' , ( ) => {
550+ it ( 'returns list of modified files' , async ( ) => {
551+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
552+ spawn . mockResolvedValue ( {
553+ status : 0 ,
554+ stdout : 'file1.txt\nfile2.txt\n' ,
555+ stderr : '' ,
556+ } as any )
557+
558+ const result = await gitUnstagedModifiedFiles ( '/test/dir' )
559+ expect ( result . ok ) . toBe ( true )
560+ if ( result . ok ) {
561+ expect ( result . data ) . toContain ( 'file1.txt' )
562+ expect ( result . data ) . toContain ( 'file2.txt' )
563+ }
564+ } )
565+
566+ it ( 'returns error when spawn fails' , async ( ) => {
567+ const { spawn } = vi . mocked ( await import ( '@socketsecurity/lib/spawn' ) )
568+ spawn . mockRejectedValue ( new Error ( 'Git error' ) )
569+
570+ const result = await gitUnstagedModifiedFiles ( '/test/dir' )
571+ expect ( result . ok ) . toBe ( false )
572+ if ( ! result . ok ) {
573+ expect ( result . message ) . toBe ( 'Git Error' )
574+ }
575+ } )
576+ } )
334577} )
0 commit comments