@@ -14,6 +14,8 @@ var customMigrations = []CustomMigration{ | |||
{"Custom v1 Topic struct change to support chinese", syncTopicStruct}, | |||
} | |||
var customMigrationsStatic = []CustomMigration{} | |||
func MigrateCustom(x *xorm.Engine) { | |||
for _, m := range customMigrations { | |||
@@ -27,6 +29,17 @@ func MigrateCustom(x *xorm.Engine) { | |||
} | |||
func MigrateCustomStatic(x *xorm.Engine) { | |||
for _, m := range customMigrationsStatic { | |||
log.Info("Migration: %s", m.Description) | |||
if err := m.Migrate(x); err != nil { | |||
log.Error("Migration: %v", err) | |||
} | |||
} | |||
} | |||
func syncTopicStruct(x *xorm.Engine) error { | |||
query := "ALTER TABLE topic ALTER COLUMN name TYPE varchar(105);" | |||
@@ -190,7 +190,7 @@ func setEngine(engine *xorm.Engine, table []interface{}, database *setting.DBInf | |||
engine.SetMaxIdleConns(setting.Database.MaxIdleConns) | |||
engine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime) | |||
engine.Sync2(table...) | |||
MigrateCustom(engine) | |||
return nil | |||
} | |||
@@ -222,7 +222,7 @@ func NewEngine(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err e | |||
if err = newEngine(ctx, migrateFunc, x, tables, setting.Database); err != nil { | |||
return fmt.Errorf("newEngine failed: %v", err) | |||
} | |||
MigrateCustom(x) | |||
xStatistic, err = getEngine(setting.DatabaseStatistic) | |||
if err != nil { | |||
return fmt.Errorf("Failed to connect to database: %v", err) | |||
@@ -230,6 +230,7 @@ func NewEngine(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err e | |||
if err = newEngine(ctx, migrateFunc, xStatistic, tablesStatistic, setting.DatabaseStatistic); err != nil { | |||
return fmt.Errorf("newEngine statistic failed: %v", err) | |||
} | |||
MigrateCustomStatic(xStatistic) | |||
HasEngine = true | |||
@@ -5,7 +5,9 @@ import ( | |||
"time" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/timeutil" | |||
"xorm.io/builder" | |||
) | |||
type UserBusinessAnalysis struct { | |||
@@ -71,6 +73,14 @@ type UserBusinessAnalysis struct { | |||
Name string `xorm:"NOT NULL"` | |||
} | |||
type UserBusinessAnalysisQueryOptions struct { | |||
ListOptions | |||
UserName string | |||
SortType string | |||
StartTime int64 | |||
EndTime int64 | |||
} | |||
func QueryUserStaticData(startTime int64, endTime int64) []*UserBusinessAnalysis { | |||
log.Info("query startTime =" + fmt.Sprint(startTime) + " endTime=" + fmt.Sprint(endTime)) | |||
statictisSess := xStatistic.NewSession() | |||
@@ -114,11 +124,114 @@ func QueryUserStaticData(startTime int64, endTime int64) []*UserBusinessAnalysis | |||
return userBusinessAnalysisReturnList | |||
} | |||
func QueryUserStaticDataPage(opts *UserBusinessAnalysisQueryOptions) ([]*UserBusinessAnalysis, int64) { | |||
log.Info("query startTime =" + fmt.Sprint(opts.StartTime) + " endTime=" + fmt.Sprint(opts.EndTime)) | |||
statictisSess := xStatistic.NewSession() | |||
defer statictisSess.Close() | |||
currentTimeNow := time.Now() | |||
pageStartTime := time.Date(currentTimeNow.Year(), currentTimeNow.Month(), currentTimeNow.Day(), 0, 0, 0, 0, currentTimeNow.Location()) | |||
pageEndTime := time.Date(currentTimeNow.Year(), currentTimeNow.Month(), currentTimeNow.Day(), 23, 59, 59, 0, currentTimeNow.Location()) | |||
var cond = builder.NewCond() | |||
if len(opts.UserName) > 0 { | |||
cond = cond.And( | |||
builder.Eq{"name": opts.UserName}, | |||
) | |||
} | |||
cond = cond.And( | |||
builder.Gte{"count_date": fmt.Sprint(pageStartTime)}, | |||
) | |||
cond = cond.And( | |||
builder.Lte{"count_date": fmt.Sprint(pageEndTime)}, | |||
) | |||
count, err := statictisSess.Where(cond).Count(new(UserBusinessAnalysis)) | |||
if err != nil { | |||
log.Info("query error." + err.Error()) | |||
return nil, 0 | |||
} | |||
if opts.Page >= 0 && opts.PageSize > 0 { | |||
var start int | |||
if opts.Page == 0 { | |||
start = 0 | |||
} else { | |||
start = (opts.Page - 1) * opts.PageSize | |||
} | |||
statictisSess.Limit(opts.PageSize, start) | |||
} | |||
statictisSess.OrderBy("count_date desc") | |||
userBusinessAnalysisList := make([]*UserBusinessAnalysis, 0, setting.UI.IssuePagingNum) | |||
if err := statictisSess.Table("user_business_analysis").Where(cond). | |||
Find(&userBusinessAnalysisList); err != nil { | |||
return nil, 0 | |||
} | |||
resultMap := make(map[int64]*UserBusinessAnalysis) | |||
var newAndCond = builder.NewCond() | |||
var newOrCond = builder.NewCond() | |||
for _, userRecord := range userBusinessAnalysisList { | |||
newOrCond.Or( | |||
builder.Eq{"id": userRecord.ID}, | |||
) | |||
} | |||
newAndCond = newAndCond.And( | |||
newOrCond, | |||
) | |||
newAndCond = newAndCond.And( | |||
builder.Gte{"count_date": fmt.Sprint(opts.StartTime)}, | |||
) | |||
newAndCond = newAndCond.And( | |||
builder.Lte{"count_date": fmt.Sprint(opts.EndTime)}, | |||
) | |||
userBusinessAnalysisList = make([]*UserBusinessAnalysis, 0) | |||
if err := statictisSess.Table("user_business_analysis").Where(newAndCond). | |||
Find(&userBusinessAnalysisList); err != nil { | |||
return nil, 0 | |||
} | |||
log.Info("query result size=" + fmt.Sprint(len(userBusinessAnalysisList))) | |||
for _, userRecord := range userBusinessAnalysisList { | |||
if _, ok := resultMap[userRecord.ID]; !ok { | |||
resultMap[userRecord.ID] = userRecord | |||
} else { | |||
resultMap[userRecord.ID].CodeMergeCount += userRecord.CodeMergeCount | |||
resultMap[userRecord.ID].CommitCount += userRecord.CommitCount | |||
resultMap[userRecord.ID].IssueCount += userRecord.IssueCount | |||
resultMap[userRecord.ID].CommentCount += userRecord.CommentCount | |||
resultMap[userRecord.ID].FocusRepoCount += userRecord.FocusRepoCount | |||
resultMap[userRecord.ID].StarRepoCount += userRecord.StarRepoCount | |||
resultMap[userRecord.ID].WatchedCount += userRecord.WatchedCount | |||
resultMap[userRecord.ID].CommitCodeSize += userRecord.CommitCodeSize | |||
resultMap[userRecord.ID].CommitDatasetSize += userRecord.CommitDatasetSize | |||
resultMap[userRecord.ID].CommitModelCount += userRecord.CommitModelCount | |||
resultMap[userRecord.ID].SolveIssueCount += userRecord.SolveIssueCount | |||
resultMap[userRecord.ID].EncyclopediasCount += userRecord.EncyclopediasCount | |||
resultMap[userRecord.ID].CreateRepoCount += userRecord.CreateRepoCount | |||
resultMap[userRecord.ID].LoginCount += userRecord.LoginCount | |||
} | |||
} | |||
userBusinessAnalysisReturnList := make([]*UserBusinessAnalysis, len(resultMap)) | |||
index := 0 | |||
for _, v := range resultMap { | |||
userBusinessAnalysisReturnList[index] = v | |||
index += 1 | |||
} | |||
log.Info("return size=" + fmt.Sprint(len(userBusinessAnalysisReturnList))) | |||
return userBusinessAnalysisReturnList, count | |||
} | |||
func CounDataByDate(wikiCountMap map[string]int, startTime time.Time, endTime time.Time) { | |||
log.Info("start to count other user info data") | |||
sess := x.NewSession() | |||
defer sess.Close() | |||
sess.Select("`user`.*").Table("user") | |||
sess.Select("`user`.*").Table("user").Where("type != 1 and is_active=true") | |||
userList := make([]*User, 0) | |||
sess.Find(&userList) | |||
@@ -6,6 +6,7 @@ package storage | |||
import ( | |||
"io" | |||
"net/url" | |||
"path" | |||
"strconv" | |||
"strings" | |||
@@ -262,6 +263,7 @@ func GetObsCreateSignedUrl(jobName, parentDir, fileName string) (string, error) | |||
input.Method = obs.HttpMethodGet | |||
reqParams := make(map[string]string) | |||
fileName = url.QueryEscape(fileName) | |||
reqParams["response-content-disposition"] = "attachment; filename=\"" + fileName + "\"" | |||
input.QueryParams = reqParams | |||
output, err := ObsCli.CreateSignedUrl(input) | |||
@@ -5,6 +5,7 @@ import ( | |||
"fmt" | |||
"io/ioutil" | |||
"net/http" | |||
"net/url" | |||
"os" | |||
"path" | |||
"strconv" | |||
@@ -107,6 +108,7 @@ func GetAllProjectsPeriodStatistics(ctx *context.Context) { | |||
ctx.Error(http.StatusBadRequest, ctx.Tr("repo.total_count_get_error")) | |||
return | |||
} | |||
sql := generateSqlByType(ctx, beginTime, endTime, latestDate, q, orderBy, page, pageSize) | |||
projectsPeriodData := ProjectsPeriodData{ | |||
RecordBeginTime: recordBeginTime.Format(DATE_FORMAT), | |||
@@ -114,13 +116,23 @@ func GetAllProjectsPeriodStatistics(ctx *context.Context) { | |||
TotalPage: getTotalPage(total, pageSize), | |||
TotalCount: total, | |||
LastUpdatedTime: latestUpdatedTime, | |||
PageRecords: models.GetRepoStatisticByRawSql(generatePageSql(beginTime, endTime, latestDate, q, orderBy, page, pageSize)), | |||
PageRecords: models.GetRepoStatisticByRawSql(sql), | |||
} | |||
ctx.JSON(http.StatusOK, projectsPeriodData) | |||
} | |||
func generateSqlByType(ctx *context.Context, beginTime time.Time, endTime time.Time, latestDate string, q string, orderBy string, page int, pageSize int) string { | |||
sql := "" | |||
if ctx.QueryTrim("type") == "all" { | |||
sql = generateTypeAllSql(beginTime, endTime, latestDate, q, orderBy, page, pageSize) | |||
} else { | |||
sql = generatePageSql(beginTime, endTime, latestDate, q, orderBy, page, pageSize) | |||
} | |||
return sql | |||
} | |||
func ServeAllProjectsPeriodStatisticsFile(ctx *context.Context) { | |||
recordBeginTime, err := getRecordBeginTime() | |||
@@ -158,7 +170,7 @@ func ServeAllProjectsPeriodStatisticsFile(ctx *context.Context) { | |||
return | |||
} | |||
fileName := getFileName(ctx, beginTime, endTime) | |||
fileName, frontName := getFileName(ctx, beginTime, endTime) | |||
if err := os.MkdirAll(setting.RadarMap.Path, os.ModePerm); err != nil { | |||
ctx.Error(http.StatusBadRequest, fmt.Errorf("Failed to create dir %s: %v", setting.AvatarUploadPath, err).Error()) | |||
@@ -175,7 +187,7 @@ func ServeAllProjectsPeriodStatisticsFile(ctx *context.Context) { | |||
writer.Write(allProjectsPeroidHeader(ctx)) | |||
for i := 0; i <= totalPage; i++ { | |||
pageRecords := models.GetRepoStatisticByRawSql(generatePageSql(beginTime, endTime, latestDate, q, orderBy, i+1, pageSize)) | |||
pageRecords := models.GetRepoStatisticByRawSql(generateSqlByType(ctx, beginTime, endTime, latestDate, q, orderBy, i+1, pageSize)) | |||
for _, record := range pageRecords { | |||
e = writer.Write(allProjectsPeroidValues(record, ctx)) | |||
if e != nil { | |||
@@ -186,20 +198,24 @@ func ServeAllProjectsPeriodStatisticsFile(ctx *context.Context) { | |||
} | |||
ctx.ServeFile(fileName) | |||
ctx.ServeFile(fileName, url.QueryEscape(frontName)) | |||
} | |||
func getFileName(ctx *context.Context, beginTime time.Time, endTime time.Time) string { | |||
baseName := setting.RadarMap.Path + "/" | |||
if ctx.QueryTrim("type") != "" { | |||
baseName = baseName + ctx.QueryTrim("type") + "_" | |||
} | |||
func getFileName(ctx *context.Context, beginTime time.Time, endTime time.Time) (string, string) { | |||
baseName := setting.RadarMap.Path + "/项目分析_" | |||
if ctx.QueryTrim("q") != "" { | |||
baseName = baseName + ctx.QueryTrim("q") + "_" | |||
} | |||
baseName = baseName + beginTime.AddDate(0, 0, -1).Format(DATE_FORMAT) + "_to_" + endTime.AddDate(0, 0, -1).Format(DATE_FORMAT) + "_" + strconv.FormatInt(time.Now().Unix(), 10) + ".csv" | |||
return baseName | |||
if ctx.QueryTrim("type") == "all" { | |||
baseName = baseName + "所有" | |||
} else { | |||
baseName = baseName + beginTime.AddDate(0, 0, -1).Format(DATE_FORMAT) + "_" + endTime.AddDate(0, 0, -1).Format(DATE_FORMAT) | |||
} | |||
frontName := baseName + ".csv" | |||
localName := baseName + "_" + strconv.FormatInt(time.Now().Unix(), 10) + ".csv" | |||
return localName, path.Base(frontName) | |||
} | |||
func ClearUnusedStatisticsFile() { | |||
@@ -219,8 +235,8 @@ func ClearUnusedStatisticsFile() { | |||
func allProjectsPeroidHeader(ctx *context.Context) []string { | |||
return []string{ctx.Tr("repos.id"), ctx.Tr("repos.projectName"), ctx.Tr("repos.isPrivate"), ctx.Tr("repos.openi"), ctx.Tr("repos.visit"), ctx.Tr("repos.download"), ctx.Tr("repos.pr"), ctx.Tr("repos.commit"), | |||
ctx.Tr("repos.watches"), ctx.Tr("repos.stars"), ctx.Tr("repos.forks"), ctx.Tr("repos.issues"), ctx.Tr("repos.closedIssues"), ctx.Tr("repos.contributor")} | |||
return []string{ctx.Tr("admin.repos.id"), ctx.Tr("admin.repos.projectName"), ctx.Tr("admin.repos.isPrivate"), ctx.Tr("admin.repos.openi"), ctx.Tr("admin.repos.visit"), ctx.Tr("admin.repos.download"), ctx.Tr("admin.repos.pr"), ctx.Tr("admin.repos.commit"), | |||
ctx.Tr("admin.repos.watches"), ctx.Tr("admin.repos.stars"), ctx.Tr("admin.repos.forks"), ctx.Tr("admin.repos.issues"), ctx.Tr("admin.repos.closedIssues"), ctx.Tr("admin.repos.contributor")} | |||
} | |||
@@ -234,9 +250,9 @@ func allProjectsPeroidValues(rs *models.RepoStatistic, ctx *context.Context) []s | |||
func getIsPrivateDisplay(private bool, ctx *context.Context) string { | |||
if private { | |||
return ctx.Tr("repos.yes") | |||
return ctx.Tr("admin.repos.yes") | |||
} else { | |||
return ctx.Tr("repos.no") | |||
return ctx.Tr("admin.repos.no") | |||
} | |||
} | |||
@@ -358,11 +374,11 @@ func generateTargetSql(beginTime time.Time, endTime time.Time, repoId int64) str | |||
return sql | |||
} | |||
func generateCountSql(beginTime time.Time, endTime time.Time, yesterday string, q string) string { | |||
func generateCountSql(beginTime time.Time, endTime time.Time, latestDate string, q string) string { | |||
countSql := "SELECT count(*) FROM " + | |||
"(SELECT repo_id FROM repo_statistic where created_unix >=" + strconv.FormatInt(beginTime.Unix(), 10) + | |||
" and created_unix<" + strconv.FormatInt(endTime.Unix(), 10) + " group by repo_id) A," + | |||
"(SELECT repo_id,name,is_private,radar_total from public.repo_statistic where date='" + yesterday + "') B" + | |||
"(SELECT repo_id,name,is_private,radar_total from public.repo_statistic where date='" + latestDate + "') B" + | |||
" where A.repo_id=B.repo_id" | |||
if q != "" { | |||
countSql = countSql + " and B.name like '%" + q + "%'" | |||
@@ -370,18 +386,34 @@ func generateCountSql(beginTime time.Time, endTime time.Time, yesterday string, | |||
return countSql | |||
} | |||
func generatePageSql(beginTime time.Time, endTime time.Time, yesterday string, q string, orderBy string, page int, pageSize int) string { | |||
countSql := "SELECT A.repo_id,name,is_private,radar_total,num_watches,num_visits,num_downloads,num_pulls,num_commits,num_stars,num_forks,num_issues,num_closed_issues,num_contributor FROM " + | |||
func generateTypeAllSql(beginTime time.Time, endTime time.Time, latestDate string, q string, orderBy string, page int, pageSize int) string { | |||
sql := "SELECT A.repo_id,name,is_private,radar_total,num_watches,num_visits,num_downloads,num_pulls,num_commits,num_stars,num_forks,num_issues,num_closed_issues,num_contributor FROM " + | |||
"(SELECT repo_id,sum(num_visits) as num_visits " + | |||
" FROM repo_statistic where created_unix >=" + strconv.FormatInt(beginTime.Unix(), 10) + | |||
" and created_unix<" + strconv.FormatInt(endTime.Unix(), 10) + " group by repo_id) A," + | |||
"(SELECT repo_id,name,is_private,radar_total,num_watches,num_downloads,num_pulls,num_commits,num_stars,num_forks,num_issues,num_closed_issues,num_contributor from public.repo_statistic where date='" + latestDate + "') B" + | |||
" where A.repo_id=B.repo_id" | |||
if q != "" { | |||
sql = sql + " and name like '%" + q + "%'" | |||
} | |||
sql = sql + " order by " + orderBy + " desc,repo_id" + " limit " + strconv.Itoa(pageSize) + " offset " + strconv.Itoa((page-1)*pageSize) | |||
return sql | |||
} | |||
func generatePageSql(beginTime time.Time, endTime time.Time, latestDate string, q string, orderBy string, page int, pageSize int) string { | |||
sql := "SELECT A.repo_id,name,is_private,radar_total,num_watches,num_visits,num_downloads,num_pulls,num_commits,num_stars,num_forks,num_issues,num_closed_issues,num_contributor FROM " + | |||
"(SELECT repo_id,sum(num_watches_added) as num_watches,sum(num_visits) as num_visits, sum(num_downloads_added) as num_downloads,sum(num_pulls_added) as num_pulls,sum(num_commits_added) as num_commits,sum(num_stars_added) as num_stars,sum(num_forks_added) num_forks,sum(num_issues_added) as num_issues,sum(num_closed_issues_added) as num_closed_issues,sum(num_contributor_added) as num_contributor " + | |||
" FROM repo_statistic where created_unix >=" + strconv.FormatInt(beginTime.Unix(), 10) + | |||
" and created_unix<" + strconv.FormatInt(endTime.Unix(), 10) + " group by repo_id) A," + | |||
"(SELECT repo_id,name,is_private,radar_total from public.repo_statistic where date='" + yesterday + "') B" + | |||
"(SELECT repo_id,name,is_private,radar_total from public.repo_statistic where date='" + latestDate + "') B" + | |||
" where A.repo_id=B.repo_id" | |||
if q != "" { | |||
countSql = countSql + " and B.name like '%" + q + "%'" | |||
sql = sql + " and B.name like '%" + q + "%'" | |||
} | |||
countSql = countSql + " order by " + orderBy + " desc,A.repo_id" + " limit " + strconv.Itoa(pageSize) + " offset " + strconv.Itoa((page-1)*pageSize) | |||
return countSql | |||
sql = sql + " order by " + orderBy + " desc,A.repo_id" + " limit " + strconv.Itoa(pageSize) + " offset " + strconv.Itoa((page-1)*pageSize) | |||
return sql | |||
} | |||
func getOrderBy(ctx *context.Context) string { | |||
@@ -9,6 +9,7 @@ import ( | |||
"code.gitea.io/gitea/modules/context" | |||
"code.gitea.io/gitea/modules/git" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/setting" | |||
) | |||
func QueryUserStaticData(ctx *context.Context) { | |||
@@ -19,7 +20,36 @@ func QueryUserStaticData(ctx *context.Context) { | |||
endTime, _ := time.Parse("2006-01-02", endDate) | |||
log.Info("startTime=" + fmt.Sprint(startTime.Unix()) + " endDate=" + fmt.Sprint(endTime.Unix())) | |||
ctx.JSON(http.StatusOK, models.QueryUserStaticData(startTime.Unix(), endTime.Unix())) | |||
} | |||
func QueryUserStaticDataPage(ctx *context.Context) { | |||
startDate := ctx.Query("startDate") | |||
endDate := ctx.Query("endDate") | |||
page := ctx.QueryInt("page") | |||
if page <= 0 { | |||
page = 1 | |||
} | |||
userName := ctx.Query("userName") | |||
log.Info("startDate=" + startDate + " endDate=" + endDate + " userName=" + userName + " page=" + fmt.Sprint(page)) | |||
startTime, _ := time.Parse("2006-01-02", startDate) | |||
endTime, _ := time.Parse("2006-01-02", endDate) | |||
log.Info("startTime=" + fmt.Sprint(startTime.Unix()) + " endDate=" + fmt.Sprint(endTime.Unix())) | |||
pageOpts := &models.UserBusinessAnalysisQueryOptions{ | |||
ListOptions: models.ListOptions{ | |||
Page: page, | |||
PageSize: setting.UI.IssuePagingNum, | |||
}, | |||
UserName: userName, | |||
StartTime: startTime.Unix(), | |||
EndTime: endTime.Unix(), | |||
} | |||
mapInterface := make(map[string]interface{}) | |||
re, count := models.QueryUserStaticDataPage(pageOpts) | |||
mapInterface["data"] = re | |||
mapInterface["count"] = count | |||
ctx.JSON(http.StatusOK, mapInterface) | |||
} | |||
func TimingCountDataByDate(date string) { | |||
@@ -919,6 +919,7 @@ func Forks(ctx *context.Context) { | |||
} | |||
func Contributors(ctx *context.Context) { | |||
ctx.Data["PageIsViewCode"] = true | |||
ctx.HTML(http.StatusOK, tplContributors) | |||
} | |||
@@ -793,6 +793,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
m.Post("/:username/:reponame/action/:action", reqSignIn, context.RepoAssignment(), context.UnitTypes(), repo.Action) | |||
m.Get("/tool/query_user_static", repo.QueryUserStaticData) | |||
m.Get("/tool/query_user_static_page", repo.QueryUserStaticDataPage) | |||
// Grouping for those endpoints not requiring authentication | |||
m.Group("/:username/:reponame", func() { | |||
m.Get("/contributors", repo.Contributors) | |||
@@ -93,7 +93,7 @@ | |||
<div class="ui tabular stackable menu navbar"> | |||
{{if .Permission.CanRead $.UnitTypeCode}} | |||
<div class="dropdown-menu"> | |||
<a class="{{if or .PageIsViewCode .PageIsReleaseList .PageIsWiki .PageIsActivity}}active{{end}} item" href="{{.RepoLink}}{{if (ne .BranchName .Repository.DefaultBranch)}}/src/{{.BranchNameSubURL | EscapePound}}{{end}}"> | |||
<a class="{{if or .PageIsViewCode .PageIsReleaseList .PageIsWiki .PageIsActivity .PageIsViewCode}}active{{end}} item" href="{{.RepoLink}}{{if (ne .BranchName .Repository.DefaultBranch)}}/src/{{.BranchNameSubURL | EscapePound}}{{end}}"> | |||
<span>{{svg "octicon-code" 16}} {{.i18n.Tr "repo.code"}} <i class="dropdown icon"></i></span> | |||
</a> | |||
<div class="dropdown-content"> | |||