Skip to content

Commit 6848de2

Browse files
committed
Enhance contributor stats aggregation by including author email and improve contributors chart toggle functionality with delegated event handling
1 parent d4d74dd commit 6848de2

2 files changed

Lines changed: 74 additions & 48 deletions

File tree

src/services/StatsService.ts

Lines changed: 53 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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));

src/views/webview/components/contributors.ts

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -82,24 +82,28 @@ export const renderContributorsChart = (
8282
});
8383
}
8484
85-
const toggle = document.getElementById('contributors-toggle');
86-
const all = document.getElementById('contributors-all');
87-
const list = document.getElementById('contributors-list');
88-
if (toggle && all && list) {
89-
toggle.addEventListener('click', () => {
90-
if (all.style.display === 'none') {
91-
list.style.display = 'none';
92-
all.style.display = 'block';
93-
toggle.textContent = 'Show fewer contributors';
94-
} else {
95-
list.style.display = 'block';
96-
all.style.display = 'none';
97-
toggle.textContent = 'Show all contributors';
98-
}
85+
// Use delegated click handling and guard for DOM readiness
86+
document.addEventListener('click', (ev) => {
87+
const target = ev.target;
88+
if (!(target instanceof HTMLElement)) return;
89+
if (target.id !== 'contributors-toggle') return;
9990
100-
window.__codecountRequestLayout?.();
101-
});
102-
}
91+
const allEl = document.getElementById('contributors-all');
92+
const listEl = document.getElementById('contributors-list');
93+
if (!allEl || !listEl) return;
94+
95+
if (allEl.style.display === 'none' || getComputedStyle(allEl).display === 'none') {
96+
listEl.style.display = 'none';
97+
allEl.style.display = 'block';
98+
target.textContent = 'Show fewer contributors';
99+
} else {
100+
listEl.style.display = 'block';
101+
allEl.style.display = 'none';
102+
target.textContent = 'Show all contributors';
103+
}
104+
105+
window.__codecountRequestLayout?.();
106+
});
103107
</script>
104108
</div>
105109
`;

0 commit comments

Comments
 (0)