You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

language_stats.go 6.1KB

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