mirror of
https://github.com/lukaszraczylo/git-velocity.git
synced 2026-06-05 22:43:56 +00:00
Fixup user profiles display.
This commit is contained in:
@@ -562,6 +562,7 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
|
||||
return &models.GlobalMetrics{
|
||||
Period: period,
|
||||
Repositories: repositories,
|
||||
Contributors: contributors,
|
||||
Teams: teams,
|
||||
TotalContributors: len(contributors),
|
||||
TotalCommits: totalCommits,
|
||||
|
||||
@@ -125,11 +125,12 @@ type TeamMetrics struct {
|
||||
|
||||
// GlobalMetrics holds metrics aggregated across all repositories
|
||||
type GlobalMetrics struct {
|
||||
Period Period `json:"period"`
|
||||
Repositories []RepositoryMetrics `json:"repositories"`
|
||||
Teams []TeamMetrics `json:"teams"`
|
||||
Leaderboard []LeaderboardEntry `json:"leaderboard"`
|
||||
TopAchievers map[string]string `json:"top_achievers"` // category -> login
|
||||
Period Period `json:"period"`
|
||||
Repositories []RepositoryMetrics `json:"repositories"`
|
||||
Contributors []ContributorMetrics `json:"contributors"` // Aggregated across all repos
|
||||
Teams []TeamMetrics `json:"teams"`
|
||||
Leaderboard []LeaderboardEntry `json:"leaderboard"`
|
||||
TopAchievers map[string]string `json:"top_achievers"` // category -> login
|
||||
|
||||
// Summary stats
|
||||
TotalContributors int `json:"total_contributors"`
|
||||
|
||||
@@ -118,6 +118,7 @@ func (c *Calculator) Calculate(metrics *models.GlobalMetrics) *models.GlobalMetr
|
||||
// Update the metrics
|
||||
metrics.Leaderboard = leaderboard
|
||||
metrics.TopAchievers = topAchievers
|
||||
metrics.Contributors = contributors // Update global contributors with scored data
|
||||
|
||||
// Calculate per-repository scores (based on repo-specific metrics, not global)
|
||||
for i := range metrics.Repositories {
|
||||
|
||||
@@ -98,6 +98,108 @@ func TestCalculator_BasicScoring(t *testing.T) {
|
||||
assert.Equal(t, 840, entry.Score)
|
||||
}
|
||||
|
||||
func TestCalculator_GlobalContributorsPopulatedWithScores(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Scoring.Enabled = true
|
||||
cfg.Scoring.Points = config.PointsConfig{
|
||||
Commit: 10,
|
||||
PROpened: 25,
|
||||
PRMerged: 50,
|
||||
PRReviewed: 30,
|
||||
ReviewComment: 5,
|
||||
LinesAdded: 0.1,
|
||||
LinesDeleted: 0.05,
|
||||
}
|
||||
calc := NewCalculator(cfg)
|
||||
|
||||
// Contributor appears in multiple repos with different stats
|
||||
metrics := &models.GlobalMetrics{
|
||||
Repositories: []models.RepositoryMetrics{
|
||||
{
|
||||
FullName: "owner/repo1",
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{
|
||||
Login: "alice",
|
||||
Name: "Alice",
|
||||
CommitCount: 50,
|
||||
PRsOpened: 5,
|
||||
PRsMerged: 3,
|
||||
},
|
||||
{
|
||||
Login: "bob",
|
||||
Name: "Bob",
|
||||
CommitCount: 20,
|
||||
PRsOpened: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "owner/repo2",
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{
|
||||
Login: "alice",
|
||||
Name: "Alice",
|
||||
CommitCount: 30, // Additional commits in second repo
|
||||
PRsOpened: 3,
|
||||
PRsMerged: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := calc.Calculate(metrics)
|
||||
|
||||
// Verify metrics.Contributors is populated
|
||||
require.NotEmpty(t, result.Contributors, "metrics.Contributors should be populated")
|
||||
require.Len(t, result.Contributors, 2, "Should have 2 unique contributors")
|
||||
|
||||
// Find alice in Contributors
|
||||
var alice *models.ContributorMetrics
|
||||
for i := range result.Contributors {
|
||||
if result.Contributors[i].Login == "alice" {
|
||||
alice = &result.Contributors[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, alice, "Alice should be in Contributors")
|
||||
|
||||
// Verify alice has AGGREGATED stats
|
||||
assert.Equal(t, 80, alice.CommitCount, "Alice should have aggregated commits (50+30)")
|
||||
assert.Equal(t, 8, alice.PRsOpened, "Alice should have aggregated PRs opened (5+3)")
|
||||
assert.Equal(t, 5, alice.PRsMerged, "Alice should have aggregated PRs merged (3+2)")
|
||||
|
||||
// Verify alice has a calculated score with breakdown
|
||||
assert.Greater(t, alice.Score.Total, 0, "Alice should have a calculated score")
|
||||
assert.Greater(t, alice.Score.Breakdown.Commits, 0, "Score breakdown should have commits")
|
||||
assert.Greater(t, alice.Score.Breakdown.PRs, 0, "Score breakdown should have PRs")
|
||||
|
||||
// Verify score calculation:
|
||||
// Commits: 80 * 10 = 800
|
||||
// PRs: 8 * 25 + 5 * 50 = 200 + 250 = 450
|
||||
// Total: 800 + 450 = 1250
|
||||
assert.Equal(t, 800, alice.Score.Breakdown.Commits, "Commit points should be 80 * 10 = 800")
|
||||
assert.Equal(t, 450, alice.Score.Breakdown.PRs, "PR points should be 8*25 + 5*50 = 450")
|
||||
assert.Equal(t, 1250, alice.Score.Total, "Total score should be 1250")
|
||||
|
||||
// Verify rank is assigned
|
||||
assert.Equal(t, 1, alice.Score.Rank, "Alice should be rank 1 (highest scorer)")
|
||||
|
||||
// Verify bob also has scores
|
||||
var bob *models.ContributorMetrics
|
||||
for i := range result.Contributors {
|
||||
if result.Contributors[i].Login == "bob" {
|
||||
bob = &result.Contributors[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, bob, "Bob should be in Contributors")
|
||||
assert.Greater(t, bob.Score.Total, 0, "Bob should have a calculated score")
|
||||
assert.Equal(t, 2, bob.Score.Rank, "Bob should be rank 2")
|
||||
}
|
||||
|
||||
func TestCalculator_FastReviewBonus(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -8,7 +8,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<script type="module" crossorigin src="./assets/index-R3eb927Q.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-LBN7XWrH.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/chart-Bcjh2pZL.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-8XjWwD9J.css">
|
||||
</head>
|
||||
|
||||
@@ -106,23 +106,15 @@ func (g *Generator) generateDataFiles(metrics *models.GlobalMetrics) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Per-contributor data
|
||||
contributorsSeen := make(map[string]bool)
|
||||
// Per-contributor data (use aggregated global contributors, not per-repo)
|
||||
contributorDir := filepath.Join(dataDir, "contributors")
|
||||
if err := os.MkdirAll(contributorDir, 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, repo := range metrics.Repositories {
|
||||
for _, contributor := range repo.Contributors {
|
||||
if contributorsSeen[contributor.Login] {
|
||||
continue
|
||||
}
|
||||
contributorsSeen[contributor.Login] = true
|
||||
|
||||
if err := writeJSON(filepath.Join(contributorDir, contributor.Login+".json"), contributor); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, contributor := range metrics.Contributors {
|
||||
if err := writeJSON(filepath.Join(contributorDir, contributor.Login+".json"), contributor); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -254,17 +254,14 @@ func TestGenerator_GenerateContributorJSON(t *testing.T) {
|
||||
gen, err := NewGenerator(tempDir, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generator now uses global Contributors, not per-repo Contributors
|
||||
metrics := &models.GlobalMetrics{
|
||||
Repositories: []models.RepositoryMetrics{
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{
|
||||
Login: "john-doe",
|
||||
Name: "John Doe",
|
||||
CommitCount: 50,
|
||||
PRsOpened: 10,
|
||||
},
|
||||
},
|
||||
Login: "john-doe",
|
||||
Name: "John Doe",
|
||||
CommitCount: 50,
|
||||
PRsOpened: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -287,33 +284,43 @@ func TestGenerator_GenerateContributorJSON(t *testing.T) {
|
||||
assert.Equal(t, 10, result.PRsOpened)
|
||||
}
|
||||
|
||||
func TestGenerator_ContributorDeduplication(t *testing.T) {
|
||||
func TestGenerator_UsesGlobalContributorsNotPerRepo(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
gen, err := NewGenerator(tempDir, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Same contributor in multiple repos
|
||||
// Same contributor in multiple repos with different per-repo stats
|
||||
// But GlobalMetrics.Contributors should have AGGREGATED stats
|
||||
metrics := &models.GlobalMetrics{
|
||||
// Per-repo data (used for repository-specific pages)
|
||||
Repositories: []models.RepositoryMetrics{
|
||||
{
|
||||
Owner: "org",
|
||||
Name: "repo1",
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{Login: "user1", CommitCount: 50},
|
||||
{Login: "user1", CommitCount: 50, PRsOpened: 5},
|
||||
},
|
||||
},
|
||||
{
|
||||
Owner: "org",
|
||||
Name: "repo2",
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{Login: "user1", CommitCount: 75}, // Same user, different count
|
||||
{Login: "user1", CommitCount: 75, PRsOpened: 10}, // Same user, different count
|
||||
},
|
||||
},
|
||||
},
|
||||
// Global aggregated data (this is what the generator should use for contributor files)
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{Login: "user1", CommitCount: 125, PRsOpened: 15}, // Sum: 50+75=125, 5+10=15
|
||||
},
|
||||
}
|
||||
|
||||
err = gen.Generate(metrics)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should only have one contributor file (first one seen)
|
||||
// Contributor file should have AGGREGATED data from GlobalMetrics.Contributors
|
||||
contributorPath := filepath.Join(tempDir, "data", "contributors", "user1.json")
|
||||
data, err := os.ReadFile(contributorPath)
|
||||
require.NoError(t, err)
|
||||
@@ -322,8 +329,84 @@ func TestGenerator_ContributorDeduplication(t *testing.T) {
|
||||
err = json.Unmarshal(data, &result)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should be the first one (50 commits)
|
||||
assert.Equal(t, 50, result.CommitCount)
|
||||
// Should be the aggregated count (125 commits, 15 PRs), NOT 50 or 75
|
||||
assert.Equal(t, 125, result.CommitCount, "Should use aggregated commits from GlobalMetrics.Contributors")
|
||||
assert.Equal(t, 15, result.PRsOpened, "Should use aggregated PRs from GlobalMetrics.Contributors")
|
||||
}
|
||||
|
||||
func TestGenerator_MultipleContributorsAcrossRepos(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
cfg := config.DefaultConfig()
|
||||
gen, err := NewGenerator(tempDir, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Multiple contributors across multiple repos with aggregated global data
|
||||
metrics := &models.GlobalMetrics{
|
||||
Repositories: []models.RepositoryMetrics{
|
||||
{
|
||||
Owner: "org",
|
||||
Name: "repo1",
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{Login: "alice", CommitCount: 100, LinesAdded: 5000},
|
||||
{Login: "bob", CommitCount: 50, LinesAdded: 2000},
|
||||
},
|
||||
},
|
||||
{
|
||||
Owner: "org",
|
||||
Name: "repo2",
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{Login: "alice", CommitCount: 50, LinesAdded: 3000},
|
||||
{Login: "charlie", CommitCount: 75, LinesAdded: 4000},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Aggregated global contributors
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{Login: "alice", CommitCount: 150, LinesAdded: 8000}, // 100+50, 5000+3000
|
||||
{Login: "bob", CommitCount: 50, LinesAdded: 2000}, // Only in repo1
|
||||
{Login: "charlie", CommitCount: 75, LinesAdded: 4000}, // Only in repo2
|
||||
},
|
||||
}
|
||||
|
||||
err = gen.Generate(metrics)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify alice has aggregated data
|
||||
alicePath := filepath.Join(tempDir, "data", "contributors", "alice.json")
|
||||
aliceData, err := os.ReadFile(alicePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
var aliceResult models.ContributorMetrics
|
||||
err = json.Unmarshal(aliceData, &aliceResult)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 150, aliceResult.CommitCount, "Alice should have aggregated commits")
|
||||
assert.Equal(t, 8000, aliceResult.LinesAdded, "Alice should have aggregated lines added")
|
||||
|
||||
// Verify bob exists with his data
|
||||
bobPath := filepath.Join(tempDir, "data", "contributors", "bob.json")
|
||||
bobData, err := os.ReadFile(bobPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
var bobResult models.ContributorMetrics
|
||||
err = json.Unmarshal(bobData, &bobResult)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 50, bobResult.CommitCount)
|
||||
assert.Equal(t, 2000, bobResult.LinesAdded)
|
||||
|
||||
// Verify charlie exists with his data
|
||||
charliePath := filepath.Join(tempDir, "data", "contributors", "charlie.json")
|
||||
charlieData, err := os.ReadFile(charliePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
var charlieResult models.ContributorMetrics
|
||||
err = json.Unmarshal(charlieData, &charlieResult)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 75, charlieResult.CommitCount)
|
||||
assert.Equal(t, 4000, charlieResult.LinesAdded)
|
||||
}
|
||||
|
||||
func TestGenerator_NoTeamsDoesNotCreateTeamDir(t *testing.T) {
|
||||
@@ -466,6 +549,12 @@ func TestGenerator_GenerateWithFullMetrics(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
// Global aggregated contributors (used for individual contributor files)
|
||||
Contributors: []models.ContributorMetrics{
|
||||
{Login: "alice", Name: "Alice", CommitCount: 150}, // 100+50 aggregated
|
||||
{Login: "bob", Name: "Bob", CommitCount: 200}, // Only in repo1
|
||||
{Login: "charlie", Name: "Charlie", CommitCount: 150}, // Only in repo2
|
||||
},
|
||||
Teams: []models.TeamMetrics{
|
||||
{
|
||||
Name: "Core Team",
|
||||
@@ -499,4 +588,15 @@ func TestGenerator_GenerateWithFullMetrics(t *testing.T) {
|
||||
_, err := os.Stat(path)
|
||||
assert.NoError(t, err, "Expected file to exist: %s", path)
|
||||
}
|
||||
|
||||
// Verify alice's file has aggregated data (150 commits, not 100 from first repo)
|
||||
alicePath := filepath.Join(tempDir, "data", "contributors", "alice.json")
|
||||
aliceData, err := os.ReadFile(alicePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
var aliceResult models.ContributorMetrics
|
||||
err = json.Unmarshal(aliceData, &aliceResult)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 150, aliceResult.CommitCount, "Alice should have aggregated commits from global Contributors")
|
||||
}
|
||||
|
||||
@@ -226,10 +226,10 @@ const progressItems = computed(() => {
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by progress (closest to completion first)
|
||||
// Sort by progress descending (closest to next tier first - highest % complete)
|
||||
results.sort((a, b) => b.progress - a.progress)
|
||||
|
||||
return results.slice(0, props.maxDisplay)
|
||||
return results
|
||||
})
|
||||
|
||||
// Get count of remaining achievements (all unearned across all types)
|
||||
|
||||
Reference in New Issue
Block a user