Browse Source

Merge pull request '贡献者列表详情页展示用户名、commit次数' (#746) from fix-224 into V20211115

Reviewed-on: https://git.openi.org.cn/OpenI/aiforge/pulls/746
Reviewed-by: lewis <747342561@qq.com>
tags/v1.21.12.1
A00老虎 4 years ago
parent
commit
71c8aaf016
11 changed files with 400 additions and 57 deletions
  1. +15
    -13
      modules/setting/setting.go
  2. +1
    -0
      options/locale/locale_en-US.ini
  3. +2
    -0
      options/locale/locale_zh-CN.ini
  4. +115
    -36
      routers/repo/view.go
  5. +2
    -0
      routers/routes/routes.go
  6. +9
    -0
      templates/repo/contributors.tmpl
  7. +13
    -7
      templates/repo/home.tmpl
  8. +109
    -0
      web_src/js/components/Contributors.vue
  9. +74
    -0
      web_src/js/features/letteravatar.js
  10. +18
    -1
      web_src/js/index.js
  11. +42
    -0
      web_src/less/openi.less

+ 15
- 13
modules/setting/setting.go View File

@@ -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


+ 1
- 0
options/locale/locale_en-US.ini View File

@@ -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


+ 2
- 0
options/locale/locale_zh-CN.ini View File

@@ -220,6 +220,8 @@ show_only_public=只显示公开的

issues.in_your_repos=属于该用户项目的

contributors=贡献者

[explore]
repos=项目
users=用户


+ 115
- 36
routers/repo/view.go View File

@@ -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,
})
}

+ 2
- 0
routers/routes/routes.go View File

@@ -794,6 +794,8 @@ func RegisterRoutes(m *macaron.Macaron) {
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())


+ 9
- 0
templates/repo/contributors.tmpl View File

@@ -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" .}}

+ 13
- 7
templates/repo/home.tmpl View File

@@ -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" .}}

+ 109
- 0
web_src/js/components/Contributors.vue View File

@@ -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>

+ 74
- 0
web_src/js/features/letteravatar.js View File

@@ -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);

+ 18
- 1
web_src/js/index.js View File

@@ -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)


+ 42
- 0
web_src/less/openi.less View File

@@ -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;


Loading…
Cancel
Save