mirror of
https://github.com/lukaszraczylo/git-velocity.git
synced 2026-06-14 03:02:10 +00:00
improvements jan2025 (#9)
* feat(scoring): add tests bonus and fix average calculations - [x] Add CommitsWithTests metric to track commits with test file changes - [x] Add TestsBonus to score breakdown (15 points per commit with tests) - [x] Fix AvgTimeToMerge calculation to use count of PRs with valid data - [x] Fix AvgReviewTime calculation to use count of reviews with valid data - [x] Fix AvgPRSize calculation to only include merged PRs - [x] Add trackActivityDay helper to deduplicate activity tracking code - [x] Track activity days for PR creation, reviews, and issue comments - [x] Separate issue close tracking from issue open tracking - [x] Update early bird window from 5am-9am to 6am-9am - [x] Add time-based multipliers to velocity timeline scoring - [x] Update GraphQL query to fetch OPEN, MERGED, CLOSED PRs - [x] Fix PR filtering logic to handle all PR states correctly - [x] Improve watch handlers in Vue components to prevent double-loading - [x] Fix formatDuration to handle zero and negative values - [x] Update scoring documentation to include Tests component * refactor: use standard library and consolidate constants - [x] Replace custom contains function with slices.Contains - [x] Remove duplicate contains function implementations - [x] Extract magic numbers to named constants in formatters - [x] Create constants composable for app-wide values - [x] Add ESLint configuration with browser globals - [x] Add lint npm scripts to package.json - [x] Reorder Vue template attributes for consistency - [x] Remove unused variable in AchievementProgress - [x] Add pnpm lock file
This commit is contained in:
@@ -18,10 +18,11 @@ type ContributorMetrics struct {
|
||||
Period Period `json:"period"`
|
||||
|
||||
// Commit metrics
|
||||
CommitCount int `json:"commit_count"`
|
||||
LinesAdded int `json:"lines_added"`
|
||||
LinesDeleted int `json:"lines_deleted"`
|
||||
FilesChanged int `json:"files_changed"`
|
||||
CommitCount int `json:"commit_count"`
|
||||
CommitsWithTests int `json:"commits_with_tests"` // Commits that include test files
|
||||
LinesAdded int `json:"lines_added"`
|
||||
LinesDeleted int `json:"lines_deleted"`
|
||||
FilesChanged int `json:"files_changed"`
|
||||
|
||||
// Meaningful line counts (excludes comments and whitespace)
|
||||
MeaningfulLinesAdded int `json:"meaningful_lines_added"`
|
||||
@@ -98,7 +99,8 @@ type ScoreBreakdown struct {
|
||||
Issues int `json:"issues"` // Issue-related points (opened, closed, comments, references)
|
||||
ResponseBonus int `json:"response_bonus"`
|
||||
LineChanges int `json:"line_changes"`
|
||||
OutOfHours int `json:"out_of_hours"` // Bonus for out-of-hours commits
|
||||
TestsBonus int `json:"tests_bonus"` // Bonus for commits that include test files
|
||||
OutOfHours int `json:"out_of_hours"` // Bonus for out-of-hours commits
|
||||
}
|
||||
|
||||
// RepositoryMetrics holds aggregated metrics for a single repository
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package scoring
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/lukaszraczylo/git-velocity/internal/config"
|
||||
@@ -23,52 +24,73 @@ func (c *Calculator) Calculate(metrics *models.GlobalMetrics) *models.GlobalMetr
|
||||
return metrics
|
||||
}
|
||||
|
||||
// Collect all contributor metrics across repositories
|
||||
// Build contributor map for scoring
|
||||
// IMPORTANT: Prefer metrics.Contributors if populated (from aggregator) since it contains
|
||||
// properly calculated values that can't be reconstructed from per-repo data:
|
||||
// - Weighted average times (AvgReviewTime, AvgTimeToMerge)
|
||||
// - Cross-repo streaks (ActiveDays, LongestStreak, WorkWeekStreak)
|
||||
// - Max values (LargestPRSize)
|
||||
// - Deduplicated counts (UniqueReviewees, FilesChanged)
|
||||
// - Summed counts (SmallPRCount, PerfectPRs)
|
||||
// Fall back to aggregating from repos only for tests that don't use the full pipeline.
|
||||
contributorMap := make(map[string]*models.ContributorMetrics)
|
||||
|
||||
for _, repo := range metrics.Repositories {
|
||||
for i := range repo.Contributors {
|
||||
login := repo.Contributors[i].Login
|
||||
if _, ok := contributorMap[login]; !ok {
|
||||
// Copy the contributor metrics
|
||||
cm := repo.Contributors[i]
|
||||
contributorMap[login] = &cm
|
||||
} else {
|
||||
// Aggregate metrics from multiple repos
|
||||
existing := contributorMap[login]
|
||||
cm := repo.Contributors[i]
|
||||
existing.CommitCount += cm.CommitCount
|
||||
existing.LinesAdded += cm.LinesAdded
|
||||
existing.LinesDeleted += cm.LinesDeleted
|
||||
existing.MeaningfulLinesAdded += cm.MeaningfulLinesAdded
|
||||
existing.MeaningfulLinesDeleted += cm.MeaningfulLinesDeleted
|
||||
existing.CommentLinesAdded += cm.CommentLinesAdded
|
||||
existing.CommentLinesDeleted += cm.CommentLinesDeleted
|
||||
existing.PRsOpened += cm.PRsOpened
|
||||
existing.PRsMerged += cm.PRsMerged
|
||||
existing.ReviewsGiven += cm.ReviewsGiven
|
||||
existing.ReviewComments += cm.ReviewComments
|
||||
// Issue metrics
|
||||
existing.IssuesOpened += cm.IssuesOpened
|
||||
existing.IssuesClosed += cm.IssuesClosed
|
||||
existing.IssueComments += cm.IssueComments
|
||||
existing.IssueReferencesInCommits += cm.IssueReferencesInCommits
|
||||
// Activity pattern metrics (for achievements)
|
||||
existing.EarlyBirdCount += cm.EarlyBirdCount
|
||||
existing.NightOwlCount += cm.NightOwlCount
|
||||
existing.MidnightCount += cm.MidnightCount
|
||||
existing.WeekendWarrior += cm.WeekendWarrior
|
||||
existing.OutOfHoursCount += cm.OutOfHoursCount
|
||||
// Time-based commit counts (for multiplier scoring)
|
||||
existing.RegularHoursCount += cm.RegularHoursCount
|
||||
existing.EveningCount += cm.EveningCount
|
||||
existing.LateNightCount += cm.LateNightCount
|
||||
existing.OvernightCount += cm.OvernightCount
|
||||
existing.EarlyMorningCount += cm.EarlyMorningCount
|
||||
// Combine unique repositories
|
||||
for _, r := range cm.RepositoriesContributed {
|
||||
if !contains(existing.RepositoriesContributed, r) {
|
||||
existing.RepositoriesContributed = append(existing.RepositoriesContributed, r)
|
||||
if len(metrics.Contributors) > 0 {
|
||||
// Use already-aggregated global contributors (production path)
|
||||
for i := range metrics.Contributors {
|
||||
login := metrics.Contributors[i].Login
|
||||
cm := metrics.Contributors[i]
|
||||
contributorMap[login] = &cm
|
||||
}
|
||||
} else {
|
||||
// Fallback: aggregate from per-repo contributors (test compatibility path)
|
||||
// Note: This path cannot properly aggregate computed fields like AvgReviewTime,
|
||||
// LongestStreak, etc. - it only sums count-based metrics.
|
||||
for _, repo := range metrics.Repositories {
|
||||
for i := range repo.Contributors {
|
||||
login := repo.Contributors[i].Login
|
||||
if _, ok := contributorMap[login]; !ok {
|
||||
// Copy the contributor metrics
|
||||
cm := repo.Contributors[i]
|
||||
contributorMap[login] = &cm
|
||||
} else {
|
||||
// Aggregate metrics from multiple repos
|
||||
existing := contributorMap[login]
|
||||
cm := repo.Contributors[i]
|
||||
existing.CommitCount += cm.CommitCount
|
||||
existing.CommitsWithTests += cm.CommitsWithTests
|
||||
existing.LinesAdded += cm.LinesAdded
|
||||
existing.LinesDeleted += cm.LinesDeleted
|
||||
existing.MeaningfulLinesAdded += cm.MeaningfulLinesAdded
|
||||
existing.MeaningfulLinesDeleted += cm.MeaningfulLinesDeleted
|
||||
existing.CommentLinesAdded += cm.CommentLinesAdded
|
||||
existing.CommentLinesDeleted += cm.CommentLinesDeleted
|
||||
existing.PRsOpened += cm.PRsOpened
|
||||
existing.PRsMerged += cm.PRsMerged
|
||||
existing.ReviewsGiven += cm.ReviewsGiven
|
||||
existing.ReviewComments += cm.ReviewComments
|
||||
// Issue metrics
|
||||
existing.IssuesOpened += cm.IssuesOpened
|
||||
existing.IssuesClosed += cm.IssuesClosed
|
||||
existing.IssueComments += cm.IssueComments
|
||||
existing.IssueReferencesInCommits += cm.IssueReferencesInCommits
|
||||
// Activity pattern metrics (for achievements)
|
||||
existing.EarlyBirdCount += cm.EarlyBirdCount
|
||||
existing.NightOwlCount += cm.NightOwlCount
|
||||
existing.MidnightCount += cm.MidnightCount
|
||||
existing.WeekendWarrior += cm.WeekendWarrior
|
||||
existing.OutOfHoursCount += cm.OutOfHoursCount
|
||||
// Time-based commit counts (for multiplier scoring)
|
||||
existing.RegularHoursCount += cm.RegularHoursCount
|
||||
existing.EveningCount += cm.EveningCount
|
||||
existing.LateNightCount += cm.LateNightCount
|
||||
existing.OvernightCount += cm.OvernightCount
|
||||
existing.EarlyMorningCount += cm.EarlyMorningCount
|
||||
// Combine unique repositories
|
||||
for _, r := range cm.RepositoriesContributed {
|
||||
if !slices.Contains(existing.RepositoriesContributed, r) {
|
||||
existing.RepositoriesContributed = append(existing.RepositoriesContributed, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,13 +282,16 @@ func (c *Calculator) calculateScore(cm *models.ContributorMetrics) models.Score
|
||||
}
|
||||
}
|
||||
|
||||
// Tests bonus - bonus points for commits that include test files
|
||||
breakdown.TestsBonus = cm.CommitsWithTests * points.CommitWithTests
|
||||
|
||||
// Out of hours bonus (legacy - kept for backwards compatibility but default is 0)
|
||||
breakdown.OutOfHours = cm.OutOfHoursCount * points.OutOfHours
|
||||
|
||||
// Calculate total
|
||||
total := breakdown.Commits + breakdown.LineChanges + breakdown.PRs +
|
||||
breakdown.Reviews + breakdown.ResponseBonus + breakdown.Comments +
|
||||
breakdown.Issues + breakdown.OutOfHours
|
||||
breakdown.Issues + breakdown.TestsBonus + breakdown.OutOfHours
|
||||
|
||||
return models.Score{
|
||||
Total: total,
|
||||
@@ -406,12 +431,3 @@ func (c *Calculator) findTopAchievers(contributors []models.ContributorMetrics,
|
||||
topAchievers["pull_requests"] = topPRAuthor
|
||||
}
|
||||
}
|
||||
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -843,18 +843,6 @@ func TestCalculator_WorkWeekStreakAchievement(t *testing.T) {
|
||||
assert.Contains(t, contributor.Achievements, "workweek-5")
|
||||
}
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
slice := []string{"a", "b", "c"}
|
||||
|
||||
assert.True(t, contains(slice, "a"))
|
||||
assert.True(t, contains(slice, "b"))
|
||||
assert.True(t, contains(slice, "c"))
|
||||
assert.False(t, contains(slice, "d"))
|
||||
assert.False(t, contains([]string{}, "a"))
|
||||
}
|
||||
|
||||
func TestCalculator_MeaningfulLinesScoring(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user