@@ -234,14 +234,15 @@ export class StatsService {
234234 // Limit log to the current branch (HEAD) and follow first-parent
235235 // so repository metrics (total contributors, commits, etc.) are
236236 // computed relative to the current branch only.
237+ // Include author email in the pretty format so we can aggregate by email
237238 const output = await this . execGit (
238- [ 'log' , 'HEAD' , '--first-parent' , '--date=iso-strict' , `--pretty=${ GIT_COMMIT_PREFIX } %aI|%an` , '--numstat' ] ,
239+ [ 'log' , 'HEAD' , '--first-parent' , '--date=iso-strict' , `--pretty=${ GIT_COMMIT_PREFIX } %aI|%aE|% an` , '--numstat' ] ,
239240 rootPath
240241 ) ;
241242
242243 const byDate = new Map < string , { commits : number ; added : number ; deleted : number } > ( ) ;
243244 const byMonth = new Map < string , { commits : number ; added : number ; deleted : number } > ( ) ;
244- const byAuthor = new Map < string , { commits : number ; added : number ; deleted : number } > ( ) ;
245+ const byAuthor = new Map < string , { name : string ; email ?: string ; commits : number ; added : number ; deleted : number } > ( ) ;
245246 const byFile = new Map < string , { added : number ; deleted : number } > ( ) ;
246247 const commitsByWeekday = Array . from ( { length : 7 } , ( ) => 0 ) ;
247248 const commitsByHour = Array . from ( { length : 24 } , ( ) => 0 ) ;
@@ -258,7 +259,10 @@ export class StatsService {
258259 for ( const line of lines ) {
259260 if ( line . startsWith ( GIT_COMMIT_PREFIX ) ) {
260261 const payload = line . slice ( GIT_COMMIT_PREFIX . length ) . trim ( ) ;
261- const [ dateTime , authorName ] = payload . split ( '|' ) ;
262+ const parts = payload . split ( '|' ) ;
263+ const dateTime = parts [ 0 ] ;
264+ const authorEmail = ( parts [ 1 ] ?? '' ) . trim ( ) ;
265+ const authorName = ( parts [ 2 ] ?? '' ) . trim ( ) ;
262266 if ( ! dateTime ) {
263267 currentDateKey = undefined ;
264268 currentMonthKey = undefined ;
@@ -268,7 +272,7 @@ export class StatsService {
268272
269273 const dateKey = dateTime . slice ( 0 , 10 ) ;
270274 const monthKey = dateTime . slice ( 0 , 7 ) ;
271- const author = ( authorName ?? '' ) . trim ( ) || 'Unknown' ;
275+ const author = ( authorName ?? '' ) . trim ( ) || ( authorEmail || 'Unknown' ) ;
272276 const date = new Date ( dateTime ) ;
273277 if ( Number . isNaN ( date . getTime ( ) ) ) {
274278 currentDateKey = undefined ;
@@ -303,9 +307,12 @@ export class StatsService {
303307 monthBucket . commits += 1 ;
304308 byMonth . set ( monthKey , monthBucket ) ;
305309
306- const authorBucket = byAuthor . get ( author ) ?? { commits : 0 , added : 0 , deleted : 0 } ;
310+ const key = authorEmail || author ;
311+ const authorBucket = byAuthor . get ( key ) ?? { name : authorName || author , email : authorEmail || undefined , commits : 0 , added : 0 , deleted : 0 } ;
307312 authorBucket . commits += 1 ;
308- byAuthor . set ( author , authorBucket ) ;
313+ byAuthor . set ( key , authorBucket ) ;
314+ // store the aggregation key so following numstat lines map correctly
315+ currentAuthor = key ;
309316 continue ;
310317 }
311318
@@ -341,7 +348,7 @@ export class StatsService {
341348 byMonth . set ( currentMonthKey , monthBucket ) ;
342349 }
343350
344- const authorBucket = byAuthor . get ( currentAuthor ) ;
351+ const authorBucket = currentAuthor ? byAuthor . get ( currentAuthor ) : undefined ;
345352 if ( authorBucket ) {
346353 authorBucket . added += added ;
347354 authorBucket . deleted += deleted ;
@@ -374,12 +381,15 @@ export class StatsService {
374381 . sort ( ( a , b ) => a . month . localeCompare ( b . month ) ) ;
375382
376383 const commitsByAuthor = Array . from ( byAuthor . entries ( ) )
377- . map ( ( [ author , value ] ) => ( {
378- author,
379- commits : value . commits ,
380- added : value . added ,
381- deleted : value . deleted
382- } ) )
384+ . map ( ( [ key , value ] ) => {
385+ const display = value . email ? `${ value . name } <${ value . email } >` : value . name ;
386+ return {
387+ author : display ,
388+ commits : value . commits ,
389+ added : value . added ,
390+ deleted : value . deleted
391+ } ;
392+ } )
383393 . sort ( ( a , b ) => b . commits - a . commits ) ;
384394
385395 const topChangedFiles = Array . from ( byFile . entries ( ) )
@@ -501,20 +511,26 @@ export class StatsService {
501511 }
502512
503513 private async getContributorStatsFromGit ( rootPath : string ) : Promise < ContributorStat [ ] > {
514+ // Use author email as the aggregation key to avoid duplicate names
504515 const output = await this . execGit (
505- [ 'log' , 'HEAD' , '--first-parent' , '--numstat' , `--pretty=${ GIT_AUTHOR_PREFIX } %an` ] ,
516+ [ 'log' , 'HEAD' , '--first-parent' , '--numstat' , `--pretty=${ GIT_AUTHOR_PREFIX } %an|%aE ` ] ,
506517 rootPath
507518 ) ;
508519
509- const stats = new Map < string , { added : number ; deleted : number } > ( ) ;
520+ const stats = new Map < string , { name : string ; added : number ; deleted : number } > ( ) ;
510521 const existsCache = new Map < string , boolean > ( ) ;
511- let currentAuthor = 'Unknown' ;
522+ let currentKey = 'unknown' ;
523+ let currentName = 'Unknown' ;
512524 const lines = output . split ( / \r ? \n / ) ;
513525 for ( const line of lines ) {
514526 if ( line . startsWith ( GIT_AUTHOR_PREFIX ) ) {
515- currentAuthor = line . slice ( GIT_AUTHOR_PREFIX . length ) . trim ( ) || 'Unknown' ;
516- if ( ! stats . has ( currentAuthor ) ) {
517- stats . set ( currentAuthor , { added : 0 , deleted : 0 } ) ;
527+ const payload = line . slice ( GIT_AUTHOR_PREFIX . length ) . trim ( ) ;
528+ const [ namePart , emailPart ] = payload . split ( '|' ) ;
529+ currentName = ( namePart ?? 'Unknown' ) . trim ( ) || 'Unknown' ;
530+ const email = ( emailPart ?? '' ) . trim ( ) ;
531+ currentKey = email || currentName ;
532+ if ( ! stats . has ( currentKey ) ) {
533+ stats . set ( currentKey , { name : currentName , added : 0 , deleted : 0 } ) ;
518534 }
519535 continue ;
520536 }
@@ -543,14 +559,14 @@ export class StatsService {
543559 continue ;
544560 }
545561
546- const current = stats . get ( currentAuthor ) ?? { added : 0 , deleted : 0 } ;
562+ const current = stats . get ( currentKey ) ?? { name : currentName , added : 0 , deleted : 0 } ;
547563 current . added += added ;
548564 current . deleted += deleted ;
549- stats . set ( currentAuthor , current ) ;
565+ stats . set ( currentKey , current ) ;
550566 }
551567
552568 return Array . from ( stats . entries ( ) )
553- . map ( ( [ name , v ] ) => ( { name, added : v . added , deleted : v . deleted } ) )
569+ . map ( ( [ key , v ] ) => ( { name : v . name , added : v . added , deleted : v . deleted } ) )
554570 // Filter out authors with no recorded changes (added + deleted === 0)
555571 . filter ( item => ( item . added + item . deleted ) > 0 )
556572 . sort ( ( a , b ) => ( b . added + b . deleted ) - ( a . added + a . deleted ) ) ;
@@ -559,19 +575,25 @@ export class StatsService {
559575 private async getContributorStatsFromGitAll ( rootPath : string ) : Promise < ContributorStat [ ] > {
560576 // For "all" contributors we want to consider the entire repository
561577 // history across all refs/branches. Use --all to include all refs.
578+ // Aggregate by email to avoid duplicates across name variations.
562579 const output = await this . execGit (
563- [ 'log' , '--all' , '--numstat' , `--pretty=${ GIT_AUTHOR_PREFIX } %an` ] ,
580+ [ 'log' , '--all' , '--numstat' , `--pretty=${ GIT_AUTHOR_PREFIX } %an|%aE ` ] ,
564581 rootPath
565582 ) ;
566583
567- const stats = new Map < string , { added : number ; deleted : number } > ( ) ;
568- let currentAuthor = 'Unknown' ;
584+ const stats = new Map < string , { name : string ; added : number ; deleted : number } > ( ) ;
585+ let currentKey = 'unknown' ;
586+ let currentName = 'Unknown' ;
569587 const lines = output . split ( / \r ? \n / ) ;
570588 for ( const line of lines ) {
571589 if ( line . startsWith ( GIT_AUTHOR_PREFIX ) ) {
572- currentAuthor = line . slice ( GIT_AUTHOR_PREFIX . length ) . trim ( ) || 'Unknown' ;
573- if ( ! stats . has ( currentAuthor ) ) {
574- stats . set ( currentAuthor , { added : 0 , deleted : 0 } ) ;
590+ const payload = line . slice ( GIT_AUTHOR_PREFIX . length ) . trim ( ) ;
591+ const [ namePart , emailPart ] = payload . split ( '|' ) ;
592+ currentName = ( namePart ?? 'Unknown' ) . trim ( ) || 'Unknown' ;
593+ const email = ( emailPart ?? '' ) . trim ( ) ;
594+ currentKey = email || currentName ;
595+ if ( ! stats . has ( currentKey ) ) {
596+ stats . set ( currentKey , { name : currentName , added : 0 , deleted : 0 } ) ;
575597 }
576598 continue ;
577599 }
@@ -593,14 +615,14 @@ export class StatsService {
593615 continue ;
594616 }
595617
596- const current = stats . get ( currentAuthor ) ?? { added : 0 , deleted : 0 } ;
618+ const current = stats . get ( currentKey ) ?? { name : currentName , added : 0 , deleted : 0 } ;
597619 current . added += added ;
598620 current . deleted += deleted ;
599- stats . set ( currentAuthor , current ) ;
621+ stats . set ( currentKey , current ) ;
600622 }
601623
602624 return Array . from ( stats . entries ( ) )
603- . map ( ( [ name , v ] ) => ( { name, added : v . added , deleted : v . deleted } ) )
625+ . map ( ( [ key , v ] ) => ( { name : v . name , added : v . added , deleted : v . deleted } ) )
604626 // Exclude authors with zero total changes when aggregating across all refs
605627 . filter ( item => ( item . added + item . deleted ) > 0 )
606628 . sort ( ( a , b ) => ( b . added + b . deleted ) - ( a . added + a . deleted ) ) ;
0 commit comments