@@ -9,10 +9,16 @@ import type {
99 CloneProgressPayload ,
1010 DetectRepoResult ,
1111 DiffStats ,
12+ GetCommitConventionsOutput ,
13+ GetPrTemplateOutput ,
1214 GitCommitInfo ,
1315 GitFileStatus ,
1416 GitRepoInfo ,
1517 GitSyncStatus ,
18+ PublishOutput ,
19+ PullOutput ,
20+ PushOutput ,
21+ SyncOutput ,
1622} from "./schemas.js" ;
1723import { parseGitHubUrl } from "./utils.js" ;
1824
@@ -560,6 +566,180 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
560566 }
561567 }
562568
569+ public async push (
570+ directoryPath : string ,
571+ remote = "origin" ,
572+ branch ?: string ,
573+ setUpstream = false ,
574+ ) : Promise < PushOutput > {
575+ try {
576+ const targetBranch =
577+ branch || ( await this . getCurrentBranch ( directoryPath ) ) ;
578+ if ( ! targetBranch ) {
579+ return { success : false , message : "No branch to push" } ;
580+ }
581+
582+ const args = [ "push" ] ;
583+ if ( setUpstream ) {
584+ args . push ( "-u" ) ;
585+ }
586+ args . push ( remote , targetBranch ) ;
587+
588+ const { stdout, stderr } = await execFileAsync ( "git" , args , {
589+ cwd : directoryPath ,
590+ } ) ;
591+
592+ return {
593+ success : true ,
594+ message : stdout || stderr || "Push successful" ,
595+ } ;
596+ } catch ( error ) {
597+ const message = error instanceof Error ? error . message : String ( error ) ;
598+ return { success : false , message } ;
599+ }
600+ }
601+
602+ public async pull (
603+ directoryPath : string ,
604+ remote = "origin" ,
605+ branch ?: string ,
606+ ) : Promise < PullOutput > {
607+ try {
608+ const targetBranch =
609+ branch || ( await this . getCurrentBranch ( directoryPath ) ) ;
610+ const args = [ "pull" , remote ] ;
611+ if ( targetBranch ) {
612+ args . push ( targetBranch ) ;
613+ }
614+
615+ const { stdout, stderr } = await execFileAsync ( "git" , args , {
616+ cwd : directoryPath ,
617+ } ) ;
618+
619+ // Parse number of files changed from output
620+ const output = stdout || stderr || "" ;
621+ const filesMatch = output . match ( / ( \d + ) f i l e s ? c h a n g e d / ) ;
622+ const updatedFiles = filesMatch ? parseInt ( filesMatch [ 1 ] , 10 ) : undefined ;
623+
624+ return {
625+ success : true ,
626+ message : output || "Pull successful" ,
627+ updatedFiles,
628+ } ;
629+ } catch ( error ) {
630+ const message = error instanceof Error ? error . message : String ( error ) ;
631+ return { success : false , message } ;
632+ }
633+ }
634+
635+ public async publish (
636+ directoryPath : string ,
637+ remote = "origin" ,
638+ ) : Promise < PublishOutput > {
639+ const currentBranch = await this . getCurrentBranch ( directoryPath ) ;
640+ if ( ! currentBranch ) {
641+ return { success : false , message : "No branch to publish" , branch : "" } ;
642+ }
643+
644+ const result = await this . push ( directoryPath , remote , currentBranch , true ) ;
645+ return { ...result , branch : currentBranch } ;
646+ }
647+
648+ public async sync (
649+ directoryPath : string ,
650+ remote = "origin" ,
651+ ) : Promise < SyncOutput > {
652+ const pullResult = await this . pull ( directoryPath , remote ) ;
653+ if ( ! pullResult . success ) {
654+ return {
655+ success : false ,
656+ pullMessage : pullResult . message ,
657+ pushMessage : "Skipped due to pull failure" ,
658+ } ;
659+ }
660+
661+ const pushResult = await this . push ( directoryPath , remote ) ;
662+ return {
663+ success : pushResult . success ,
664+ pullMessage : pullResult . message ,
665+ pushMessage : pushResult . message ,
666+ } ;
667+ }
668+
669+ public async getPrTemplate (
670+ directoryPath : string ,
671+ ) : Promise < GetPrTemplateOutput > {
672+ const templatePaths = [
673+ ".github/PULL_REQUEST_TEMPLATE.md" ,
674+ ".github/pull_request_template.md" ,
675+ "PULL_REQUEST_TEMPLATE.md" ,
676+ "pull_request_template.md" ,
677+ "docs/PULL_REQUEST_TEMPLATE.md" ,
678+ ] ;
679+
680+ for ( const relativePath of templatePaths ) {
681+ const fullPath = path . join ( directoryPath , relativePath ) ;
682+ try {
683+ const content = await fsPromises . readFile ( fullPath , "utf-8" ) ;
684+ return { template : content , templatePath : relativePath } ;
685+ } catch {
686+ // Template not found at this path, continue
687+ }
688+ }
689+
690+ return { template : null , templatePath : null } ;
691+ }
692+
693+ public async getCommitConventions (
694+ directoryPath : string ,
695+ sampleSize = 20 ,
696+ ) : Promise < GetCommitConventionsOutput > {
697+ try {
698+ const { stdout } = await execAsync (
699+ `git log --oneline -n ${ sampleSize } --format="%s"` ,
700+ { cwd : directoryPath } ,
701+ ) ;
702+
703+ const messages = stdout . trim ( ) . split ( "\n" ) . filter ( Boolean ) ;
704+
705+ // Check for conventional commit pattern: type(scope): message or type: message
706+ const conventionalPattern =
707+ / ^ ( f e a t | f i x | d o c s | s t y l e | r e f a c t o r | t e s t | c h o r e | b u i l d | c i | p e r f | r e v e r t ) ( \( .+ \) ) ? : / ;
708+ const conventionalCount = messages . filter ( ( m ) =>
709+ conventionalPattern . test ( m ) ,
710+ ) . length ;
711+ const conventionalCommits = conventionalCount > messages . length * 0.5 ;
712+
713+ // Extract common prefixes
714+ const prefixes = messages
715+ . map ( ( m ) => m . match ( / ^ ( [ a - z ] + ) ( \( .+ \) ) ? : / ) ?. [ 1 ] )
716+ . filter ( ( p ) : p is string => Boolean ( p ) ) ;
717+ const prefixCounts = prefixes . reduce (
718+ ( acc , p ) => {
719+ acc [ p ] = ( acc [ p ] || 0 ) + 1 ;
720+ return acc ;
721+ } ,
722+ { } as Record < string , number > ,
723+ ) ;
724+ const commonPrefixes = Object . entries ( prefixCounts )
725+ . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
726+ . slice ( 0 , 5 )
727+ . map ( ( [ prefix ] ) => prefix ) ;
728+
729+ return {
730+ conventionalCommits,
731+ commonPrefixes,
732+ sampleMessages : messages . slice ( 0 , 5 ) ,
733+ } ;
734+ } catch {
735+ return {
736+ conventionalCommits : false ,
737+ commonPrefixes : [ ] ,
738+ sampleMessages : [ ] ,
739+ } ;
740+ }
741+ }
742+
563743 // Private helper methods
564744
565745 private async countFileLines ( filePath : string ) : Promise < number > {
0 commit comments