2020-12-17 17:00:47 +03:00
// Copyright 2020 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2020-12-17 17:00:47 +03:00
2021-08-24 19:47:09 +03:00
//go:build !gogit
2020-12-17 17:00:47 +03:00
package git
import (
2021-02-17 22:32:47 +03:00
"bufio"
2020-12-17 17:00:47 +03:00
"bytes"
"io"
2021-02-17 22:32:47 +03:00
"math"
2021-11-17 23:37:00 +03:00
"strings"
2020-12-17 17:00:47 +03:00
"code.gitea.io/gitea/modules/analyze"
2021-06-25 19:54:08 +03:00
"code.gitea.io/gitea/modules/log"
2020-12-17 17:00:47 +03:00
"github.com/go-enry/go-enry/v2"
)
// GetLanguageStats calculates language stats for git repository at specified commit
func ( repo * Repository ) GetLanguageStats ( commitID string ) ( map [ string ] int64 , error ) {
2021-02-17 22:32:47 +03:00
// We will feed the commit IDs in order into cat-file --batch, followed by blobs as necessary.
// so let's create a batch stdin and stdout
2021-11-30 23:06:32 +03:00
batchStdinWriter , batchReader , cancel := repo . CatFileBatch ( repo . Ctx )
2021-03-04 05:57:01 +03:00
defer cancel ( )
2021-02-17 22:32:47 +03:00
writeID := func ( id string ) error {
2021-06-17 01:16:47 +03:00
_ , err := batchStdinWriter . Write ( [ ] byte ( id + "\n" ) )
2021-02-17 22:32:47 +03:00
return err
}
if err := writeID ( commitID ) ; err != nil {
return nil , err
}
shaBytes , typ , size , err := ReadBatchLine ( batchReader )
if typ != "commit" {
2021-06-25 19:54:08 +03:00
log . Debug ( "Unable to get commit for: %s. Err: %v" , commitID , err )
2021-02-17 22:32:47 +03:00
return nil , ErrNotExist { commitID , "" }
}
2023-12-19 10:20:47 +03:00
sha , err := NewIDFromString ( string ( shaBytes ) )
2020-12-17 17:00:47 +03:00
if err != nil {
2021-06-25 19:54:08 +03:00
log . Debug ( "Unable to get commit for: %s. Err: %v" , commitID , err )
2021-02-17 22:32:47 +03:00
return nil , ErrNotExist { commitID , "" }
}
commit , err := CommitFromReader ( repo , sha , io . LimitReader ( batchReader , size ) )
if err != nil {
2021-06-25 19:54:08 +03:00
log . Debug ( "Unable to get commit for: %s. Err: %v" , commitID , err )
2020-12-17 17:00:47 +03:00
return nil , err
}
2021-06-21 01:00:46 +03:00
if _ , err = batchReader . Discard ( 1 ) ; err != nil {
return nil , err
}
2020-12-17 17:00:47 +03:00
tree := commit . Tree
2022-10-07 20:20:53 +03:00
entries , err := tree . ListEntriesRecursiveWithSize ( )
2020-12-17 17:00:47 +03:00
if err != nil {
return nil , err
}
2022-06-16 18:47:44 +03:00
checker , deferable := repo . CheckAttributeReader ( commitID )
defer deferable ( )
2021-09-09 23:13:36 +03:00
2021-02-17 22:32:47 +03:00
contentBuf := bytes . Buffer { }
var content [ ] byte
2022-10-29 10:04:21 +03:00
// sizes contains the current calculated size of all files by language
2020-12-17 17:00:47 +03:00
sizes := make ( map [ string ] int64 )
2022-10-29 10:04:21 +03:00
// by default we will only count the sizes of programming languages or markup languages
// unless they are explicitly set using linguist-language
includedLanguage := map [ string ] bool { }
// or if there's only one language in the repository
firstExcludedLanguage := ""
firstExcludedLanguageSize := int64 ( 0 )
2020-12-17 17:00:47 +03:00
for _ , f := range entries {
2021-11-30 23:06:32 +03:00
select {
case <- repo . Ctx . Done ( ) :
return sizes , repo . Ctx . Err ( )
default :
}
2021-02-17 22:32:47 +03:00
contentBuf . Reset ( )
content = contentBuf . Bytes ( )
2021-09-09 23:13:36 +03:00
if f . Size ( ) == 0 {
continue
}
notVendored := false
notGenerated := false
if checker != nil {
attrs , err := checker . CheckPath ( f . Name ( ) )
if err == nil {
if vendored , has := attrs [ "linguist-vendored" ] ; has {
if vendored == "set" || vendored == "true" {
continue
}
notVendored = vendored == "false"
}
if generated , has := attrs [ "linguist-generated" ] ; has {
if generated == "set" || generated == "true" {
continue
}
notGenerated = generated == "false"
}
if language , has := attrs [ "linguist-language" ] ; has && language != "unspecified" && language != "" {
// group languages, such as Pug -> HTML; SCSS -> CSS
group := enry . GetLanguageGroup ( language )
2021-09-20 22:46:51 +03:00
if len ( group ) != 0 {
2021-09-09 23:13:36 +03:00
language = group
}
2022-10-29 10:04:21 +03:00
// this language will always be added to the size
2021-09-09 23:13:36 +03:00
sizes [ language ] += f . Size ( )
continue
2021-11-17 23:37:00 +03:00
} else if language , has := attrs [ "gitlab-language" ] ; has && language != "unspecified" && language != "" {
// strip off a ? if present
if idx := strings . IndexByte ( language , '?' ) ; idx >= 0 {
language = language [ : idx ]
}
if len ( language ) != 0 {
// group languages, such as Pug -> HTML; SCSS -> CSS
group := enry . GetLanguageGroup ( language )
if len ( group ) != 0 {
language = group
}
2022-10-29 10:04:21 +03:00
// this language will always be added to the size
2021-11-17 23:37:00 +03:00
sizes [ language ] += f . Size ( )
continue
}
2021-09-09 23:13:36 +03:00
}
2021-11-17 23:37:00 +03:00
2021-09-09 23:13:36 +03:00
}
}
if ( ! notVendored && analyze . IsVendor ( f . Name ( ) ) ) || enry . IsDotFile ( f . Name ( ) ) ||
2020-12-17 17:00:47 +03:00
enry . IsDocumentation ( f . Name ( ) ) || enry . IsConfiguration ( f . Name ( ) ) {
continue
}
// If content can not be read or file is too big just do detection by filename
2021-02-17 22:32:47 +03:00
2020-12-17 17:00:47 +03:00
if f . Size ( ) <= bigFileSize {
2021-02-17 22:32:47 +03:00
if err := writeID ( f . ID . String ( ) ) ; err != nil {
return nil , err
}
_ , _ , size , err := ReadBatchLine ( batchReader )
if err != nil {
2021-06-25 19:54:08 +03:00
log . Debug ( "Error reading blob: %s Err: %v" , f . ID . String ( ) , err )
2021-02-17 22:32:47 +03:00
return nil , err
}
sizeToRead := size
2021-06-17 01:16:47 +03:00
discard := int64 ( 1 )
2021-02-17 22:32:47 +03:00
if size > fileSizeLimit {
sizeToRead = fileSizeLimit
2021-06-17 01:16:47 +03:00
discard = size - fileSizeLimit + 1
2021-02-17 22:32:47 +03:00
}
_ , err = contentBuf . ReadFrom ( io . LimitReader ( batchReader , sizeToRead ) )
if err != nil {
return nil , err
}
content = contentBuf . Bytes ( )
err = discardFull ( batchReader , discard )
if err != nil {
return nil , err
}
2020-12-17 17:00:47 +03:00
}
2021-09-09 23:13:36 +03:00
if ! notGenerated && enry . IsGenerated ( f . Name ( ) , content ) {
2020-12-17 17:00:47 +03:00
continue
}
// FIXME: Why can't we split this and the IsGenerated tests to avoid reading the blob unless absolutely necessary?
// - eg. do the all the detection tests using filename first before reading content.
language := analyze . GetCodeLanguage ( f . Name ( ) , content )
2023-05-24 22:37:36 +03:00
if language == "" {
2020-12-17 17:00:47 +03:00
continue
}
// group languages, such as Pug -> HTML; SCSS -> CSS
group := enry . GetLanguageGroup ( language )
if group != "" {
language = group
}
2022-10-29 10:04:21 +03:00
included , checked := includedLanguage [ language ]
if ! checked {
2023-05-24 22:37:36 +03:00
langType := enry . GetLanguageType ( language )
included = langType == enry . Programming || langType == enry . Markup
2022-10-29 10:04:21 +03:00
includedLanguage [ language ] = included
}
if included {
sizes [ language ] += f . Size ( )
} else if len ( sizes ) == 0 && ( firstExcludedLanguage == "" || firstExcludedLanguage == language ) {
firstExcludedLanguage = language
firstExcludedLanguageSize += f . Size ( )
}
2020-12-17 17:00:47 +03:00
continue
}
2022-10-29 10:04:21 +03:00
// If there are no included languages add the first excluded language
if len ( sizes ) == 0 && firstExcludedLanguage != "" {
sizes [ firstExcludedLanguage ] = firstExcludedLanguageSize
2020-12-17 17:00:47 +03:00
}
2023-05-24 22:37:36 +03:00
return mergeLanguageStats ( sizes ) , nil
2020-12-17 17:00:47 +03:00
}
2021-02-17 22:32:47 +03:00
func discardFull ( rd * bufio . Reader , discard int64 ) error {
if discard > math . MaxInt32 {
n , err := rd . Discard ( math . MaxInt32 )
discard -= int64 ( n )
if err != nil {
return err
}
2020-12-17 17:00:47 +03:00
}
2021-02-17 22:32:47 +03:00
for discard > 0 {
n , err := rd . Discard ( int ( discard ) )
discard -= int64 ( n )
if err != nil {
return err
}
2020-12-17 17:00:47 +03:00
}
2021-02-17 22:32:47 +03:00
return nil
2020-12-17 17:00:47 +03:00
}