@@ -7,13 +7,72 @@ import { parseStatusLine } from './gitParser';
77
88const execFileAsync = promisify ( execFile ) ;
99
10- async function runGit ( cwd : string , args : string [ ] , options ?: { timeoutMs ?: number } ) {
11- const { stdout } = await execFileAsync ( 'git' , [ '-C' , cwd , ...args ] , {
12- maxBuffer : 1024 * 1024 * 10 ,
13- timeout : options ?. timeoutMs ?? 30000 ,
14- env : { ...process . env , GIT_TERMINAL_PROMPT : '0' } ,
15- } ) ;
16- return stdout . toString ( ) ;
10+ interface GitResult { stdout : string ; stderr : string }
11+
12+ export class GitCommandError extends Error {
13+ stdout : string ;
14+ stderr : string ;
15+ remoteErrorKind : 'auth' | 'terminal-required' | 'other' ;
16+
17+ constructor ( message : string , stdout : string , stderr : string ) {
18+ super ( message ) ;
19+ this . name = 'GitCommandError' ;
20+ this . stdout = stdout ;
21+ this . stderr = stderr ;
22+ this . remoteErrorKind = isAuthErrorText ( `${ stderr } \n${ stdout } ` ) ? 'auth' : 'other' ;
23+ }
24+ }
25+
26+ function formatGitError ( stdout : string , stderr : string ) {
27+ const detail = `${ stderr } \n${ stdout } ` . split ( / \r ? \n / ) . map ( ( line ) => line . trim ( ) ) . filter ( Boolean ) . slice ( 0 , 8 ) . join ( '\n' ) ;
28+ return detail || 'Git command failed' ;
29+ }
30+
31+ async function runGit ( cwd : string , args : string [ ] , options ?: { timeoutMs ?: number } ) : Promise < GitResult > {
32+ try {
33+ const { stdout, stderr } = await execFileAsync ( 'git' , [ '-C' , cwd , ...args ] , {
34+ maxBuffer : 1024 * 1024 * 10 ,
35+ timeout : options ?. timeoutMs ?? 30000 ,
36+ env : { ...process . env , GIT_TERMINAL_PROMPT : '0' } ,
37+ } ) ;
38+ return { stdout : stdout . toString ( ) , stderr : stderr . toString ( ) } ;
39+ } catch ( error ) {
40+ const stdout = typeof error === 'object' && error && 'stdout' in error ? String ( ( error as { stdout ?: unknown } ) . stdout ?? '' ) : '' ;
41+ const stderr = typeof error === 'object' && error && 'stderr' in error ? String ( ( error as { stderr ?: unknown } ) . stderr ?? '' ) : '' ;
42+ throw new GitCommandError ( formatGitError ( stdout , stderr ) , stdout , stderr ) ;
43+ }
44+ }
45+
46+ function isAuthErrorText ( text : string ) {
47+ return / A u t h e n t i c a t i o n f a i l e d | C r e d e n t i a l s a r e i n c o r r e c t o r h a v e e x p i r e d | c o u l d n o t r e a d U s e r n a m e | t e r m i n a l p r o m p t s d i s a b l e d | P e r m i s s i o n d e n i e d \( p u b l i c k e y \) | R e p o s i t o r y n o t f o u n d .* ( A u t h e n t i c a t i o n | a u t h | c r e d e n t i a l | p e r m i s s i o n ) / i. test ( text ) ;
48+ }
49+
50+ export function isAuthError ( error : unknown ) {
51+ if ( error instanceof GitCommandError ) return error . remoteErrorKind === 'auth' ;
52+ return error instanceof Error ? isAuthErrorText ( error . message ) : isAuthErrorText ( String ( error ) ) ;
53+ }
54+
55+ async function gitOutput ( cwd : string , args : string [ ] , options ?: { timeoutMs ?: number } ) {
56+ return ( await runGit ( cwd , args , options ) ) . stdout ;
57+ }
58+
59+ async function gitPathExists ( cwd : string , gitPath : string ) {
60+ try {
61+ const resolved = ( await gitOutput ( cwd , [ 'rev-parse' , '--git-path' , gitPath ] ) ) . trim ( ) ;
62+ if ( ! resolved ) return false ;
63+ const target = path . isAbsolute ( resolved ) ? resolved : path . join ( cwd , resolved ) ;
64+ await fs . access ( target ) ;
65+ return true ;
66+ } catch {
67+ return false ;
68+ }
69+ }
70+
71+ async function detectOperation ( cwd : string ) : Promise < GitStatus [ 'operation' ] > {
72+ if ( await gitPathExists ( cwd , 'MERGE_HEAD' ) ) return 'merge' ;
73+ if ( await gitPathExists ( cwd , 'REBASE_HEAD' ) || await gitPathExists ( cwd , 'rebase-merge' ) || await gitPathExists ( cwd , 'rebase-apply' ) ) return 'rebase' ;
74+ if ( await gitPathExists ( cwd , 'CHERRY_PICK_HEAD' ) ) return 'cherry-pick' ;
75+ return undefined ;
1776}
1877
1978function assertSafeGitRef ( ref : string ) {
@@ -25,7 +84,7 @@ function assertSafeGitRef(ref: string) {
2584
2685export async function listBranches ( cwd : string ) : Promise < string [ ] > {
2786 try {
28- const output = await runGit ( cwd , [ 'branch' , '--format=%(refname:short)' ] ) ;
87+ const output = await gitOutput ( cwd , [ 'branch' , '--format=%(refname:short)' ] ) ;
2988 return [ ...new Set ( output . split ( / \r ? \n / ) . map ( ( line ) => line . trim ( ) ) . filter ( Boolean ) ) ] ;
3089 } catch {
3190 return [ ] ;
@@ -34,7 +93,7 @@ export async function listBranches(cwd: string): Promise<string[]> {
3493
3594export async function getGitStatus ( cwd : string ) : Promise < GitStatus > {
3695 try {
37- const output = await runGit ( cwd , [ 'status' , '--porcelain=v1' , '-b' ] ) ;
96+ const output = await gitOutput ( cwd , [ 'status' , '--porcelain=v1' , '-b' ] ) ;
3897 const lines = output . trim ( ) . split ( / \r ? \n / ) . filter ( Boolean ) ;
3998 const branchLine = lines . shift ( ) ?? '' ;
4099 const status : GitStatus = { isRepo : true , files : [ ] } ;
@@ -49,6 +108,9 @@ export async function getGitStatus(cwd: string): Promise<GitStatus> {
49108 }
50109 status . files = lines . map ( parseStatusLine ) . filter ( Boolean ) as GitFileStatus [ ] ;
51110 status . branches = await listBranches ( cwd ) ;
111+ status . operation = await detectOperation ( cwd ) ;
112+ status . conflicts = status . files . filter ( ( file ) => file . conflicted ) . length ;
113+ status . mergeReady = status . operation === 'merge' && status . conflicts === 0 ;
52114 return status ;
53115 } catch {
54116 return { isRepo : false , files : [ ] } ;
@@ -60,70 +122,38 @@ export async function getGitDiff(cwd: string, filePath?: string, staged = false)
60122 const args = [ 'diff' ] ;
61123 if ( staged ) args . push ( '--staged' ) ;
62124 if ( filePath ) args . push ( '--' , filePath ) ;
63- return await runGit ( cwd , args ) ;
125+ return await gitOutput ( cwd , args ) ;
64126 } catch ( error ) {
65127 return error instanceof Error ? error . message : String ( error ) ;
66128 }
67129}
68130
69131async function readGitObject ( cwd : string , revision : string , filePath : string ) {
70- try {
71- return await runGit ( cwd , [ 'show' , `${ revision } :${ filePath } ` ] ) ;
72- } catch {
73- return '' ;
74- }
132+ try { return await gitOutput ( cwd , [ 'show' , `${ revision } :${ filePath } ` ] ) ; } catch { return '' ; }
75133}
76-
77134async function readIndexObject ( cwd : string , filePath : string ) {
78- try {
79- return await runGit ( cwd , [ 'show' , `:${ filePath } ` ] ) ;
80- } catch {
81- return '' ;
82- }
135+ try { return await gitOutput ( cwd , [ 'show' , `:${ filePath } ` ] ) ; } catch { return '' ; }
83136}
84-
85137async function readWorkingFile ( cwd : string , filePath : string ) {
86- try {
87- return await fs . readFile ( path . join ( cwd , filePath ) , 'utf8' ) ;
88- } catch {
89- return '' ;
90- }
138+ try { return await fs . readFile ( path . join ( cwd , filePath ) , 'utf8' ) ; } catch { return '' ; }
91139}
92140
93141export async function getGitFileContents ( cwd : string , filePath : string , staged = false ) : Promise < GitFileContents > {
94142 if ( staged ) {
95- const [ original , modified ] = await Promise . all ( [
96- readGitObject ( cwd , 'HEAD' , filePath ) ,
97- readIndexObject ( cwd , filePath ) ,
98- ] ) ;
143+ const [ original , modified ] = await Promise . all ( [ readGitObject ( cwd , 'HEAD' , filePath ) , readIndexObject ( cwd , filePath ) ] ) ;
99144 return { path : filePath , original, modified } ;
100145 }
101-
102146 const indexContent = await readIndexObject ( cwd , filePath ) ;
103147 const original = indexContent || await readGitObject ( cwd , 'HEAD' , filePath ) ;
104148 const modified = await readWorkingFile ( cwd , filePath ) ;
105149 return { path : filePath , original, modified } ;
106150}
107151
108- export async function stageFile ( cwd : string , filePath : string ) {
109- await runGit ( cwd , [ 'add' , '--' , filePath ] ) ;
110- }
111-
112- export async function unstageFile ( cwd : string , filePath : string ) {
113- await runGit ( cwd , [ 'restore' , '--staged' , '--' , filePath ] ) ;
114- }
115-
116- export async function discardFile ( cwd : string , filePath : string ) {
117- await runGit ( cwd , [ 'restore' , '--' , filePath ] ) ;
118- }
119-
120- export async function commit ( cwd : string , message : string ) {
121- await runGit ( cwd , [ 'commit' , '-m' , message ] ) ;
122- }
123-
124- export async function addAll ( cwd : string ) {
125- await runGit ( cwd , [ 'add' , '.' ] ) ;
126- }
152+ export async function stageFile ( cwd : string , filePath : string ) { await runGit ( cwd , [ 'add' , '--' , filePath ] ) ; }
153+ export async function unstageFile ( cwd : string , filePath : string ) { await runGit ( cwd , [ 'restore' , '--staged' , '--' , filePath ] ) ; }
154+ export async function discardFile ( cwd : string , filePath : string ) { await runGit ( cwd , [ 'restore' , '--' , filePath ] ) ; }
155+ export async function commit ( cwd : string , message : string ) { await runGit ( cwd , [ 'commit' , '-m' , message ] ) ; }
156+ export async function addAll ( cwd : string ) { await runGit ( cwd , [ 'add' , '.' ] ) ; }
127157
128158export async function switchBranch ( cwd : string , branch : string ) {
129159 const safeBranch = assertSafeGitRef ( branch ) ;
@@ -132,26 +162,28 @@ export async function switchBranch(cwd: string, branch: string) {
132162 await runGit ( cwd , [ 'switch' , safeBranch ] ) ;
133163}
134164
135- export async function push ( cwd : string ) {
136- await runGit ( cwd , [ 'push' ] , { timeoutMs : 120000 } ) ;
137- }
138-
139- export async function pull ( cwd : string ) {
140- await runGit ( cwd , [ 'pull' , '--ff-only' ] , { timeoutMs : 120000 } ) ;
141- }
142-
143- export async function fetch ( cwd : string ) {
144- await runGit ( cwd , [ 'fetch' ] , { timeoutMs : 120000 } ) ;
165+ export async function push ( cwd : string ) { await runGit ( cwd , [ 'push' ] , { timeoutMs : 120000 } ) ; }
166+ export async function pull ( cwd : string ) { await runGit ( cwd , [ 'pull' , '--ff-only' ] , { timeoutMs : 120000 } ) ; }
167+ export async function pullMerge ( cwd : string ) {
168+ try {
169+ await runGit ( cwd , [ 'pull' , '--no-rebase' , '--no-edit' ] , { timeoutMs : 120000 } ) ;
170+ } catch ( error ) {
171+ const status = await getGitStatus ( cwd ) ;
172+ if ( status . operation === 'merge' && ( status . conflicts ?? 0 ) > 0 ) throw new Error ( 'Merge has conflicts. Resolve them, stage the files, then commit the merge.' ) ;
173+ throw error ;
174+ }
145175}
176+ export async function abortMerge ( cwd : string ) { await runGit ( cwd , [ 'merge' , '--abort' ] ) ; }
177+ export async function fetch ( cwd : string ) { await runGit ( cwd , [ 'fetch' ] , { timeoutMs : 120000 } ) ; }
146178
147179export async function getIgnoredFiles ( cwd : string , filePaths : string [ ] ) : Promise < string [ ] > {
148180 if ( ! filePaths . length ) return [ ] ;
149181 const relativePaths = filePaths . map ( ( filePath ) => path . relative ( cwd , path . isAbsolute ( filePath ) ? filePath : path . join ( cwd , filePath ) ) . replace ( / \\ / g, '/' ) ) ;
150182 try {
151- const output = await runGit ( cwd , [ 'check-ignore' , ...relativePaths ] ) ;
183+ const output = await gitOutput ( cwd , [ 'check-ignore' , ...relativePaths ] ) ;
152184 return output . split ( / \r ? \n / ) . map ( ( line ) => line . trim ( ) ) . filter ( Boolean ) ;
153185 } catch ( error ) {
154- const output = typeof error === 'object' && error && 'stdout' in error ? String ( ( error as { stdout ?: unknown } ) . stdout ?? '' ) : '' ;
186+ const output = error instanceof GitCommandError ? error . stdout : '' ;
155187 return output . split ( / \r ? \n / ) . map ( ( line ) => line . trim ( ) ) . filter ( Boolean ) ;
156188 }
157189}
0 commit comments