@@ -10,20 +10,23 @@ export function handleGetCommitStats(args: any) {
1010 let cmd = `git log --since="${ since } "` ;
1111 if ( until ) cmd += ` --until="${ until } 23:59:59"` ;
1212 if ( author ) cmd += ` --author="${ author } "` ;
13- cmd += ` --pretty=format:"%H|%an|%ae|%ad|%s " --date=short --numstat ` ;
13+ cmd += ` --pretty=format:"%H" --shortstat ` ;
1414
1515 const output = runGitCommand ( repo_path , cmd ) ;
16- const lines = output . trim ( ) . split ( "\n" ) . slice ( 0 , 10000 ) ;
16+ const lines = output . trim ( ) . split ( "\n" ) ;
1717
1818 let commits = 0 , additions = 0 , deletions = 0 , filesChanged = 0 ;
1919
2020 for ( const line of lines ) {
21- if ( line . includes ( "|" ) ) commits ++ ;
22- else if ( line . match ( / ^ \d + \s + \d + / ) ) {
23- const [ add , del ] = line . split ( / \s + / ) ;
24- additions += parseInt ( add ) || 0 ;
25- deletions += parseInt ( del ) || 0 ;
26- filesChanged ++ ;
21+ if ( line . match ( / ^ [ 0 - 9 a - f ] { 40 } $ / ) ) {
22+ commits ++ ;
23+ } else if ( line . includes ( "changed" ) ) {
24+ const addMatch = line . match ( / ( \d + ) i n s e r t i o n / ) ;
25+ const delMatch = line . match ( / ( \d + ) d e l e t i o n / ) ;
26+ const fileMatch = line . match ( / ( \d + ) f i l e / ) ;
27+ if ( addMatch ) additions += parseInt ( addMatch [ 1 ] ) ;
28+ if ( delMatch ) deletions += parseInt ( delMatch [ 1 ] ) ;
29+ if ( fileMatch ) filesChanged += parseInt ( fileMatch [ 1 ] ) ;
2730 }
2831 }
2932
@@ -163,36 +166,35 @@ export function handleGetCodeOwnership(args: any) {
163166 validateDate ( since , "since" ) ;
164167 if ( until ) validateDate ( until , "until" ) ;
165168
166- const filesCmd = `git ls-files` ;
167- const files = runGitCommand ( repo_path , filesCmd ) . trim ( ) . split ( "\n" ) ;
169+ let cmd = `git log --since="${ since } "` ;
170+ if ( until ) cmd += ` --until="${ until } 23:59:59"` ;
171+ cmd += ` --pretty=format:"%an <%ae>" --name-only` ;
172+
173+ const output = runGitCommand ( repo_path , cmd ) ;
174+ const lines = output . trim ( ) . split ( "\n" ) ;
168175
169176 const fileAuthors : Record < string , Set < string > > = { } ;
177+ let currentAuthor = "" ;
170178
171- for ( const file of files ) {
172- let cmd = `git log --since="${ since } "` ;
173- if ( until ) cmd += ` --until="${ until } 23:59:59"` ;
174- cmd += ` --pretty=format:"%an <%ae>" -- "${ file } "` ;
175- try {
176- const output = runGitCommand ( repo_path , cmd ) ;
177- const authors = new Set ( output . trim ( ) . split ( "\n" ) . filter ( a => a ) ) ;
178- if ( authors . size > 0 ) {
179- fileAuthors [ file ] = authors ;
180- }
181- } catch {
182- // Skip files with no history
179+ for ( const line of lines ) {
180+ if ( line . includes ( "<" ) && line . includes ( ">" ) ) {
181+ currentAuthor = line ;
182+ } else if ( line . trim ( ) && currentAuthor ) {
183+ if ( ! fileAuthors [ line ] ) fileAuthors [ line ] = new Set ( ) ;
184+ fileAuthors [ line ] . add ( currentAuthor ) ;
183185 }
184186 }
185187
186188 const authorFiles : Record < string , number > = { } ;
187- for ( const authors of Object . values ( fileAuthors ) ) {
189+ for ( const [ file , authors ] of Object . entries ( fileAuthors ) ) {
188190 if ( authors . size === 1 ) {
189191 const author = Array . from ( authors ) [ 0 ] ;
190192 authorFiles [ author ] = ( authorFiles [ author ] || 0 ) + 1 ;
191193 }
192194 }
193195
194196 return {
195- totalFiles : files . length ,
197+ totalFiles : Object . keys ( fileAuthors ) . length ,
196198 sharedFiles : Object . values ( fileAuthors ) . filter ( a => a . size > 1 ) . length ,
197199 soloFiles : Object . values ( fileAuthors ) . filter ( a => a . size === 1 ) . length ,
198200 busFactor : Object . entries ( authorFiles )
@@ -246,39 +248,40 @@ export function handleGetCollaborationMetrics(args: any) {
246248 validateDate ( since , "since" ) ;
247249 if ( until ) validateDate ( until , "until" ) ;
248250
249- const filesCmd = `git ls-files` ;
250- const files = runGitCommand ( repo_path , filesCmd ) . trim ( ) . split ( "\n" ) . slice ( 0 , 1000 ) ;
251+ let cmd = `git log --since="${ since } "` ;
252+ if ( until ) cmd += ` --until="${ until } 23:59:59"` ;
253+ cmd += ` --pretty=format:"%an <%ae>" --name-only` ;
254+
255+ const output = runGitCommand ( repo_path , cmd ) ;
256+ const lines = output . trim ( ) . split ( "\n" ) ;
251257
252258 const fileAuthors : Record < string , Set < string > > = { } ;
259+ let currentAuthor = "" ;
253260
254- for ( const file of files ) {
255- let cmd = `git log --since="${ since } "` ;
256- if ( until ) cmd += ` --until="${ until } 23:59:59"` ;
257- cmd += ` --pretty=format:"%an <%ae>" -- "${ file } "` ;
258- try {
259- const output = runGitCommand ( repo_path , cmd ) ;
260- const authors = new Set ( output . trim ( ) . split ( "\n" ) . filter ( a => a ) ) ;
261- if ( authors . size > 1 ) {
262- fileAuthors [ file ] = authors ;
263- }
264- } catch {
265- // Skip
261+ for ( const line of lines ) {
262+ if ( line . includes ( "<" ) && line . includes ( ">" ) ) {
263+ currentAuthor = line ;
264+ } else if ( line . trim ( ) && currentAuthor ) {
265+ if ( ! fileAuthors [ line ] ) fileAuthors [ line ] = new Set ( ) ;
266+ fileAuthors [ line ] . add ( currentAuthor ) ;
266267 }
267268 }
268269
269270 const collaborations : Record < string , number > = { } ;
270271 for ( const authors of Object . values ( fileAuthors ) ) {
271- const authorList = Array . from ( authors ) . sort ( ) ;
272- for ( let i = 0 ; i < authorList . length ; i ++ ) {
273- for ( let j = i + 1 ; j < authorList . length ; j ++ ) {
274- const pair = `${ authorList [ i ] } <-> ${ authorList [ j ] } ` ;
275- collaborations [ pair ] = ( collaborations [ pair ] || 0 ) + 1 ;
272+ if ( authors . size > 1 ) {
273+ const authorList = Array . from ( authors ) . sort ( ) ;
274+ for ( let i = 0 ; i < authorList . length ; i ++ ) {
275+ for ( let j = i + 1 ; j < authorList . length ; j ++ ) {
276+ const pair = `${ authorList [ i ] } <-> ${ authorList [ j ] } ` ;
277+ collaborations [ pair ] = ( collaborations [ pair ] || 0 ) + 1 ;
278+ }
276279 }
277280 }
278281 }
279282
280283 return {
281- collaborativeFiles : Object . keys ( fileAuthors ) . length ,
284+ collaborativeFiles : Object . values ( fileAuthors ) . filter ( a => a . size > 1 ) . length ,
282285 topCollaborations : Object . entries ( collaborations )
283286 . sort ( ( [ , a ] , [ , b ] ) => b - a )
284287 . slice ( 0 , 10 )
@@ -334,34 +337,62 @@ export function handleGetTechnicalDebt(args: any) {
334337
335338 validateRepoPath ( repo_path ) ;
336339
337- const filesCmd = `git ls-files` ;
338- const files = runGitCommand ( repo_path , filesCmd ) . trim ( ) . split ( "\n" ) . slice ( 0 , 500 ) ;
339-
340- const staleFiles : any [ ] = [ ] ;
341- const largeFiles : any [ ] = [ ] ;
342-
343- for ( const file of files ) {
344- const lastChangeCmd = `git log -1 --pretty=format:"%ar" -- "${ file } "` ;
345- try {
346- const lastChange = runGitCommand ( repo_path , lastChangeCmd ) . trim ( ) ;
347- const daysMatch = lastChange . match ( / ( \d + ) \s + d a y s ? \s + a g o / ) ;
348- if ( daysMatch && parseInt ( daysMatch [ 1 ] ) > stale_days ) {
349- staleFiles . push ( { file, daysSinceLastChange : parseInt ( daysMatch [ 1 ] ) } ) ;
350- }
351-
352- const churnCmd = `git log --oneline -- "${ file } " | wc -l` ;
353- const churn = parseInt ( runGitCommand ( repo_path , churnCmd ) . trim ( ) ) ;
354- if ( churn > 20 ) {
355- largeFiles . push ( { file, churn } ) ;
356- }
357- } catch {
358- // Skip
340+ const cutoffDate = new Date ( ) ;
341+ cutoffDate . setDate ( cutoffDate . getDate ( ) - stale_days ) ;
342+ const cutoffTimestamp = Math . floor ( cutoffDate . getTime ( ) / 1000 ) ;
343+
344+ const staleCmd = `git ls-files -z | xargs -0 -n1 -I{} sh -c 'echo "{}|$(git log -1 --format=%ct -- "{}")"' | awk -F'|' '$2 < ${ cutoffTimestamp } {print $1"|"$2}'` ;
345+ const churnCmd = `git log --name-only --pretty=format: | sort | uniq -c | sort -rn | head -20` ;
346+
347+ let staleFiles : any [ ] = [ ] ;
348+ try {
349+ const staleOutput = runGitCommand ( repo_path , staleCmd ) ;
350+ const now = Math . floor ( Date . now ( ) / 1000 ) ;
351+ staleFiles = staleOutput . trim ( ) . split ( "\n" )
352+ . filter ( l => l )
353+ . map ( line => {
354+ const [ file , timestamp ] = line . split ( "|" ) ;
355+ const days = Math . floor ( ( now - parseInt ( timestamp ) ) / 86400 ) ;
356+ return { file, daysSinceLastChange : days } ;
357+ } )
358+ . slice ( 0 , 10 ) ;
359+ } catch {
360+ // Fallback to simpler approach
361+ const filesCmd = `git ls-files | head -100` ;
362+ const files = runGitCommand ( repo_path , filesCmd ) . trim ( ) . split ( "\n" ) ;
363+
364+ for ( const file of files ) {
365+ try {
366+ const lastChangeCmd = `git log -1 --format=%ct -- "${ file } "` ;
367+ const timestamp = parseInt ( runGitCommand ( repo_path , lastChangeCmd ) . trim ( ) ) ;
368+ const days = Math . floor ( ( Date . now ( ) / 1000 - timestamp ) / 86400 ) ;
369+ if ( days > stale_days ) {
370+ staleFiles . push ( { file, daysSinceLastChange : days } ) ;
371+ }
372+ } catch { }
359373 }
374+ staleFiles = staleFiles . slice ( 0 , 10 ) ;
360375 }
361376
377+ let complexityHotspots : any [ ] = [ ] ;
378+ try {
379+ const churnOutput = runGitCommand ( repo_path , churnCmd ) ;
380+ complexityHotspots = churnOutput . trim ( ) . split ( "\n" )
381+ . filter ( l => l . trim ( ) )
382+ . map ( line => {
383+ const match = line . trim ( ) . match ( / ^ \s * ( \d + ) \s + ( .+ ) $ / ) ;
384+ if ( match ) {
385+ return { file : match [ 2 ] , churn : parseInt ( match [ 1 ] ) } ;
386+ }
387+ return null ;
388+ } )
389+ . filter ( x => x !== null )
390+ . slice ( 0 , 10 ) ;
391+ } catch { }
392+
362393 return {
363- staleFiles : staleFiles . slice ( 0 , 10 ) ,
364- complexityHotspots : largeFiles . sort ( ( a , b ) => b . churn - a . churn ) . slice ( 0 , 10 ) ,
394+ staleFiles,
395+ complexityHotspots,
365396 } ;
366397}
367398
0 commit comments