Skip to content

Commit d63328b

Browse files
KebanFiruCopilot
andcommitted
Add email to ContributorStat and include branch information in stats results
Co-authored-by: Copilot <copilot@github.com>
1 parent 6848de2 commit d63328b

4 files changed

Lines changed: 56 additions & 22 deletions

File tree

src/services/StatsService.ts

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,14 @@ export class StatsService {
122122
return { available: true, stats: [] };
123123
}
124124

125-
return { available: true, stats };
125+
let branch: string | undefined;
126+
try {
127+
branch = (await this.execGit(['rev-parse', '--abbrev-ref', 'HEAD'], rootPath)).trim();
128+
} catch {
129+
branch = undefined;
130+
}
131+
132+
return { available: true, stats, branch };
126133
}
127134

128135
async getContributorStatsAll(): Promise<ContributorStatsResult> {
@@ -138,7 +145,9 @@ export class StatsService {
138145
return { available: true, stats: [] };
139146
}
140147

141-
return { available: true, stats };
148+
149+
// mark as 'all' to indicate repo-wide data
150+
return { available: true, stats, branch: 'all' };
142151
}
143152

144153
async getContributorLanguageStats(authorName: string): Promise<{ available: boolean; stats: ContributorLanguageStat[] }> {
@@ -566,7 +575,7 @@ export class StatsService {
566575
}
567576

568577
return Array.from(stats.entries())
569-
.map(([key, v]) => ({ name: v.name, added: v.added, deleted: v.deleted }))
578+
.map(([key, v]) => ({ name: v.name, email: key && key.includes('@') ? key : undefined, added: v.added, deleted: v.deleted }))
570579
// Filter out authors with no recorded changes (added + deleted === 0)
571580
.filter(item => (item.added + item.deleted) > 0)
572581
.sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted));
@@ -622,7 +631,7 @@ export class StatsService {
622631
}
623632

