| @@ -182,6 +182,7 @@ func CreateOrganization(org, owner *User) (err error) { | |||
| if _, err = sess.Insert(&OrgUser{ | |||
| UID: owner.ID, | |||
| OrgID: org.ID, | |||
| IsPublic: setting.Service.DefaultOrgMemberVisible, | |||
| }); err != nil { | |||
| return fmt.Errorf("insert org-user relation: %v", err) | |||
| } | |||
| @@ -210,9 +210,12 @@ type Repository struct { | |||
| Balance string `xorm:"NOT NULL DEFAULT '0'"` | |||
| BlockChainStatus RepoBlockChainStatus `xorm:"NOT NULL DEFAULT 0"` | |||
| // git clone total count | |||
| // git clone and git pull total count | |||
| CloneCnt int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| // only git clone total count | |||
| GitCloneCnt int64 `xorm:"NOT NULL DEFAULT 0"` | |||
| CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | |||
| UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | |||
| @@ -2473,6 +2476,24 @@ func (repo *Repository) IncreaseCloneCnt() { | |||
| return | |||
| } | |||
| func (repo *Repository) IncreaseGitCloneCnt() { | |||
| sess := x.NewSession() | |||
| defer sess.Close() | |||
| if err := sess.Begin(); err != nil { | |||
| return | |||
| } | |||
| if _, err := sess.Exec("UPDATE `repository` SET git_clone_cnt = git_clone_cnt + 1 WHERE id = ?", repo.ID); err != nil { | |||
| return | |||
| } | |||
| if err := sess.Commit(); err != nil { | |||
| return | |||
| } | |||
| return | |||
| } | |||
| func UpdateRepositoryCommitNum(repo *Repository) error { | |||
| if _, err := x.Exec("UPDATE `repository` SET num_commit = ? where id = ?", repo.NumCommit, repo.ID); err != nil { | |||
| return err | |||
| @@ -11,7 +11,8 @@ import ( | |||
| type ContributorWithUserId struct { | |||
| git.Contributor | |||
| UserId int64 | |||
| UserId int64 | |||
| IsAdmin bool | |||
| } | |||
| func GetRepoKPIStats(repo *Repository) (*git.RepoKPIStats, error) { | |||
| @@ -144,6 +145,7 @@ func GetTop10Contributor(repoPath string) ([]ContributorWithUserId, error) { | |||
| contributorDistinctDict[user.Email] = ContributorWithUserId{ | |||
| contributor, | |||
| user.ID, | |||
| user.IsAdmin, | |||
| } | |||
| } else { | |||
| @@ -156,6 +158,7 @@ func GetTop10Contributor(repoPath string) ([]ContributorWithUserId, error) { | |||
| contributorDistinctDict[contributor.Email] = ContributorWithUserId{ | |||
| contributor, | |||
| -1, | |||
| false, | |||
| } | |||
| } else { | |||
| value.CommitCnt += contributor.CommitCnt | |||
| @@ -158,6 +158,13 @@ func InsertRepoStat(repoStat *RepoStatistic) (int64, error) { | |||
| return xStatistic.Insert(repoStat) | |||
| } | |||
| func RestoreRepoStatFork(numForks int64, repoId int64) error { | |||
| sql := "update repo_statistic set num_forks=? where repo_id=?" | |||
| _, err := xStatistic.Exec(sql, numForks, repoId) | |||
| return err | |||
| } | |||
| func UpdateRepoStat(repoStat *RepoStatistic) error { | |||
| sql := "update repo_statistic set impact=?,completeness=?,liveness=?,project_health=?,team_health=?,growth=?,radar_total=? where repo_id=? and date=?" | |||
| @@ -11,6 +11,7 @@ import ( | |||
| "code.gitea.io/gitea/models" | |||
| "code.gitea.io/gitea/modules/migrations" | |||
| repository_service "code.gitea.io/gitea/modules/repository" | |||
| api_repo "code.gitea.io/gitea/routers/api/v1/repo" | |||
| "code.gitea.io/gitea/routers/repo" | |||
| mirror_service "code.gitea.io/gitea/services/mirror" | |||
| ) | |||
| @@ -195,6 +196,17 @@ func registerHandleUserStatistic() { | |||
| }) | |||
| } | |||
| func registerHandleClearRepoStatisticFile() { | |||
| RegisterTaskFatal("handle_repo_clear_statistic_file", &BaseConfig{ | |||
| Enabled: true, | |||
| RunAtStart: false, | |||
| Schedule: "@daily", | |||
| }, func(ctx context.Context, _ *models.User, _ Config) error { | |||
| api_repo.ClearUnusedStatisticsFile() | |||
| return nil | |||
| }) | |||
| } | |||
| func initBasicTasks() { | |||
| registerUpdateMirrorTask() | |||
| registerRepoHealthCheck() | |||
| @@ -163,6 +163,7 @@ var ( | |||
| // UI settings | |||
| UI = struct { | |||
| ExplorePagingNum int | |||
| ContributorPagingNum int | |||
| IssuePagingNum int | |||
| RepoSearchPagingNum int | |||
| MembersPagingNum int | |||
| @@ -203,19 +204,20 @@ var ( | |||
| Keywords string | |||
| } `ini:"ui.meta"` | |||
| }{ | |||
| ExplorePagingNum: 20, | |||
| IssuePagingNum: 10, | |||
| RepoSearchPagingNum: 10, | |||
| MembersPagingNum: 20, | |||
| FeedMaxCommitNum: 5, | |||
| GraphMaxCommitNum: 100, | |||
| CodeCommentLines: 4, | |||
| ReactionMaxUserNum: 10, | |||
| ThemeColorMetaTag: `#6cc644`, | |||
| MaxDisplayFileSize: 8388608, | |||
| DefaultTheme: `gitea`, | |||
| Themes: []string{`gitea`, `arc-green`}, | |||
| Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, | |||
| ExplorePagingNum: 20, | |||
| ContributorPagingNum: 50, | |||
| IssuePagingNum: 10, | |||
| RepoSearchPagingNum: 10, | |||
| MembersPagingNum: 20, | |||
| FeedMaxCommitNum: 5, | |||
| GraphMaxCommitNum: 100, | |||
| CodeCommentLines: 4, | |||
| ReactionMaxUserNum: 10, | |||
| ThemeColorMetaTag: `#6cc644`, | |||
| MaxDisplayFileSize: 8388608, | |||
| DefaultTheme: `gitea`, | |||
| Themes: []string{`gitea`, `arc-green`}, | |||
| Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, | |||
| Notification: struct { | |||
| MinTimeout time.Duration | |||
| TimeoutStep time.Duration | |||
| @@ -545,6 +547,7 @@ var ( | |||
| GrowthCommit float64 | |||
| GrowthComments float64 | |||
| RecordBeginTime string | |||
| Path string | |||
| }{} | |||
| ) | |||
| @@ -1325,7 +1328,8 @@ func SetRadarMapConfig() { | |||
| RadarMap.GrowthContributors = sec.Key("growth_contributors").MustFloat64(0.2) | |||
| RadarMap.GrowthCommit = sec.Key("growth_commit").MustFloat64(0.2) | |||
| RadarMap.GrowthComments = sec.Key("growth_comments").MustFloat64(0.2) | |||
| RadarMap.RecordBeginTime = sec.Key("record_beigin_time").MustString("2021-11-04") | |||
| RadarMap.RecordBeginTime = sec.Key("record_beigin_time").MustString("2021-11-05") | |||
| RadarMap.Path = sec.Key("PATH").MustString("data/projectborad") | |||
| } | |||
| @@ -10,11 +10,11 @@ import ( | |||
| "strconv" | |||
| "strings" | |||
| "github.com/unknwon/com" | |||
| "code.gitea.io/gitea/modules/log" | |||
| "code.gitea.io/gitea/modules/obs" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| "github.com/unknwon/com" | |||
| ) | |||
| type FileInfo struct { | |||
| @@ -178,30 +178,45 @@ func GetObsListObject(jobName, parentDir string) ([]FileInfo, error) { | |||
| input := &obs.ListObjectsInput{} | |||
| input.Bucket = setting.Bucket | |||
| input.Prefix = strings.TrimPrefix(path.Join(setting.TrainJobModelPath, jobName, setting.OutPutPath, parentDir), "/") | |||
| strPrefix := strings.Split(input.Prefix, "/") | |||
| output, err := ObsCli.ListObjects(input) | |||
| fileInfos := make([]FileInfo, 0) | |||
| if err == nil { | |||
| for _, val := range output.Contents { | |||
| str1 := strings.Split(val.Key, "/") | |||
| var isDir bool | |||
| var fileName,nextParentDir string | |||
| var fileName, nextParentDir string | |||
| if strings.HasSuffix(val.Key, "/") { | |||
| //dirs in next level dir | |||
| if len(str1)-len(strPrefix) > 2 { | |||
| continue | |||
| } | |||
| fileName = str1[len(str1)-2] | |||
| isDir = true | |||
| nextParentDir = fileName | |||
| if fileName == parentDir || (fileName + "/") == setting.OutPutPath { | |||
| if parentDir == "" { | |||
| nextParentDir = fileName | |||
| } else { | |||
| nextParentDir = parentDir + "/" + fileName | |||
| } | |||
| if fileName == strPrefix[len(strPrefix)-1] || (fileName+"/") == setting.OutPutPath { | |||
| continue | |||
| } | |||
| } else { | |||
| //files in next level dir | |||
| if len(str1)-len(strPrefix) > 1 { | |||
| continue | |||
| } | |||
| fileName = str1[len(str1)-1] | |||
| isDir = false | |||
| nextParentDir = parentDir | |||
| } | |||
| fileInfo := FileInfo{ | |||
| ModTime: val.LastModified.Format("2006-01-02 15:04:05"), | |||
| ModTime: val.LastModified.Local().Format("2006-01-02 15:04:05"), | |||
| FileName: fileName, | |||
| Size: val.Size, | |||
| IsDir:isDir, | |||
| Size: val.Size, | |||
| IsDir: isDir, | |||
| ParenDir: nextParentDir, | |||
| } | |||
| fileInfos = append(fileInfos, fileInfo) | |||
| @@ -242,7 +257,7 @@ func GetObsCreateSignedUrl(jobName, parentDir, fileName string) (string, error) | |||
| input := &obs.CreateSignedUrlInput{} | |||
| input.Bucket = setting.Bucket | |||
| input.Key = strings.TrimPrefix(path.Join(setting.TrainJobModelPath, jobName, setting.OutPutPath, parentDir, fileName), "/") | |||
| input.Expires = 60 * 60 | |||
| input.Method = obs.HttpMethodGet | |||
| @@ -218,6 +218,7 @@ show_only_private = Showing only private | |||
| show_only_public = Showing only public | |||
| issues.in_your_repos = In your repositories | |||
| contributors = Contributors | |||
| [explore] | |||
| repos = Repositories | |||
| @@ -2158,6 +2159,19 @@ repos.stars = Stars | |||
| repos.forks = Forks | |||
| repos.issues = Issues | |||
| repos.size = Size | |||
| repos.id=ID | |||
| repos.projectName=Project Name | |||
| repos.isPrivate=Private | |||
| repos.openi=OpenI | |||
| repos.visit=Visit | |||
| repos.download=Code Download | |||
| repos.pr=PR | |||
| repos.commit=Commit | |||
| repos.closedIssues=Closed Issue | |||
| repos.contributor=Contributor | |||
| repos.yes=Yes | |||
| repos.no=No | |||
| datasets.dataset_manage_panel= Dataset Manage | |||
| datasets.owner=Owner | |||
| @@ -220,6 +220,8 @@ show_only_public=只显示公开的 | |||
| issues.in_your_repos=属于该用户项目的 | |||
| contributors=贡献者 | |||
| [explore] | |||
| repos=项目 | |||
| users=用户 | |||
| @@ -2160,6 +2162,19 @@ repos.stars=点赞数 | |||
| repos.forks=派生数 | |||
| repos.issues=任务数 | |||
| repos.size=大小 | |||
| repos.id=ID | |||
| repos.projectName=项目名称 | |||
| repos.isPrivate=私有 | |||
| repos.openi=OpenI指数 | |||
| repos.visit=浏览量 | |||
| repos.download=代码下载量 | |||
| repos.pr=PR数 | |||
| repos.commit=Commit数 | |||
| repos.closedIssues=已解决任务数 | |||
| repos.contributor=贡献者数 | |||
| repos.yes=是 | |||
| repos.no=否 | |||
| datasets.dataset_manage_panel=数据集管理 | |||
| datasets.owner=所有者 | |||
| @@ -527,8 +527,11 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| //Project board | |||
| m.Group("/projectboard", func() { | |||
| m.Get("/restoreFork", adminReq, repo.RestoreForkNumber) | |||
| m.Group("/project", func() { | |||
| m.Get("", adminReq, repo.GetAllProjectsPeriodStatistics) | |||
| m.Get("/download", adminReq, repo.ServeAllProjectsPeriodStatisticsFile) | |||
| m.Group("/:id", func() { | |||
| m.Get("", adminReq, repo.GetProjectLatestStatistics) | |||
| m.Get("/period", adminReq, repo.GetProjectPeriodStatistics) | |||
| @@ -1,8 +1,12 @@ | |||
| package repo | |||
| import ( | |||
| "encoding/csv" | |||
| "fmt" | |||
| "io/ioutil" | |||
| "net/http" | |||
| "os" | |||
| "path" | |||
| "strconv" | |||
| "time" | |||
| @@ -21,6 +25,7 @@ type ProjectsPeriodData struct { | |||
| LastUpdatedTime string `json:"lastUpdatedTime"` | |||
| PageSize int `json:"pageSize"` | |||
| TotalPage int `json:"totalPage"` | |||
| TotalCount int64 `json:"totalCount"` | |||
| PageRecords []*models.RepoStatistic `json:"pageRecords"` | |||
| } | |||
| @@ -50,6 +55,19 @@ type ProjectLatestData struct { | |||
| Top10 []UserInfo `json:"top10"` | |||
| } | |||
| func RestoreForkNumber(ctx *context.Context) { | |||
| repos, err := models.GetAllRepositories() | |||
| if err != nil { | |||
| log.Error("GetAllRepositories failed: %v", err.Error()) | |||
| return | |||
| } | |||
| for _, repo := range repos { | |||
| models.RestoreRepoStatFork(int64(repo.NumForks), repo.ID) | |||
| } | |||
| ctx.JSON(http.StatusOK, struct{}{}) | |||
| } | |||
| func GetAllProjectsPeriodStatistics(ctx *context.Context) { | |||
| recordBeginTime, err := getRecordBeginTime() | |||
| @@ -94,6 +112,7 @@ func GetAllProjectsPeriodStatistics(ctx *context.Context) { | |||
| RecordBeginTime: recordBeginTime.Format(DATE_FORMAT), | |||
| PageSize: pageSize, | |||
| TotalPage: getTotalPage(total, pageSize), | |||
| TotalCount: total, | |||
| LastUpdatedTime: latestUpdatedTime, | |||
| PageRecords: models.GetRepoStatisticByRawSql(generatePageSql(beginTime, endTime, latestDate, q, orderBy, page, pageSize)), | |||
| } | |||
| @@ -102,6 +121,125 @@ func GetAllProjectsPeriodStatistics(ctx *context.Context) { | |||
| } | |||
| func ServeAllProjectsPeriodStatisticsFile(ctx *context.Context) { | |||
| recordBeginTime, err := getRecordBeginTime() | |||
| if err != nil { | |||
| log.Error("Can not get record begin time", err) | |||
| ctx.Error(http.StatusBadRequest, ctx.Tr("repo.record_begintime_get_err")) | |||
| return | |||
| } | |||
| beginTime, endTime, err := getTimePeroid(ctx, recordBeginTime) | |||
| if err != nil { | |||
| log.Error("Parameter is wrong", err) | |||
| ctx.Error(http.StatusBadRequest, ctx.Tr("repo.parameter_is_wrong")) | |||
| return | |||
| } | |||
| q := ctx.QueryTrim("q") | |||
| page := ctx.QueryInt("page") | |||
| if page <= 0 { | |||
| page = 1 | |||
| } | |||
| pageSize := 1000 | |||
| orderBy := getOrderBy(ctx) | |||
| _, latestDate, err := models.GetRepoStatLastUpdatedTime() | |||
| if err != nil { | |||
| log.Error("Can not query the last updated time.", err) | |||
| ctx.Error(http.StatusBadRequest, ctx.Tr("repo.last_update_time_error")) | |||
| return | |||
| } | |||
| countSql := generateCountSql(beginTime, endTime, latestDate, q) | |||
| total, err := models.CountRepoStatByRawSql(countSql) | |||
| if err != nil { | |||
| log.Error("Can not query total count.", err) | |||
| ctx.Error(http.StatusBadRequest, ctx.Tr("repo.total_count_get_error")) | |||
| return | |||
| } | |||
| fileName := 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()) | |||
| } | |||
| totalPage := getTotalPage(total, pageSize) | |||
| f, e := os.Create(fileName) | |||
| defer f.Close() | |||
| if e != nil { | |||
| log.Warn("Failed to create file", e) | |||
| } | |||
| writer := csv.NewWriter(f) | |||
| writer.Write(allProjectsPeroidHeader(ctx)) | |||
| for i := 0; i <= totalPage; i++ { | |||
| pageRecords := models.GetRepoStatisticByRawSql(generatePageSql(beginTime, endTime, latestDate, q, orderBy, i+1, pageSize)) | |||
| for _, record := range pageRecords { | |||
| e = writer.Write(allProjectsPeroidValues(record, ctx)) | |||
| if e != nil { | |||
| log.Warn("Failed to write record", e) | |||
| } | |||
| } | |||
| writer.Flush() | |||
| } | |||
| ctx.ServeFile(fileName) | |||
| } | |||
| 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") + "_" | |||
| } | |||
| if ctx.QueryTrim("q") != "" { | |||
| baseName = baseName + ctx.QueryTrim("q") + "_" | |||
| } | |||
| baseName = baseName + beginTime.Format(DATE_FORMAT) + "_to_" + endTime.Format(DATE_FORMAT) + "_" + strconv.FormatInt(time.Now().Unix(), 10) + ".csv" | |||
| return baseName | |||
| } | |||
| func ClearUnusedStatisticsFile() { | |||
| fileInfos, err := ioutil.ReadDir(setting.RadarMap.Path) | |||
| if err != nil { | |||
| log.Warn("can not read dir: "+setting.RadarMap.Path, err) | |||
| return | |||
| } | |||
| for _, fileInfo := range fileInfos { | |||
| if !fileInfo.IsDir() && fileInfo.ModTime().Before(time.Now().AddDate(0, 0, -1)) { | |||
| os.Remove(path.Join(setting.RadarMap.Path, fileInfo.Name())) | |||
| } | |||
| } | |||
| } | |||
| 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")} | |||
| } | |||
| func allProjectsPeroidValues(rs *models.RepoStatistic, ctx *context.Context) []string { | |||
| return []string{strconv.FormatInt(rs.RepoID, 10), rs.Name, getIsPrivateDisplay(rs.IsPrivate, ctx), strconv.FormatFloat(rs.RadarTotal, 'f', 2, 64), | |||
| strconv.FormatInt(rs.NumVisits, 10), strconv.FormatInt(rs.NumDownloads, 10), strconv.FormatInt(rs.NumPulls, 10), strconv.FormatInt(rs.NumCommits, 10), | |||
| strconv.FormatInt(rs.NumWatches, 10), strconv.FormatInt(rs.NumStars, 10), strconv.FormatInt(rs.NumForks, 10), strconv.FormatInt(rs.NumIssues, 10), | |||
| strconv.FormatInt(rs.NumClosedIssues, 10), strconv.FormatInt(rs.NumContributor, 10), | |||
| } | |||
| } | |||
| func getIsPrivateDisplay(private bool, ctx *context.Context) string { | |||
| if private { | |||
| return ctx.Tr("repos.yes") | |||
| } else { | |||
| return ctx.Tr("repos.no") | |||
| } | |||
| } | |||
| func GetProjectLatestStatistics(ctx *context.Context) { | |||
| repoId := ctx.Params(":id") | |||
| recordBeginTime, err := getRecordBeginTime() | |||
| @@ -151,6 +289,15 @@ func GetProjectLatestStatistics(ctx *context.Context) { | |||
| for _, contributor := range contributors { | |||
| mode := repository.GetCollaboratorMode(contributor.UserId) | |||
| if mode == -1 { | |||
| if contributor.IsAdmin { | |||
| mode = int(models.AccessModeAdmin) | |||
| } | |||
| if contributor.UserId == repository.OwnerID { | |||
| mode = int(models.AccessModeOwner) | |||
| } | |||
| } | |||
| pr := models.GetPullCountByUserAndRepoId(repoIdInt, contributor.UserId) | |||
| userInfo := UserInfo{ | |||
| User: contributor.Committer, | |||
| @@ -317,6 +317,12 @@ func HTTP(ctx *context.Context) { | |||
| go repo.IncreaseCloneCnt() | |||
| } | |||
| if ctx.Req.Method == "POST" { | |||
| if strings.HasSuffix(ctx.Req.URL.Path, "git-upload-pack") { | |||
| go repo.IncreaseGitCloneCnt() | |||
| } | |||
| } | |||
| w := ctx.Resp | |||
| r := ctx.Req.Request | |||
| cfg := &serviceConfig{ | |||
| @@ -93,6 +93,7 @@ func RepoStatisticDaily(date string) { | |||
| IsPrivate: repo.IsPrivate, | |||
| NumWatches: int64(repo.NumWatches), | |||
| NumStars: int64(repo.NumStars), | |||
| NumForks: int64(repo.NumForks), | |||
| NumDownloads: repo.CloneCnt, | |||
| NumComments: numComments, | |||
| NumVisits: int64(numVisits), | |||
| @@ -1,13 +1,27 @@ | |||
| package repo | |||
| import ( | |||
| "fmt" | |||
| "net/http" | |||
| "time" | |||
| "code.gitea.io/gitea/models" | |||
| "code.gitea.io/gitea/modules/context" | |||
| "code.gitea.io/gitea/modules/git" | |||
| "code.gitea.io/gitea/modules/log" | |||
| ) | |||
| func QueryUserStaticData(ctx *context.Context) { | |||
| startDate := ctx.Query("startDate") | |||
| endDate := ctx.Query("endDate") | |||
| log.Info("startDate=" + startDate + " endDate=" + endDate) | |||
| 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())) | |||
| ctx.JSON(http.StatusOK, models.QueryUserStaticData(startTime.Unix(), endTime.Unix())) | |||
| } | |||
| func TimingCountDataByDate(date string) { | |||
| t, _ := time.Parse("2006-01-02", date) | |||
| @@ -12,6 +12,7 @@ import ( | |||
| "fmt" | |||
| gotemplate "html/template" | |||
| "io/ioutil" | |||
| "net/http" | |||
| "net/url" | |||
| "path" | |||
| "strings" | |||
| @@ -31,11 +32,12 @@ import ( | |||
| ) | |||
| const ( | |||
| tplRepoEMPTY base.TplName = "repo/empty" | |||
| tplRepoHome base.TplName = "repo/home" | |||
| tplWatchers base.TplName = "repo/watchers" | |||
| tplForks base.TplName = "repo/forks" | |||
| tplMigrating base.TplName = "repo/migrating" | |||
| tplRepoEMPTY base.TplName = "repo/empty" | |||
| tplRepoHome base.TplName = "repo/home" | |||
| tplWatchers base.TplName = "repo/watchers" | |||
| tplForks base.TplName = "repo/forks" | |||
| tplMigrating base.TplName = "repo/migrating" | |||
| tplContributors base.TplName = "repo/contributors" | |||
| ) | |||
| type namedBlob struct { | |||
| @@ -575,19 +577,29 @@ func safeURL(address string) string { | |||
| } | |||
| type ContributorInfo struct { | |||
| UserInfo *models.User // nil for contributor who is not a registered user | |||
| Email string | |||
| CommitCnt int | |||
| UserInfo *models.User // nil for contributor who is not a registered user | |||
| RelAvatarLink string `json:"rel_avatar_link"` | |||
| UserName string `json:"user_name"` | |||
| Email string `json:"email"` | |||
| CommitCnt int `json:"commit_cnt"` | |||
| } | |||
| func getContributorInfo(contributorInfos []*ContributorInfo, email string) *ContributorInfo{ | |||
| type GetContributorsInfo struct { | |||
| ErrorCode int `json:"error_code"` | |||
| ErrorMsg string `json:"error_msg"` | |||
| Count int `json:"count"` | |||
| ContributorInfo []*ContributorInfo `json:"contributor_info"` | |||
| } | |||
| func getContributorInfo(contributorInfos []*ContributorInfo, email string) *ContributorInfo { | |||
| for _, c := range contributorInfos { | |||
| if strings.Compare(c.Email,email) == 0 { | |||
| if strings.Compare(c.Email, email) == 0 { | |||
| return c | |||
| } | |||
| } | |||
| return nil | |||
| } | |||
| // Home render repository home page | |||
| func Home(ctx *context.Context) { | |||
| if len(ctx.Repo.Units) > 0 { | |||
| @@ -596,35 +608,41 @@ func Home(ctx *context.Context) { | |||
| if err == nil && contributors != nil { | |||
| startTime := time.Now() | |||
| var contributorInfos []*ContributorInfo | |||
| contributorInfoHash:= make(map[string]*ContributorInfo) | |||
| contributorInfoHash := make(map[string]*ContributorInfo) | |||
| count := 0 | |||
| for _, c := range contributors { | |||
| if strings.Compare(c.Email,"") == 0 { | |||
| if count >= 25 { | |||
| continue | |||
| } | |||
| if strings.Compare(c.Email, "") == 0 { | |||
| continue | |||
| } | |||
| // get user info from committer email | |||
| user, err := models.GetUserByActivateEmail(c.Email) | |||
| if err == nil { | |||
| // committer is system user, get info through user's primary email | |||
| if existedContributorInfo,ok:=contributorInfoHash[user.Email];ok { | |||
| if existedContributorInfo, ok := contributorInfoHash[user.Email]; ok { | |||
| // existed: same primary email, different committer name | |||
| existedContributorInfo.CommitCnt += c.CommitCnt | |||
| }else{ | |||
| } else { | |||
| // new committer info | |||
| var newContributor = &ContributorInfo{ | |||
| user, user.Email,c.CommitCnt, | |||
| user, user.RelAvatarLink(), user.Name, user.Email, c.CommitCnt, | |||
| } | |||
| contributorInfos = append(contributorInfos, newContributor ) | |||
| count++ | |||
| contributorInfos = append(contributorInfos, newContributor) | |||
| contributorInfoHash[user.Email] = newContributor | |||
| } | |||
| } else { | |||
| // committer is not system user | |||
| if existedContributorInfo,ok:=contributorInfoHash[c.Email];ok { | |||
| if existedContributorInfo, ok := contributorInfoHash[c.Email]; ok { | |||
| // existed: same primary email, different committer name | |||
| existedContributorInfo.CommitCnt += c.CommitCnt | |||
| }else{ | |||
| } else { | |||
| var newContributor = &ContributorInfo{ | |||
| user, c.Email,c.CommitCnt, | |||
| user, "", "",c.Email, c.CommitCnt, | |||
| } | |||
| count++ | |||
| contributorInfos = append(contributorInfos, newContributor) | |||
| contributorInfoHash[c.Email] = newContributor | |||
| } | |||
| @@ -632,7 +650,7 @@ func Home(ctx *context.Context) { | |||
| } | |||
| ctx.Data["ContributorInfo"] = contributorInfos | |||
| var duration = time.Since(startTime) | |||
| log.Info("getContributorInfo cost: %v seconds",duration.Seconds()) | |||
| log.Info("getContributorInfo cost: %v seconds", duration.Seconds()) | |||
| } | |||
| if ctx.Repo.Repository.IsBeingCreated() { | |||
| task, err := models.GetMigratingTask(ctx.Repo.Repository.ID) | |||
| @@ -699,13 +717,13 @@ func renderLicense(ctx *context.Context) { | |||
| log.Error("failed to get license content: %v, err:%v", f, err) | |||
| continue | |||
| } | |||
| if bytes.Compare(buf,license) == 0 { | |||
| log.Info("got matched license:%v",f) | |||
| if bytes.Compare(buf, license) == 0 { | |||
| log.Info("got matched license:%v", f) | |||
| ctx.Data["LICENSE"] = f | |||
| return | |||
| } | |||
| } | |||
| log.Info("not found matched license,repo:%v",ctx.Repo.Repository.Name) | |||
| log.Info("not found matched license,repo:%v", ctx.Repo.Repository.Name) | |||
| } | |||
| func renderLanguageStats(ctx *context.Context) { | |||
| @@ -806,31 +824,31 @@ func renderCode(ctx *context.Context) { | |||
| baseGitRepo, err := git.OpenRepository(ctx.Repo.Repository.BaseRepo.RepoPath()) | |||
| defer baseGitRepo.Close() | |||
| if err != nil { | |||
| log.Error("error open baseRepo:%s",ctx.Repo.Repository.BaseRepo.RepoPath()) | |||
| log.Error("error open baseRepo:%s", ctx.Repo.Repository.BaseRepo.RepoPath()) | |||
| ctx.Data["FetchUpstreamCnt"] = -1 // minus value indicates error | |||
| }else{ | |||
| if _,error:= baseGitRepo.GetBranch(ctx.Repo.BranchName);error==nil{ | |||
| } else { | |||
| if _, error := baseGitRepo.GetBranch(ctx.Repo.BranchName); error == nil { | |||
| //base repo has the same branch, then compare between current repo branch and base repo's branch | |||
| compareUrl := ctx.Repo.BranchName + "..." + ctx.Repo.Repository.BaseRepo.OwnerName + "/" + ctx.Repo.Repository.BaseRepo.Name + ":" + ctx.Repo.BranchName | |||
| ctx.SetParams("*",compareUrl) | |||
| compareUrl := ctx.Repo.BranchName + "..." + ctx.Repo.Repository.BaseRepo.OwnerName + "/" + ctx.Repo.Repository.BaseRepo.Name + ":" + ctx.Repo.BranchName | |||
| ctx.SetParams("*", compareUrl) | |||
| ctx.Data["UpstreamSameBranchName"] = true | |||
| }else{ | |||
| } else { | |||
| //else, compare between current repo branch and base repo's default branch | |||
| compareUrl := ctx.Repo.BranchName + "..." + ctx.Repo.Repository.BaseRepo.OwnerName + "/" + ctx.Repo.Repository.BaseRepo.Name + ":" + ctx.Repo.Repository.BaseRepo.DefaultBranch | |||
| ctx.SetParams("*",compareUrl) | |||
| compareUrl := ctx.Repo.BranchName + "..." + ctx.Repo.Repository.BaseRepo.OwnerName + "/" + ctx.Repo.Repository.BaseRepo.Name + ":" + ctx.Repo.Repository.BaseRepo.DefaultBranch | |||
| ctx.SetParams("*", compareUrl) | |||
| ctx.Data["UpstreamSameBranchName"] = false | |||
| } | |||
| _, _, headGitRepo, compareInfo, _, _ := ParseCompareInfo(ctx) | |||
| defer headGitRepo.Close() | |||
| if compareInfo!= nil { | |||
| if compareInfo.Commits!=nil { | |||
| log.Info("compareInfoCommits数量:%d",compareInfo.Commits.Len()) | |||
| if compareInfo != nil { | |||
| if compareInfo.Commits != nil { | |||
| log.Info("compareInfoCommits数量:%d", compareInfo.Commits.Len()) | |||
| ctx.Data["FetchUpstreamCnt"] = compareInfo.Commits.Len() | |||
| }else{ | |||
| } else { | |||
| log.Info("compareInfo nothing different") | |||
| ctx.Data["FetchUpstreamCnt"] = 0 | |||
| } | |||
| }else{ | |||
| } else { | |||
| ctx.Data["FetchUpstreamCnt"] = -1 // minus value indicates error | |||
| } | |||
| } | |||
| @@ -898,3 +916,64 @@ func Forks(ctx *context.Context) { | |||
| ctx.HTML(200, tplForks) | |||
| } | |||
| func Contributors(ctx *context.Context) { | |||
| ctx.HTML(http.StatusOK, tplContributors) | |||
| } | |||
| func ContributorsAPI(ctx *context.Context) { | |||
| count := 0 | |||
| errorCode := 0 | |||
| errorMsg := "" | |||
| contributors, err := git.GetContributors(ctx.Repo.Repository.RepoPath()) | |||
| var contributorInfos []*ContributorInfo | |||
| if err == nil && contributors != nil { | |||
| contributorInfoHash := make(map[string]*ContributorInfo) | |||
| for _, c := range contributors { | |||
| if strings.Compare(c.Email, "") == 0 { | |||
| continue | |||
| } | |||
| // get user info from committer email | |||
| user, err := models.GetUserByActivateEmail(c.Email) | |||
| if err == nil { | |||
| // committer is system user, get info through user's primary email | |||
| if existedContributorInfo, ok := contributorInfoHash[user.Email]; ok { | |||
| // existed: same primary email, different committer name | |||
| existedContributorInfo.CommitCnt += c.CommitCnt | |||
| } else { | |||
| // new committer info | |||
| var newContributor = &ContributorInfo{ | |||
| user, user.RelAvatarLink(),user.Name, user.Email,c.CommitCnt, | |||
| } | |||
| count++ | |||
| contributorInfos = append(contributorInfos, newContributor) | |||
| contributorInfoHash[user.Email] = newContributor | |||
| } | |||
| } else { | |||
| // committer is not system user | |||
| if existedContributorInfo, ok := contributorInfoHash[c.Email]; ok { | |||
| // existed: same primary email, different committer name | |||
| existedContributorInfo.CommitCnt += c.CommitCnt | |||
| } else { | |||
| var newContributor = &ContributorInfo{ | |||
| user, "", "",c.Email,c.CommitCnt, | |||
| } | |||
| count++ | |||
| contributorInfos = append(contributorInfos, newContributor) | |||
| contributorInfoHash[c.Email] = newContributor | |||
| } | |||
| } | |||
| } | |||
| } else { | |||
| log.Error("GetContributors failed: %v", err) | |||
| errorCode = -1 | |||
| errorMsg = err.Error() | |||
| } | |||
| ctx.JSON(http.StatusOK, GetContributorsInfo{ | |||
| ErrorCode: errorCode, | |||
| ErrorMsg: errorMsg, | |||
| Count: count, | |||
| ContributorInfo: contributorInfos, | |||
| }) | |||
| } | |||
| @@ -791,9 +791,11 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| }, reqSignIn, context.RepoAssignment(), context.UnitTypes(), reqRepoAdmin, context.RepoRef()) | |||
| m.Post("/:username/:reponame/action/:action", reqSignIn, context.RepoAssignment(), context.UnitTypes(), repo.Action) | |||
| m.Get("/tool/query_user_static", repo.QueryUserStaticData) | |||
| // Grouping for those endpoints not requiring authentication | |||
| m.Group("/:username/:reponame", func() { | |||
| m.Get("/contributors", repo.Contributors) | |||
| m.Get("/contributors/list", repo.ContributorsAPI) | |||
| m.Group("/milestone", func() { | |||
| m.Get("/:id", repo.MilestoneIssuesAndPulls) | |||
| }, reqRepoIssuesOrPullsReader, context.RepoRef()) | |||
| @@ -180,8 +180,8 @@ | |||
| var _hmt = _hmt || []; | |||
| (function() { | |||
| var hm = document.createElement("script"); | |||
| hm.src = "https://hm.baidu.com/hm.js?7c4ef0a24be6109ab22e63c832ab21cf"; | |||
| var s = document.getElementsByTagName("script")[0]; | |||
| hm.src = "https://hm.baidu.com/hm.js?46149a0b61fdeddfe427ff4de63794ba"; | |||
| var s = document.getElementsByTagName("script")[0]; | |||
| s.parentNode.insertBefore(hm, s); | |||
| })(); | |||
| </script> | |||
| @@ -181,8 +181,8 @@ | |||
| var _hmt = _hmt || []; | |||
| (function() { | |||
| var hm = document.createElement("script"); | |||
| hm.src = "https://hm.baidu.com/hm.js?7c4ef0a24be6109ab22e63c832ab21cf"; | |||
| var s = document.getElementsByTagName("script")[0]; | |||
| hm.src = "https://hm.baidu.com/hm.js?46149a0b61fdeddfe427ff4de63794ba"; | |||
| var s = document.getElementsByTagName("script")[0]; | |||
| s.parentNode.insertBefore(hm, s); | |||
| })(); | |||
| </script> | |||
| @@ -39,7 +39,7 @@ | |||
| <h4 class="ui top attached header"> | |||
| <strong>{{.i18n.Tr "org.people"}}</strong> | |||
| <div class="ui right"> | |||
| <a class="text grey" href="{{.OrgLink}}/members">{{.Org.NumMembers}} {{svg "octicon-chevron-right" 16}}</a> | |||
| <a class="text grey" href="{{.OrgLink}}/members">{{.MembersTotal}} {{svg "octicon-chevron-right" 16}}</a> | |||
| </div> | |||
| <!-- {{if .IsOrganizationMember}} --> | |||
| @@ -0,0 +1,9 @@ | |||
| {{template "base/head" .}} | |||
| <div class="repository watchers"> | |||
| {{template "repo/header" .}} | |||
| <div class="ui container" id="Contributors"> | |||
| </div> | |||
| </div> | |||
| {{template "base/footer" .}} | |||
| @@ -4,7 +4,7 @@ | |||
| font-size: 1.0em; | |||
| margin-bottom: 1.0rem; | |||
| } | |||
| #contributorInfo > a:nth-child(n+25){ | |||
| #contributorInfo > a:nth-child(n+26){ | |||
| display:none; | |||
| } | |||
| #contributorInfo > a{ | |||
| @@ -329,9 +329,15 @@ | |||
| <div> | |||
| <h4 class="ui header"> | |||
| {{$lenCon := len .ContributorInfo}} | |||
| {{if lt $lenCon 25 }} | |||
| <strong>贡献者 ({{len .ContributorInfo}})</strong> | |||
| {{else}} | |||
| <strong>贡献者 ({{len .ContributorInfo}}+)</strong> | |||
| {{end}} | |||
| <div class="ui right"> | |||
| <a class="membersmore text grey" href="javascript:;">全部 {{svg "octicon-chevron-right" 16}}</a> | |||
| <a class="membersmore text grey" href="{{.RepoLink}}/contributors">全部 {{svg "octicon-chevron-right" 16}}</a> | |||
| </div> | |||
| </h4> | |||
| <div class="ui members" id="contributorInfo"> | |||
| @@ -353,10 +359,10 @@ | |||
| </div> | |||
| <script type="text/javascript"> | |||
| $(document).ready(function(){ | |||
| $(".membersmore").click(function(){ | |||
| $("#contributorInfo > a:nth-child(n+25)").show(); | |||
| }); | |||
| }); | |||
| // $(document).ready(function(){ | |||
| // $(".membersmore").click(function(){ | |||
| // $("#contributorInfo > a:nth-child(n+25)").show(); | |||
| // }); | |||
| // }); | |||
| </script> | |||
| {{template "base/footer" .}} | |||
| @@ -0,0 +1,109 @@ | |||
| <template> | |||
| <div class="ui container"> | |||
| <div class="row git-user-content"> | |||
| <h3 class="ui header"> | |||
| <div class="ui breadcrumb"> | |||
| <a class="section" href="/">代码</a> | |||
| <div class="divider"> / </div> | |||
| <div class="active section" >贡献者({{totalNum}})</div> | |||
| </div> | |||
| </h3> | |||
| <div class="ui horizontal relaxed list"> | |||
| <div class="item user-list-item" v-for="(contributor,i) in contributors_list_page" > | |||
| <a v-if="contributor.user_name" :href="AppSubUrl +'/'+ contributor.user_name"><img class="ui avatar s16 image js-popover-card" :src="contributor.rel_avatar_link"></a> | |||
| <a v-else :href="'mailto:' + contributor.email "><img class="ui avatar s16 image js-popover-card" :avatar="contributor.email"></a> | |||
| <div class="content"> | |||
| <div class="header" > | |||
| <a v-if="contributor.user_name" :href="AppSubUrl +'/'+ contributor.user_name">{{contributor.user_name}}</a> | |||
| <a v-else :href="'mailto:' + contributor.email ">{{contributor.email}}</a> | |||
| </div> | |||
| <span class="commit-btn">Commits: {{contributor.commit_cnt}}</span> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="ui container" style="margin-top:50px;text-align:center"> | |||
| <el-pagination | |||
| background | |||
| @current-change="handleCurrentChange" | |||
| :current-page="currentPage" | |||
| :page-size="pageSize" | |||
| layout="total, prev, pager, next" | |||
| :total="totalNum"> | |||
| </el-pagination> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| const {AppSubUrl, StaticUrlPrefix, csrf} = window.config; | |||
| export default { | |||
| data() { | |||
| return { | |||
| url:'', | |||
| contributors_list:[], | |||
| contributors_list_page:[], | |||
| currentPage:1, | |||
| pageSize:50, | |||
| totalNum:0, | |||
| AppSubUrl:AppSubUrl | |||
| }; | |||
| }, | |||
| methods: { | |||
| getContributorsList(){ | |||
| this.$axios.get(this.url+'/contributors/list').then((res)=>{ | |||
| this.contributors_list = res.data.contributor_info | |||
| this.totalNum = this.contributors_list.length | |||
| this.contributors_list_page = this.contributors_list.slice(0,this.pageSize) | |||
| }) | |||
| }, | |||
| handleCurrentChange(val){ | |||
| this.contributors_list_page = this.contributors_list.slice((val-1)*this.pageSize,val*this.pageSize) | |||
| }, | |||
| }, | |||
| computed:{ | |||
| }, | |||
| watch: { | |||
| }, | |||
| created(){ | |||
| this.url=document.head.querySelector("[property~='og:url'][content]").content | |||
| this.getContributorsList() | |||
| }, | |||
| updated(){ | |||
| if(document.querySelectorAll('img[avatar]').length!==0){ | |||
| window.LetterAvatar.transform() | |||
| } | |||
| } | |||
| }; | |||
| </script> | |||
| <style scoped> | |||
| /deep/ .el-pagination.is-background .el-pager li:not(.disabled).active { | |||
| background-color: #5bb973; | |||
| color: #FFF; | |||
| } | |||
| /deep/ .el-pagination.is-background .el-pager li.active { | |||
| color: #fff; | |||
| cursor: default; | |||
| } | |||
| /deep/ .el-pagination.is-background .el-pager li:hover { | |||
| color: #5bb973; | |||
| } | |||
| /deep/ .el-pagination.is-background .el-pager li:not(.disabled):hover { | |||
| color: #5bb973; | |||
| } | |||
| /deep/ .el-pagination.is-background .el-pager li:not(.disabled).active:hover { | |||
| background-color: #5bb973; | |||
| color: #FFF; | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,74 @@ | |||
| /** | |||
| * LetterAvatar | |||
| * | |||
| * Artur Heinze | |||
| * Create Letter avatar based on Initials | |||
| * based on https://gist.github.com/leecrossley/6027780 | |||
| */ | |||
| (function(w, d){ | |||
| function LetterAvatar (name, size, color) { | |||
| name = name || ''; | |||
| size = size || 60; | |||
| var colours = [ | |||
| "#1abc9c", "#2ecc71", "#3498db", "#9b59b6", "#34495e", "#16a085", "#27ae60", "#2980b9", "#8e44ad", "#2c3e50", | |||
| "#f1c40f", "#e67e22", "#e74c3c", "#00bcd4", "#95a5a6", "#f39c12", "#d35400", "#c0392b", "#bdc3c7", "#7f8c8d" | |||
| ], | |||
| nameSplit = String(name).split(' '), | |||
| initials, charIndex, colourIndex, canvas, context, dataURI; | |||
| if (nameSplit.length == 1) { | |||
| initials = nameSplit[0] ? nameSplit[0].charAt(0):'?'; | |||
| } else { | |||
| initials = nameSplit[0].charAt(0) + nameSplit[1].charAt(0); | |||
| } | |||
| if (w.devicePixelRatio) { | |||
| size = (size * w.devicePixelRatio); | |||
| } | |||
| charIndex = (initials == '?' ? 72 : initials.charCodeAt(0)) - 64; | |||
| colourIndex = charIndex % 20; | |||
| canvas = d.createElement('canvas'); | |||
| canvas.width = size; | |||
| canvas.height = size; | |||
| context = canvas.getContext("2d"); | |||
| context.fillStyle = color ? color : colours[colourIndex - 1]; | |||
| context.fillRect (0, 0, canvas.width, canvas.height); | |||
| context.font = Math.round(canvas.width/2)+"px 'Microsoft Yahei'"; | |||
| context.textAlign = "center"; | |||
| context.fillStyle = "#FFF"; | |||
| context.fillText(initials, size / 2, size / 1.5); | |||
| dataURI = canvas.toDataURL(); | |||
| canvas = null; | |||
| return dataURI; | |||
| } | |||
| LetterAvatar.transform = function() { | |||
| Array.prototype.forEach.call(d.querySelectorAll('img[avatar]'), function(img, name, color) { | |||
| name = img.getAttribute('avatar'); | |||
| color = img.getAttribute('color'); | |||
| img.src = LetterAvatar(name, img.getAttribute('width'), color); | |||
| img.removeAttribute('avatar'); | |||
| img.setAttribute('alt', name); | |||
| }); | |||
| }; | |||
| // AMD support | |||
| if (typeof define === 'function' && define.amd) { | |||
| define(function () { return LetterAvatar; }); | |||
| // CommonJS and Node.js module support. | |||
| } else if (typeof exports !== 'undefined') { | |||
| // Support Node.js specific `module.exports` (which can be a function) | |||
| if (typeof module != 'undefined' && module.exports) { | |||
| exports = module.exports = LetterAvatar; | |||
| } | |||
| // But always support CommonJS module 1.1.1 spec (`exports` cannot be a function) | |||
| exports.LetterAvatar = LetterAvatar; | |||
| } else { | |||
| window.LetterAvatar = LetterAvatar; | |||
| d.addEventListener('DOMContentLoaded', function(event) { | |||
| LetterAvatar.transform(); | |||
| }); | |||
| } | |||
| })(window, document); | |||
| @@ -4,7 +4,7 @@ | |||
| import './publicpath.js'; | |||
| import './polyfills.js'; | |||
| import './features/letteravatar.js' | |||
| import Vue from 'vue'; | |||
| import ElementUI from 'element-ui'; | |||
| import 'element-ui/lib/theme-chalk/index.css'; | |||
| @@ -37,6 +37,7 @@ import ObsUploader from './components/ObsUploader.vue'; | |||
| import EditAboutInfo from './components/EditAboutInfo.vue'; | |||
| import Images from './components/Images.vue' | |||
| import EditTopics from './components/EditTopics.vue' | |||
| import Contributors from './components/Contributors.vue' | |||
| Vue.use(ElementUI); | |||
| Vue.prototype.$axios = axios; | |||
| @@ -2969,6 +2970,7 @@ $(document).ready(async () => { | |||
| initObsUploader(); | |||
| initVueEditAbout(); | |||
| initVueEditTopic(); | |||
| initVueContributors(); | |||
| initVueImages(); | |||
| initTeamSettings(); | |||
| initCtrlEnterSubmit(); | |||
| @@ -3682,6 +3684,21 @@ function initVueEditTopic() { | |||
| render:h=>h(EditTopics) | |||
| }) | |||
| } | |||
| function initVueContributors() { | |||
| const el = document.getElementById('Contributors'); | |||
| if (!el) { | |||
| return; | |||
| } | |||
| new Vue({ | |||
| el:'#Contributors', | |||
| render:h=>h(Contributors) | |||
| }) | |||
| } | |||
| function initVueImages() { | |||
| const el = document.getElementById('images'); | |||
| console.log("el",el) | |||
| @@ -243,6 +243,48 @@ footer .column{margin-bottom:0!important; padding-bottom:0!important;} | |||
| display: inline-block; | |||
| width: 100%; | |||
| } | |||
| .git-user-content{ | |||
| margin-bottom: 16px; | |||
| word-break: break-all; | |||
| } | |||
| .row.git-user-content .ui.avatar.s16.image { | |||
| width: 50px !important; | |||
| height: 50px !important; | |||
| vertical-align: middle; | |||
| border-radius: 500rem;} | |||
| .user-list-item { | |||
| padding: 0.3em 0px !important; | |||
| margin: 15px 0 !important; | |||
| width: 220px !important; | |||
| } | |||
| .row.git-user-content .content { | |||
| margin-left: 6px; | |||
| display: inline-block; | |||
| vertical-align: top !important; | |||
| overflow: hidden; | |||
| } | |||
| .row.git-user-content .content .header { | |||
| font-weight: bold; | |||
| } | |||
| .item.user-list-item .header { | |||
| line-height: 23px !important; | |||
| font-size: 16px !important; | |||
| text-overflow: ellipsis; | |||
| overflow: hidden; | |||
| width: 145px; | |||
| white-space:nowrap; | |||
| } | |||
| .item.user-list-item .header a { | |||
| color: #587284 !important; | |||
| } | |||
| .row.git-user-content .content .commit-btn { | |||
| margin-top: 6px; | |||
| float: left; | |||
| font-size: 11px; | |||
| color: #40485b !important; | |||
| } | |||
| .dropdown-menu { | |||
| position: relative; | |||