package git import ( "math" "os" "path/filepath" "testing" "time" gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" ) // commitLanguageFiles creates real Git objects, including bare-mirror compatible trees. func commitLanguageFiles(t *testing.T, files map[string]string) (string, *gogit.Repository) { t.Helper() path := t.TempDir() repo, err := gogit.PlainInit(path, false) if err != nil { t.Fatal(err) } wt, err := repo.Worktree() if err != nil { t.Fatal(err) } for name, content := range files { target := filepath.Join(path, name) if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(target, []byte(content), 0644); err != nil { t.Fatal(err) } if _, err := wt.Add(name); err != nil { t.Fatal(err) } } author := &object.Signature{Name: "Test Author", Email: "author@example.com", When: time.Now()} if _, err := wt.Commit("Language fixtures", &gogit.CommitOptions{Author: author}); err != nil { t.Fatal(err) } return path, repo } func TestLanguagesSourceBytesAndExclusions(t *testing.T) { files := map[string]string{ "main.go": "package main\n\nfunc main() {}\n", "lib/helpers.go": "package main\n\nconst answer = 42\n", "src/index.js": "export const answer = 42;\n", "page.html": "Hello\n", "README.md": "# Documentation\n", "data.json": "{\"name\":\"data\"}", "settings.yaml": "enabled: true\n", "binary.go": "\x00\x01\x02not source", "empty.py": "", "vendor/dep/code.go": "package dependency\n", "node_modules/pkg/index.js": "export const dependency = true;\n", "generated.go": "// Code generated by a tool. DO NOT EDIT.\npackage main\n", } path, _ := commitLanguageFiles(t, files) if err := os.Symlink("main.go", filepath.Join(path, "alias.py")); err != nil { t.Fatal(err) } repo, err := gogit.PlainOpen(path) if err != nil { t.Fatal(err) } wt, err := repo.Worktree() if err != nil { t.Fatal(err) } if _, err := wt.Add("alias.py"); err != nil { t.Fatal(err) } if _, err := wt.Commit("Add source link", &gogit.CommitOptions{Author: &object.Signature{Name: "Test", Email: "test@example.com", When: time.Now()}}); err != nil { t.Fatal(err) } g, err := Open(path, "") if err != nil { t.Fatal(err) } stats, err := g.Languages() if err != nil { t.Fatal(err) } want := map[string]int64{"Go": int64(len(files["main.go"]) + len(files["lib/helpers.go"])), "JavaScript": int64(len(files["src/index.js"])), "HTML": int64(len(files["page.html"]))} if len(stats) != len(want) { t.Fatalf("languages: %+v; want %v", stats, want) } var total int64 for _, size := range want { total += size } for i, stat := range stats { if stat.Bytes != want[stat.Name] { t.Errorf("%s: %d bytes, want %d", stat.Name, stat.Bytes, want[stat.Name]) } percentage := 100 * float64(want[stat.Name]) / float64(total) if math.Abs(stat.Percentage-percentage) > 1e-8 { t.Errorf("%s: percentage %f, want %f", stat.Name, stat.Percentage, percentage) } if i > 0 && stat.Bytes > stats[i-1].Bytes { t.Error("languages are not sorted by code size") } } // Mirrors have no working tree but must produce the same statistics. barePath := filepath.Join(t.TempDir(), "mirror.git") if _, err := gogit.PlainClone(barePath, true, &gogit.CloneOptions{URL: path}); err != nil { t.Fatal(err) } bare, err := Open(barePath, "") if err != nil { t.Fatal(err) } bareStats, err := bare.Languages() if err != nil { t.Fatal(err) } if len(bareStats) != len(stats) { t.Fatalf("bare mirror statistics: %+v", bareStats) } } func TestLanguagesCacheFollowsCommits(t *testing.T) { path, repo := commitLanguageFiles(t, map[string]string{"main.go": "package main\n"}) g, err := Open(path, "") if err != nil { t.Fatal(err) } before, err := g.Languages() if err != nil { t.Fatal(err) } if len(before) != 1 || before[0].Name != "Go" { t.Fatalf("initial languages: %+v", before) } before[0].Name = "mutated" cached, err := g.Languages() if err != nil { t.Fatal(err) } if cached[0].Name != "Go" { t.Fatal("caller modified the cached statistics") } if err := os.WriteFile(filepath.Join(path, "main.py"), []byte("print('new language')\n"), 0644); err != nil { t.Fatal(err) } wt, err := repo.Worktree() if err != nil { t.Fatal(err) } if _, err := wt.Add("main.py"); err != nil { t.Fatal(err) } if _, err := wt.Commit("Add Python", &gogit.CommitOptions{Author: &object.Signature{Name: "Test", Email: "test@example.com", When: time.Now()}}); err != nil { t.Fatal(err) } updated, err := Open(path, "") if err != nil { t.Fatal(err) } after, err := updated.Languages() if err != nil { t.Fatal(err) } if len(after) != 2 || after[0].Name != "Python" { t.Fatalf("new commit did not refresh statistics: %+v", after) } } func TestLanguagesDocumentationOnly(t *testing.T) { path, _ := commitLanguageFiles(t, map[string]string{"README.md": "# Just documentation\n"}) g, err := Open(path, "") if err != nil { t.Fatal(err) } stats, err := g.Languages() if err != nil { t.Fatal(err) } if len(stats) != 0 { t.Fatalf("documentation counted as source: %+v", stats) } }