code.gitea.io/gitea@v1.21.7/models/repo/language_stats.go (about)

     1  // Copyright 2020 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package repo
     5  
     6  import (
     7  	"context"
     8  	"math"
     9  	"sort"
    10  	"strings"
    11  
    12  	"code.gitea.io/gitea/models/db"
    13  	"code.gitea.io/gitea/modules/timeutil"
    14  
    15  	"github.com/go-enry/go-enry/v2"
    16  )
    17  
    18  // LanguageStat describes language statistics of a repository
    19  type LanguageStat struct {
    20  	ID          int64 `xorm:"pk autoincr"`
    21  	RepoID      int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
    22  	CommitID    string
    23  	IsPrimary   bool
    24  	Language    string             `xorm:"VARCHAR(50) UNIQUE(s) INDEX NOT NULL"`
    25  	Percentage  float32            `xorm:"-"`
    26  	Size        int64              `xorm:"NOT NULL DEFAULT 0"`
    27  	Color       string             `xorm:"-"`
    28  	CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
    29  }
    30  
    31  func init() {
    32  	db.RegisterModel(new(LanguageStat))
    33  }
    34  
    35  // LanguageStatList defines a list of language statistics
    36  type LanguageStatList []*LanguageStat
    37  
    38  // LoadAttributes loads attributes
    39  func (stats LanguageStatList) LoadAttributes() {
    40  	for i := range stats {
    41  		stats[i].Color = enry.GetColor(stats[i].Language)
    42  	}
    43  }
    44  
    45  func (stats LanguageStatList) getLanguagePercentages() map[string]float32 {
    46  	langPerc := make(map[string]float32)
    47  	var otherPerc float32
    48  	var total int64
    49  
    50  	for _, stat := range stats {
    51  		total += stat.Size
    52  	}
    53  	if total > 0 {
    54  		for _, stat := range stats {
    55  			perc := float32(float64(stat.Size) / float64(total) * 100)
    56  			if perc <= 0.1 {
    57  				otherPerc += perc
    58  				continue
    59  			}
    60  			langPerc[stat.Language] = perc
    61  		}
    62  	}
    63  	if otherPerc > 0 {
    64  		langPerc["other"] = otherPerc
    65  	}
    66  	roundByLargestRemainder(langPerc, 100)
    67  	return langPerc
    68  }
    69  
    70  // Rounds to 1 decimal point, target should be the expected sum of percs
    71  func roundByLargestRemainder(percs map[string]float32, target float32) {
    72  	leftToDistribute := int(target * 10)
    73  
    74  	keys := make([]string, 0, len(percs))
    75  
    76  	for k, v := range percs {
    77  		percs[k] = v * 10
    78  		floored := math.Floor(float64(percs[k]))
    79  		leftToDistribute -= int(floored)
    80  		keys = append(keys, k)
    81  	}
    82  
    83  	// Sort the keys by the largest remainder
    84  	sort.SliceStable(keys, func(i, j int) bool {
    85  		_, remainderI := math.Modf(float64(percs[keys[i]]))
    86  		_, remainderJ := math.Modf(float64(percs[keys[j]]))
    87  		return remainderI > remainderJ
    88  	})
    89  
    90  	// Increment the values in order of largest remainder
    91  	for _, k := range keys {
    92  		percs[k] = float32(math.Floor(float64(percs[k])))
    93  		if leftToDistribute > 0 {
    94  			percs[k]++
    95  			leftToDistribute--
    96  		}
    97  		percs[k] /= 10
    98  	}
    99  }
   100  
   101  // GetLanguageStats returns the language statistics for a repository
   102  func GetLanguageStats(ctx context.Context, repo *Repository) (LanguageStatList, error) {
   103  	stats := make(LanguageStatList, 0, 6)
   104  	if err := db.GetEngine(ctx).Where("`repo_id` = ?", repo.ID).Desc("`size`").Find(&stats); err != nil {
   105  		return nil, err
   106  	}
   107  	return stats, nil
   108  }
   109  
   110  // GetTopLanguageStats returns the top language statistics for a repository
   111  func GetTopLanguageStats(repo *Repository, limit int) (LanguageStatList, error) {
   112  	stats, err := GetLanguageStats(db.DefaultContext, repo)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  	perc := stats.getLanguagePercentages()
   117  	topstats := make(LanguageStatList, 0, limit)
   118  	var other float32
   119  	for i := range stats {
   120  		if _, ok := perc[stats[i].Language]; !ok {
   121  			continue
   122  		}
   123  		if stats[i].Language == "other" || len(topstats) >= limit {
   124  			other += perc[stats[i].Language]
   125  			continue
   126  		}
   127  		stats[i].Percentage = perc[stats[i].Language]
   128  		topstats = append(topstats, stats[i])
   129  	}
   130  	if other > 0 {
   131  		topstats = append(topstats, &LanguageStat{
   132  			RepoID:     repo.ID,
   133  			Language:   "other",
   134  			Color:      "#cccccc",
   135  			Percentage: float32(math.Round(float64(other)*10) / 10),
   136  		})
   137  	}
   138  	topstats.LoadAttributes()
   139  	return topstats, nil
   140  }
   141  
   142  // UpdateLanguageStats updates the language statistics for repository
   143  func UpdateLanguageStats(repo *Repository, commitID string, stats map[string]int64) error {
   144  	ctx, committer, err := db.TxContext(db.DefaultContext)
   145  	if err != nil {
   146  		return err
   147  	}
   148  	defer committer.Close()
   149  	sess := db.GetEngine(ctx)
   150  
   151  	oldstats, err := GetLanguageStats(ctx, repo)
   152  	if err != nil {
   153  		return err
   154  	}
   155  	var topLang string
   156  	var s int64
   157  	for lang, size := range stats {
   158  		if size > s {
   159  			s = size
   160  			topLang = strings.ToLower(lang)
   161  		}
   162  	}
   163  
   164  	for lang, size := range stats {
   165  		upd := false
   166  		llang := strings.ToLower(lang)
   167  		for _, s := range oldstats {
   168  			// Update already existing language
   169  			if strings.ToLower(s.Language) == llang {
   170  				s.CommitID = commitID
   171  				s.IsPrimary = llang == topLang
   172  				s.Size = size
   173  				if _, err := sess.ID(s.ID).Cols("`commit_id`", "`size`", "`is_primary`").Update(s); err != nil {
   174  					return err
   175  				}
   176  				upd = true
   177  				break
   178  			}
   179  		}
   180  		// Insert new language
   181  		if !upd {
   182  			if err := db.Insert(ctx, &LanguageStat{
   183  				RepoID:    repo.ID,
   184  				CommitID:  commitID,
   185  				IsPrimary: llang == topLang,
   186  				Language:  lang,
   187  				Size:      size,
   188  			}); err != nil {
   189  				return err
   190  			}
   191  		}
   192  	}
   193  	// Delete old languages
   194  	statsToDelete := make([]int64, 0, len(oldstats))
   195  	for _, s := range oldstats {
   196  		if s.CommitID != commitID {
   197  			statsToDelete = append(statsToDelete, s.ID)
   198  		}
   199  	}
   200  	if len(statsToDelete) > 0 {
   201  		if _, err := sess.In("`id`", statsToDelete).Delete(&LanguageStat{}); err != nil {
   202  			return err
   203  		}
   204  	}
   205  
   206  	// Update indexer status
   207  	if err = UpdateIndexerStatus(ctx, repo, RepoIndexerTypeStats, commitID); err != nil {
   208  		return err
   209  	}
   210  
   211  	return committer.Commit()
   212  }
   213  
   214  // CopyLanguageStat Copy originalRepo language stat information to destRepo (use for forked repo)
   215  func CopyLanguageStat(originalRepo, destRepo *Repository) error {
   216  	ctx, committer, err := db.TxContext(db.DefaultContext)
   217  	if err != nil {
   218  		return err
   219  	}
   220  	defer committer.Close()
   221  
   222  	RepoLang := make(LanguageStatList, 0, 6)
   223  	if err := db.GetEngine(ctx).Where("`repo_id` = ?", originalRepo.ID).Desc("`size`").Find(&RepoLang); err != nil {
   224  		return err
   225  	}
   226  	if len(RepoLang) > 0 {
   227  		for i := range RepoLang {
   228  			RepoLang[i].ID = 0
   229  			RepoLang[i].RepoID = destRepo.ID
   230  			RepoLang[i].CreatedUnix = timeutil.TimeStampNow()
   231  		}
   232  		// update destRepo's indexer status
   233  		tmpCommitID := RepoLang[0].CommitID
   234  		if err := UpdateIndexerStatus(ctx, destRepo, RepoIndexerTypeStats, tmpCommitID); err != nil {
   235  			return err
   236  		}
   237  		if err := db.Insert(ctx, &RepoLang); err != nil {
   238  			return err
   239  		}
   240  	}
   241  	return committer.Commit()
   242  }