Skip to content

User and repository statistic #7728

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
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 main chart
  • Loading branch information
pikomonde committed Apr 15, 2024
commit 5d9877e79539c2d0a8cedc9639410527ace9eaae
10 changes: 10 additions & 0 deletions conf/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,16 @@ wiki.page_already_exists = Wiki page with same name already exists.
wiki.pages = Pages
wiki.last_updated = Last updated %s

insights = Insights
insights.contributors = Contributors
insights.contributions = Contributions
insights.contributions_commits = Commits
insights.contributions_additions = Additions
insights.contributions_deletions = Deletions
insights.commits = Commits
insights.code_frequency = Code Frequency
insights.contributors_desc = Contributions to branch %s, excluding merge commits

settings = Settings
settings.options = Options
settings.collaboration = Collaboration
Expand Down
10 changes: 10 additions & 0 deletions internal/cmd/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,16 @@ func runWeb(c *cli.Context) error {

m.Get("/compare/:before([a-z0-9]{40})\\.\\.\\.:after([a-z0-9]{40})", repo.MustBeNotBare, context.RepoRef(), repo.CompareDiff)
}, ignSignIn, context.RepoAssignment())

m.Group("/:username/:reponame", func() {
// below are the group for repo insights path
m.Group("/graphs", func() {
m.Get("/contributors", repo.MustBeNotBare, repo.InsightContributorsPage)
// m.Get("/commits", repo.MustBeNotBare, repo.InsightCommitsPage)
// m.Get("/code_frequency", repo.MustBeNotBare, repo.InsightCodeFrequencyPage)
}, repo.InsightsGroup)
}, ignSignIn, context.RepoAssignment())

m.Group("/:username/:reponame", func() {
m.Get("", repo.Home)
m.Get("/stars", repo.Stars)
Expand Down
251 changes: 251 additions & 0 deletions internal/route/repo/insight.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package repo

import (
"sort"
"time"

"github.com/gogs/git-module"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/context"
"gogs.io/gogs/internal/gitutil"
)

const (
INSIGHT_CONTRIBUTORS string = "repo/insights/contributors"
INSIGHT_COMMITS string = "repo/insights/commits"
INSIGHT_CODE_FREQUENCY string = "repo/insights/code_frequency"
)

// ChartData represents the data structure for the chart.
type ChartData struct {
Labels []string `json:"labels"`
Dataset struct {
Label string `json:"label"`
Data []int `json:"data"`
BackgroundColor []string `json:"backgroundColor"`
BorderColor []string `json:"borderColor"`
BorderWidth int `json:"borderWidth"`
} `json:"dataset"`
}

// ContributorsMainChartData represents the data structure for the contributors main data.
type ContributorsMainChartData struct {
Additions ChartData `json:"additions"`
Deletions ChartData `json:"deletions"`
Commits ChartData `json:"commits"`
}

// commitData represents the data structure for the commit.
type commitData struct {
Commit *git.Commit
Additions int
Deletions int
}

// contributionType represents the type of contribution.
const (
contributionTypeCommit string = "c"
contributionTypeAddition string = "a"
contributionTypeDeletion string = "d"
)

// InsightContributorsPage represents the GET method for the contributors insight page.
func InsightContributorsPage(c *context.Context) {
c.Title("repo.insights.contributors")
c.PageIs("InsightsContributors")

// Get context query
ctxQueryFrom := c.Query("from")
ctxQueryTo := c.Query("to")
ctxQueryType := c.Query("type")

defaultBranch := c.Repo.Repository.DefaultBranch

// Get commit data for the default branch
commits, err := getCommitData(c, defaultBranch)
if err != nil {
c.Error(err, "get commits")
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)
})

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

contributorsMainChartData := getContributorsMainChartData(commits)

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 {
case contributionTypeAddition:
c.Data["ContributorsMainChartData"] = contributorsMainChartData.Additions
case contributionTypeDeletion:
c.Data["ContributorsMainChartData"] = contributorsMainChartData.Deletions
default:
c.Data["ContributorsMainChartData"] = contributorsMainChartData.Commits
}
c.Data["RequireChartJS"] = true

c.RequireAutosize()
c.Success(INSIGHT_CONTRIBUTORS)
}

