From 3bd9807e5095ba048d360ca2a02051e9d2eef39f Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Fri, 19 Dec 2025 10:44:00 +0000 Subject: [PATCH] Fixes calculations (#2) Git Level (per commit): - Track unique file paths in FilesModified slice - FilesChanged = count of unique files in THIS commit Aggregator Level (per contributor): - Collect all file paths from all commits into a SET - FilesChanged = size of the unique file set Result: - Contributor.FilesChanged = count of UNIQUE files they touched - Repository contributor = unique files in THAT repo only --- internal/aggregator/aggregator.go | 40 ++++- internal/diff/analyzer.go | 110 +++++++++++- internal/diff/analyzer_test.go | 185 ++++++++++++++++++++- internal/domain/models/commit.go | 31 ++-- internal/domain/scoring/calculator.go | 6 +- internal/domain/scoring/calculator_test.go | 2 + internal/git/repository.go | 95 +++++++---- web/src/components/TeamCard.vue | 8 +- 8 files changed, 420 insertions(+), 57 deletions(-) diff --git a/internal/aggregator/aggregator.go b/internal/aggregator/aggregator.go index fb372a9..1a387b3 100644 --- a/internal/aggregator/aggregator.go +++ b/internal/aggregator/aggregator.go @@ -72,6 +72,11 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat // Per-repo activity days repoActivityDays := make(map[string]map[string]map[string]bool) // repo -> login -> set of date strings + // Track unique files per contributor for accurate FilesChanged count + contributorFiles := make(map[string]map[string]bool) // login -> set of file paths + // Per-repo unique files per contributor + repoContributorFiles := make(map[string]map[string]map[string]bool) // repo -> login -> set of file paths + // Helper to get or create per-repo contributor getRepoContributor := func(repo, login, name, avatarURL string) *models.ContributorMetrics { if repoContributorMap[repo] == nil { @@ -141,7 +146,13 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat cm.MeaningfulLinesDeleted += commit.MeaningfulDeletions cm.CommentLinesAdded += commit.CommentAdditions cm.CommentLinesDeleted += commit.CommentDeletions - cm.FilesChanged += commit.FilesChanged + // Track unique files (don't sum - we'll count unique files at the end) + if contributorFiles[login] == nil { + contributorFiles[login] = make(map[string]bool) + } + for _, filePath := range commit.FilesModified { + contributorFiles[login][filePath] = true + } // Update per-repo contributor stats rcm := getRepoContributor(commit.Repository, login, cm.Name, cm.AvatarURL) @@ -152,7 +163,16 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat rcm.MeaningfulLinesDeleted += commit.MeaningfulDeletions rcm.CommentLinesAdded += commit.CommentAdditions rcm.CommentLinesDeleted += commit.CommentDeletions - rcm.FilesChanged += commit.FilesChanged + // Track unique files per repo (don't sum - we'll count unique files at the end) + if repoContributorFiles[commit.Repository] == nil { + repoContributorFiles[commit.Repository] = make(map[string]map[string]bool) + } + if repoContributorFiles[commit.Repository][login] == nil { + repoContributorFiles[commit.Repository][login] = make(map[string]bool) + } + for _, filePath := range commit.FilesModified { + repoContributorFiles[commit.Repository][login][filePath] = true + } // Track activity patterns based on commit time hour := commit.Date.Hour() @@ -253,6 +273,13 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat } } + // Calculate unique files changed for each contributor + for login, files := range contributorFiles { + if cm, ok := contributorMap[login]; ok { + cm.FilesChanged = len(files) + } + } + // Track PRs with changes requested per contributor prChangesRequested := make(map[string]map[int]bool) // login -> set of PR numbers with changes requested @@ -579,6 +606,15 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat } } + // Calculate unique files changed for per-repo contributors + if repoFiles, ok := repoContributorFiles[repo]; ok { + for login, files := range repoFiles { + if rcm, ok := repoContribs[login]; ok { + rcm.FilesChanged = len(files) + } + } + } + // Calculate averages for per-repo contributors for login, rcm := range repoContribs { if rcm.PRsMerged > 0 { diff --git a/internal/diff/analyzer.go b/internal/diff/analyzer.go index f93c59c..5937b6b 100644 --- a/internal/diff/analyzer.go +++ b/internal/diff/analyzer.go @@ -5,19 +5,22 @@ import ( ) // IsCommentLine checks if a line is a code comment (should not count as meaningful contribution) +// Note: Empty/whitespace lines are NOT comments - use IsWhitespaceLine for those. func IsCommentLine(line string) bool { trimmed := strings.TrimSpace(line) if trimmed == "" { - return true // Empty lines don't count + return false // Empty lines are whitespace, not comments } // Common comment patterns across languages + // Order matters for overlapping prefixes (e.g., "///" before "//") commentPrefixes := []string{ + "///", // Rust/Swift/C# doc comments "//", // C, C++, Java, Go, JS, TS, Swift, Kotlin, etc. "#", // Python, Ruby, Shell, YAML, Perl, etc. + "/**", // JSDoc/JavaDoc block start "/*", // C-style block comment start "*/", // C-style block comment end - "*", // C-style block comment continuation "", // HTML/XML comment end "--", // SQL, Lua, Haskell @@ -33,6 +36,19 @@ func IsCommentLine(line string) bool { } } + // C-style block comment continuation: line starts with * followed by space or end of line + // This avoids false positives like "*ptr = value" (pointer dereference) + if strings.HasPrefix(trimmed, "*") { + if len(trimmed) == 1 { + return true // Just "*" alone + } + // Must be followed by whitespace or common comment characters, not alphanumeric + nextChar := trimmed[1] + if nextChar == ' ' || nextChar == '\t' || nextChar == '/' { + return true + } + } + return false } @@ -64,6 +80,96 @@ func IsMeaningfulLine(line string) bool { return !IsWhitespaceLine(line) && !IsCommentLine(line) } +// IsDocCommentLine checks if a line is a documentation comment (JSDoc, JavaDoc, Rust doc, etc.) +// These are comments specifically meant to document code, as opposed to regular comments. +func IsDocCommentLine(line string) bool { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + return false + } + + // Documentation comment patterns + docPrefixes := []string{ + "///", // Rust, Swift, C# doc comments + "//!", // Rust inner doc comments + "/**", // JSDoc, JavaDoc block start + "\"\"\"", // Python docstring + "'''", // Python docstring + } + + for _, prefix := range docPrefixes { + if strings.HasPrefix(trimmed, prefix) { + return true + } + } + + // JSDoc/JavaDoc continuation lines with annotations (@param, @return, etc.) + if strings.HasPrefix(trimmed, "* @") || strings.HasPrefix(trimmed, "* @") { + return true + } + + // Check for common doc annotations at the start of a comment + if strings.HasPrefix(trimmed, "// @") || strings.HasPrefix(trimmed, "# @") { + return true + } + + return false +} + +// IsCommentedOutCode attempts to detect if a comment line contains commented-out code +// rather than an actual comment. This is a heuristic and may have false positives/negatives. +func IsCommentedOutCode(line string) bool { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + return false + } + + // Remove comment prefix to get the content + var content string + commentPrefixes := []string{"///", "//", "#", "/*", "--", ";"} + for _, prefix := range commentPrefixes { + if strings.HasPrefix(trimmed, prefix) { + content = strings.TrimSpace(trimmed[len(prefix):]) + break + } + } + + if content == "" { + return false + } + + // Heuristics for detecting commented-out code: + // 1. Ends with common code patterns + codeEndings := []string{";", "{", "}", ")", ",", ":", "=>", "->"} + for _, ending := range codeEndings { + if strings.HasSuffix(content, ending) { + return true + } + } + + // 2. Starts with common code keywords + codeKeywords := []string{ + "if ", "else ", "for ", "while ", "switch ", "case ", "return ", "break", "continue", + "const ", "let ", "var ", "func ", "function ", "def ", "class ", "struct ", "type ", + "import ", "from ", "package ", "public ", "private ", "protected ", "static ", + "async ", "await ", "try ", "catch ", "throw ", "raise ", + } + contentLower := strings.ToLower(content) + for _, keyword := range codeKeywords { + if strings.HasPrefix(contentLower, keyword) { + return true + } + } + + // 3. Contains assignment operators + if strings.Contains(content, " = ") || strings.Contains(content, " := ") || + strings.Contains(content, " == ") || strings.Contains(content, " != ") { + return true + } + + return false +} + // IsRenameOrMove checks if a file change represents a rename or move operation // rather than actual content modification. A rename/move is detected when both // the source (fromName) and destination (toName) paths exist and differ. diff --git a/internal/diff/analyzer_test.go b/internal/diff/analyzer_test.go index f90480b..88524d0 100644 --- a/internal/diff/analyzer_test.go +++ b/internal/diff/analyzer_test.go @@ -12,11 +12,11 @@ func TestIsCommentLine(t *testing.T) { line string expected bool }{ - // Empty and whitespace - {"empty string", "", true}, - {"whitespace only", " ", true}, - {"tab only", "\t", true}, - {"mixed whitespace", " \t ", true}, + // Empty and whitespace - NOT comments (use IsWhitespaceLine instead) + {"empty string", "", false}, + {"whitespace only", " ", false}, + {"tab only", "\t", false}, + {"mixed whitespace", " \t ", false}, // C-style comments (Go, Java, JS, C++, etc.) {"C single line comment", "// this is a comment", true}, @@ -25,6 +25,18 @@ func TestIsCommentLine(t *testing.T) { {"C block end", "*/", true}, {"C block continuation", "* continuation", true}, {"C block continuation with space", " * continuation", true}, + {"just asterisk", "*", true}, + {"asterisk with slash", "*/", true}, + + // Pointer dereferences - NOT comments + {"pointer dereference", "*ptr = value", false}, + {"pointer in expression", "*foo.bar", false}, + {"multiplication", "*result", false}, + + // Doc comments + {"Rust doc comment", "/// This documents the function", true}, + {"Rust inner doc", "//! Module documentation", true}, + {"JSDoc start", "/** @param x the value */", true}, // Python/Shell comments {"Python comment", "# python comment", true}, @@ -61,6 +73,70 @@ func TestIsCommentLine(t *testing.T) { {"Function call", "fmt.Println(x)", false}, {"String with slash", `"http://example.com"`, false}, {"Code after whitespace", " x := 5", false}, + + // Indented code (common in diffs) - NOT comments + {"tab indented code", "\tfunc main() {", false}, + {"space indented code", " if x > 0 {", false}, + {"deeply indented", "\t\t\t\treturn nil", false}, + {"mixed indentation", " \t for i := range items {", false}, + {"indented closing brace", "\t}", false}, + {"indented method call", " obj.Method()", false}, + + // TypeScript/JavaScript specific - NOT comments + {"TS interface", "interface User {", false}, + {"TS type alias", "type Handler = () => void;", false}, + {"TS arrow function", "const fn = () => {", false}, + {"TS arrow function with type", "const fn = (x: number): string => {", false}, + {"JS const", "const x = 5;", false}, + {"JS let", "let counter = 0;", false}, + {"JS async", "async function fetch() {", false}, + {"JS await", "const result = await fetch(url);", false}, + {"JS template literal", "const msg = `Hello ${name}`;", false}, + {"JS export", "export default Component;", false}, + {"JS import", "import { useState } from 'react';", false}, + {"TS generic", "function identity(arg: T): T {", false}, + {"React JSX", "", false}, + {"JSX with children", "
", false}, + + // TypeScript/JavaScript comments + {"TS comment", "// TypeScript comment", true}, + {"JSDoc block", "/** @type {string} */", true}, + {"TSDoc", "/** @param name - the user name */", true}, + + // Go specific - NOT comments + {"Go struct", "type User struct {", false}, + {"Go interface def", "type Reader interface {", false}, + {"Go func with receiver", "func (u *User) Name() string {", false}, + {"Go goroutine", "go processItem(item)", false}, + {"Go defer", "defer file.Close()", false}, + {"Go channel send", "ch <- value", false}, + {"Go channel receive", "value := <-ch", false}, + {"Go select", "select {", false}, + {"Go case", "case <-done:", false}, + {"Go map literal", "m := map[string]int{}", false}, + {"Go slice literal", "s := []int{1, 2, 3}", false}, + {"Go error handling", "if err != nil {", false}, + {"Go short var decl", "x := 5", false}, + {"Go range", "for i, v := range items {", false}, + + // Python specific - NOT comments + {"Python def", "def main():", false}, + {"Python class", "class User:", false}, + {"Python async def", "async def fetch():", false}, + {"Python decorator", "@property", false}, + {"Python with", "with open('file') as f:", false}, + {"Python try", "try:", false}, + {"Python except", "except ValueError as e:", false}, + {"Python lambda", "fn = lambda x: x * 2", false}, + {"Python list comp", "squares = [x**2 for x in range(10)]", false}, + {"Python dict comp", "d = {k: v for k, v in items}", false}, + {"Python f-string", "msg = f\"Hello {name}\"", false}, + {"Python import from", "from typing import List", false}, + {"Python type hint", "def greet(name: str) -> str:", false}, + + // Python comments + {"Python comment with hash", "# This is a comment", true}, + {"Python inline comment would be code", "x = 5 # inline", false}, // The line starts with code } for _, tt := range tests { @@ -158,6 +234,17 @@ func TestIsMeaningfulLine(t *testing.T) { {"whitespace line", " ", false}, {"python comment", "# comment", false}, {"code with leading whitespace", " x := 5", true}, + + // Indented code is still meaningful + {"tab indented code", "\tfunc main() {", true}, + {"deeply indented code", "\t\t\treturn result", true}, + {"space indented code", " if err != nil {", true}, + {"mixed indentation code", " \t for _, item := range items {", true}, + {"indented closing brace", "\t\t}", true}, + + // Indented comments are still comments (not meaningful) + {"indented comment", "\t// TODO: fix this", false}, + {"space indented comment", " # Python comment", false}, } for _, tt := range tests { @@ -202,3 +289,91 @@ func TestIsRenameOrMove(t *testing.T) { }) } } + +func TestIsDocCommentLine(t *testing.T) { + tests := []struct { + name string + line string + expected bool + }{ + // Documentation comments + {"Rust doc comment", "/// This documents the function", true}, + {"Rust doc with leading space", " /// This documents the function", true}, + {"Rust inner doc", "//! Module documentation", true}, + {"JSDoc block start", "/** @param x the value */", true}, + {"JSDoc block start with space", " /** @param x */", true}, + {"Python docstring double", "\"\"\"This is a docstring", true}, + {"Python docstring single", "'''This is a docstring", true}, + {"JSDoc annotation line", "* @param x the value", true}, + {"JSDoc annotation with extra space", "* @returns the result", true}, + {"annotation comment", "// @deprecated use newFunc instead", true}, + {"Python annotation", "# @param x the value", true}, + + // Regular comments - NOT doc comments + {"regular C comment", "// this is a comment", false}, + {"regular Python comment", "# just a comment", false}, + {"block comment start", "/* start of block */", false}, + {"block continuation", "* continuation without annotation", false}, + + // Empty and whitespace + {"empty string", "", false}, + {"whitespace only", " ", false}, + + // Code - NOT doc comments + {"Go code", "func main() {", false}, + {"Python code", "def main():", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsDocCommentLine(tt.line) + assert.Equal(t, tt.expected, result, "IsDocCommentLine(%q)", tt.line) + }) + } +} + +func TestIsCommentedOutCode(t *testing.T) { + tests := []struct { + name string + line string + expected bool + }{ + // Commented-out code - should return true + {"commented variable declaration", "// const x = 5;", true}, + {"commented function call", "// fmt.Println(x)", true}, // Ends with ) + {"commented function def", "// func main() {", true}, + {"commented return", "// return nil", true}, + {"commented import", "// import fmt", true}, + {"commented if statement", "// if x > 0 {", true}, + {"commented else", "// else {", true}, + {"commented for loop", "// for i := 0; i < 10; i++ {", true}, + {"commented assignment", "// x = 10", true}, // Contains = operator + {"commented with equals", "// x = y + 10;", true}, // Ends with ; + {"Python commented code", "# def main():", true}, // colon at end + {"commented arrow function", "// const fn = () => {", true}, + {"commented Go assignment", "// x := 5", true}, + + // Regular comments - should return false + {"todo comment", "// TODO: fix this", false}, + {"note comment", "// Note: this is important", false}, + {"explanation comment", "// This function handles errors", false}, + {"section comment", "// ============", false}, + {"url in comment", "// See https://example.com", false}, + + // Empty and edge cases + {"empty string", "", false}, + {"just comment prefix", "//", false}, + {"whitespace only", " ", false}, + + // Code (not commented) - should return false + {"actual code", "const x = 5;", false}, + {"actual function", "func main() {", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsCommentedOutCode(tt.line) + assert.Equal(t, tt.expected, result, "IsCommentedOutCode(%q)", tt.line) + }) + } +} diff --git a/internal/domain/models/commit.go b/internal/domain/models/commit.go index a452514..3178b8b 100644 --- a/internal/domain/models/commit.go +++ b/internal/domain/models/commit.go @@ -4,25 +4,34 @@ import "time" // Commit represents a Git commit type Commit struct { - SHA string `json:"sha"` - Message string `json:"message"` - Author Author `json:"author"` - Committer Author `json:"committer"` - Date time.Time `json:"date"` - Additions int `json:"additions"` - Deletions int `json:"deletions"` - FilesChanged int `json:"files_changed"` - Repository string `json:"repository"` // owner/repo format - URL string `json:"url"` + SHA string `json:"sha"` + Message string `json:"message"` + Author Author `json:"author"` + Committer Author `json:"committer"` + Date time.Time `json:"date"` + Additions int `json:"additions"` + Deletions int `json:"deletions"` + FilesChanged int `json:"files_changed"` + FilesModified []string `json:"files_modified,omitempty"` // List of file paths modified in this commit + Repository string `json:"repository"` // owner/repo format + URL string `json:"url"` // Meaningful line counts (excludes comments and whitespace) MeaningfulAdditions int `json:"meaningful_additions"` MeaningfulDeletions int `json:"meaningful_deletions"` - // Comment line counts + // Comment line counts (all types of comments) CommentAdditions int `json:"comment_additions"` CommentDeletions int `json:"comment_deletions"` + // Documentation comment counts (JSDoc, Rust doc comments, docstrings, etc.) + DocCommentAdditions int `json:"doc_comment_additions"` + DocCommentDeletions int `json:"doc_comment_deletions"` + + // Commented-out code counts (code that was commented rather than deleted) + CommentedCodeAdditions int `json:"commented_code_additions"` + CommentedCodeDeletions int `json:"commented_code_deletions"` + // Derived fields HasTests bool `json:"has_tests"` } diff --git a/internal/domain/scoring/calculator.go b/internal/domain/scoring/calculator.go index d699198..a0af54f 100644 --- a/internal/domain/scoring/calculator.go +++ b/internal/domain/scoring/calculator.go @@ -293,9 +293,11 @@ func (c *Calculator) checkAchievements(cm *models.ContributorMetrics) []string { case "comment_count": earned = float64(cm.ReviewComments) >= ach.Condition.Threshold case "lines_added": - earned = float64(cm.LinesAdded) >= ach.Condition.Threshold + // Use meaningful lines to match scoring calculation (excludes comments/whitespace) + earned = float64(cm.MeaningfulLinesAdded) >= ach.Condition.Threshold case "lines_deleted": - earned = float64(cm.LinesDeleted) >= ach.Condition.Threshold + // Use meaningful lines to match scoring calculation (excludes comments/whitespace) + earned = float64(cm.MeaningfulLinesDeleted) >= ach.Condition.Threshold case "avg_review_time_hours": // For avg review time, lower is better, so lower threshold = harder achievement if cm.AvgReviewTime > 0 && cm.AvgReviewTime <= ach.Condition.Threshold { diff --git a/internal/domain/scoring/calculator_test.go b/internal/domain/scoring/calculator_test.go index 576dfeb..9b2324b 100644 --- a/internal/domain/scoring/calculator_test.go +++ b/internal/domain/scoring/calculator_test.go @@ -452,6 +452,8 @@ func TestCalculator_AllAchievementTypes(t *testing.T) { ReviewComments: 25, LinesAdded: 1500, LinesDeleted: 600, + MeaningfulLinesAdded: 1500, + MeaningfulLinesDeleted: 600, AvgReviewTime: 1.5, UniqueReviewees: 7, RepositoriesContributed: []string{"owner/repo1", "owner/repo2"}, diff --git a/internal/git/repository.go b/internal/git/repository.go index 94785bf..eaba9e4 100644 --- a/internal/git/repository.go +++ b/internal/git/repository.go @@ -310,17 +310,22 @@ func (r *Repository) FetchCommits(ctx context.Context, owner, name string, since Name: c.Committer.Name, Email: c.Committer.Email, }, - Date: commitTime, - Additions: stats.Additions, - Deletions: stats.Deletions, - MeaningfulAdditions: stats.MeaningfulAdditions, - MeaningfulDeletions: stats.MeaningfulDeletions, - CommentAdditions: stats.CommentAdditions, - CommentDeletions: stats.CommentDeletions, - FilesChanged: stats.FilesChanged, - Repository: fmt.Sprintf("%s/%s", owner, name), - URL: fmt.Sprintf("https://github.com/%s/%s/commit/%s", owner, name, c.Hash.String()), - HasTests: stats.HasTests, + Date: commitTime, + Additions: stats.Additions, + Deletions: stats.Deletions, + MeaningfulAdditions: stats.MeaningfulAdditions, + MeaningfulDeletions: stats.MeaningfulDeletions, + CommentAdditions: stats.CommentAdditions, + CommentDeletions: stats.CommentDeletions, + DocCommentAdditions: stats.DocCommentAdditions, + DocCommentDeletions: stats.DocCommentDeletions, + CommentedCodeAdditions: stats.CommentedCodeAdditions, + CommentedCodeDeletions: stats.CommentedCodeDeletions, + FilesChanged: stats.FilesChanged, + FilesModified: stats.FilesModified, + Repository: fmt.Sprintf("%s/%s", owner, name), + URL: fmt.Sprintf("https://github.com/%s/%s/commit/%s", owner, name, c.Hash.String()), + HasTests: stats.HasTests, } commits = append(commits, commit) @@ -353,14 +358,19 @@ func (r *Repository) FetchCommits(ctx context.Context, owner, name string, since // commitStats holds the statistics for a commit type commitStats struct { - Additions int - Deletions int - MeaningfulAdditions int - MeaningfulDeletions int - CommentAdditions int - CommentDeletions int - FilesChanged int - HasTests bool + Additions int + Deletions int + MeaningfulAdditions int + MeaningfulDeletions int + CommentAdditions int + CommentDeletions int + DocCommentAdditions int + DocCommentDeletions int + CommentedCodeAdditions int + CommentedCodeDeletions int + FilesChanged int + FilesModified []string // List of file paths modified + HasTests bool } // getCommitStats calculates additions, deletions, files changed for a commit @@ -397,12 +407,7 @@ func (r *Repository) getCommitStats(c *object.Commit, testPatterns []string) com filesSet := make(map[string]bool) for _, change := range changes { - // Skip rename/move operations - they don't represent actual code contribution - if diff.IsRenameOrMove(change.From.Name, change.To.Name) { - continue - } - - // Get the file path + // Get the file path (prefer destination for renames/moves, fallback to source) var filePath string if change.To.Name != "" { filePath = change.To.Name @@ -410,15 +415,24 @@ func (r *Repository) getCommitStats(c *object.Commit, testPatterns []string) com filePath = change.From.Name } - // Skip documentation files + // Skip if no file path (shouldn't happen, but defensive) + if filePath == "" { + continue + } + + // Skip documentation files entirely if diff.IsDocumentationFile(filePath) { continue } - // Count unique files - if !filesSet[filePath] { + // Check if this is a rename/move operation + isRename := diff.IsRenameOrMove(change.From.Name, change.To.Name) + + // Count unique files (but NOT for renames - the file already existed) + if !isRename && !filesSet[filePath] { filesSet[filePath] = true stats.FilesChanged++ + stats.FilesModified = append(stats.FilesModified, filePath) // Check for test files for _, pattern := range testPatterns { @@ -429,13 +443,18 @@ func (r *Repository) getCommitStats(c *object.Commit, testPatterns []string) com } } - // Get patch to count lines + // Get patch to count lines (even for renames, there may be content changes) patch, err := change.Patch() if err != nil { continue } for _, filePatch := range patch.FilePatches() { + // For binary files, skip line counting + if filePatch.IsBinary() { + continue + } + for _, chunk := range filePatch.Chunks() { content := chunk.Content() lines := strings.Split(content, "\n") @@ -446,18 +465,32 @@ func (r *Repository) getCommitStats(c *object.Commit, testPatterns []string) com stats.Additions++ if diff.IsMeaningfulLine(line) { stats.MeaningfulAdditions++ - } else if diff.IsCommentLine(line) && !diff.IsWhitespaceLine(line) { + } else if diff.IsCommentLine(line) { stats.CommentAdditions++ + // Further classify the comment type + if diff.IsDocCommentLine(line) { + stats.DocCommentAdditions++ + } else if diff.IsCommentedOutCode(line) { + stats.CommentedCodeAdditions++ + } } + // Whitespace lines are neither meaningful nor comments } case 2: // Delete for _, line := range lines { stats.Deletions++ if diff.IsMeaningfulLine(line) { stats.MeaningfulDeletions++ - } else if diff.IsCommentLine(line) && !diff.IsWhitespaceLine(line) { + } else if diff.IsCommentLine(line) { stats.CommentDeletions++ + // Further classify the comment type + if diff.IsDocCommentLine(line) { + stats.DocCommentDeletions++ + } else if diff.IsCommentedOutCode(line) { + stats.CommentedCodeDeletions++ + } } + // Whitespace lines are neither meaningful nor comments } } } diff --git a/web/src/components/TeamCard.vue b/web/src/components/TeamCard.vue index 36b64ad..4e40160 100644 --- a/web/src/components/TeamCard.vue +++ b/web/src/components/TeamCard.vue @@ -29,14 +29,14 @@ defineProps({
-