Language statistics bar for repositories (#8037)
* Implementation for calculating language statistics Impement saving code language statistics to database Implement rendering langauge stats Add primary laguage to show in repository list Implement repository stats indexer queue Add indexer test Refactor to use queue module * Do not timeout for queues
This commit is contained in:
1
go.mod
1
go.mod
@ -85,6 +85,7 @@ require (
|
||||
github.com/sergi/go-diff v1.0.0
|
||||
github.com/shurcooL/httpfs v0.0.0-20190527155220-6a4d4a70508b // indirect
|
||||
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd
|
||||
github.com/src-d/enry/v2 v2.1.0
|
||||
github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2 // indirect
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/tecbot/gorocksdb v0.0.0-20181010114359-8752a9433481 // indirect
|
||||
|
9
go.sum
9
go.sum
@ -508,8 +508,13 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/src-d/enry v1.7.3 h1:jG2fmEaQaURh0qqU/sn82BRzVa6d4EVHJIw6gc98bak=
|
||||
github.com/src-d/enry/v2 v2.1.0 h1:z1L8t+B8bh3mmjPkJrgOTnVRpFGmTPJsplHX9wAn6BI=
|
||||
github.com/src-d/enry/v2 v2.1.0/go.mod h1:qQeCMRwzMF3ckeGr+h0tJLdxXnq+NVZsIDMELj0t028=
|
||||
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
|
||||
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||
github.com/src-d/go-oniguruma v1.1.0 h1:EG+Nm5n2JqWUaCjtM0NtutPxU7ZN5Tp50GWrrV8bTww=
|
||||
github.com/src-d/go-oniguruma v1.1.0/go.mod h1:chVbff8kcVtmrhxtZ3yBVLLquXbzCS6DrxQaAK/CeqM=
|
||||
github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2 h1:JNEGSiWg6D3lcBCMCBqN3ELniXujt+0QNHLhNnO0w3s=
|
||||
github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2/go.mod h1:mjqs7N0Q6m5HpR7QfXVBZXZWSqTjQLeTujjA/xUp2uw=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@ -530,6 +535,8 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV
|
||||
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
|
||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/toqueteos/trie v1.0.0 h1:8i6pXxNUXNRAqP246iibb7w/pSFquNTQ+uNfriG7vlk=
|
||||
github.com/toqueteos/trie v1.0.0/go.mod h1:Ywk48QhEqhU1+DwhMkJ2x7eeGxDHiGkAdc9+0DYcbsM=
|
||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||
github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ=
|
||||
@ -747,6 +754,8 @@ gopkg.in/testfixtures.v2 v2.5.0 h1:N08B7l2GzFQenyYbzqthDnKAA+cmb17iAZhhFxr7JHw=
|
||||
gopkg.in/testfixtures.v2 v2.5.0/go.mod h1:vyAq+MYCgNpR29qitQdLZhdbLFf4mR/2MFJRFoQZZ2M=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/toqueteos/substring.v1 v1.0.2 h1:urLqCeMm6x/eTuQa1oZerNw8N1KNOIp5hD5kGL7lFsE=
|
||||
gopkg.in/toqueteos/substring.v1 v1.0.2/go.mod h1:Eb2Z1UYehlVK8LYW2WBVR2rwbujsz3aX8XDrM1vbNew=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
|
@ -13,7 +13,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
@ -101,7 +100,7 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() {
|
||||
}
|
||||
writerCloser.setT(&t)
|
||||
return func() {
|
||||
if err := queue.GetManager().FlushAll(context.Background(), 20*time.Second); err != nil {
|
||||
if err := queue.GetManager().FlushAll(context.Background(), -1); err != nil {
|
||||
t.Errorf("Flushing queues failed with error %v", err)
|
||||
}
|
||||
_ = writerCloser.Close()
|
||||
|
@ -186,6 +186,8 @@ var migrations = []Migration{
|
||||
NewMigration("Add some columns on review for migration", addReviewMigrateInfo),
|
||||
// v126 -> v127
|
||||
NewMigration("Fix topic repository count", fixTopicRepositoryCount),
|
||||
// v127 -> v128
|
||||
NewMigration("add repository code language statistics", addLanguageStats),
|
||||
}
|
||||
|
||||
// Migrate database to current version
|
||||
|
45
models/migrations/v127.go
Normal file
45
models/migrations/v127.go
Normal file
@ -0,0 +1,45 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addLanguageStats(x *xorm.Engine) error {
|
||||
// LanguageStat see models/repo_language_stats.go
|
||||
type LanguageStat struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
|
||||
CommitID string
|
||||
IsPrimary bool
|
||||
Language string `xorm:"VARCHAR(30) UNIQUE(s) INDEX NOT NULL"`
|
||||
Percentage float32 `xorm:"NUMERIC(5,2) NOT NULL DEFAULT 0"`
|
||||
Color string `xorm:"-"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
}
|
||||
|
||||
type RepoIndexerType int
|
||||
|
||||
// RepoIndexerStatus see models/repo_stats_indexer.go
|
||||
type RepoIndexerStatus struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX(s)"`
|
||||
CommitSha string `xorm:"VARCHAR(40)"`
|
||||
IndexerType RepoIndexerType `xorm:"INDEX(s) NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
if err := x.Sync2(new(LanguageStat)); err != nil {
|
||||
return fmt.Errorf("Sync2: %v", err)
|
||||
}
|
||||
if err := x.Sync2(new(RepoIndexerStatus)); err != nil {
|
||||
return fmt.Errorf("Sync2: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -116,6 +116,7 @@ func init() {
|
||||
new(OAuth2AuthorizationCode),
|
||||
new(OAuth2Grant),
|
||||
new(Task),
|
||||
new(LanguageStat),
|
||||
)
|
||||
|
||||
gonicNames := []string{"SSL", "UID"}
|
||||
|
@ -177,6 +177,7 @@ type Repository struct {
|
||||
|
||||
RenderingMetas map[string]string `xorm:"-"`
|
||||
Units []*RepoUnit `xorm:"-"`
|
||||
PrimaryLanguage *LanguageStat `xorm:"-"`
|
||||
|
||||
IsFork bool `xorm:"INDEX NOT NULL DEFAULT false"`
|
||||
ForkID int64 `xorm:"INDEX"`
|
||||
@ -185,7 +186,8 @@ type Repository struct {
|
||||
TemplateID int64 `xorm:"INDEX"`
|
||||
TemplateRepo *Repository `xorm:"-"`
|
||||
Size int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
IndexerStatus *RepoIndexerStatus `xorm:"-"`
|
||||
CodeIndexerStatus *RepoIndexerStatus `xorm:"-"`
|
||||
StatsIndexerStatus *RepoIndexerStatus `xorm:"-"`
|
||||
IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"`
|
||||
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
|
||||
Topics []string `xorm:"TEXT JSON"`
|
||||
@ -1504,6 +1506,7 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
|
||||
&Notification{RepoID: repoID},
|
||||
&CommitStatus{RepoID: repoID},
|
||||
&RepoIndexerStatus{RepoID: repoID},
|
||||
&LanguageStat{RepoID: repoID},
|
||||
&Comment{RefRepoID: repoID},
|
||||
&Task{RepoID: repoID},
|
||||
); err != nil {
|
||||
|
@ -10,21 +10,32 @@ import (
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// RepoIndexerType specifies the repository indexer type
|
||||
type RepoIndexerType int
|
||||
|
||||
const (
|
||||
// RepoIndexerTypeCode code indexer
|
||||
RepoIndexerTypeCode RepoIndexerType = iota // 0
|
||||
// RepoIndexerTypeStats repository stats indexer
|
||||
RepoIndexerTypeStats // 1
|
||||
)
|
||||
|
||||
// RepoIndexerStatus status of a repo's entry in the repo indexer
|
||||
// For now, implicitly refers to default branch
|
||||
type RepoIndexerStatus struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
RepoID int64 `xorm:"INDEX(s)"`
|
||||
CommitSha string `xorm:"VARCHAR(40)"`
|
||||
IndexerType RepoIndexerType `xorm:"INDEX(s) NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
// GetUnindexedRepos returns repos which do not have an indexer status
|
||||
func GetUnindexedRepos(maxRepoID int64, page, pageSize int) ([]int64, error) {
|
||||
func GetUnindexedRepos(indexerType RepoIndexerType, maxRepoID int64, page, pageSize int) ([]int64, error) {
|
||||
ids := make([]int64, 0, 50)
|
||||
cond := builder.Cond(builder.IsNull{
|
||||
"repo_indexer_status.id",
|
||||
})
|
||||
sess := x.Table("repository").Join("LEFT OUTER", "repo_indexer_status", "repository.id = repo_indexer_status.repo_id")
|
||||
sess := x.Table("repository").Join("LEFT OUTER", "repo_indexer_status", "repository.id = repo_indexer_status.repo_id AND repo_indexer_status.indexer_type = ?", indexerType)
|
||||
if maxRepoID > 0 {
|
||||
cond = builder.And(cond, builder.Lte{
|
||||
"repository.id": maxRepoID,
|
||||
@ -43,40 +54,64 @@ func GetUnindexedRepos(maxRepoID int64, page, pageSize int) ([]int64, error) {
|
||||
return ids, err
|
||||
}
|
||||
|
||||
// GetIndexerStatus loads repo codes indxer status
|
||||
func (repo *Repository) GetIndexerStatus() error {
|
||||
if repo.IndexerStatus != nil {
|
||||
return nil
|
||||
// getIndexerStatus loads repo codes indxer status
|
||||
func (repo *Repository) getIndexerStatus(e Engine, indexerType RepoIndexerType) (*RepoIndexerStatus, error) {
|
||||
switch indexerType {
|
||||
case RepoIndexerTypeCode:
|
||||
if repo.CodeIndexerStatus != nil {
|
||||
return repo.CodeIndexerStatus, nil
|
||||
}
|
||||
status := &RepoIndexerStatus{RepoID: repo.ID}
|
||||
has, err := x.Get(status)
|
||||
case RepoIndexerTypeStats:
|
||||
if repo.StatsIndexerStatus != nil {
|
||||
return repo.StatsIndexerStatus, nil
|
||||
}
|
||||
}
|
||||
status := &RepoIndexerStatus{RepoID: repo.ID, IndexerType: indexerType}
|
||||
has, err := e.Get(status)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
} else if !has {
|
||||
status.CommitSha = ""
|
||||
}
|
||||
repo.IndexerStatus = status
|
||||
return nil
|
||||
switch indexerType {
|
||||
case RepoIndexerTypeCode:
|
||||
repo.CodeIndexerStatus = status
|
||||
case RepoIndexerTypeStats:
|
||||
repo.StatsIndexerStatus = status
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// UpdateIndexerStatus updates indexer status
|
||||
func (repo *Repository) UpdateIndexerStatus(sha string) error {
|
||||
if err := repo.GetIndexerStatus(); err != nil {
|
||||
// GetIndexerStatus loads repo codes indxer status
|
||||
func (repo *Repository) GetIndexerStatus(indexerType RepoIndexerType) (*RepoIndexerStatus, error) {
|
||||
return repo.getIndexerStatus(x, indexerType)
|
||||
}
|
||||
|
||||
// updateIndexerStatus updates indexer status
|
||||
func (repo *Repository) updateIndexerStatus(e Engine, indexerType RepoIndexerType, sha string) error {
|
||||
status, err := repo.getIndexerStatus(e, indexerType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateIndexerStatus: Unable to getIndexerStatus for repo: %s Error: %v", repo.FullName(), err)
|
||||
}
|
||||
if len(repo.IndexerStatus.CommitSha) == 0 {
|
||||
repo.IndexerStatus.CommitSha = sha
|
||||
_, err := x.Insert(repo.IndexerStatus)
|
||||
|
||||
if len(status.CommitSha) == 0 {
|
||||
status.CommitSha = sha
|
||||
_, err := e.Insert(status)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateIndexerStatus: Unable to insert repoIndexerStatus for repo: %s Sha: %s Error: %v", repo.FullName(), sha, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
repo.IndexerStatus.CommitSha = sha
|
||||
_, err := x.ID(repo.IndexerStatus.ID).Cols("commit_sha").
|
||||
Update(repo.IndexerStatus)
|
||||
status.CommitSha = sha
|
||||
_, err = e.ID(status.ID).Cols("commit_sha").
|
||||
Update(status)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateIndexerStatus: Unable to update repoIndexerStatus for repo: %s Sha: %s Error: %v", repo.FullName(), sha, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateIndexerStatus updates indexer status
|
||||
func (repo *Repository) UpdateIndexerStatus(indexerType RepoIndexerType, sha string) error {
|
||||
return repo.updateIndexerStatus(x, indexerType, sha)
|
||||
}
|
||||
|
137
models/repo_language_stats.go
Normal file
137
models/repo_language_stats.go
Normal file
@ -0,0 +1,137 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/src-d/enry/v2"
|
||||
)
|
||||
|
||||
// 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
|
||||
Language string `xorm:"VARCHAR(30) UNIQUE(s) INDEX NOT NULL"`
|
||||
Percentage float32 `xorm:"NUMERIC(5,2) NOT NULL DEFAULT 0"`
|
||||
Color string `xorm:"-"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
}
|
||||
|
||||
// LanguageStatList defines a list of language statistics
|
||||
type LanguageStatList []*LanguageStat
|
||||
|
||||
func (stats LanguageStatList) loadAttributes() {
|
||||
for i := range stats {
|
||||
stats[i].Color = enry.GetColor(stats[i].Language)
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *Repository) getLanguageStats(e Engine) (LanguageStatList, error) {
|
||||
stats := make(LanguageStatList, 0, 6)
|
||||
if err := e.Where("`repo_id` = ?", repo.ID).Desc("`percentage`").Find(&stats); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.loadAttributes()
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetLanguageStats returns the language statistics for a repository
|
||||
func (repo *Repository) GetLanguageStats() (LanguageStatList, error) {
|
||||
return repo.getLanguageStats(x)
|
||||
}
|
||||
|
||||
// GetTopLanguageStats returns the top language statistics for a repository
|
||||
func (repo *Repository) GetTopLanguageStats(limit int) (LanguageStatList, error) {
|
||||
stats, err := repo.getLanguageStats(x)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
topstats := make(LanguageStatList, 0, limit)
|
||||
var other float32
|
||||
for i := range stats {
|
||||
if stats[i].Language == "other" || len(topstats) >= limit {
|
||||
other += stats[i].Percentage
|
||||
continue
|
||||
}
|
||||
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),
|
||||
})
|
||||
}
|
||||
return topstats, nil
|
||||
}
|
||||
|
||||
// UpdateLanguageStats updates the language statistics for repository
|
||||
func (repo *Repository) UpdateLanguageStats(commitID string, stats map[string]float32) error {
|
||||
sess := x.NewSession()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer sess.Close()
|
||||
|
||||
oldstats, err := repo.getLanguageStats(sess)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var topLang string
|
||||
var p float32
|
||||
for lang, perc := range stats {
|
||||
if perc > p {
|
||||
p = perc
|
||||
topLang = strings.ToLower(lang)
|
||||
}
|
||||
}
|
||||
|
||||
for lang, perc := range stats {
|
||||
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
|
||||
s.Percentage = perc
|
||||
if _, err := sess.ID(s.ID).Cols("`commit_id`", "`percentage`", "`is_primary`").Update(s); err != nil {
|
||||
return err
|
||||
}
|
||||
upd = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// Insert new language
|
||||
if !upd {
|
||||
if _, err := sess.Insert(&LanguageStat{
|
||||
RepoID: repo.ID,
|
||||
CommitID: commitID,
|
||||
IsPrimary: llang == topLang,
|
||||
Language: lang,
|
||||
Percentage: perc,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
// Delete old languages
|
||||
if _, err := sess.Where("`id` IN (SELECT `id` FROM `language_stat` WHERE `repo_id` = ? AND `commit_id` != ?)", repo.ID, commitID).Delete(&LanguageStat{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = repo.updateIndexerStatus(sess, RepoIndexerTypeStats, commitID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
@ -46,11 +46,14 @@ func (repos RepositoryList) loadAttributes(e Engine) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load owners.
|
||||
set := make(map[int64]struct{})
|
||||
repoIDs := make([]int64, len(repos))
|
||||
for i := range repos {
|
||||
set[repos[i].OwnerID] = struct{}{}
|
||||
repoIDs[i] = repos[i].ID
|
||||
}
|
||||
|
||||
// Load owners.
|
||||
users := make(map[int64]*User, len(set))
|
||||
if err := e.
|
||||
Where("id > 0").
|
||||
@ -61,6 +64,25 @@ func (repos RepositoryList) loadAttributes(e Engine) error {
|
||||
for i := range repos {
|
||||
repos[i].Owner = users[repos[i].OwnerID]
|
||||
}
|
||||
|
||||
// Load primary language.
|
||||
stats := make(LanguageStatList, 0, len(repos))
|
||||
if err := e.
|
||||
Where("`is_primary` = ? AND `language` != ?", true, "other").
|
||||
In("`repo_id`", repoIDs).
|
||||
Find(&stats); err != nil {
|
||||
return fmt.Errorf("find primary languages: %v", err)
|
||||
}
|
||||
stats.loadAttributes()
|
||||
for i := range repos {
|
||||
for _, st := range stats {
|
||||
if st.RepoID == repos[i].ID {
|
||||
repos[i].PrimaryLanguage = st
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -119,7 +141,6 @@ type SearchRepoOptions struct {
|
||||
OrderBy SearchOrderBy
|
||||
Private bool // Include private repositories in results
|
||||
StarredByID int64
|
||||
IsProfile bool
|
||||
AllPublic bool // Include also all public repositories of users and public organisations
|
||||
AllLimited bool // Include also all public repositories of limited organisations
|
||||
// None -> include collaborative AND non-collaborative
|
||||
@ -306,11 +327,9 @@ func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) {
|
||||
return nil, 0, fmt.Errorf("Repo: %v", err)
|
||||
}
|
||||
|
||||
if !opts.IsProfile {
|
||||
if err = repos.loadAttributes(sess); err != nil {
|
||||
return nil, 0, fmt.Errorf("LoadAttributes: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return repos, count, nil
|
||||
}
|
||||
|
116
modules/git/repo_language_stats.go
Normal file
116
modules/git/repo_language_stats.go
Normal file
@ -0,0 +1,116 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/src-d/enry/v2"
|
||||
"gopkg.in/src-d/go-git.v4"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
||||
)
|
||||
|
||||
const fileSizeLimit int64 = 16 * 1024 * 1024
|
||||
|
||||
// GetLanguageStats calculates language stats for git repository at specified commit
|
||||
func (repo *Repository) GetLanguageStats(commitID string) (map[string]float32, error) {
|
||||
r, err := git.PlainOpen(repo.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rev, err := r.ResolveRevision(plumbing.Revision(commitID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commit, err := r.CommitObject(*rev)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tree, err := commit.Tree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sizes := make(map[string]int64)
|
||||
var total int64
|
||||
err = tree.Files().ForEach(func(f *object.File) error {
|
||||
if enry.IsVendor(f.Name) || enry.IsDotFile(f.Name) ||
|
||||
enry.IsDocumentation(f.Name) || enry.IsConfiguration(f.Name) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Use .gitattributes file for linguist overrides
|
||||
|
||||
language, ok := enry.GetLanguageByExtension(f.Name)
|
||||
if !ok {
|
||||
if language, ok = enry.GetLanguageByFilename(f.Name); !ok {
|
||||
content, err := readFile(f, fileSizeLimit)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
language = enry.GetLanguage(filepath.Base(f.Name), content)
|
||||
if language == enry.OtherLanguage {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if language != "" {
|
||||
sizes[language] += f.Size
|
||||
total += f.Size
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats := make(map[string]float32)
|
||||
var otherPerc float32 = 100
|
||||
for language, size := range sizes {
|
||||
perc := float32(math.Round(float64(size)/float64(total)*1000) / 10)
|
||||
if perc <= 0.1 {
|
||||
continue
|
||||
}
|
||||
otherPerc -= perc
|
||||
stats[language] = perc
|
||||
}
|
||||
otherPerc = float32(math.Round(float64(otherPerc)*10) / 10)
|
||||
if otherPerc > 0 {
|
||||
stats["other"] = otherPerc
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func readFile(f *object.File, limit int64) ([]byte, error) {
|
||||
r, err := f.Reader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
if limit <= 0 {
|
||||
return ioutil.ReadAll(r)
|
||||
}
|
||||
|
||||
size := f.Size
|
||||
if limit > 0 && size > limit {
|
||||
size = limit
|
||||
}
|
||||
buf := bytes.NewBuffer(nil)
|
||||
buf.Grow(int(size))
|
||||
_, err = io.Copy(buf, io.LimitReader(r, limit))
|
||||
return buf.Bytes(), err
|
||||
}
|
@ -267,7 +267,7 @@ func (b *BleveIndexer) Index(repoID int64) error {
|
||||
if err = batch.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
return repo.UpdateIndexerStatus(sha)
|
||||
return repo.UpdateIndexerStatus(models.RepoIndexerTypeCode, sha)
|
||||
}
|
||||
|
||||
// Delete deletes indexes by ids
|
||||
|
@ -35,11 +35,12 @@ func getDefaultBranchSha(repo *models.Repository) (string, error) {
|
||||
|
||||
// getRepoChanges returns changes to repo since last indexer update
|
||||
func getRepoChanges(repo *models.Repository, revision string) (*repoChanges, error) {
|
||||
if err := repo.GetIndexerStatus(); err != nil {
|
||||
status, err := repo.GetIndexerStatus(models.RepoIndexerTypeCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(repo.IndexerStatus.CommitSha) == 0 {
|
||||
if len(status.CommitSha) == 0 {
|
||||
return genesisChanges(repo, revision)
|
||||
}
|
||||
return nonGenesisChanges(repo, revision)
|
||||
@ -98,7 +99,7 @@ func genesisChanges(repo *models.Repository, revision string) (*repoChanges, err
|
||||
// nonGenesisChanges get changes since the previous indexer update
|
||||
func nonGenesisChanges(repo *models.Repository, revision string) (*repoChanges, error) {
|
||||
diffCmd := git.NewCommand("diff", "--name-status",
|
||||
repo.IndexerStatus.CommitSha, revision)
|
||||
repo.CodeIndexerStatus.CommitSha, revision)
|
||||
stdout, err := diffCmd.RunInDir(repo.RepoPath())
|
||||
if err != nil {
|
||||
// previous commit sha may have been removed by a force push, so
|
||||
|
@ -109,7 +109,7 @@ func populateRepoIndexer() {
|
||||
return
|
||||
default:
|
||||
}
|
||||
ids, err := models.GetUnindexedRepos(maxRepoID, 0, 50)
|
||||
ids, err := models.GetUnindexedRepos(models.RepoIndexerTypeCode, maxRepoID, 0, 50)
|
||||
if err != nil {
|
||||
log.Error("populateRepoIndexer: %v", err)
|
||||
return
|
||||
|
54
modules/indexer/stats/db.go
Normal file
54
modules/indexer/stats/db.go
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
)
|
||||
|
||||
// DBIndexer implements Indexer interface to use database's like search
|
||||
type DBIndexer struct {
|
||||
}
|
||||
|
||||
// Index repository status function
|
||||
func (db *DBIndexer) Index(id int64) error {
|
||||
repo, err := models.GetRepositoryByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status, err := repo.GetIndexerStatus(models.RepoIndexerTypeStats)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gitRepo, err := git.OpenRepository(repo.RepoPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
// Get latest commit for default branch
|
||||
commitID, err := gitRepo.GetBranchCommitID(repo.DefaultBranch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Do not recalculate stats if already calculated for this commit
|
||||
if status.CommitSha == commitID {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Calculate and save language statistics to database
|
||||
stats, err := gitRepo.GetLanguageStats(commitID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return repo.UpdateLanguageStats(commitID, stats)
|
||||
}
|
||||
|
||||
// Close dummy function
|
||||
func (db *DBIndexer) Close() {
|
||||
}
|
85
modules/indexer/stats/indexer.go
Normal file
85
modules/indexer/stats/indexer.go
Normal file
@ -0,0 +1,85 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// Indexer defines an interface to index repository stats
|
||||
type Indexer interface {
|
||||
Index(id int64) error
|
||||
Close()
|
||||
}
|
||||
|
||||
// indexer represents a indexer instance
|
||||
var indexer Indexer
|
||||
|
||||
// Init initialize the repo indexer
|
||||
func Init() error {
|
||||
indexer = &DBIndexer{}
|
||||
|
||||
if err := initStatsQueue(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go populateRepoIndexer()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// populateRepoIndexer populate the repo indexer with pre-existing data. This
|
||||
// should only be run when the indexer is created for the first time.
|
||||
func populateRepoIndexer() {
|
||||
log.Info("Populating the repo stats indexer with existing repositories")
|
||||
|
||||
isShutdown := graceful.GetManager().IsShutdown()
|
||||
|
||||
exist, err := models.IsTableNotEmpty("repository")
|
||||
if err != nil {
|
||||
log.Fatal("System error: %v", err)
|
||||
} else if !exist {
|
||||
return
|
||||
}
|
||||
|
||||
var maxRepoID int64
|
||||
if maxRepoID, err = models.GetMaxID("repository"); err != nil {
|
||||
log.Fatal("System error: %v", err)
|
||||
}
|
||||
|
||||
// start with the maximum existing repo ID and work backwards, so that we
|
||||
// don't include repos that are created after gitea starts; such repos will
|
||||
// already be added to the indexer, and we don't need to add them again.
|
||||
for maxRepoID > 0 {
|
||||
select {
|
||||
case <-isShutdown:
|
||||
log.Info("Repository Stats Indexer population shutdown before completion")
|
||||
return
|
||||
default:
|
||||
}
|
||||
ids, err := models.GetUnindexedRepos(models.RepoIndexerTypeStats, maxRepoID, 0, 50)
|
||||
if err != nil {
|
||||
log.Error("populateRepoIndexer: %v", err)
|
||||
return
|
||||
} else if len(ids) == 0 {
|
||||
break
|
||||
}
|
||||
for _, id := range ids {
|
||||
select {
|
||||
case <-isShutdown:
|
||||
log.Info("Repository Stats Indexer population shutdown before completion")
|
||||
return
|
||||
default:
|
||||
}
|
||||
if err := statsQueue.Push(id); err != nil {
|
||||
log.Error("statsQueue.Push: %v", err)
|
||||
}
|
||||
maxRepoID = id - 1
|
||||
}
|
||||
}
|
||||
log.Info("Done (re)populating the repo stats indexer with existing repositories")
|
||||
}
|
42
modules/indexer/stats/indexer_test.go
Normal file
42
modules/indexer/stats/indexer_test.go
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
models.MainTest(m, filepath.Join("..", "..", ".."))
|
||||
}
|
||||
|
||||
func TestRepoStatsIndex(t *testing.T) {
|
||||
assert.NoError(t, models.PrepareTestDatabase())
|
||||
setting.Cfg = ini.Empty()
|
||||
|
||||
setting.NewQueueService()
|
||||
|
||||
err := Init()
|
||||
assert.NoError(t, err)
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
repo, err := models.GetRepositoryByID(1)
|
||||
assert.NoError(t, err)
|
||||
langs, err := repo.GetTopLanguageStats(5)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, langs, 1)
|
||||
assert.Equal(t, "other", langs[0].Language)
|
||||
assert.Equal(t, float32(100), langs[0].Percentage)
|
||||
}
|
43
modules/indexer/stats/queue.go
Normal file
43
modules/indexer/stats/queue.go
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
)
|
||||
|
||||
// statsQueue represents a queue to handle repository stats updates
|
||||
var statsQueue queue.Queue
|
||||
|
||||
// handle passed PR IDs and test the PRs
|
||||
func handle(data ...queue.Data) {
|
||||
for _, datum := range data {
|
||||
opts := datum.(int64)
|
||||
if err := indexer.Index(opts); err != nil {
|
||||
log.Error("stats queue idexer.Index(%d) failed: %v", opts, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initStatsQueue() error {
|
||||
statsQueue = queue.CreateQueue("repo_stats_update", handle, int64(0)).(queue.Queue)
|
||||
if statsQueue == nil {
|
||||
return fmt.Errorf("Unable to create repo_stats_update Queue")
|
||||
}
|
||||
|
||||
go graceful.GetManager().RunWithShutdownFns(statsQueue.Run)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRepoIndexer update a repository's entries in the indexer
|
||||
func UpdateRepoIndexer(repo *models.Repository) error {
|
||||
return statsQueue.Push(repo.ID)
|
||||
}
|
@ -9,6 +9,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
code_indexer "code.gitea.io/gitea/modules/indexer/code"
|
||||
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
|
||||
stats_indexer "code.gitea.io/gitea/modules/indexer/stats"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/notification/base"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
@ -117,12 +118,18 @@ func (r *indexerNotifier) NotifyMigrateRepository(doer *models.User, u *models.U
|
||||
if setting.Indexer.RepoIndexerEnabled && !repo.IsEmpty {
|
||||
code_indexer.UpdateRepoIndexer(repo)
|
||||
}
|
||||
if err := stats_indexer.UpdateRepoIndexer(repo); err != nil {
|
||||
log.Error("stats_indexer.UpdateRepoIndexer(%d) failed: %v", repo.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *indexerNotifier) NotifyPushCommits(pusher *models.User, repo *models.Repository, refName, oldCommitID, newCommitID string, commits *repository.PushCommits) {
|
||||
if setting.Indexer.RepoIndexerEnabled && refName == git.BranchPrefix+repo.DefaultBranch {
|
||||
code_indexer.UpdateRepoIndexer(repo)
|
||||
}
|
||||
if err := stats_indexer.UpdateRepoIndexer(repo); err != nil {
|
||||
log.Error("stats_indexer.UpdateRepoIndexer(%d) failed: %v", repo.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *indexerNotifier) NotifyIssueChangeContent(doer *models.User, issue *models.Issue, oldContent string) {
|
||||
|
@ -641,6 +641,7 @@ forks = Forks
|
||||
pick_reaction = Pick your reaction
|
||||
reactions_more = and %d more
|
||||
unit_disabled = The site administrator has disabled this repository section.
|
||||
language_other = Other
|
||||
|
||||
template.items = Template Items
|
||||
template.git_content = Git Content (Default Branch)
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/highlight"
|
||||
code_indexer "code.gitea.io/gitea/modules/indexer/code"
|
||||
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
|
||||
stats_indexer "code.gitea.io/gitea/modules/indexer/stats"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/external"
|
||||
@ -111,6 +112,9 @@ func GlobalInit(ctx context.Context) {
|
||||
cron.NewContext()
|
||||
issue_indexer.InitIssueIndexer(false)
|
||||
code_indexer.Init()
|
||||
if err := stats_indexer.Init(); err != nil {
|
||||
log.Fatal("Failed to initialize repository stats indexer queue: %v", err)
|
||||
}
|
||||
mirror_service.InitSyncMirrors()
|
||||
webhook.InitDeliverHooks()
|
||||
if err := pull_service.Init(); err != nil {
|
||||
|
@ -85,7 +85,6 @@ func Home(ctx *context.Context) {
|
||||
OrderBy: orderBy,
|
||||
Private: ctx.IsSigned,
|
||||
Actor: ctx.User,
|
||||
IsProfile: true,
|
||||
IncludeDescription: setting.UI.SearchRepoDescription,
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -457,6 +457,16 @@ func Home(ctx *context.Context) {
|
||||
ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo")))
|
||||
}
|
||||
|
||||
func renderLanguageStats(ctx *context.Context) {
|
||||
langs, err := ctx.Repo.Repository.GetTopLanguageStats(5)
|
||||
if err != nil {
|
||||
ctx.ServerError("Repo.GetTopLanguageStats", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["LanguageStats"] = langs
|
||||
}
|
||||
|
||||
func renderCode(ctx *context.Context) {
|
||||
ctx.Data["PageIsViewCode"] = true
|
||||
|
||||
@ -497,6 +507,11 @@ func renderCode(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
renderLanguageStats(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if entry.IsDir() {
|
||||
renderDirectory(ctx, treeLink)
|
||||
} else {
|
||||
|
@ -220,7 +220,6 @@ func Profile(ctx *context.Context) {
|
||||
OwnerID: ctxUser.ID,
|
||||
OrderBy: orderBy,
|
||||
Private: ctx.IsSigned,
|
||||
IsProfile: true,
|
||||
Collaborate: util.OptionalBoolFalse,
|
||||
TopicOnly: topicOnly,
|
||||
IncludeDescription: setting.UI.SearchRepoDescription,
|
||||
|
@ -21,6 +21,9 @@
|
||||
{{end}}
|
||||
{{end}}
|
||||
<div class="ui right metas">
|
||||
{{if .PrimaryLanguage }}
|
||||
<span class="text grey"><i class="color-icon" style="background-color: {{.PrimaryLanguage.Color}}"></i>{{ .PrimaryLanguage.Language }}</span>
|
||||
{{end}}
|
||||
<span class="text grey"><i class="octicon octicon-star"></i> {{.NumStars}}</span>
|
||||
<span class="text grey"><i class="octicon octicon-git-branch"></i> {{.NumForks}}</span>
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
<div class="ui segment sub-menu">
|
||||
<div class="ui segments">
|
||||
<div class="ui segment sub-menu repository-menu">
|
||||
<div class="ui two horizontal center link list">
|
||||
{{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo)}}
|
||||
<div class="item{{if .PageIsCommits}} active{{end}}">
|
||||
@ -14,4 +15,28 @@
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo) .LanguageStats }}
|
||||
<div class="ui segment sub-menu language-stats-details" style="display: none">
|
||||
<div class="ui horizontal center link list">
|
||||
{{range .LanguageStats}}
|
||||
<div class="item">
|
||||
<i class="color-icon" style="background-color: {{ .Color }}"></i>
|
||||
<span class="ui"><b>
|
||||
{{if eq .Language "other" }}
|
||||
{{ $.i18n.Tr "repo.language_other" }}
|
||||
{{else}}
|
||||
{{ .Language }}
|
||||
{{end}}
|
||||
</b> {{ .Percentage }}%</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<a class="ui segment language-stats">
|
||||
{{range .LanguageStats}}
|
||||
<div class="bar" style="width: {{ .Percentage }}%; background-color: {{ .Color }}"> </div>
|
||||
{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
11
vendor/github.com/src-d/enry/v2/.gitignore
generated
vendored
Normal file
11
vendor/github.com/src-d/enry/v2/.gitignore
generated
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
.linguist
|
||||
benchmarks/output
|
||||
.ci
|
||||
Makefile.main
|
||||
.shared
|
||||
.idea
|
||||
.docsrv-resources
|
||||
build/
|
||||
vendor/
|
||||
java/lib/
|
||||
.vscode/
|
132
vendor/github.com/src-d/enry/v2/.travis.yml
generated
vendored
Normal file
132
vendor/github.com/src-d/enry/v2/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,132 @@
|
||||
dist: trusty
|
||||
language: go
|
||||
go:
|
||||
- '1.12.x'
|
||||
- '1.11.x'
|
||||
env:
|
||||
global:
|
||||
- GO_VERSION_FOR_JVM='1.11.x'
|
||||
- CGO_ENABLED=0
|
||||
- GO111MODULE=on
|
||||
- ONIGURUMA_VERSION='6.9.1'
|
||||
matrix:
|
||||
- ONIGURUMA=0
|
||||
- ONIGURUMA=1
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
stages:
|
||||
- name: test
|
||||
- name: release
|
||||
if: tag IS present
|
||||
- name: publish
|
||||
if: tag IS present
|
||||
|
||||
stage: test
|
||||
install:
|
||||
- >
|
||||
if [[ "${ONIGURUMA}" -gt 0 ]]; then
|
||||
export CGO_ENABLED=1
|
||||
export GO_TAGS='oniguruma'
|
||||
# install oniguruma manually as trusty has only ancient 5.x
|
||||
sudo apt-get install -y dpkg # dpkg >= 1.17.5ubuntu5.8 fixes https://bugs.launchpad.net/ubuntu/+source/dpkg/+bug/1730627
|
||||
wget "http://archive.ubuntu.com/ubuntu/pool/universe/libo/libonig/libonig5_${ONIGURUMA_VERSION}-1_amd64.deb"
|
||||
sudo dpkg -i "libonig5_${ONIGURUMA_VERSION}-1_amd64.deb"
|
||||
wget "http://archive.ubuntu.com/ubuntu/pool/universe/libo/libonig/libonig-dev_${ONIGURUMA_VERSION}-1_amd64.deb"
|
||||
sudo dpkg -i "libonig-dev_${ONIGURUMA_VERSION}-1_amd64.deb"
|
||||
fi;
|
||||
script:
|
||||
- make test-coverage
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- name: 'java unit-tests'
|
||||
stage: test
|
||||
language: scala
|
||||
jdk: oraclejdk8
|
||||
install:
|
||||
- export CGO_ENABLED=1
|
||||
- eval "$(curl -sL https://raw.githubusercontent.com/travis-ci/gimme/master/gimme | GIMME_GO_VERSION=$GO_VERSION_FOR_JVM bash)"
|
||||
- go version
|
||||
before_script:
|
||||
- cd java
|
||||
- make
|
||||
script:
|
||||
- make test
|
||||
|
||||
- name: 'linux packages'
|
||||
stage: release
|
||||
install:
|
||||
- go version
|
||||
script: make packages
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: $GITHUB_TOKEN
|
||||
file_glob: true
|
||||
file: build/*.tar.gz
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
|
||||
- name: 'linux shared lib'
|
||||
stage: release
|
||||
install:
|
||||
- go version
|
||||
script: make linux-shared
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: $GITHUB_TOKEN
|
||||
file:
|
||||
- ./.shared/linux-x86-64/libenry.so
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
|
||||
- name: 'macOS shared lib'
|
||||
stage: release
|
||||
env:
|
||||
- OSXCROSS_PACKAGE="osxcross_3034f7149716d815bc473d0a7b35d17e4cf175aa.tar.gz"
|
||||
- OSXCROSS_URL="https://github.com/bblfsh/client-scala/releases/download/v1.5.2/${OSXCROSS_PACKAGE}"
|
||||
- PATH="/$HOME/osxcross/bin:$PATH"
|
||||
install:
|
||||
- go version
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install -y --no-install-recommends clang g++ gcc gcc-multilib libc6-dev libc6-dev-i386 mingw-w64 patch xz-utils
|
||||
- cd ${HOME}
|
||||
- curl -sfSL ${OSXCROSS_URL} | tar -C ${HOME} -xzf -
|
||||
- cd $GOPATH/src/github.com/src-d/enry
|
||||
script: make darwin-shared
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: $GITHUB_TOKEN
|
||||
file: ./.shared/darwin/libenry.dylib
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
|
||||
- name: 'java: publish to maven'
|
||||
stage: publish
|
||||
language: scala
|
||||
jdk: oraclejdk8
|
||||
install:
|
||||
- export CGO_ENABLED=1
|
||||
- eval "$(curl -sL https://raw.githubusercontent.com/travis-ci/gimme/master/gimme | GIMME_GO_VERSION=$GO_VERSION_FOR_JVM bash)"
|
||||
- go version
|
||||
before_script:
|
||||
- cd java
|
||||
- make
|
||||
- curl -o ./shared/linux-x86-64/libenry.so -sfL "https://github.com/$TRAVIS_REPO_SLUG/releases/download/$TRAVIS_TAG/libenry.so" || travis_terminate 1
|
||||
- mkdir -p ./shared/darwin
|
||||
- curl -o ./shared/darwin/libenry.dylib -sfL "https://github.com/$TRAVIS_REPO_SLUG/releases/download/$TRAVIS_TAG/libenry.dylib" || travis_terminate 1
|
||||
- openssl aes-256-cbc -K $encrypted_a0e1c69dbbc7_key -iv $encrypted_a0e1c69dbbc7_iv -in key.asc.enc -out key.asc -d
|
||||
- gpg --no-default-keyring --primary-keyring ./project/.gnupg/pubring.gpg --secret-keyring ./project/.gnupg/secring.gpg --keyring ./project/.gnupg/pubring.gpg --fingerprint --import key.asc
|
||||
script:
|
||||
- make test # ensure the shared objects are functional
|
||||
- ./sbt publishLocal
|
||||
- ./sbt publishSigned
|
||||
- ./sbt sonatypeRelease
|
61
vendor/github.com/src-d/enry/v2/CONTRIBUTING.md
generated
vendored
Normal file
61
vendor/github.com/src-d/enry/v2/CONTRIBUTING.md
generated
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
# source{d} Contributing Guidelines
|
||||
|
||||
source{d} projects accept contributions via GitHub pull requests.
|
||||
This document outlines some of the
|
||||
conventions on development workflow, commit message formatting, contact points,
|
||||
and other resources to make it easier to get your contribution accepted.
|
||||
|
||||
## Certificate of Origin
|
||||
|
||||
By contributing to this project, you agree to the [Developer Certificate of
|
||||
Origin (DCO)](DCO). This document was created by the Linux Kernel community and is a
|
||||
simple statement that you, as a contributor, have the legal right to make the
|
||||
contribution.
|
||||
|
||||
In order to show your agreement with the DCO you should include at the end of the commit message,
|
||||
the following line: `Signed-off-by: John Doe <john.doe@example.com>`, using your real name.
|
||||
|
||||
This can be done easily using the [`-s`](https://github.com/git/git/blob/b2c150d3aa82f6583b9aadfecc5f8fa1c74aca09/Documentation/git-commit.txt#L154-L161) flag on the `git commit`.
|
||||
|
||||
If you find yourself pushed a few commits without `Signed-off-by`, you can still add it afterwards. We wrote a manual which can help: [fix-DCO.md](https://github.com/src-d/guide/blob/master/developer-community/fix-DCO.md).
|
||||
|
||||
## Support Channels
|
||||
|
||||
The official support channels, for both users and contributors, are:
|
||||
|
||||
- GitHub issues: each repository has its own list of issues.
|
||||
- Slack: join the [source{d} Slack](https://join.slack.com/t/sourced-community/shared_invite/enQtMjc4Njk5MzEyNzM2LTFjNzY4NjEwZGEwMzRiNTM4MzRlMzQ4MmIzZjkwZmZlM2NjODUxZmJjNDI1OTcxNDAyMmZlNmFjODZlNTg0YWM) community.
|
||||
|
||||
*Before opening a new issue or submitting a new pull request, it's helpful to
|
||||
search the project - it's likely that another user has already reported the
|
||||
issue you're facing, or it's a known issue that we're already aware of.
|
||||
|
||||
|
||||
## How to Contribute
|
||||
|
||||
Pull Requests (PRs) are the main and exclusive way to contribute code to source{d} projects.
|
||||
In order for a PR to be accepted it needs to pass this list of requirements:
|
||||
|
||||
- The contribution must be correctly explained with natural language and providing a minimum working example that reproduces it.
|
||||
- All PRs must be written idiomaticly:
|
||||
- for Go: formatted according to [gofmt](https://golang.org/cmd/gofmt/), and without any warnings from [go lint](https://github.com/golang/lint) nor [go vet](https://golang.org/cmd/vet/)
|
||||
- for other languages, similar constraints apply.
|
||||
- They should in general include tests, and those shall pass.
|
||||
- If the PR is a bug fix, it has to include a new unit test that fails before the patch is merged.
|
||||
- If the PR is a new feature, it has to come with a suite of unit tests, that tests the new functionality.
|
||||
- In any case, all the PRs have to pass the personal evaluation of at least one of the [maintainers](MAINTAINERS) of the project.
|
||||
|
||||
|
||||
### Format of the commit message
|
||||
|
||||
Every commit message should describe what was changed, under which context and, if applicable, the GitHub issue it relates to:
|
||||
|
||||
```
|
||||
plumbing: packp, Skip argument validations for unknown capabilities. Fixes #623
|
||||
```
|
||||
|
||||
The format can be described more formally as follows:
|
||||
|
||||
```
|
||||
<package>: <subpackage>, <what changed>. [Fixes #<issue-number>]
|
||||
```
|
25
vendor/github.com/src-d/enry/v2/DCO
generated
vendored
Normal file
25
vendor/github.com/src-d/enry/v2/DCO
generated
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user