// // InsightCommitsPage represents the GET method for the commits insight page.
// func InsightCommitsPage(c *context.Context) {
// c.Title("repo.insights.commits")
// c.PageIs("InsightsCommits")
// c.RequireAutosize()
// c.Success(INSIGHT_COMMITS)
// }

// // InsightCodeFrequencyPage represents the GET method for the code frequency insight page.
// func InsightCodeFrequencyPage(c *context.Context) {
// c.Title("repo.insights.code_frequency")
// c.PageIs("InsightsCodeFrequency")
// c.RequireAutosize()
// c.Success(INSIGHT_CODE_FREQUENCY)
// }

// InsightsGroup represents the handler for the insights group.
func InsightsGroup(c *context.Context) {
c.PageIs("Insights")
}

// getContributorsMainChartData returns the ContributorsMainChartData struct that
// will be used in page's template. It takes a slice of *commitData and returns a
// ContributorsMainChartData struct. The ContributorsMainChartData 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)
commitChartData := ChartData{}
additionChartData := ChartData{}
deletionChartData := ChartData{}
date := commits[len(commits)-1].Commit.Author.When

for _, commits := range commitsByDay {
commitChartData.Labels = append(commitChartData.Labels, date.Format("2006-01-02"))
additionChartData.Labels = append(additionChartData.Labels, date.Format("2006-01-02"))
deletionChartData.Labels = append(deletionChartData.Labels, date.Format("2006-01-02"))
totalDailyAddition := 0
totalDailyDeletion := 0
for _, commit := range commits {
totalDailyAddition += commit.Additions
totalDailyDeletion += commit.Deletions
}
date = date.Add(24 * time.Hour)
commitChartData.Dataset.Data = append(commitChartData.Dataset.Data, len(commits))
additionChartData.Dataset.Data = append(additionChartData.Dataset.Data, totalDailyAddition)
deletionChartData.Dataset.Data = append(deletionChartData.Dataset.Data, totalDailyDeletion)
}
commitChartData.Dataset.Label = "Commits"
additionChartData.Dataset.Label = "Additions"
deletionChartData.Dataset.Label = "Deletions"

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

// 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.
func getCommitData(c *context.Context, branch string) ([]*commitData, error) {
res := make([]*commitData, 0)

commits, err := c.Repo.GitRepo.Log(branch)
if err != nil {
c.Error(err, "get commits")
return nil, err
}
if len(commits) == 0 {
c.Error(err, "no commits")
return nil, err
}

for _, commit := range commits {
startCommitID := commit.ID.String()
if commit.ParentsCount() > 0 {
startCommit, _ := commit.ParentID(0)
startCommitID = startCommit.String()
}
endCommitID := commit.ID.String()

diff, _ := gitutil.RepoDiff(c.Repo.GitRepo,
endCommitID, conf.Git.MaxDiffFiles, conf.Git.MaxDiffLines, conf.Git.MaxDiffLineChars,
git.DiffOptions{Base: startCommitID, Timeout: time.Duration(conf.Git.Timeout.Diff) * time.Second},
)
res = append(res, &commitData{
Commit: commit,
Additions: diff.TotalAdditions(),
Deletions: diff.TotalDeletions(),
})
}

return res, nil
}

// groupCommitsByDay groups commits by the day they were made. It takes a slice of
// *commitData and returns a slice of slices of *commitData. Each inner slice
// represents a day and contains all commits made on that day. If no commits
// were made on a particular day, an empty slice is appended.
//
// Example:
// Input: []*commitData{commitA_day01, commitB_day01, commitC_day02, commitD_day04}
//
// Output: [][]*commitData{
// []*commitData{commitA_day01, commitB_day01}, []*commitData{commitC_day02},
// []*commitData{}, []*commitData{commitD_day04}}
//
// NOTE: The input commits slice is expected to be already sorted by commit time.
func groupCommitsByDay(commits []*commitData) [][]*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)
numOfDays := int(latestCommitTime.Sub(firstCommitTime)/(24*time.Hour)) + 1

commitBucketMap := make(map[string][]*commitData)
for _, commit := range commits {
dateStr := commit.Commit.Author.When.Format("2006-01-02")
commitBucketMap[dateStr] = append(commitBucketMap[dateStr], commit)
}

for i := 0; i < numOfDays; i++ {
dateStr := firstCommitTime.Add(time.Duration(i) * 24 * time.Hour).Format("2006-01-02")
if _, ok := commitBucketMap[dateStr]; ok {
res = append(res, commitBucketMap[dateStr])
} else {
res = append(res, make([]*commitData, 0))
}
}

