arnaucube
arnaucube/gitviewPublic Self-hosted repository
gitview/internal/git/languages.go
internal/git/languages.goRaw
  1package git
  2
  3import (
  4	"fmt"
  5	"io"
  6	"slices"
  7	"sort"
  8	"sync"
  9
 10	enry "github.com/go-enry/go-enry/v2"
 11	"github.com/go-git/go-git/v5/plumbing"
 12	"github.com/go-git/go-git/v5/plumbing/filemode"
 13	"github.com/go-git/go-git/v5/plumbing/object"
 14)
 15
 16// LanguageStat describes a language's share of the repository's source bytes.
 17type LanguageStat struct {
 18	Name       string
 19	Color      string
 20	Bytes      int64
 21	Percentage float64
 22}
 23
 24// Immutable Git tree IDs keep cached statistics valid across mirror updates.
 25var languageStatsCache = struct {
 26	sync.Mutex
 27	entries map[plumbing.Hash][]LanguageStat
 28	order   []plumbing.Hash
 29}{entries: make(map[plumbing.Hash][]LanguageStat)}
 30
 31// Languages classifies committed source files without requiring a working tree.
 32func (g *GitRepo) Languages() ([]LanguageStat, error) {
 33	commit, err := g.r.CommitObject(g.h)
 34	if err != nil {
 35		return nil, fmt.Errorf("language commit: %w", err)
 36	}
 37	tree, err := commit.Tree()
 38	if err != nil {
 39		return nil, fmt.Errorf("language tree: %w", err)
 40	}
 41	languageStatsCache.Lock()
 42	cached, ok := languageStatsCache.entries[tree.Hash]
 43	languageStatsCache.Unlock()
 44	if ok {
 45		return slices.Clone(cached), nil
 46	}
 47
 48	sizes := make(map[string]int64)
 49	err = tree.Files().ForEach(func(file *object.File) error {
 50		if file.Size == 0 || file.Mode == filemode.Symlink || enry.IsVendor(file.Name) || enry.IsDocumentation(file.Name) {
 51			return nil
 52		}
 53		reader, err := file.Reader()
 54		if err != nil {
 55			return err
 56		}
 57		// Bound content inspection while weighing every included file by its full size.
 58		content, readErr := io.ReadAll(io.LimitReader(reader, 64*1024))
 59		closeErr := reader.Close()
 60		if readErr != nil {
 61			return readErr
 62		}
 63		if closeErr != nil {
 64			return closeErr
 65		}
 66		if enry.IsBinary(content) || enry.IsGenerated(file.Name, content) {
 67			return nil
 68		}
 69		name := enry.GetLanguage(file.Name, content)
 70		kind := enry.GetLanguageType(name)
 71		if kind != enry.Programming && kind != enry.Markup {
 72			return nil
 73		}
 74		if group := enry.GetLanguageGroup(name); group != "" {
 75			name = group
 76		}
 77		sizes[name] += file.Size
 78		return nil
 79	})
 80	if err != nil {
 81		return nil, fmt.Errorf("classifying languages: %w", err)
 82	}
 83	stats := make([]LanguageStat, 0, len(sizes))
 84	var total int64
 85	for name, size := range sizes {
 86		stats = append(stats, LanguageStat{Name: name, Color: enry.GetColor(name), Bytes: size})
 87		total += size
 88	}
 89	sort.Slice(stats, func(i, j int) bool {
 90		if stats[i].Bytes == stats[j].Bytes {
 91			return stats[i].Name < stats[j].Name
 92		}
 93		return stats[i].Bytes > stats[j].Bytes
 94	})
 95	for i := range stats {
 96		stats[i].Percentage = 100 * float64(stats[i].Bytes) / float64(total)
 97	}
 98	languageStatsCache.Lock()
 99	defer languageStatsCache.Unlock()
100	if _, exists := languageStatsCache.entries[tree.Hash]; !exists {
101		const maxCachedTrees = 128
102		if len(languageStatsCache.order) == maxCachedTrees {
103			delete(languageStatsCache.entries, languageStatsCache.order[0])
104			languageStatsCache.order = languageStatsCache.order[1:]
105		}
106		languageStatsCache.order = append(languageStatsCache.order, tree.Hash)
107		languageStatsCache.entries[tree.Hash] = slices.Clone(stats)
108	}
109	return stats, nil
110}