1- import { execFile } from "node:child_process" ;
1+ import { exec , execFile , spawn } from "node:child_process" ;
22import { promisify } from "node:util" ;
33import { injectable } from "inversify" ;
4- import type { DetectRepoResult } from "./schemas.js" ;
4+ import { TypedEventEmitter } from "../../lib/typed-event-emitter.js" ;
5+ import type { CloneProgressPayload , DetectRepoResult } from "./schemas.js" ;
56import { parseGitHubUrl } from "./utils.js" ;
67
8+ const execAsync = promisify ( exec ) ;
79const execFileAsync = promisify ( execFile ) ;
810
11+ export const GitServiceEvent = {
12+ CloneProgress : "cloneProgress" ,
13+ } as const ;
14+
15+ export interface GitServiceEvents {
16+ [ GitServiceEvent . CloneProgress ] : CloneProgressPayload ;
17+ }
18+
919@injectable ( )
10- export class GitService {
20+ export class GitService extends TypedEventEmitter < GitServiceEvents > {
1121 public async detectRepo (
1222 directoryPath : string ,
1323 ) : Promise < DetectRepoResult | null > {
@@ -30,11 +40,80 @@ export class GitService {
3040 } ;
3141 }
3242
33- public async getRemoteUrl ( directoryPath : string ) : Promise < string | null > {
43+ public async validateRepo ( directoryPath : string ) : Promise < boolean > {
44+ if ( ! directoryPath ) return false ;
45+
3446 try {
35- const { stdout } = await execFileAsync ( "git remote get-url origin " , {
47+ await execAsync ( "git rev-parse --is-inside-work-tree " , {
3648 cwd : directoryPath ,
3749 } ) ;
50+ return true ;
51+ } catch {
52+ return false ;
53+ }
54+ }
55+
56+ public async cloneRepository (
57+ repoUrl : string ,
58+ targetPath : string ,
59+ cloneId : string ,
60+ ) : Promise < { cloneId : string } > {
61+ const emitProgress = (
62+ status : CloneProgressPayload [ "status" ] ,
63+ message : string ,
64+ ) => {
65+ this . emit ( GitServiceEvent . CloneProgress , { cloneId, status, message } ) ;
66+ } ;
67+
68+ emitProgress ( "cloning" , `Starting clone of ${ repoUrl } ...` ) ;
69+
70+ const gitProcess = spawn (
71+ "git" ,
72+ [ "clone" , "--progress" , repoUrl , targetPath ] ,
73+ {
74+ stdio : [ "ignore" , "pipe" , "pipe" ] ,
75+ } ,
76+ ) ;
77+
78+ gitProcess . stderr . on ( "data" , ( data : Buffer ) => {
79+ const output = data . toString ( ) ;
80+ emitProgress ( "cloning" , output . trim ( ) ) ;
81+ } ) ;
82+
83+ gitProcess . stdout . on ( "data" , ( data : Buffer ) => {
84+ const output = data . toString ( ) ;
85+ emitProgress ( "cloning" , output . trim ( ) ) ;
86+ } ) ;
87+
88+ return new Promise ( ( resolve , reject ) => {
89+ gitProcess . on ( "close" , ( code ) => {
90+ if ( code === 0 ) {
91+ emitProgress ( "complete" , "Clone completed successfully" ) ;
92+ resolve ( { cloneId } ) ;
93+ } else {
94+ const errorMsg = `Clone failed with exit code ${ code } ` ;
95+ emitProgress ( "error" , errorMsg ) ;
96+ reject ( new Error ( errorMsg ) ) ;
97+ }
98+ } ) ;
99+
100+ gitProcess . on ( "error" , ( err ) => {
101+ const errorMsg = `Clone failed: ${ err . message } ` ;
102+ emitProgress ( "error" , errorMsg ) ;
103+ reject ( err ) ;
104+ } ) ;
105+ } ) ;
106+ }
107+
108+ public async getRemoteUrl ( directoryPath : string ) : Promise < string | null > {
109+ try {
110+ const { stdout } = await execFileAsync (
111+ "git" ,
112+ [ "remote" , "get-url" , "origin" ] ,
113+ {
114+ cwd : directoryPath ,
115+ } ,
116+ ) ;
38117 return stdout . trim ( ) ;
39118 } catch {
40119 return null ;
@@ -43,9 +122,13 @@ export class GitService {
43122
44123 public async getCurrentBranch ( directoryPath : string ) : Promise < string | null > {
45124 try {
46- const { stdout } = await execFileAsync ( "git branch --show-current" , {
47- cwd : directoryPath ,
48- } ) ;
125+ const { stdout } = await execFileAsync (
126+ "git" ,
127+ [ "branch" , "--show-current" ] ,
128+ {
129+ cwd : directoryPath ,
130+ } ,
131+ ) ;
49132 return stdout . trim ( ) ;
50133 } catch {
51134 return null ;
0 commit comments