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 }