return res
}
3 changes: 3 additions & 0 deletions templates/base/footer.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
});
</script>
{{end}}
{{if .RequireChartJS}}
<script src="{{AppSubURL}}/plugins/chart.js-4.4.2/package/dist/chart.umd.js"></script>
{{end}}

<script src="{{AppSubURL}}/js/libs/emojify-1.1.0.min.js"></script>
<script src="{{AppSubURL}}/js/libs/clipboard-2.0.4.min.js"></script>
Expand Down
5 changes: 5 additions & 0 deletions templates/repo/header.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@
<i class="octicon octicon-book"></i> {{.i18n.Tr "repo.wiki"}}
</a>
{{end}}
{{if not $.IsGuest}}
<a class="{{if .PageIsInsights}}active{{end}} item" href="{{.RepoLink}}/graphs/contributors">
<i class="octicon octicon-graph"></i> {{.i18n.Tr "repo.insights"}}
</a>
{{end}}
{{if .IsRepositoryAdmin}}
<div class="right menu">
<a class="{{if .PageIsSettings}}active{{end}} item" href="{{.RepoLink}}/settings">
Expand Down
67 changes: 67 additions & 0 deletions templates/repo/insights/contributors.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{{template "base/head" .}}
<div class="repository insights contributors">
{{template "repo/header" .}}
<div class="ui container">
<div class="ui grid">
{{template "repo/insights/navbar" .}}
<div class="twelve wide column content">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{DateFmtShort .RangeStart}} - {{DateFmtShort .RangeEnd}}
<div class="ui" style="float: right;">
<div class="ui dropdown jump item">
<span class="text">{{.i18n.Tr "repo.insights.contributions"}}</span>
<i class="dropdown icon"></i>
<div class="menu">
<a class="item" href="{{.RepoLink}}/graphs/contributors?from={{.RangeStartStr}}&to={{.RangeEndStr}}&type=c">{{.i18n.Tr "repo.insights.contributions_commits"}}</a>
<a class="item" href="{{.RepoLink}}/graphs/contributors?from={{.RangeStartStr}}&to={{.RangeEndStr}}&type=a">{{.i18n.Tr "repo.insights.contributions_additions"}}</a>
<a class="item" href="{{.RepoLink}}/graphs/contributors?from={{.RangeStartStr}}&to={{.RangeEndStr}}&type=d">{{.i18n.Tr "repo.insights.contributions_deletions"}}</a>
</div>
</div>
</div>
</h4>

<div class="ui attached segment default-branch">
<p>{{.i18n.Tr "repo.insights.contributors_desc" .DefaultBranch}}</p>
<canvas id="contributors-chart"></canvas>
<script>
function initContibutorsMainChart() {
var ctx = document.getElementById('contributors-chart').getContext('2d');
var chart = new Chart(ctx, {
type: 'line',
data: {
labels: {{.ContributorsMainChartData.Labels}},
datasets: [
{
label: {{.ContributorsMainChartData.Dataset.Label}},
data: {{.ContributorsMainChartData.Dataset.Data}},
fill: true,
pointStyle: false,
backgroundColor: [
'rgba(255, 99, 132, 0.2)'
],
borderColor: [
'rgba(255, 99, 132, 1)'
],
borderWidth: 1
}
]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
}
window.onload = initContibutorsMainChart;
</script>
</div>

</div>
</div>
</div>
</div>
{{template "base/footer" .}}
15 changes: 15 additions & 0 deletions templates/repo/insights/navbar.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div class="four wide column">
<div class="ui vertical menu">
<div class="header item">{{.i18n.Tr "repo.insights"}}</div>
<a class="{{if .PageIsInsightsContributors}}active{{end}} item" href="{{.RepoLink}}/graphs/contributors">
{{.i18n.Tr "repo.insights.contributors"}}
</a>
<a class="{{if .PageIsInsightsCommits}}active{{end}} item" href="{{.RepoLink}}/graphs/commits">
{{.i18n.Tr "repo.insights.commits"}}
</a>
<a class="{{if .PageIsSettingsHooks}}active{{end}} item" href="{{.RepoLink}}/graphs/code_frequency">
{{.i18n.Tr "repo.insights.code_frequency"}}
</a>
</div>
</div>

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy