Skip to content

Commit

Permalink
[feat] Adding duration support on providers
Browse files Browse the repository at this point in the history
  • Loading branch information
erdemkosk committed Jan 20, 2025
1 parent 5fda217 commit ca91a16
Show file tree
Hide file tree
Showing 13 changed files with 699 additions and 139 deletions.
141 changes: 63 additions & 78 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,90 @@ Gitness - Your repo's fitness witness! Track your bus factor before your code mi

## Features

- Calculate repository bus factor
- Calculate repository bus factor and knowledge distribution
- Analyze contributor statistics and activity patterns
- Track recent contributor engagement
- Support for multiple VCS providers (GitHub, Bitbucket)
- Multiple output formats (Console, JSON, Markdown)
- Configurable analysis period (e.g., 6m, 1y, 30d)
- CI/CD pipeline integration
- Docker support

## Usage

```bash
# Analyze all time
gitness https://github.com/user/repo

# Analyze last 6 months
gitness --duration 6m https://github.com/user/repo

# Analyze last 1 year with JSON output
gitness --duration 1y --output json https://github.com/user/repo

# Analyze last 30 days
gitness --duration 30d https://github.com/user/repo
```

## Metrics Explained

### Bus Factor 🚌
The "Bus Factor" represents the minimum number of developers that would need to be hit by a bus (or win the lottery) before the project is in serious trouble. It's calculated based on the number of contributors who collectively account for 80% of the project's contributions.
The "Bus Factor" represents the minimum number of developers that would need to be hit by a bus before the project is in serious trouble. It's calculated based on contributors who collectively account for 80% of contributions.

- 🔴 Critical (< 2): Project knowledge is dangerously concentrated
- 🟡 Warning (2-3): Limited knowledge distribution
- 🟢 Good (≥ 4): Healthy knowledge distribution

### Knowledge Distribution Score 📊
Measures how evenly the knowledge is distributed across all contributors (0-100).

- 🔴 Critical (< 25): Knowledge is heavily concentrated
- 🟡 Warning (25-50): Moderate knowledge concentration
- 🟢 Good (> 50): Well-distributed knowledge

### Active Contributor Ratio 👥
Percentage of contributors who have made significant contributions (>1% of total commits). This metric helps identify the real active contributor base versus occasional contributors.
Percentage of contributors who have made significant contributions (>1% of total commits).

- 🔴 Critical (< 30%): Most contributors are occasional
- 🟡 Warning (30-50%): Moderate active participation
- 🟢 Good (≥ 50%): Healthy active community
- 🟢 Good (> 50%): Healthy active participation

### Recent Contributors 🕒
Number of contributors active in last 3 months.

- 🔴 Critical (< 2): Low recent activity
- 🟡 Warning (2-4): Moderate recent activity
- 🟢 Good (≥ 5): High recent activity

### Recent Contributors 📅
Number of unique contributors who have made commits in the last 3 months. This metric helps assess the current activity level and project momentum.
## Example Output

- 🔴 Critical (< 2): Project might be stagnating
- 🟡 Warning (2-4): Limited recent activity
- 🟢 Good (≥ 5): Active development
```
Repository Analysis: user/repo
Analysis Period: Last 6 months
🟢 Bus Factor: 4 (critical if < 2, warning if < 4)
🟢 Knowledge Distribution Score: 75.5 (0-100, higher is better)
🟡 Active Contributor Ratio: 45.5% (critical if < 30%, warning if < 50%)
🟢 Recent Contributors: 6 (active in last 3 months)
Total Commits: 330
Contributors:
------------------
John Doe: 100 commits (30.3%)
Jane Smith: 90 commits (27.3%)
Bob Wilson: 80 commits (24.2%)
Alice Brown: 60 commits (18.2%)
```

## Environment Variables

```bash
GITHUB_TOKEN=your_token
BITBUCKET_CLIENT_ID=your_client_id
BITBUCKET_CLIENT_SECRET=your_client_secret
COMMIT_HISTORY_DURATION=6m # Optional: 6m, 1y, 30d etc.
```

## Installation

Expand All @@ -51,22 +105,6 @@ docker run \
-e REPOSITORY_URL="https://github.com/user/repo" \
gitness --output json;
```
## Usage

### Command Line
```bash
gitness https://github.com/user/repo
```
### With output format
```bash
gitness --output json https://github.com/user/repo
```
### Using environment variables
```bash
export REPOSITORY_URL="https://github.com/user/repo"
export GITHUB_TOKEN="your_token"
gitness
```

### Environment Variables

Expand Down Expand Up @@ -160,59 +198,6 @@ jobs:
[Example GitHub Action Run](https://github.com/erdemkosk/stock-exchange/actions/runs/12857735422)
## Output Examples
### Console (default)
```
Repo: user/repo
Bus Factor: 🟢 3 (critical if < 2, warning if < 4)
Active Contributor Ratio: 🟡 45.5% (critical if < 30%, warning if < 50%)
Recent Contributors: 🔴 1 (active in last 3 months)