624633
return Array.from(stats.entries())
625-
.map(([key, v]) => ({ name: v.name, added: v.added, deleted: v.deleted }))
634+
.map(([key, v]) => ({ name: v.name, email: key && key.includes('@') ? key : undefined, added: v.added, deleted: v.deleted }))
626635
// Exclude authors with zero total changes when aggregating across all refs
627636
.filter(item => (item.added + item.deleted) > 0)
628637
.sort((a, b) => (b.added + b.deleted) - (a.added + a.deleted));
@@ -632,28 +641,36 @@ export class StatsService {
632641
rootPath: string,
633642
authorName: string
634643
): Promise<ContributorLanguageStat[]> {
644+
// Include author email so we can match by name or email when authorName may be an email
635645
const output = await this.execGit(
636-
['log', 'HEAD', '--first-parent', '--numstat', `--pretty=${GIT_AUTHOR_PREFIX}%an`],
646+
['log', 'HEAD', '--first-parent', '--numstat', `--pretty=${GIT_AUTHOR_PREFIX}%an|%aE`],
637647
rootPath
638648
);
639649

640650
const languageTotals = new Map<string, number>();
641651
const languageByPath = new Map<string, string>();
642652
const existsCache = new Map<string, boolean>();
643-
let currentAuthor = 'Unknown';
653+
let currentAuthorName = 'Unknown';
654+
let currentAuthorEmail = '';
644655
const lines = output.split(/\r?\n/);
645656
for (const line of lines) {
646657
if (line.startsWith(GIT_AUTHOR_PREFIX)) {
647-
currentAuthor = line.slice(GIT_AUTHOR_PREFIX.length).trim() || 'Unknown';
658+
const payload = line.slice(GIT_AUTHOR_PREFIX.length).trim();
659+
const [namePart, emailPart] = payload.split('|');
660+
currentAuthorName = (namePart ?? 'Unknown').trim() || 'Unknown';
661+
currentAuthorEmail = (emailPart ?? '').trim();
648662
continue;
649663
}
650664

651665
if (!line.trim()) {
652666
continue;
653667
}
654668

655-
if (currentAuthor !== authorName) {
656-
continue;
669+
// Match either by provided name or email
670+
if (authorName.includes('@')) {
671+
if (currentAuthorEmail !== authorName) continue;
672+
} else {
673+
if (currentAuthorName !== authorName) continue;
657674
}
658675

659676
const [addedText, , filePath] = line.split('\t');
@@ -685,28 +702,36 @@ export class StatsService {
685702
authorName: string,
686703
languageId: string
687704
): Promise<ContributorFileStat[]> {
705+
// Include author email so we can match by name or email when authorName may be an email
688706
const output = await this.execGit(
689-
['log', 'HEAD', '--first-parent', '--numstat', `--pretty=${GIT_AUTHOR_PREFIX}%an`],
707+
['log', 'HEAD', '--first-parent', '--numstat', `--pretty=${GIT_AUTHOR_PREFIX}%an|%aE`],
690708
rootPath
691709
);
692710

693711
const fileTotals = new Map<string, { added: number; deleted: number }>();
694712
const languageByPath = new Map<string, string>();
695713
const existsCache = new Map<string, boolean>();
696-
let currentAuthor = 'Unknown';
714+
let currentAuthorName = 'Unknown';
715+
let currentAuthorEmail = '';
697716
const lines = output.split(/\r?\n/);
698717
for (const line of lines) {
699718
if (line.startsWith(GIT_AUTHOR_PREFIX)) {
700-
currentAuthor = line.slice(GIT_AUTHOR_PREFIX.length).trim() || 'Unknown';
719+
const payload = line.slice(GIT_AUTHOR_PREFIX.length).trim();
720+
const [namePart, emailPart] = payload.split('|');
721+
currentAuthorName = (namePart ?? 'Unknown').trim() || 'Unknown';
722+
currentAuthorEmail = (emailPart ?? '').trim();
701723
continue;
702724
}
703725

704726
if (!line.trim()) {
705727
continue;
706728
}
707729

708-
if (currentAuthor !== authorName) {
709-
continue;
730+
// Match either by provided name or email
731+
if (authorName.includes('@')) {
732+
if (currentAuthorEmail !== authorName) continue;
733+
} else {
734+
if (currentAuthorName !== authorName) continue;
710735
}
711736

712737
const [addedText, deletedText, filePath] = line.split('\t');

src/types/stats.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export type LanguageStat = { languageId: string; lines: number };
22
export type LanguageFileStat = { filePath: string; absolutePath: string; lines: number };
3-
export type ContributorStat = { name: string; added: number; deleted: number };
3+
export type ContributorStat = { name: string; email?: string; added: number; deleted: number };
44
export type ContributorLanguageStat = { languageId: string; linesAdded: number };
55
export type ContributorFileStat = { filePath: string; added: number; deleted: number };
66
export type ContributorFileHistoryEntry = { hash: string; date: string };
@@ -15,6 +15,7 @@ export type LanguageStatsResult = {
1515
export type ContributorStatsResult = {
1616
available: boolean;
1717
stats: ContributorStat[];
18+
branch?: string;
1819
};
1920

2021
export type RepoCommitTrendStat = {

src/views/StatsTreeProvider.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,10 @@ export class StatsTreeProvider implements vscode.TreeDataProvider<StatsNode> {
186186
}
187187

188188
const totalChanges = effectiveStats.reduce((sum, s) => sum + (s.added + s.deleted), 0);
189+
const branchLabel = result.branch && result.branch !== 'all' ? ` (branch: ${result.branch})` : '';
189190
const totalNode = new StatsNode(
190191
'info',
191-
`Total contributors: ${effectiveStats.length}`,
192+
`Total contributors${branchLabel}: ${effectiveStats.length}`,
192193
vscode.TreeItemCollapsibleState.None
193194
);
194195
totalNode.description = `${totalChanges.toLocaleString()} total changes`;
@@ -197,9 +198,10 @@ export class StatsTreeProvider implements vscode.TreeDataProvider<StatsNode> {
197198
const entryTotal = entry.added + entry.deleted;
198199
const percent = totalChanges > 0 ? Math.round((entryTotal / totalChanges) * 100) : 0;
199200
const description = `${entryTotal.toLocaleString()} changes (${percent}%)`;
200-
const node = new StatsNode('contributor', entry.name, vscode.TreeItemCollapsibleState.Collapsed, entry.name);
201+
const displayName = entry.name;
202+
const node = new StatsNode('contributor', displayName, vscode.TreeItemCollapsibleState.Collapsed, entry.email ?? entry.name);
201203
node.description = description;
202-
node.tooltip = `+${entry.added.toLocaleString()} -${entry.deleted.toLocaleString()} by ${entry.name}\nWorkspace changes`;
204+
node.tooltip = `+${entry.added.toLocaleString()} -${entry.deleted.toLocaleString()} by ${displayName}\nWorkspace changes`;
203205
return node;
204206
})];
205207
}
@@ -231,7 +233,8 @@ export class StatsTreeProvider implements vscode.TreeDataProvider<StatsNode> {
231233
const entryTotal = entry.added + entry.deleted;
232234
const percent = totalChanges > 0 ? Math.round((entryTotal / totalChanges) * 100) : 0;
233235
const description = `${entryTotal.toLocaleString()} changes (${percent}%)`;
234-
const node = new StatsNode('contributorAll', entry.name, vscode.TreeItemCollapsibleState.None);
236+
const displayName = entry.name;
237+
const node = new StatsNode('contributorAll', displayName, vscode.TreeItemCollapsibleState.None, entry.email ?? entry.name);
235238
node.description = description;
236239
node.tooltip = `+${entry.added.toLocaleString()} -${entry.deleted.toLocaleString()} in repository`;
237240
return node;

src/views/webview/components/contributors.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ContributorStat } from '../../../types';
22
import { escapeHtml, generateColors } from '../utils';
33

44
export const renderContributorsChart = (
5-
result: { available: boolean; stats: ContributorStat[] }
5+
result: { available: boolean; stats: ContributorStat[]; branch?: string }
66
): string => {
77
if (!result.available || result.stats.length === 0) {
88
return `
@@ -20,11 +20,12 @@ export const renderContributorsChart = (
2020

2121
const previewRows = topPreview.map((stat, idx) => {
2222
const total = stat.added + stat.deleted;
23+
const displayName = escapeHtml(stat.name);
2324
const percent = Math.max(1, Math.round((total / maxChanges) * 100));
2425
return `
2526
<div class="row">
2627
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px;">
27-
<strong>${escapeHtml(stat.name)}</strong>
28+
<strong>${displayName}</strong>
2829
<span style="color: ${colors[idx]}; font-weight:600; font-size:14px;">+${stat.added.toLocaleString()} -${stat.deleted.toLocaleString()}</span>
2930
</div>
3031
<div class="bar"><span style="width:${percent}%; background-color: ${colors[idx]}"></span></div>
@@ -35,10 +36,11 @@ export const renderContributorsChart = (
3536

3637
const allRows = result.stats.map((stat, idx) => {
3738
const total = stat.added + stat.deleted;
39+
const displayName = escapeHtml(stat.name);
3840
return `
3941
<div class="row">
4042
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px;">
41-
<strong>${escapeHtml(stat.name)}</strong>
43+
<strong>${displayName}</strong>
4244
<span style="color: ${colors[idx]}; font-weight:600; font-size:13px;">+${stat.added.toLocaleString()} -${stat.deleted.toLocaleString()}</span>
4345
</div>
4446
<div class="muted">${total.toLocaleString()} total changes</div>
@@ -49,9 +51,12 @@ export const renderContributorsChart = (
4951
const barLabels = result.stats.slice(0, 10).map(s => escapeHtml(s.name));
5052
const barData = result.stats.slice(0, 10).map(s => s.added + s.deleted);
5153

54+
const branchLabel = result.branch && result.branch !== 'all' ? `<div class="muted" style="font-size:12px;margin-top:4px;">Branch: ${escapeHtml(result.branch)}</div>` : '';
55+
5256
return `
5357
<div class="card">
5458
<h2>Contributors</h2>
59+
${branchLabel}
5560
<div class="section-grid">
5661
<div>
5762
<div id="contributors-list">

0 commit comments

Comments
 (0)