mirror of
https://github.com/lukaszraczylo/git-velocity.git
synced 2026-06-05 22:43:56 +00:00
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
This commit is contained in:
@@ -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 {
|
||||
|
||||
+108
-2
@@ -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
|
||||
"-->", // 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.
|
||||
|
||||
@@ -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<T>(arg: T): T {", false},
|
||||
{"React JSX", "<Component prop={value} />", false},
|
||||
{"JSX with children", "<div className=\"container\">", 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"},
|
||||
|
||||
+64
-31
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,14 +29,14 @@ defineProps({
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2 mb-4">
|
||||
<template v-for="(member, i) in (team.members || []).slice(0, 5)" :key="member">
|
||||
<Avatar :name="member" size="sm" />
|
||||
<template v-for="member in (team.member_metrics || []).slice(0, 5)" :key="member.login">
|
||||
<Avatar :name="member.name || member.login" :src="member.avatar_url" size="sm" />
|
||||
</template>
|
||||
<span
|
||||
v-if="(team.members?.length || 0) > 5"
|
||||
v-if="(team.member_metrics?.length || 0) > 5"
|
||||
class="w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center text-gray-300 text-xs font-bold"
|
||||
>
|
||||
+{{ team.members.length - 5 }}
|
||||
+{{ team.member_metrics.length - 5 }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user