2020-02-11 12:34:17 +03:00
// Copyright 2020 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2020-02-11 12:34:17 +03:00
2021-12-10 04:27:50 +03:00
package repo
2020-02-11 12:34:17 +03:00
import (
2022-05-20 17:08:52 +03:00
"context"
2020-02-11 12:34:17 +03:00
"math"
2022-12-08 05:47:47 +03:00
"sort"
2020-02-11 12:34:17 +03:00
"strings"
2021-09-19 14:49:59 +03:00
"code.gitea.io/gitea/models/db"
2020-02-11 12:34:17 +03:00
"code.gitea.io/gitea/modules/timeutil"
2020-04-15 20:40:39 +03:00
"github.com/go-enry/go-enry/v2"
2020-02-11 12:34:17 +03:00
)
// LanguageStat describes language statistics of a repository
type LanguageStat struct {
ID int64 ` xorm:"pk autoincr" `
RepoID int64 ` xorm:"UNIQUE(s) INDEX NOT NULL" `
CommitID string
IsPrimary bool
2020-08-04 16:54:29 +03:00
Language string ` xorm:"VARCHAR(50) UNIQUE(s) INDEX NOT NULL" `
2020-05-30 10:46:15 +03:00
Percentage float32 ` xorm:"-" `
Size int64 ` xorm:"NOT NULL DEFAULT 0" `
2020-02-11 12:34:17 +03:00
Color string ` xorm:"-" `
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX CREATED" `
}
2021-09-19 14:49:59 +03:00
func init ( ) {
db . RegisterModel ( new ( LanguageStat ) )
}
2020-02-11 12:34:17 +03:00
// LanguageStatList defines a list of language statistics
type LanguageStatList [ ] * LanguageStat
2021-12-10 04:27:50 +03:00
// LoadAttributes loads attributes
func ( stats LanguageStatList ) LoadAttributes ( ) {
2020-02-11 12:34:17 +03:00
for i := range stats {
stats [ i ] . Color = enry . GetColor ( stats [ i ] . Language )
}
}
2020-05-30 10:46:15 +03:00
func ( stats LanguageStatList ) getLanguagePercentages ( ) map [ string ] float32 {
langPerc := make ( map [ string ] float32 )
2022-12-08 05:47:47 +03:00
var otherPerc float32
2020-05-30 10:46:15 +03:00
var total int64
2020-05-31 01:58:55 +03:00
2020-05-30 10:46:15 +03:00
for _ , stat := range stats {
total += stat . Size
}
if total > 0 {
for _ , stat := range stats {
2022-12-08 05:47:47 +03:00
perc := float32 ( float64 ( stat . Size ) / float64 ( total ) * 100 )
2020-05-30 10:46:15 +03:00
if perc <= 0.1 {
2022-12-08 05:47:47 +03:00
otherPerc += perc
2020-05-30 10:46:15 +03:00
continue
}
langPerc [ stat . Language ] = perc
}
}
if otherPerc > 0 {
langPerc [ "other" ] = otherPerc
}
2022-12-08 05:47:47 +03:00
roundByLargestRemainder ( langPerc , 100 )
2020-05-30 10:46:15 +03:00
return langPerc
}
2022-12-08 05:47:47 +03:00
// Rounds to 1 decimal point, target should be the expected sum of percs
func roundByLargestRemainder ( percs map [ string ] float32 , target float32 ) {
leftToDistribute := int ( target * 10 )
keys := make ( [ ] string , 0 , len ( percs ) )
for k , v := range percs {
percs [ k ] = v * 10
floored := math . Floor ( float64 ( percs [ k ] ) )
leftToDistribute -= int ( floored )
keys = append ( keys , k )
}
// Sort the keys by the largest remainder
sort . SliceStable ( keys , func ( i , j int ) bool {
_ , remainderI := math . Modf ( float64 ( percs [ keys [ i ] ] ) )
_ , remainderJ := math . Modf ( float64 ( percs [ keys [ j ] ] ) )
return remainderI > remainderJ
} )
// Increment the values in order of largest remainder
for _ , k := range keys {
percs [ k ] = float32 ( math . Floor ( float64 ( percs [ k ] ) ) )
if leftToDistribute > 0 {
percs [ k ] ++
leftToDistribute --
}
percs [ k ] /= 10
}
}
2022-05-20 17:08:52 +03:00
// GetLanguageStats returns the language statistics for a repository
func GetLanguageStats ( ctx context . Context , repo * Repository ) ( LanguageStatList , error ) {
2020-02-11 12:34:17 +03:00
stats := make ( LanguageStatList , 0 , 6 )
2022-05-20 17:08:52 +03:00
if err := db . GetEngine ( ctx ) . Where ( "`repo_id` = ?" , repo . ID ) . Desc ( "`size`" ) . Find ( & stats ) ; err != nil {
2020-02-11 12:34:17 +03:00
return nil , err
}
return stats , nil
}
// GetTopLanguageStats returns the top language statistics for a repository
2023-10-11 07:24:07 +03:00
func GetTopLanguageStats ( ctx context . Context , repo * Repository , limit int ) ( LanguageStatList , error ) {
stats , err := GetLanguageStats ( ctx , repo )
2020-02-11 12:34:17 +03:00
if err != nil {
return nil , err
}
2020-05-30 10:46:15 +03:00
perc := stats . getLanguagePercentages ( )
2020-02-11 12:34:17 +03:00
topstats := make ( LanguageStatList , 0 , limit )
var other float32
for i := range stats {
2020-05-30 10:46:15 +03:00
if _ , ok := perc [ stats [ i ] . Language ] ; ! ok {
continue
}
2020-02-11 12:34:17 +03:00
if stats [ i ] . Language == "other" || len ( topstats ) >= limit {
2020-05-30 10:46:15 +03:00
other += perc [ stats [ i ] . Language ]
2020-02-11 12:34:17 +03:00
continue
}
2020-05-30 10:46:15 +03:00
stats [ i ] . Percentage = perc [ stats [ i ] . Language ]
2020-02-11 12:34:17 +03:00
topstats = append ( topstats , stats [ i ] )
}
if other > 0 {
topstats = append ( topstats , & LanguageStat {
RepoID : repo . ID ,
Language : "other" ,
Color : "#cccccc" ,
Percentage : float32 ( math . Round ( float64 ( other ) * 10 ) / 10 ) ,
} )
}
2021-12-10 04:27:50 +03:00
topstats . LoadAttributes ( )
2020-02-11 12:34:17 +03:00
return topstats , nil
}
// UpdateLanguageStats updates the language statistics for repository
2023-10-11 07:24:07 +03:00
func UpdateLanguageStats ( ctx context . Context , repo * Repository , commitID string , stats map [ string ] int64 ) error {
ctx , committer , err := db . TxContext ( ctx )
2021-11-21 18:41:00 +03:00
if err != nil {
2020-02-11 12:34:17 +03:00
return err
}
2021-11-21 18:41:00 +03:00
defer committer . Close ( )
sess := db . GetEngine ( ctx )
2020-02-11 12:34:17 +03:00
2022-05-20 17:08:52 +03:00
oldstats , err := GetLanguageStats ( ctx , repo )
2020-02-11 12:34:17 +03:00
if err != nil {
return err
}
var topLang string
2020-05-30 10:46:15 +03:00
var s int64
for lang , size := range stats {
if size > s {
s = size
2020-02-11 12:34:17 +03:00
topLang = strings . ToLower ( lang )
}
}
2020-05-30 10:46:15 +03:00
for lang , size := range stats {
2020-02-11 12:34:17 +03:00
upd := false
llang := strings . ToLower ( lang )
for _ , s := range oldstats {
// Update already existing language
if strings . ToLower ( s . Language ) == llang {
s . CommitID = commitID
s . IsPrimary = llang == topLang
2020-05-30 10:46:15 +03:00
s . Size = size
if _ , err := sess . ID ( s . ID ) . Cols ( "`commit_id`" , "`size`" , "`is_primary`" ) . Update ( s ) ; err != nil {
2020-02-11 12:34:17 +03:00
return err
}
upd = true
break
}
}
// Insert new language
if ! upd {
2022-05-20 17:08:52 +03:00
if err := db . Insert ( ctx , & LanguageStat {
2020-05-30 10:46:15 +03:00
RepoID : repo . ID ,
CommitID : commitID ,
IsPrimary : llang == topLang ,
Language : lang ,
Size : size ,
2020-02-11 12:34:17 +03:00
} ) ; err != nil {
return err
}
}
}
// Delete old languages
2020-02-14 15:42:30 +03:00
statsToDelete := make ( [ ] int64 , 0 , len ( oldstats ) )
for _ , s := range oldstats {
if s . CommitID != commitID {
statsToDelete = append ( statsToDelete , s . ID )
}
}
if len ( statsToDelete ) > 0 {
if _ , err := sess . In ( "`id`" , statsToDelete ) . Delete ( & LanguageStat { } ) ; err != nil {
return err
}
2020-02-11 12:34:17 +03:00
}
2020-02-14 15:42:30 +03:00
// Update indexer status
2022-05-20 17:08:52 +03:00
if err = UpdateIndexerStatus ( ctx , repo , RepoIndexerTypeStats , commitID ) ; err != nil {
2020-02-11 12:34:17 +03:00
return err
}
2021-11-21 18:41:00 +03:00
return committer . Commit ( )
2020-02-11 12:34:17 +03:00
}
2020-04-08 15:13:04 +03:00
// CopyLanguageStat Copy originalRepo language stat information to destRepo (use for forked repo)
2023-10-11 07:24:07 +03:00
func CopyLanguageStat ( ctx context . Context , originalRepo , destRepo * Repository ) error {
ctx , committer , err := db . TxContext ( ctx )
2021-11-21 18:41:00 +03:00
if err != nil {
2020-04-08 15:13:04 +03:00
return err
}
2021-11-21 18:41:00 +03:00
defer committer . Close ( )
2020-04-08 15:13:04 +03:00
RepoLang := make ( LanguageStatList , 0 , 6 )
2022-05-20 17:08:52 +03:00
if err := db . GetEngine ( ctx ) . Where ( "`repo_id` = ?" , originalRepo . ID ) . Desc ( "`size`" ) . Find ( & RepoLang ) ; err != nil {
2020-04-08 15:13:04 +03:00
return err
}
if len ( RepoLang ) > 0 {
for i := range RepoLang {
RepoLang [ i ] . ID = 0
RepoLang [ i ] . RepoID = destRepo . ID
RepoLang [ i ] . CreatedUnix = timeutil . TimeStampNow ( )
}
2021-03-14 21:52:12 +03:00
// update destRepo's indexer status
2020-04-08 15:13:04 +03:00
tmpCommitID := RepoLang [ 0 ] . CommitID
2022-05-20 17:08:52 +03:00
if err := UpdateIndexerStatus ( ctx , destRepo , RepoIndexerTypeStats , tmpCommitID ) ; err != nil {
2020-04-08 15:13:04 +03:00
return err
}
2022-05-20 17:08:52 +03:00
if err := db . Insert ( ctx , & RepoLang ) ; err != nil {
2020-04-08 15:13:04 +03:00
return err
}
}
2021-11-21 18:41:00 +03:00
return committer . Commit ( )
2020-04-08 15:13:04 +03:00
}