@@ -383,6 +383,158 @@ export interface DiffStats {
383383 linesRemoved : number ;
384384}
385385
386+ export interface GitSyncStatus {
387+ ahead : number ;
388+ behind : number ;
389+ hasRemote : boolean ;
390+ currentBranch : string | null ;
391+ isFeatureBranch : boolean ;
392+ }
393+
394+ export interface GitCommitInfo {
395+ sha : string ;
396+ shortSha : string ;
397+ message : string ;
398+ author : string ;
399+ date : string ;
400+ }
401+
402+ export interface GitRepoInfo {
403+ organization : string ;
404+ repository : string ;
405+ currentBranch : string | null ;
406+ defaultBranch : string ;
407+ compareUrl : string | null ;
408+ }
409+
410+ const getLatestCommit = async (
411+ directoryPath : string ,
412+ ) : Promise < GitCommitInfo | null > => {
413+ try {
414+ const { stdout } = await execAsync (
415+ 'git log -1 --format="%H|%h|%s|%an|%aI"' ,
416+ { cwd : directoryPath } ,
417+ ) ;
418+
419+ const [ sha , shortSha , message , author , date ] = stdout . trim ( ) . split ( "|" ) ;
420+ if ( ! sha ) return null ;
421+
422+ return { sha, shortSha, message, author, date } ;
423+ } catch {
424+ return null ;
425+ }
426+ } ;
427+
428+ const getGitRepoInfo = async (
429+ directoryPath : string ,
430+ ) : Promise < GitRepoInfo | null > => {
431+ try {
432+ const remoteUrl = await getRemoteUrl ( directoryPath ) ;
433+ if ( ! remoteUrl ) return null ;
434+
435+ const parsed = parseGitHubUrl ( remoteUrl ) ;
436+ if ( ! parsed ) return null ;
437+
438+ const currentBranch = await getCurrentBranch ( directoryPath ) ;
439+ const defaultBranch = await getDefaultBranch ( directoryPath ) ;
440+
441+ let compareUrl : string | null = null ;
442+ if ( currentBranch && currentBranch !== defaultBranch ) {
443+ compareUrl = `https://github.com/${ parsed . organization } /${ parsed . repository } /compare/${ defaultBranch } ...${ currentBranch } ?expand=1` ;
444+ }
445+
446+ return {
447+ organization : parsed . organization ,
448+ repository : parsed . repository ,
449+ currentBranch : currentBranch ?? null ,
450+ defaultBranch,
451+ compareUrl,
452+ } ;
453+ } catch {
454+ return null ;
455+ }
456+ } ;
457+
458+ const getGitSyncStatus = async (
459+ directoryPath : string ,
460+ ) : Promise < GitSyncStatus > => {
461+ try {
462+ const currentBranch = await getCurrentBranch ( directoryPath ) ;
463+ if ( ! currentBranch ) {
464+ return {
465+ ahead : 0 ,
466+ behind : 0 ,
467+ hasRemote : false ,
468+ currentBranch : null ,
469+ isFeatureBranch : false ,
470+ } ;
471+ }
472+
473+ const defaultBranch = await getDefaultBranch ( directoryPath ) ;
474+ const isFeatureBranch = currentBranch !== defaultBranch ;
475+
476+ try {
477+ const { stdout : upstream } = await execAsync (
478+ `git rev-parse --abbrev-ref ${ currentBranch } @{upstream}` ,
479+ { cwd : directoryPath } ,
480+ ) ;
481+
482+ const upstreamBranch = upstream . trim ( ) ;
483+ if ( ! upstreamBranch ) {
484+ return {
485+ ahead : 0 ,
486+ behind : 0 ,
487+ hasRemote : false ,
488+ currentBranch,
489+ isFeatureBranch,
490+ } ;
491+ }
492+
493+ // Use --quiet to suppress output, ignore errors (network may be unavailable)
494+ try {
495+ await execAsync ( "git fetch --quiet" , {
496+ cwd : directoryPath ,
497+ timeout : 10000 ,
498+ } ) ;
499+ } catch {
500+ // Fetch failed (likely offline), continue with stale data
501+ }
502+
503+ const { stdout : revList } = await execAsync (
504+ `git rev-list --left-right --count ${ currentBranch } ...${ upstreamBranch } ` ,
505+ { cwd : directoryPath } ,
506+ ) ;
507+
508+ const [ ahead , behind ] = revList . trim ( ) . split ( "\t" ) . map ( Number ) ;
509+
510+ return {
511+ ahead : ahead || 0 ,
512+ behind : behind || 0 ,
513+ hasRemote : true ,
514+ currentBranch,
515+ isFeatureBranch,
516+ } ;
517+ } catch {
518+ return {
519+ ahead : 0 ,
520+ behind : 0 ,
521+ hasRemote : false ,
522+ currentBranch,
523+ isFeatureBranch,
524+ } ;
525+ }
526+ } catch ( error ) {
527+ log . error ( "Error getting git sync status:" , error ) ;
528+ return {
529+ ahead : 0 ,
530+ behind : 0 ,
531+ hasRemote : false ,
532+ currentBranch : null ,
533+ isFeatureBranch : false ,
534+ } ;
535+ }
536+ } ;
537+
386538const discardFileChanges = async (
387539 directoryPath : string ,
388540 filePath : string ,
@@ -857,4 +1009,34 @@ export function registerGitIpc(
8571009 return discardFileChanges ( directoryPath , filePath , fileStatus ) ;
8581010 } ,
8591011 ) ;
1012+
1013+ ipcMain . handle (
1014+ "get-git-sync-status" ,
1015+ async (
1016+ _event : IpcMainInvokeEvent ,
1017+ directoryPath : string ,
1018+ ) : Promise < GitSyncStatus > => {
1019+ return getGitSyncStatus ( directoryPath ) ;
1020+ } ,
1021+ ) ;
1022+
1023+ ipcMain . handle (
1024+ "get-latest-commit" ,
1025+ async (
1026+ _event : IpcMainInvokeEvent ,
1027+ directoryPath : string ,
1028+ ) : Promise < GitCommitInfo | null > => {
1029+ return getLatestCommit ( directoryPath ) ;
1030+ } ,
1031+ ) ;
1032+
1033+ ipcMain . handle (
1034+ "get-git-repo-info" ,
1035+ async (
1036+ _event : IpcMainInvokeEvent ,
1037+ directoryPath : string ,
1038+ ) : Promise < GitRepoInfo | null > => {
1039+ return getGitRepoInfo ( directoryPath ) ;
1040+ } ,
1041+ ) ;
8601042}
0 commit comments