@@ -24,6 +24,7 @@ const fail = (err: unknown) =>
2424 text : ( ) => "" ,
2525 stdout : Buffer . alloc ( 0 ) ,
2626 stderr : Buffer . from ( err instanceof Error ? err . message : String ( err ) ) ,
27+ truncated : false ,
2728 } ) satisfies Result
2829
2930export type Kind = "added" | "deleted" | "modified"
@@ -45,16 +46,28 @@ export type Stat = {
4546 readonly deletions : number
4647}
4748
49+ export type Patch = {
50+ readonly text : string
51+ readonly truncated : boolean
52+ }
53+
54+ export interface PatchOptions {
55+ readonly context ?: number
56+ readonly maxOutputBytes ?: number
57+ }
58+
4859export interface Result {
4960 readonly exitCode : number
5061 readonly text : ( ) => string
5162 readonly stdout : Buffer
5263 readonly stderr : Buffer
64+ readonly truncated : boolean
5365}
5466
5567export interface Options {
5668 readonly cwd : string
5769 readonly env ?: Record < string , string >
70+ readonly maxOutputBytes ?: number
5871}
5972
6073export interface Interface {
@@ -68,6 +81,10 @@ export interface Interface {
6881 readonly status : ( cwd : string ) => Effect . Effect < Item [ ] >
6982 readonly diff : ( cwd : string , ref : string ) => Effect . Effect < Item [ ] >
7083 readonly stats : ( cwd : string , ref : string ) => Effect . Effect < Stat [ ] >
84+ readonly patch : ( cwd : string , ref : string , file : string , options ?: PatchOptions ) => Effect . Effect < Patch >
85+ readonly patchAll : ( cwd : string , ref : string , options ?: PatchOptions ) => Effect . Effect < Patch >
86+ readonly patchUntracked : ( cwd : string , file : string , options ?: PatchOptions ) => Effect . Effect < Patch >
87+ readonly statUntracked : ( cwd : string , file : string ) => Effect . Effect < Stat | undefined >
7188}
7289
7390const kind = ( code : string ) : Kind => {
@@ -96,15 +113,31 @@ export const layer = Layer.effect(
96113 stderr : "pipe" ,
97114 } )
98115 const handle = yield * spawner . spawn ( proc )
99- const [ stdout , stderr ] = yield * Effect . all (
100- [ Stream . mkString ( Stream . decodeText ( handle . stdout ) ) , Stream . mkString ( Stream . decodeText ( handle . stderr ) ) ] ,
101- { concurrency : 2 } ,
102- )
116+ const collect = ( stream : typeof handle . stdout ) =>
117+ Stream . runFold (
118+ stream ,
119+ ( ) => ( { chunks : [ ] as Uint8Array [ ] , bytes : 0 , truncated : false } ) ,
120+ ( acc , chunk ) => {
121+ if ( opts . maxOutputBytes === undefined ) {
122+ acc . chunks . push ( chunk )
123+ acc . bytes += chunk . length
124+ return acc
125+ }
126+
127+ const remaining = opts . maxOutputBytes - acc . bytes
128+ if ( remaining > 0 ) acc . chunks . push ( remaining >= chunk . length ? chunk : chunk . slice ( 0 , remaining ) )
129+ acc . bytes += chunk . length
130+ acc . truncated = acc . truncated || acc . bytes > opts . maxOutputBytes
131+ return acc
132+ } ,
133+ ) . pipe ( Effect . map ( ( x ) => ( { buffer : Buffer . concat ( x . chunks ) , truncated : x . truncated } ) ) )
134+ const [ stdout , stderr ] = yield * Effect . all ( [ collect ( handle . stdout ) , collect ( handle . stderr ) ] , { concurrency : 2 } )
103135 return {
104136 exitCode : yield * handle . exitCode ,
105- text : ( ) => stdout ,
106- stdout : Buffer . from ( stdout ) ,
107- stderr : Buffer . from ( stderr ) ,
137+ text : ( ) => stdout . buffer . toString ( "utf8" ) ,
138+ stdout : stdout . buffer ,
139+ stderr : stderr . buffer ,
140+ truncated : stdout . truncated || stderr . truncated ,
108141 } satisfies Result
109142 } ,
110143 Effect . scoped ,
@@ -240,6 +273,61 @@ export const layer = Layer.effect(
240273 } )
241274 } )
242275
276+ const patch = Effect . fn ( "Git.patch" ) ( function * ( cwd : string , ref : string , file : string , options ?: PatchOptions ) {
277+ const result = yield * run (
278+ [ "diff" , "--patch" , "--no-ext-diff" , "--no-renames" , `--unified=${ options ?. context ?? 3 } ` , ref , "--" , file ] ,
279+ { cwd, maxOutputBytes : options ?. maxOutputBytes } ,
280+ )
281+ return { text : result . truncated ? "" : result . text ( ) , truncated : result . truncated } satisfies Patch
282+ } )
283+
284+ const patchAll = Effect . fn ( "Git.patchAll" ) ( function * ( cwd : string , ref : string , options ?: PatchOptions ) {
285+ const result = yield * run (
286+ [ "diff" , "--patch" , "--no-ext-diff" , "--no-renames" , `--unified=${ options ?. context ?? 3 } ` , ref , "--" , "." ] ,
287+ { cwd, maxOutputBytes : options ?. maxOutputBytes } ,
288+ )
289+ return { text : result . text ( ) , truncated : result . truncated } satisfies Patch
290+ } )
291+
292+ const patchUntracked = Effect . fn ( "Git.patchUntracked" ) ( function * (
293+ cwd : string ,
294+ file : string ,
295+ options ?: PatchOptions ,
296+ ) {
297+ const result = yield * run (
298+ [
299+ "diff" ,
300+ "--no-index" ,
301+ "--patch" ,
302+ "--no-ext-diff" ,
303+ "--no-renames" ,
304+ `--unified=${ options ?. context ?? 3 } ` ,
305+ "--" ,
306+ "/dev/null" ,
307+ file ,
308+ ] ,
309+ { cwd, maxOutputBytes : options ?. maxOutputBytes } ,
310+ )
311+ return { text : result . truncated ? "" : result . text ( ) , truncated : result . truncated } satisfies Patch
312+ } )
313+
314+ const statUntracked = Effect . fn ( "Git.statUntracked" ) ( function * ( cwd : string , file : string ) {
315+ const result = yield * run ( [ "diff" , "--no-index" , "--numstat" , "--" , "/dev/null" , file ] , {
316+ cwd,
317+ maxOutputBytes : 4096 ,
318+ } )
319+ if ( result . truncated ) return
320+ const parts = result . text ( ) . split ( "\t" )
321+ if ( parts . length < 2 ) return
322+ const additions = parts [ 0 ] === "-" ? 0 : Number . parseInt ( parts [ 0 ] || "0" , 10 )
323+ const deletions = parts [ 1 ] === "-" ? 0 : Number . parseInt ( parts [ 1 ] || "0" , 10 )
324+ return {
325+ file,
326+ additions : Number . isFinite ( additions ) ? additions : 0 ,
327+ deletions : Number . isFinite ( deletions ) ? deletions : 0 ,
328+ } satisfies Stat
329+ } )
330+
243331 return Service . of ( {
244332 run,
245333 branch,
@@ -251,6 +339,10 @@ export const layer = Layer.effect(
251339 status,
252340 diff,
253341 stats,
342+ patch,
343+ patchAll,
344+ patchUntracked,
345+ statUntracked,
254346 } )
255347 } ) ,
256348)
0 commit comments