@@ -41,7 +41,7 @@ import {
4141 WorktreeDeleteErrorReason ,
4242} from '../../../git/errors' ;
4343import type {
44- BranchContributorOverview ,
44+ BranchContributionsOverview ,
4545 GitCaches ,
4646 GitDir ,
4747 GitProvider ,
@@ -75,6 +75,7 @@ import type { GitStashCommit } from '../../../git/models/commit';
7575import { GitCommit , GitCommitIdentity } from '../../../git/models/commit' ;
7676import type { GitContributorStats } from '../../../git/models/contributor' ;
7777import { GitContributor } from '../../../git/models/contributor' ;
78+ import { calculateContributionScore } from '../../../git/models/contributor.utils' ;
7879import type {
7980 GitDiff ,
8081 GitDiffFile ,
@@ -3083,37 +3084,54 @@ export class LocalGitProvider implements GitProvider, Disposable {
30833084 const data = await this . git . log ( repoPath , { ref : options ?. ref } , ...args ) ;
30843085
30853086 const contributors = new Map < string , GitContributor > ( ) ;
3086-
30873087 const commits = parser . parse ( data ) ;
30883088 for ( const c of commits ) {
30893089 const key = `${ c . author } |${ c . email } ` ;
3090- let contributor = contributors . get ( key ) ;
3090+ const timestamp = Number ( c . date ) * 1000 ;
3091+
3092+ let contributor : Mutable < GitContributor > | undefined = contributors . get ( key ) ;
30913093 if ( contributor == null ) {
30923094 contributor = new GitContributor (
30933095 repoPath ,
30943096 c . author ,
30953097 c . email ,
30963098 1 ,
3097- new Date ( Number ( c . date ) * 1000 ) ,
3099+ new Date ( timestamp ) ,
3100+ new Date ( timestamp ) ,
30983101 isUserMatch ( currentUser , c . author , c . email ) ,
3099- c . stats ,
3102+ c . stats
3103+ ? {
3104+ ...c . stats ,
3105+ contributionScore : calculateContributionScore ( c . stats , timestamp ) ,
3106+ }
3107+ : undefined ,
31003108 ) ;
31013109 contributors . set ( key , contributor ) ;
31023110 } else {
3103- ( contributor as PickMutable < GitContributor , 'count' > ) . count ++ ;
3104- if ( options ?. stats && c . stats != null ) {
3105- ( contributor as PickMutable < GitContributor , 'stats' > ) . stats =
3106- contributor . stats == null
3107- ? c . stats
3108- : {
3109- additions : contributor . stats . additions + c . stats . additions ,
3110- deletions : contributor . stats . deletions + c . stats . deletions ,
3111- files : contributor . stats . files + c . stats . files ,
3112- } ;
3111+ contributor . commits ++ ;
3112+ const date = new Date ( timestamp ) ;
3113+ if ( date > contributor . latestCommitDate ! ) {
3114+ contributor . latestCommitDate = date ;
31133115 }
3114- const date = new Date ( Number ( c . date ) * 1000 ) ;
3115- if ( date > contributor . date ! ) {
3116- ( contributor as PickMutable < GitContributor , 'date' > ) . date = date ;
3116+ if ( date < contributor . firstCommitDate ! ) {
3117+ contributor . firstCommitDate = date ;
3118+ }
3119+ if ( options ?. stats && c . stats != null ) {
3120+ if ( contributor . stats == null ) {
3121+ contributor . stats = {
3122+ ...c . stats ,
3123+ contributionScore : calculateContributionScore ( c . stats , timestamp ) ,
3124+ } ;
3125+ } else {
3126+ contributor . stats = {
3127+ additions : contributor . stats . additions + c . stats . additions ,
3128+ deletions : contributor . stats . deletions + c . stats . deletions ,
3129+ files : contributor . stats . files + c . stats . files ,
3130+ contributionScore :
3131+ contributor . stats . contributionScore +
3132+ calculateContributionScore ( c . stats , timestamp ) ,
3133+ } ;
3134+ }
31173135 }
31183136 }
31193137 }
@@ -3211,8 +3229,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
32113229
32123230 @log ( { exit : true } )
32133231 async getBaseBranchName ( repoPath : string , ref : string ) : Promise < string | undefined > {
3214- const mergeBaseConfigKey : GitConfigKeys = `branch.${ ref } .gk-merge-base` ;
3215-
32163232 try {
32173233 const pattern = `^branch\\.${ ref } \\.` ;
32183234 const data = await this . git . config__get_regex ( pattern , repoPath ) ;
@@ -3238,27 +3254,38 @@ export class LocalGitProvider implements GitProvider, Disposable {
32383254 }
32393255
32403256 if ( mergeBase != null ) {
3241- const [ branch ] = ( await this . getBranches ( repoPath , { filter : b => b . name === mergeBase } ) ) . values ;
3257+ const branch = await this . getValidatedBranchName ( repoPath , mergeBase ) ;
32423258 if ( branch != null ) {
32433259 if ( update ) {
3244- void this . setConfig ( repoPath , mergeBaseConfigKey , branch . name ) ;
3260+ void this . setBaseBranchName ( repoPath , ref , branch ) ;
32453261 }
3246- return branch . name ;
3262+ return branch ;
32473263 }
32483264 }
32493265 }
32503266 } catch { }
32513267
3252- const branch = await this . getBaseBranchFromReflog ( repoPath , ref ) ;
3253- if ( branch ?. upstream != null ) {
3254- void this . setConfig ( repoPath , mergeBaseConfigKey , branch . upstream . name ) ;
3255- return branch . upstream . name ;
3268+ const branch = await this . getBaseBranchFromReflog ( repoPath , ref , { upstream : true } ) ;
3269+ if ( branch != null ) {
3270+ void this . setBaseBranchName ( repoPath , ref , branch ) ;
3271+ return branch ;
32563272 }
32573273
32583274 return undefined ;
32593275 }
32603276
3261- private async getBaseBranchFromReflog ( repoPath : string , ref : string ) : Promise < GitBranch | undefined > {
3277+ @log ( )
3278+ async setBaseBranchName ( repoPath : string , ref : string , base : string ) : Promise < void > {
3279+ const mergeBaseConfigKey : GitConfigKeys = `branch.${ ref } .gk-merge-base` ;
3280+
3281+ await this . setConfig ( repoPath , mergeBaseConfigKey , base ) ;
3282+ }
3283+
3284+ private async getBaseBranchFromReflog (
3285+ repoPath : string ,
3286+ ref : string ,
3287+ options ?: { upstream : true } ,
3288+ ) : Promise < string | undefined > {
32623289 try {
32633290 let data = await this . git . reflog ( repoPath , undefined , ref , '--grep-reflog=branch: Created from *.' ) ;
32643291
@@ -3268,10 +3295,10 @@ export class LocalGitProvider implements GitProvider, Disposable {
32683295 // Check if branch created from an explicit branch
32693296 let match = entries [ 0 ] . match ( / b r a n c h : C r e a t e d f r o m ( .* ) $ / ) ;
32703297 if ( match != null && match . length === 2 ) {
3271- const name = match [ 1 ] ;
3298+ let name : string | undefined = match [ 1 ] ;
32723299 if ( name !== 'HEAD' ) {
3273- const [ branch ] = ( await this . getBranches ( repoPath , { filter : b => b . name === name } ) ) . values ;
3274- return branch ;
3300+ name = await this . getValidatedBranchName ( repoPath , options ?. upstream ? ` ${ name } @{u}` : name ) ;
3301+ if ( name ) return name ;
32753302 }
32763303 }
32773304
@@ -3288,15 +3315,28 @@ export class LocalGitProvider implements GitProvider, Disposable {
32883315
32893316 match = entries [ entries . length - 1 ] . match ( / c h e c k o u t : m o v i n g f r o m ( [ ^ \s ] + ) \s / ) ;
32903317 if ( match != null && match . length === 2 ) {
3291- const name = match [ 1 ] ;
3292- const [ branch ] = ( await this . getBranches ( repoPath , { filter : b => b . name === name } ) ) . values ;
3293- return branch ;
3318+ let name : string | undefined = match [ 1 ] ;
3319+ name = await this . getValidatedBranchName ( repoPath , options ?. upstream ? ` ${ name } @{u}` : name ) ;
3320+ if ( name ) return name ;
32943321 }
32953322 } catch { }
32963323
32973324 return undefined ;
32983325 }
32993326
3327+ private async getValidatedBranchName ( repoPath : string , name : string ) : Promise < string | undefined > {
3328+ const data = await this . git . git < string > (
3329+ { cwd : repoPath } ,
3330+ 'rev-parse' ,
3331+ '--verify' ,
3332+ '--quiet' ,
3333+ '--symbolic-full-name' ,
3334+ '--abbrev-ref' ,
3335+ name ,
3336+ ) ;
3337+ return data ?. trim ( ) || undefined ;
3338+ }
3339+
33003340 @log ( { exit : true } )
33013341 async getDefaultBranchName ( repoPath : string | undefined , remote ?: string ) : Promise < string | undefined > {
33023342 if ( repoPath == null ) return undefined ;
@@ -3316,12 +3356,30 @@ export class LocalGitProvider implements GitProvider, Disposable {
33163356
33173357 try {
33183358 const data = await this . git . symbolic_ref ( repoPath , `refs/remotes/origin/HEAD` ) ;
3319- if ( data != null ) return data . trim ( ) ;
3359+ return data ? .trim ( ) || undefined ;
33203360 } catch { }
33213361
33223362 return undefined ;
33233363 }
33243364
3365+ @log ( { exit : true } )
3366+ async getTargetBranchName ( repoPath : string , ref : string ) : Promise < string | undefined > {
3367+ const targetBaseConfigKey : GitConfigKeys = `branch.${ ref } .gk-target-base` ;
3368+
3369+ let target = await this . getConfig ( repoPath , targetBaseConfigKey ) ;
3370+ if ( target != null ) {
3371+ target = await this . getValidatedBranchName ( repoPath , target ) ;
3372+ }
3373+ return target ?. trim ( ) || undefined ;
3374+ }
3375+
3376+ @log ( )
3377+ async setTargetBranchName ( repoPath : string , ref : string , target : string ) : Promise < void > {
3378+ const targetBaseConfigKey : GitConfigKeys = `branch.${ ref } .gk-target-base` ;
3379+
3380+ await this . setConfig ( repoPath , targetBaseConfigKey , target ) ;
3381+ }
3382+
33253383 @log ( )
33263384 async getDiff (
33273385 repoPath : string ,
@@ -6439,19 +6497,74 @@ export class LocalGitProvider implements GitProvider, Disposable {
64396497 }
64406498
64416499 @log ( )
6442- async getBranchContributorOverview ( repoPath : string , ref : string ) : Promise < BranchContributorOverview | undefined > {
6500+ async getBranchContributionsOverview (
6501+ repoPath : string ,
6502+ ref : string ,
6503+ ) : Promise < BranchContributionsOverview | undefined > {
64436504 const scope = getLogScope ( ) ;
64446505
64456506 try {
6446- const base = await this . getBaseBranchName ( repoPath , ref ) ;
6507+ let baseOrTargetBranch = await this . getBaseBranchName ( repoPath , ref ) ;
6508+ // If the base looks like its remote branch, look for the target or default
6509+ if ( baseOrTargetBranch == null || baseOrTargetBranch . endsWith ( `/${ ref } ` ) ) {
6510+ baseOrTargetBranch = await this . getTargetBranchName ( repoPath , ref ) ;
6511+ baseOrTargetBranch ??= await this . getDefaultBranchName ( repoPath ) ;
6512+ if ( baseOrTargetBranch == null ) return undefined ;
6513+ }
6514+
6515+ const mergeBase = await this . getMergeBase ( repoPath , ref , baseOrTargetBranch ) ;
6516+ if ( mergeBase == null ) return undefined ;
6517+
64476518 const contributors = await this . getContributors ( repoPath , {
6448- ref : createRevisionRange ( ref , base , '. ..' ) ,
6519+ ref : createRevisionRange ( mergeBase , ref , '..' ) ,
64496520 stats : true ,
64506521 } ) ;
64516522
6452- sortContributors ( contributors , { orderBy : 'count:desc' } ) ;
6523+ sortContributors ( contributors , { orderBy : 'score:desc' } ) ;
6524+
6525+ let totalCommits = 0 ;
6526+ let totalFiles = 0 ;
6527+ let totalAdditions = 0 ;
6528+ let totalDeletions = 0 ;
6529+ let firstCommitTimestamp ;
6530+ let latestCommitTimestamp ;
6531+
6532+ for ( const c of contributors ) {
6533+ totalCommits += c . commits ;
6534+ totalFiles += c . stats ?. files ?? 0 ;
6535+ totalAdditions += c . stats ?. additions ?? 0 ;
6536+ totalDeletions += c . stats ?. deletions ?? 0 ;
6537+
6538+ const firstTimestamp = c . firstCommitDate ?. getTime ( ) ;
6539+ const latestTimestamp = c . latestCommitDate ?. getTime ( ) ;
6540+
6541+ if ( firstTimestamp != null || latestTimestamp != null ) {
6542+ firstCommitTimestamp =
6543+ firstCommitTimestamp != null
6544+ ? Math . min ( firstCommitTimestamp , firstTimestamp ?? Infinity , latestTimestamp ?? Infinity )
6545+ : firstTimestamp ?? latestTimestamp ;
6546+
6547+ latestCommitTimestamp =
6548+ latestCommitTimestamp != null
6549+ ? Math . max ( latestCommitTimestamp , firstTimestamp ?? - Infinity , latestTimestamp ?? - Infinity )
6550+ : latestTimestamp ?? firstTimestamp ;
6551+ }
6552+ }
6553+
64536554 return {
6454- // owner: contributors.find(c => c.email === this.getCurrentUser(repoPath)?.email),
6555+ repoPath : repoPath ,
6556+ branch : ref ,
6557+ baseOrTargetBranch : baseOrTargetBranch ,
6558+ mergeBase : mergeBase ,
6559+
6560+ commits : totalCommits ,
6561+ files : totalFiles ,
6562+ additions : totalAdditions ,
6563+ deletions : totalDeletions ,
6564+
6565+ latestCommitDate : latestCommitTimestamp != null ? new Date ( latestCommitTimestamp ) : undefined ,
6566+ firstCommitDate : firstCommitTimestamp != null ? new Date ( firstCommitTimestamp ) : undefined ,
6567+
64556568 contributors : contributors ,
64566569 } ;
64576570 } catch ( ex ) {
0 commit comments