Contributors:
------------------
John Doe: 150 commits (45.5%)
Jane Smith: 100 commits (30.3%)
Bob Johnson: 80 commits (24.2%)
```
### JSON
```json
{
"owner": "user",
"repo": "repo",
"busFactor": 3,
"totalCommits": 330,
"contributorActivity": 45.5,
"recentContributors": 1,
"contributors": [
{
"name": "John Doe",
"commits": 150,
"percentage": 45.5
}
]
}
```

### Markdown
```markdown
# Repository Analysis: user/repo

## 🟢 Bus Factor: **3** (critical if < 2, warning if < 4)
## 🟡 Active Contributor Ratio: **45.5%** (critical if < 30%, warning if < 50%)
## 🔴 Recent Contributors: **1** (active in last 3 months)

Total Commits: 330

## Contributors

| Name | Commits | Percentage |
|------|---------|------------|
| John Doe | 150 | 45.5% |
```
## Architecture
- Clean architecture principles
Expand Down
37 changes: 35 additions & 2 deletions internal/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package analyzer

import (
"fmt"
"math"
"sort"
"time"

Expand All @@ -22,12 +23,12 @@ func NewRepositoryAnalyzer(provider providers.CommitProvider) *RepositoryAnalyze
}
}

func (ra *RepositoryAnalyzer) Analyze(owner, repo string) (*models.RepositoryStats, error) {
func (ra *RepositoryAnalyzer) Analyze(owner, repo string, duration string) (*models.RepositoryStats, error) {
if owner == "" || repo == "" {
return nil, fmt.Errorf("owner and repo cannot be empty")
}

stats, err := ra.provider.FetchCommits(owner, repo)
stats, err := ra.provider.FetchCommits(owner, repo, duration)
if err != nil {
return nil, fmt.Errorf("failed to fetch commits: %w", err)
}
Expand Down Expand Up @@ -72,6 +73,9 @@ func (ra *RepositoryAnalyzer) Analyze(owner, repo string) (*models.RepositorySta

contributorActivity := float64(activeContributors) / float64(len(contributors)) * 100

// Knowledge Distribution Score hesaplama
knowledgeScore := calculateKnowledgeDistribution(stats)

return &models.RepositoryStats{
Owner: owner,
Repo: repo,
Expand All @@ -80,6 +84,8 @@ func (ra *RepositoryAnalyzer) Analyze(owner, repo string) (*models.RepositorySta
TotalCommits: total,
ContributorActivity: contributorActivity,
RecentContributors: recentContributors,
KnowledgeScore: knowledgeScore,
AnalysisDuration: duration,
}, nil
}

Expand Down Expand Up @@ -120,3 +126,30 @@ func calculateBusFactor(stats map[string]providers.CommitInfo) int {

return busFactor
}

func calculateKnowledgeDistribution(stats map[string]providers.CommitInfo) float64 {
if len(stats) == 0 {
return 0
}

var total int
for _, info := range stats {
total += info.Count
}

var sumDifferences float64
var n = float64(len(stats))

for _, info1 := range stats {
percent1 := float64(info1.Count) / float64(total)
for _, info2 := range stats {
percent2 := float64(info2.Count) / float64(total)
sumDifferences += math.Abs(percent1 - percent2)
}
}

gini := sumDifferences / (2 * n * n)
knowledgeScore := (1 - gini) * 100

return math.Round(knowledgeScore*100) / 100
}
2 changes: 2 additions & 0 deletions internal/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ type RepositoryStats struct {
TotalCommits int
ContributorActivity float64
RecentContributors int
KnowledgeScore float64
AnalysisDuration string
}

func (rs *RepositoryStats) Print() {
Expand Down
18 changes: 18 additions & 0 deletions internal/output/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ func (f *ConsoleFormatter) Format(stats *models.RepositoryStats) (string, error)

output.WriteString(fmt.Sprintf("\nRepo: %s/%s\n", stats.Owner, stats.Repo))

if stats.AnalysisDuration != "" {
output.WriteString(fmt.Sprintf("\nAnalysis Period: Last %s\n", stats.AnalysisDuration))
} else {
output.WriteString("\nAnalysis Period: All Time\n")
}

// ANSI color codes
red := "\033[31m"
yellow := "\033[33m"
Expand Down Expand Up @@ -59,6 +65,9 @@ func (f *ConsoleFormatter) Format(stats *models.RepositoryStats) (string, error)
output.WriteString(fmt.Sprintf("Bus Factor: %s%d%s (critical if < 2, warning if < 4)\n",
busFactorColor, stats.BusFactor, reset))

output.WriteString(fmt.Sprintf("Knowledge Distribution Score: %s%.1f%s (0-100, higher is better)\n",
getScoreColor(stats.KnowledgeScore), stats.KnowledgeScore, reset))

output.WriteString(fmt.Sprintf("Active Contributor Ratio: %s%.1f%%%s (contributors with >1%% contribution, critical if < 30%%, warning if < 50%%)\n",
activityColor, stats.ContributorActivity, reset))

Expand All @@ -75,3 +84,12 @@ func (f *ConsoleFormatter) Format(stats *models.RepositoryStats) (string, error)

return output.String(), nil
}

func getScoreColor(score float64) string {
if score < 25 {
return "\033[31m" // red
} else if score < 50 {
return "\033[33m" // yellow
}
return "\033[32m" // green
}
3 changes: 0 additions & 3 deletions internal/output/formatter.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package output

import (
"fmt"

"github.com/erdemkosk/gitness/internal/models"
)

Expand Down Expand Up @@ -32,7 +30,6 @@ func (f *FormatterFactory) Register(name string, formatter Formatter) {
}

func (f *FormatterFactory) GetFormatter(format string) (Formatter, bool) {
fmt.Println(format)
formatter, exists := f.formatters[format]
return formatter, exists
}
4 changes: 4 additions & 0 deletions internal/output/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@ func (f *JSONFormatter) Format(stats *models.RepositoryStats) (string, error) {
Contributors []models.Contributor `json:"contributors"`
ContributorActivity float64 `json:"contributorActivity"` // Percentage of contributors with >1% contribution
RecentContributors int `json:"recentContributors"` // Number of contributors active in last 3 months
KnowledgeScore float64 `json:"knowledgeScore"`
AnalysisDuration string `json:"analysisDuration,omitempty"`
}{
Owner: stats.Owner,
Repo: stats.Repo,
BusFactor: stats.BusFactor,
KnowledgeScore: stats.KnowledgeScore,
TotalCommits: stats.TotalCommits,
Contributors: stats.Contributors,
ContributorActivity: stats.ContributorActivity,
RecentContributors: stats.RecentContributors,
AnalysisDuration: stats.AnalysisDuration,
}

jsonData, err := json.MarshalIndent(data, "", " ")
Expand Down
17 changes: 17 additions & 0 deletions internal/output/md.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ func (f *MarkdownFormatter) Format(stats *models.RepositoryStats) (string, error
md.WriteString("![Gitness](https://github.com/erdemkosk/gitness/blob/master/logo.png?raw=true)\n\n")
md.WriteString(fmt.Sprintf("# Repository Analysis: %s/%s\n\n", stats.Owner, stats.Repo))

if stats.AnalysisDuration != "" {
md.WriteString(fmt.Sprintf("## 📅 Analysis Period: Last %s\n\n", stats.AnalysisDuration))
} else {
md.WriteString("## 📅 Analysis Period: All Time\n\n")
}

// Bus Factor status
busFactorEmoji := "🟢"
if stats.BusFactor < 2 {
Expand All @@ -39,9 +45,20 @@ func (f *MarkdownFormatter) Format(stats *models.RepositoryStats) (string, error
recentEmoji = "🟡"
}

// Knowledge Distribution status
scoreEmoji := "🟢"
if stats.KnowledgeScore < 25 {
scoreEmoji = "🔴"
} else if stats.KnowledgeScore < 50 {
scoreEmoji = "🟡"
}

md.WriteString(fmt.Sprintf("## %s Bus Factor: **%d** (critical if < 2, warning if < 4)\n\n",
busFactorEmoji, stats.BusFactor))

md.WriteString(fmt.Sprintf("## %s Knowledge Distribution Score: **%.1f** (0-100, higher is better)\n\n",
scoreEmoji, stats.KnowledgeScore))

md.WriteString(fmt.Sprintf("## %s Active Contributor Ratio: **%.1f%%** (contributors with >1%% contribution, critical if < 30%%, warning if < 50%%)\n\n",
activityEmoji, stats.ContributorActivity))

Expand Down
Loading

0 comments on commit ca91a16

Please sign in to comment.