🌐 AI搜索 & 代理 主页
Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
create contributors user chart
  • Loading branch information
pikomonde committed Apr 15, 2024
commit 2ffed24c6d922c6c9577d1142611eeeced920522
3 changes: 3 additions & 0 deletions conf/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,9 @@ insights.contributions = Contributions
insights.contributions_commits = Commits
insights.contributions_additions = Additions
insights.contributions_deletions = Deletions
insights.contributor.num_of_commits = "%d commits"
insights.contributor.num_of_additions = "%d ++"
insights.contributor.num_of_deletions = "%d --"
insights.commits = Commits
insights.code_frequency = Code Frequency
insights.contributors_desc = Contributions to branch %s, excluding merge commits
Expand Down
142 changes: 117 additions & 25 deletions internal/route/repo/insight.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ type ChartData struct {
} `json:"dataset"`
}

// ContributorsMainChartData represents the data structure for the contributors main data.
type ContributorsMainChartData struct {
// ContributorChartData represents the data structure for the contributors main data.
type ContributorChartData struct {
Additions ChartData `json:"additions"`
Deletions ChartData `json:"deletions"`
Commits ChartData `json:"commits"`
Expand All @@ -46,11 +46,23 @@ type commitData struct {
Deletions int
}

// authorData represents the data structure for the author.
type authorData struct {
*userCommit
CommitsData []*commitData
ChartData ChartData
NumOfCommits int
NumOfAdditions int
NumOfDeletions int
}

// contributionType represents the type of contribution.
type contributionType string

const (
contributionTypeCommit string = "c"
contributionTypeAddition string = "a"
contributionTypeDeletion string = "d"
contributionTypeCommit contributionType = "c"
contributionTypeAddition contributionType = "a"
contributionTypeDeletion contributionType = "d"
)

// InsightContributorsPage represents the GET method for the contributors insight page.
Expand All @@ -72,41 +84,49 @@ func InsightContributorsPage(c *context.Context) {
return
}

// Get first and latest commit time
firstCommitTime := commits[len(commits)-1].Commit.Author.When
latestCommitTime := commits[0].Commit.Author.When
if ctxQueryFrom != "" && ctxQueryTo != "" {
firstCommitTime, _ = time.Parse(time.DateOnly, ctxQueryFrom)
latestCommitTime, _ = time.Parse(time.DateOnly, ctxQueryTo)
}

// sort and filter commits
sort.Slice(commits, func(i, j int) bool {
return commits[i].Commit.Author.When.After(commits[j].Commit.Author.When)
})

// Get first and latest commit time
firstCommitTime := commits[len(commits)-1].Commit.Author.When.Add(-24 * time.Hour)
latestCommitTime := commits[0].Commit.Author.When.Add(24 * time.Hour)
if ctxQueryFrom != "" && ctxQueryTo != "" {
ctxQueryFromTime, _ := time.Parse(time.DateOnly, ctxQueryFrom)
if firstCommitTime.Before(ctxQueryFromTime) {
firstCommitTime = ctxQueryFromTime
}
ctxQueryToTime, _ := time.Parse(time.DateOnly, ctxQueryTo)
if latestCommitTime.After(ctxQueryToTime) {
latestCommitTime = ctxQueryToTime
}
}

filteredCommits := make([]*commitData, 0)
for _, commit := range commits {
if commit.Commit.Author.When.After(firstCommitTime) && commit.Commit.Author.When.Before(latestCommitTime) {
if !(commit.Commit.Author.When.Before(firstCommitTime) || commit.Commit.Author.When.After(latestCommitTime)) {
filteredCommits = append(filteredCommits, commit)
}
}

contributorsMainChartData := getContributorsMainChartData(commits)
contributorChartData := getContributorChartData(filteredCommits, nil, nil)
authorDataSlice := getContributorsAuthorData(c, filteredCommits, contributionType(ctxQueryType))

c.Data["RangeStart"] = firstCommitTime
c.Data["RangeEnd"] = latestCommitTime
c.Data["RangeStartStr"] = firstCommitTime.Format(time.DateOnly)
c.Data["RangeEndStr"] = latestCommitTime.Format(time.DateOnly)
c.Data["DefaultBranch"] = defaultBranch
switch ctxQueryType {
switch contributionType(ctxQueryType) {
case contributionTypeAddition:
c.Data["ContributorsMainChartData"] = contributorsMainChartData.Additions
c.Data["ContributorsMainChartData"] = contributorChartData.Additions
case contributionTypeDeletion:
c.Data["ContributorsMainChartData"] = contributorsMainChartData.Deletions
c.Data["ContributorsMainChartData"] = contributorChartData.Deletions
default:
c.Data["ContributorsMainChartData"] = contributorsMainChartData.Commits
c.Data["ContributorsMainChartData"] = contributorChartData.Commits
}
c.Data["Authors"] = authorDataSlice
c.Data["RequireChartJS"] = true

c.RequireAutosize()
Expand Down Expand Up @@ -134,15 +154,15 @@ func InsightsGroup(c *context.Context) {
c.PageIs("Insights")
}

// getContributorsMainChartData returns the ContributorsMainChartData struct that
// getContributorChartData returns the ContributorChartData struct that
// will be used in page's template. It takes a slice of *commitData and returns a
// ContributorsMainChartData struct. The ContributorsMainChartData struct contains
// ContributorChartData struct. The ContributorChartData struct contains
// three ChartData structs: Additions, Deletions, and Commits. Each ChartData struct
// represents a dataset for the chart, with labels and data.
//
// NOTE: The input commits slice is expected to be already sorted by commit time.
func getContributorsMainChartData(commits []*commitData) ContributorsMainChartData {
commitsByDay := groupCommitsByDay(commits)
func getContributorChartData(commits []*commitData, rangeFrom, rangeTo *time.Time) ContributorChartData {
commitsByDay := groupCommitsByDay(commits, rangeFrom, rangeTo)
commitChartData := ChartData{}
additionChartData := ChartData{}
deletionChartData := ChartData{}
Expand All @@ -167,13 +187,75 @@ func getContributorsMainChartData(commits []*commitData) ContributorsMainChartDa
additionChartData.Dataset.Label = "Additions"
deletionChartData.Dataset.Label = "Deletions"

return ContributorsMainChartData{
return ContributorChartData{
Additions: additionChartData,
Deletions: deletionChartData,
Commits: commitChartData,
}
}

// getContributorsAuthorData returns a slice of authorData structs. Each authorData
// struct contains the author's name, email, and the number of commits, additions,
// and deletions made by the author.
//
// NOTE: The input commits slice is expected to be already sorted by commit time.
func getContributorsAuthorData(ctx *context.Context, commitsData []*commitData, _contributiontype contributionType) []authorData {
authorDataMap := make(map[string]authorData)

firstCommitTime := commitsData[len(commitsData)-1].Commit.Author.When.Truncate(24 * time.Hour)
latestCommitTime := commitsData[0].Commit.Author.When.Truncate(24 * time.Hour)

commits := make([]*git.Commit, 0)
for _, commitData := range commitsData {
commits = append(commits, commitData.Commit)
}
userCommits := matchUsersWithCommitEmails(ctx.Req.Context(), commits)

for i, userCommit := range userCommits {
authorEmail := userCommit.Commit.Author.Email
_authorData, ok := authorDataMap[authorEmail]
if !ok {
_authorData = authorData{}
_authorData.userCommit = userCommit
_authorData.CommitsData = make([]*commitData, 0)
}
_authorData.NumOfCommits++
_authorData.NumOfAdditions += commitsData[i].Additions
_authorData.NumOfDeletions += commitsData[i].Deletions
_authorData.CommitsData = append(_authorData.CommitsData, commitsData[i])
authorDataMap[authorEmail] = _authorData
}

authorDataSlice := make([]authorData, 0, len(authorDataMap))
for _, authorData := range authorDataMap {
authorDataSlice = append(authorDataSlice, authorData)
}

sort.Slice(authorDataSlice, func(i, j int) bool {
switch _contributiontype {
case contributionTypeAddition:
return authorDataSlice[i].NumOfCommits > authorDataSlice[j].NumOfCommits
case contributionTypeDeletion:
return authorDataSlice[i].NumOfDeletions > authorDataSlice[j].NumOfDeletions
default:
return authorDataSlice[i].NumOfCommits > authorDataSlice[j].NumOfCommits
}
})

for i, _authorData := range authorDataSlice {
switch _contributiontype {
case contributionTypeAddition:
authorDataSlice[i].ChartData = getContributorChartData(_authorData.CommitsData, &firstCommitTime, &latestCommitTime).Additions
case contributionTypeDeletion:
authorDataSlice[i].ChartData = getContributorChartData(_authorData.CommitsData, &firstCommitTime, &latestCommitTime).Deletions
default:
authorDataSlice[i].ChartData = getContributorChartData(_authorData.CommitsData, &firstCommitTime, &latestCommitTime).Commits
}
}

return authorDataSlice
}

// getCommitData returns a slice of commitData structs. Each commitData struct
// contains a *git.Commit and the number of additions and deletions made in that
// commit.
Expand Down Expand Up @@ -225,11 +307,21 @@ func getCommitData(c *context.Context, branch string) ([]*commitData, error) {
// []*commitData{}, []*commitData{commitD_day04}}
//
// NOTE: The input commits slice is expected to be already sorted by commit time.
func groupCommitsByDay(commits []*commitData) [][]*commitData {
func groupCommitsByDay(commits []*commitData, rangeFrom, rangeTo *time.Time) [][]*commitData {
res := make([][]*commitData, 0)

firstCommitTime := commits[len(commits)-1].Commit.Author.When.Truncate(24 * time.Hour)
latestCommitTime := commits[0].Commit.Author.When.Truncate(24 * time.Hour)
if rangeFrom != nil {
if firstCommitTime.After(*rangeFrom) {
firstCommitTime = rangeFrom.Truncate(24 * time.Hour)
}
}
if rangeTo != nil {
if latestCommitTime.Before(*rangeTo) {
latestCommitTime = rangeTo.Truncate(24 * time.Hour)
}
}
numOfDays := int(latestCommitTime.Sub(firstCommitTime)/(24*time.Hour)) + 1

commitBucketMap := make(map[string][]*commitData)
Expand Down
68 changes: 62 additions & 6 deletions templates/repo/insights/contributors.tmpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{{template "base/head" .}}
{{$root := .}}
<div class="repository insights contributors">
{{template "repo/header" .}}
<div class="ui container">
Expand All @@ -21,11 +22,19 @@
</div>
</h4>

<div class="ui attached segment default-branch">
<div class="ui attached segment">
<p>{{.i18n.Tr "repo.insights.contributors_desc" .DefaultBranch}}</p>
<canvas id="contributors-chart"></canvas>
<script>
function initContibutorsMainChart() {
var functionsToRunOnLoad = [];
function runFunctionsToRunOnLoad() {
for (var i = 0; i < functionsToRunOnLoad.length; i++) {
functionsToRunOnLoad[i]();
}
}
window.onload = runFunctionsToRunOnLoad;

functionsToRunOnLoad.push(function() {
var ctx = document.getElementById('contributors-chart').getContext('2d');
var chart = new Chart(ctx, {
type: 'line',
Expand All @@ -38,10 +47,10 @@
fill: true,
pointStyle: false,
backgroundColor: [
'rgba(255, 99, 132, 0.2)'
'rgba(99, 255, 132, 0.2)'
],
borderColor: [
'rgba(255, 99, 132, 1)'
'rgba(99, 255, 132, 1)'
],
borderWidth: 1
}
Expand All @@ -55,11 +64,58 @@
}
}
});
}
window.onload = initContibutorsMainChart;
});
</script>
</div>

{{range $index, $author := .Authors}}
<h4 class="ui top attached header">
{{if .User}}
<img class="ui avatar image" src="{{.User.AvatarURLPath}}" alt=""/>&nbsp;&nbsp;<a href="{{AppSubURL}}/{{.User.Name}}">{{.Author.Name}}</a>
{{else}}
<img class="ui avatar image" src="{{AvatarLink .Author.Email}}" alt=""/>&nbsp;&nbsp;{{.Author.Name}}
{{end}}
</h4>
<div class="ui attached segment">
<h5>{{$root.i18n.Tr "repo.insights.contributor.num_of_commits" .NumOfCommits}}
{{$root.i18n.Tr "repo.insights.contributor.num_of_additions" .NumOfAdditions}}
{{$root.i18n.Tr "repo.insights.contributor.num_of_deletions" .NumOfDeletions}}</h5>
<canvas id="user-contribution-chart-{{$index}}"></canvas>
<script>
functionsToRunOnLoad.push(function() {
var ctx = document.getElementById('user-contribution-chart-{{$index}}').getContext('2d');
var chart = new Chart(ctx, {
type: 'line',
data: {
labels: {{.ChartData.Labels}},
datasets: [
{
label: {{.ChartData.Dataset.Label}},
data: {{.ChartData.Dataset.Data}},
fill: true,
pointStyle: false,
backgroundColor: [
'rgba(255, 159, 64, 0.2)'
],
borderColor: [
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}
]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
});
</script>
</div>
{{end}}
</div>
</div>
</div>
Expand Down