mirror of
https://github.com/lukaszraczylo/git-velocity.git
synced 2026-10-11 23:20:13 +00:00
goccy/go-json v0.11.0 and later panic in the indent encoder with a nil pointer dereference, so the dependency update failed its tests. The site generator needs nothing beyond the standard library.
603 lines
17 KiB
Go
603 lines
17 KiB
Go
package site
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"encoding/json"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/lukaszraczylo/git-velocity/internal/config"
|
|
"github.com/lukaszraczylo/git-velocity/internal/domain/models"
|
|
)
|
|
|
|
func TestNewGenerator(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
gen, err := NewGenerator("/tmp/output", cfg)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, gen)
|
|
assert.Equal(t, "/tmp/output", gen.outputDir)
|
|
assert.Equal(t, cfg, gen.config)
|
|
}
|
|
|
|
func TestGenerator_GenerateCreatesOutputDir(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
outputDir := filepath.Join(tempDir, "new-output")
|
|
|
|
cfg := config.DefaultConfig()
|
|
gen, err := NewGenerator(outputDir, cfg)
|
|
require.NoError(t, err)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Period: models.Period{
|
|
Start: time.Now().Add(-24 * time.Hour),
|
|
End: time.Now(),
|
|
},
|
|
}
|
|
|
|
err = gen.Generate(metrics)
|
|
require.NoError(t, err)
|
|
|
|
// Verify output directory was created
|
|
info, err := os.Stat(outputDir)
|
|
require.NoError(t, err)
|
|
assert.True(t, info.IsDir())
|
|
}
|
|
|
|
func TestGenerator_GenerateCreatesDataDir(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
cfg := config.DefaultConfig()
|
|
gen, err := NewGenerator(tempDir, cfg)
|
|
require.NoError(t, err)
|
|
|
|
metrics := &models.GlobalMetrics{}
|
|
|
|
err = gen.Generate(metrics)
|
|
require.NoError(t, err)
|
|
|
|
// Verify data directory was created
|
|
dataDir := filepath.Join(tempDir, "data")
|
|
info, err := os.Stat(dataDir)
|
|
require.NoError(t, err)
|
|
assert.True(t, info.IsDir())
|
|
}
|
|
|
|
func TestGenerator_GenerateGlobalJSON(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
cfg := config.DefaultConfig()
|
|
gen, err := NewGenerator(tempDir, cfg)
|
|
require.NoError(t, err)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
TotalContributors: 5,
|
|
TotalCommits: 100,
|
|
TotalPRs: 50,
|
|
TotalReviews: 75,
|
|
TotalLinesAdded: 10000,
|
|
TotalLinesDeleted: 5000,
|
|
}
|
|
|
|
err = gen.Generate(metrics)
|
|
require.NoError(t, err)
|
|
|
|
// Read and verify global.json
|
|
globalPath := filepath.Join(tempDir, "data", "global.json")
|
|
data, err := os.ReadFile(globalPath)
|
|
require.NoError(t, err)
|
|
|
|
var result struct {
|
|
TotalContributors int `json:"total_contributors"`
|
|
TotalCommits int `json:"total_commits"`
|
|
TotalPRs int `json:"total_prs"`
|
|
TotalReviews int `json:"total_reviews"`
|
|
TotalLinesAdded int `json:"total_lines_added"`
|
|
TotalLinesDeleted int `json:"total_lines_deleted"`
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
}
|
|
err = json.Unmarshal(data, &result)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 5, result.TotalContributors)
|
|
assert.Equal(t, 100, result.TotalCommits)
|
|
assert.Equal(t, 50, result.TotalPRs)
|
|
assert.Equal(t, 75, result.TotalReviews)
|
|
assert.Equal(t, 10000, result.TotalLinesAdded)
|
|
assert.Equal(t, 5000, result.TotalLinesDeleted)
|
|
assert.False(t, result.GeneratedAt.IsZero())
|
|
}
|
|
|
|
func TestGenerator_GenerateLeaderboardJSON(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
cfg := config.DefaultConfig()
|
|
gen, err := NewGenerator(tempDir, cfg)
|
|
require.NoError(t, err)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Leaderboard: []models.LeaderboardEntry{
|
|
{Rank: 1, Login: "user1", Score: 1000},
|
|
{Rank: 2, Login: "user2", Score: 800},
|
|
{Rank: 3, Login: "user3", Score: 600},
|
|
},
|
|
}
|
|
|
|
err = gen.Generate(metrics)
|
|
require.NoError(t, err)
|
|
|
|
// Read and verify leaderboard.json
|
|
leaderboardPath := filepath.Join(tempDir, "data", "leaderboard.json")
|
|
data, err := os.ReadFile(leaderboardPath)
|
|
require.NoError(t, err)
|
|
|
|
var result []models.LeaderboardEntry
|
|
err = json.Unmarshal(data, &result)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, result, 3)
|
|
assert.Equal(t, "user1", result[0].Login)
|
|
assert.Equal(t, 1000, result[0].Score)
|
|
assert.Equal(t, "user2", result[1].Login)
|
|
assert.Equal(t, 800, result[1].Score)
|
|
}
|
|
|
|
func TestGenerator_GenerateRepositoryJSON(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
cfg := config.DefaultConfig()
|
|
gen, err := NewGenerator(tempDir, cfg)
|
|
require.NoError(t, err)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
Owner: "myorg",
|
|
Name: "myrepo",
|
|
TotalCommits: 42,
|
|
TotalPRs: 10,
|
|
},
|
|
},
|
|
}
|
|
|
|
err = gen.Generate(metrics)
|
|
require.NoError(t, err)
|
|
|
|
// Read and verify repository metrics
|
|
repoPath := filepath.Join(tempDir, "data", "repos", "myorg", "myrepo", "metrics.json")
|
|
data, err := os.ReadFile(repoPath)
|
|
require.NoError(t, err)
|
|
|
|
var result models.RepositoryMetrics
|
|
err = json.Unmarshal(data, &result)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "myorg", result.Owner)
|
|
assert.Equal(t, "myrepo", result.Name)
|
|
assert.Equal(t, 42, result.TotalCommits)
|
|
assert.Equal(t, 10, result.TotalPRs)
|
|
}
|
|
|
|
func TestGenerator_GenerateMultipleRepositories(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
cfg := config.DefaultConfig()
|
|
gen, err := NewGenerator(tempDir, cfg)
|
|
require.NoError(t, err)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Repositories: []models.RepositoryMetrics{
|
|
{Owner: "org1", Name: "repo1", TotalCommits: 100},
|
|
{Owner: "org1", Name: "repo2", TotalCommits: 200},
|
|
{Owner: "org2", Name: "repo3", TotalCommits: 300},
|
|
},
|
|
}
|
|
|
|
err = gen.Generate(metrics)
|
|
require.NoError(t, err)
|
|
|
|
// Verify all repository files exist
|
|
_, err = os.Stat(filepath.Join(tempDir, "data", "repos", "org1", "repo1", "metrics.json"))
|
|
assert.NoError(t, err)
|
|
_, err = os.Stat(filepath.Join(tempDir, "data", "repos", "org1", "repo2", "metrics.json"))
|
|
assert.NoError(t, err)
|
|
_, err = os.Stat(filepath.Join(tempDir, "data", "repos", "org2", "repo3", "metrics.json"))
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestGenerator_GenerateTeamJSON(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
cfg := config.DefaultConfig()
|
|
gen, err := NewGenerator(tempDir, cfg)
|
|
require.NoError(t, err)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Teams: []models.TeamMetrics{
|
|
{
|
|
Name: "Backend Team",
|
|
Color: "#ff0000",
|
|
Members: []string{"user1", "user2"},
|
|
TotalScore: 1500,
|
|
},
|
|
},
|
|
}
|
|
|
|
err = gen.Generate(metrics)
|
|
require.NoError(t, err)
|
|
|
|
// Read and verify team JSON (slugified name)
|
|
teamPath := filepath.Join(tempDir, "data", "teams", "backend-team.json")
|
|
data, err := os.ReadFile(teamPath)
|
|
require.NoError(t, err)
|
|
|
|
var result models.TeamMetrics
|
|
err = json.Unmarshal(data, &result)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "Backend Team", result.Name)
|
|
assert.Equal(t, "#ff0000", result.Color)
|
|
assert.Equal(t, 1500, result.TotalScore)
|
|
assert.Len(t, result.Members, 2)
|
|
}
|
|
|
|
func TestGenerator_GenerateContributorJSON(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
cfg := config.DefaultConfig()
|
|
gen, err := NewGenerator(tempDir, cfg)
|
|
require.NoError(t, err)
|
|
|
|
// Generator now uses global Contributors, not per-repo Contributors
|
|
metrics := &models.GlobalMetrics{
|
|
Contributors: []models.ContributorMetrics{
|
|
{
|
|
Login: "john-doe",
|
|
Name: "John Doe",
|
|
CommitCount: 50,
|
|
PRsOpened: 10,
|
|
},
|
|
},
|
|
}
|
|
|
|
err = gen.Generate(metrics)
|
|
require.NoError(t, err)
|
|
|
|
// Read and verify contributor JSON
|
|
contributorPath := filepath.Join(tempDir, "data", "contributors", "john-doe.json")
|
|
data, err := os.ReadFile(contributorPath)
|
|
require.NoError(t, err)
|
|
|
|
var result models.ContributorMetrics
|
|
err = json.Unmarshal(data, &result)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "john-doe", result.Login)
|
|
assert.Equal(t, "John Doe", result.Name)
|
|
assert.Equal(t, 50, result.CommitCount)
|
|
assert.Equal(t, 10, result.PRsOpened)
|
|
}
|
|
|
|
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 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, PRsOpened: 5},
|
|
},
|
|
},
|
|
{
|
|
Owner: "org",
|
|
Name: "repo2",
|
|
Contributors: []models.ContributorMetrics{
|
|
{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)
|
|
|
|
// 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)
|
|
|
|
var result models.ContributorMetrics
|
|
err = json.Unmarshal(data, &result)
|
|
require.NoError(t, err)
|
|
|
|
// 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) {
|
|
tempDir := t.TempDir()
|
|
|
|
cfg := config.DefaultConfig()
|
|
gen, err := NewGenerator(tempDir, cfg)
|
|
require.NoError(t, err)
|
|
|
|
metrics := &models.GlobalMetrics{
|
|
Teams: []models.TeamMetrics{}, // Empty teams
|
|
}
|
|
|
|
err = gen.Generate(metrics)
|
|
require.NoError(t, err)
|
|
|
|
// Team directory should not exist
|
|
teamDir := filepath.Join(tempDir, "data", "teams")
|
|
_, err = os.Stat(teamDir)
|
|
assert.True(t, os.IsNotExist(err))
|
|
}
|
|
|
|
func TestSlugify(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"Backend Team", "backend-team"},
|
|
{"Frontend_Team", "frontend-team"},
|
|
{"UPPER CASE", "upper-case"},
|
|
{"already-slug", "already-slug"},
|
|
{"Multiple Spaces", "multiple---spaces"},
|
|
{"Mixed_And Spaced", "mixed-and-spaced"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
t.Parallel()
|
|
result := slugify(tt.input)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriteJSON(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
testData := map[string]interface{}{
|
|
"key": "value",
|
|
"number": 42,
|
|
"nested": map[string]string{
|
|
"inner": "data",
|
|
},
|
|
}
|
|
|
|
path := filepath.Join(tempDir, "test.json")
|
|
err := writeJSON(path, testData)
|
|
require.NoError(t, err)
|
|
|
|
// Verify file was created and is valid JSON
|
|
data, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
|
|
var result map[string]interface{}
|
|
err = json.Unmarshal(data, &result)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "value", result["key"])
|
|
assert.Equal(t, float64(42), result["number"]) // JSON numbers are float64
|
|
}
|
|
|
|
func TestWriteJSON_Indented(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
testData := map[string]string{"key": "value"}
|
|
path := filepath.Join(tempDir, "test.json")
|
|
|
|
err := writeJSON(path, testData)
|
|
require.NoError(t, err)
|
|
|
|
data, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
|
|
// Should be formatted with indentation
|
|
assert.Contains(t, string(data), "\n")
|
|
assert.Contains(t, string(data), " ") // 2-space indent
|
|
}
|
|
|
|
func TestWriteJSON_ErrorOnInvalidPath(t *testing.T) {
|
|
// Try to write to a path that doesn't exist
|
|
path := "/nonexistent/directory/test.json"
|
|
err := writeJSON(path, "data")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestGenerator_GenerateWithFullMetrics(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
|
|
cfg := config.DefaultConfig()
|
|
gen, err := NewGenerator(tempDir, cfg)
|
|
require.NoError(t, err)
|
|
|
|
// Create comprehensive metrics
|
|
metrics := &models.GlobalMetrics{
|
|
Period: models.Period{
|
|
Start: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
End: time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC),
|
|
Granularity: "monthly",
|
|
Label: "2024",
|
|
},
|
|
TotalContributors: 10,
|
|
TotalCommits: 500,
|
|
TotalPRs: 100,
|
|
TotalReviews: 200,
|
|
TotalLinesAdded: 50000,
|
|
TotalLinesDeleted: 25000,
|
|
Repositories: []models.RepositoryMetrics{
|
|
{
|
|
Owner: "org",
|
|
Name: "repo1",
|
|
TotalCommits: 300,
|
|
TotalPRs: 60,
|
|
ActiveContributors: 5,
|
|
Contributors: []models.ContributorMetrics{
|
|
{Login: "alice", Name: "Alice", CommitCount: 100},
|
|
{Login: "bob", Name: "Bob", CommitCount: 200},
|
|
},
|
|
},
|
|
{
|
|
Owner: "org",
|
|
Name: "repo2",
|
|
TotalCommits: 200,
|
|
TotalPRs: 40,
|
|
ActiveContributors: 5,
|
|
Contributors: []models.ContributorMetrics{
|
|
{Login: "alice", Name: "Alice", CommitCount: 50},
|
|
{Login: "charlie", Name: "Charlie", CommitCount: 150},
|
|
},
|
|
},
|
|
},
|
|
// 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",
|
|
Members: []string{"alice", "bob"},
|
|
TotalScore: 5000,
|
|
},
|
|
},
|
|
Leaderboard: []models.LeaderboardEntry{
|
|
{Rank: 1, Login: "alice", Score: 3000},
|
|
{Rank: 2, Login: "bob", Score: 2000},
|
|
{Rank: 3, Login: "charlie", Score: 1500},
|
|
},
|
|
}
|
|
|
|
err = gen.Generate(metrics)
|
|
require.NoError(t, err)
|
|
|
|
// Verify all expected files exist
|
|
expectedPaths := []string{
|
|
filepath.Join(tempDir, "data", "global.json"),
|
|
filepath.Join(tempDir, "data", "leaderboard.json"),
|
|
filepath.Join(tempDir, "data", "repos", "org", "repo1", "metrics.json"),
|
|
filepath.Join(tempDir, "data", "repos", "org", "repo2", "metrics.json"),
|
|
filepath.Join(tempDir, "data", "teams", "core-team.json"),
|
|
filepath.Join(tempDir, "data", "contributors", "alice.json"),
|
|
filepath.Join(tempDir, "data", "contributors", "bob.json"),
|
|
filepath.Join(tempDir, "data", "contributors", "charlie.json"),
|
|
}
|
|
|
|
for _, path := range expectedPaths {
|
|
_, 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")
|
|
}
|