diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go
index 6b06fef656ec6..df08c50602a64 100644
--- a/modules/git/repo_commit.go
+++ b/modules/git/repo_commit.go
@@ -5,15 +5,20 @@
package git
import (
+ "bufio"
"bytes"
+ "context"
"encoding/hex"
"fmt"
"io"
+ "os"
"strconv"
"strings"
"code.gitea.io/gitea/modules/cache"
+ "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
)
// GetBranchCommitID returns last commit ID string of given branch.
@@ -212,6 +217,98 @@ type CommitsByFileAndRangeOptions struct {
Page int
}
+// ExtendedCommitStats return the list of *api.ExtendedCommitStats for the given revision
+func (repo *Repository) ExtendedCommitStats(revision string /*, limit int */) ([]*api.ExtendedCommitStats, error) {
+ baseCommit, err := repo.GetCommit(revision)
+ if err != nil {
+ return nil, err
+ }
+ stdoutReader, stdoutWriter, err := os.Pipe()
+ if err != nil {
+ return nil, err
+ }
+ defer func() {
+ _ = stdoutReader.Close()
+ _ = stdoutWriter.Close()
+ }()
+
+ gitCmd := NewCommand(repo.Ctx, "log", "--shortstat", "--no-merges", "--pretty=format:---%n%aN%n%aE%n%as", "--reverse")
+ // AddOptionFormat("--max-count=%d", limit)
+ gitCmd.AddDynamicArguments(baseCommit.ID.String())
+
+ var extendedCommitStats []*api.ExtendedCommitStats
+ stderr := new(strings.Builder)
+ err = gitCmd.Run(&RunOpts{
+ Dir: repo.Path,
+ Stdout: stdoutWriter,
+ Stderr: stderr,
+ PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
+ _ = stdoutWriter.Close()
+ scanner := bufio.NewScanner(stdoutReader)
+ scanner.Split(bufio.ScanLines)
+
+ for scanner.Scan() {
+ line := strings.TrimSpace(scanner.Text())
+ if line != "---" {
+ continue
+ }
+ scanner.Scan()
+ authorName := strings.TrimSpace(scanner.Text())
+ scanner.Scan()
+ authorEmail := strings.TrimSpace(scanner.Text())
+ scanner.Scan()
+ date := strings.TrimSpace(scanner.Text())
+ scanner.Scan()
+ stats := strings.TrimSpace(scanner.Text())
+ if authorName == "" || authorEmail == "" || date == "" || stats == "" {
+ // FIXME: find a better way to parse the output so that we will handle this properly
+ log.Warn("Something is wrong with git log output, skipping...")
+ log.Warn("authorName: %s, authorEmail: %s, date: %s, stats: %s", authorName, authorEmail, date, stats)
+ continue
+ }
+ // 1 file changed, 1 insertion(+), 1 deletion(-)
+ fields := strings.Split(stats, ",")
+
+ commitStats := api.CommitStats{}
+ for _, field := range fields[1:] {
+ parts := strings.Split(strings.TrimSpace(field), " ")
+ value, contributionType := parts[0], parts[1]
+ amount, _ := strconv.Atoi(value)
+
+ if strings.HasPrefix(contributionType, "insertion") {
+ commitStats.Additions = amount
+ } else {
+ commitStats.Deletions = amount
+ }
+ }
+ commitStats.Total = commitStats.Additions + commitStats.Deletions
+ scanner.Scan()
+ scanner.Text() // empty line at the end
+
+ res := &api.ExtendedCommitStats{
+ Author: &api.CommitUser{
+ Identity: api.Identity{
+ Name: authorName,
+ Email: authorEmail,
+ },
+ Date: date,
+ },
+ Stats: &commitStats,
+ }
+ extendedCommitStats = append(extendedCommitStats, res)
+
+ }
+ _ = stdoutReader.Close()
+ return nil
+ },
+ })
+ if err != nil {
+ return nil, fmt.Errorf("Failed to get ContributorsCommitStats for repository.\nError: %w\nStderr: %s", err, stderr)
+ }
+
+ return extendedCommitStats, nil
+}
+
// CommitsByFileAndRange return the commits according revision file and the page
func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) {
skip := (opts.Page - 1) * setting.Git.CommitsRangeSize
diff --git a/modules/structs/repo_collaborator.go b/modules/structs/repo_collaborator.go
index 946a6ec7e78e0..553546272beae 100644
--- a/modules/structs/repo_collaborator.go
+++ b/modules/structs/repo_collaborator.go
@@ -14,3 +14,20 @@ type RepoCollaboratorPermission struct {
RoleName string `json:"role_name"`
User *User `json:"user"`
}
+
+type WeekData struct {
+ Week int64 `json:"week"` // Starting day of the week as Unix timestamp
+ Additions int `json:"additions"` // Number of additions in that week
+ Deletions int `json:"deletions"` // Number of deletions in that week
+ Commits int `json:"commits"` // Number of commits in that week
+}
+
+// ContributorData represents statistical git commit count data
+type ContributorData struct {
+ Name string `json:"name"` // Display name of the contributor
+ Login string `json:"login"` // Login name of the contributor in case it exists
+ AvatarLink string `json:"avatar_link"`
+ HomeLink string `json:"home_link"`
+ TotalCommits int64 `json:"total_commits"`
+ Weeks []*WeekData `json:"weeks"`
+}
diff --git a/modules/structs/repo_commit.go b/modules/structs/repo_commit.go
index fec7d97608d92..46ed99e197f88 100644
--- a/modules/structs/repo_commit.go
+++ b/modules/structs/repo_commit.go
@@ -58,6 +58,12 @@ type Commit struct {
Stats *CommitStats `json:"stats"`
}
+// ExtendedCommitStats contains information for commit stats with author data
+type ExtendedCommitStats struct {
+ Author *CommitUser `json:"author"`
+ Stats *CommitStats `json:"stats"`
+}
+
// CommitDateOptions store dates for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE
type CommitDateOptions struct {
// swagger:strfmt date-time
diff --git a/modules/util/dates.go b/modules/util/dates.go
new file mode 100644
index 0000000000000..d5f0b034671be
--- /dev/null
+++ b/modules/util/dates.go
@@ -0,0 +1,68 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package util
+
+import (
+ "time"
+)
+
+const layout = "2006-01-02"
+
+func ListSundaysBetween(startStr, endStr string) ([]int64, error) {
+ startDate, err := time.Parse(layout, startStr)
+ if err != nil {
+ return nil, err
+ }
+
+ endDate, err := time.Parse(layout, endStr)
+ if err != nil {
+ return nil, err
+ }
+
+ // Ensure the start date is a Sunday
+ for startDate.Weekday() != time.Sunday {
+ startDate = startDate.AddDate(0, 0, 1)
+ }
+
+ var sundays []int64
+
+ // Iterate from start date to end date and find all Sundays
+ for currentDate := startDate; currentDate.Before(endDate); currentDate = currentDate.AddDate(0, 0, 7) {
+ sundays = append(sundays, currentDate.UnixMilli())
+ }
+
+ return sundays, nil
+}
+
+func FindLastSundayBeforeDate(dateStr string) (string, error) {
+ date, err := time.Parse(layout, dateStr)
+ if err != nil {
+ return "", err
+ }
+
+ weekday := date.Weekday()
+ daysToSubtract := int(weekday) - int(time.Sunday)
+ if daysToSubtract < 0 {
+ daysToSubtract += 7
+ }
+
+ lastSunday := date.AddDate(0, 0, -daysToSubtract)
+ return lastSunday.Format(layout), nil
+}
+
+func FindFirstSundayAfterDate(dateStr string) (string, error) {
+ date, err := time.Parse(layout, dateStr)
+ if err != nil {
+ return "", err
+ }
+
+ weekday := date.Weekday()
+ daysToAdd := int(time.Sunday) - int(weekday)
+ if daysToAdd <= 0 {
+ daysToAdd += 7
+ }
+
+ firstSunday := date.AddDate(0, 0, daysToAdd)
+ return firstSunday.Format(layout), nil
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index a7a7a4f4c50f9..5c77a567d512c 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1898,6 +1898,8 @@ wiki.page_name_desc = Enter a name for this Wiki page. Some special names are: '
wiki.original_git_entry_tooltip = View original Git file instead of using friendly link.
activity = Activity
+activity.navbar.pulse = Pulse
+activity.navbar.contributors = Contributors
activity.period.filter_label = Period:
activity.period.daily = 1 day
activity.period.halfweekly = 3 days
@@ -1963,6 +1965,16 @@ activity.git_stats_and_deletions = and
activity.git_stats_deletion_1 = %d deletion
activity.git_stats_deletion_n = %d deletions
+contributors = Contributors
+contributors.contribution_type.filter_label = Contribution type:
+contributors.contribution_type.commits = Commits
+contributors.contribution_type.additions = Additions
+contributors.contribution_type.deletions = Deletions
+contributors.loading_title = Loading contributions...
+contributors.loading_title_failed = Could not load contributions
+contributors.loading_info = This might take a bit…
+contributors.component_failed_to_load = An unexpected error happened.
+
search = Search
search.search_repo = Search repository
search.type.tooltip = Search type
diff --git a/package-lock.json b/package-lock.json
index bb7be16a07371..4836f359c5300 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,8 +19,12 @@
"add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2",
"asciinema-player": "3.6.2",
+ "chart.js": "4.3.0",
+ "chartjs-adapter-dayjs-4": "1.0.4",
+ "chartjs-plugin-zoom": "2.0.1",
"clippie": "4.0.6",
"css-loader": "6.8.1",
+ "dayjs": "1.11.10",
"dropzone": "6.0.0-beta.2",
"easymde": "2.18.0",
"esbuild-loader": "4.0.2",
@@ -47,6 +51,7 @@
"uint8-to-base64": "0.2.0",
"vue": "3.3.4",
"vue-bar-graph": "2.0.0",
+ "vue-chartjs": "5.2.0",
"vue-loader": "17.3.0",
"vue3-calendar-heatmap": "2.0.5",
"webpack": "5.88.2",
@@ -1233,6 +1238,11 @@
"jsep": "^0.4.0||^1.0.0"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
+ "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
+ },
"node_modules/@mcaptcha/core-glue": {
"version": "0.1.0-alpha-5",
"resolved": "https://registry.npmjs.org/@mcaptcha/core-glue/-/core-glue-0.1.0-alpha-5.tgz",
@@ -3158,6 +3168,40 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/chart.js": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.3.0.tgz",
+ "integrity": "sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g==",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=7"
+ }
+ },
+ "node_modules/chartjs-adapter-dayjs-4": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/chartjs-adapter-dayjs-4/-/chartjs-adapter-dayjs-4-1.0.4.tgz",
+ "integrity": "sha512-yy9BAYW4aNzPVrCWZetbILegTRb7HokhgospPoC3b5iZ5qdlqNmXts2KdSp6AqnjkPAp/YWyHDxLvIvwt5x81w==",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "chart.js": ">=4.0.1",
+ "dayjs": "^1.9.7"
+ }
+ },
+ "node_modules/chartjs-plugin-zoom": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.0.1.tgz",
+ "integrity": "sha512-ogOmLu6e+Q7E1XWOCOz9YwybMslz9qNfGV2a+qjfmqJYpsw5ZMoRHZBUyW+NGhkpQ5PwwPA/+rikHpBZb7PZuA==",
+ "dependencies": {
+ "hammerjs": "^2.0.8"
+ },
+ "peerDependencies": {
+ "chart.js": ">=3.2.0"
+ }
+ },
"node_modules/check-error": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
@@ -5791,6 +5835,14 @@
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.2.tgz",
"integrity": "sha512-EkYnpG8qHgYBFAwsgsGEqvT1WUidX0tt/ijepx7z8EUJHElykg91RvW1XbkT59T0gZzzszOpjQv7SE41XuIXyQ=="
},
+ "node_modules/hammerjs": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
+ "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/hard-rejection": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
@@ -11420,6 +11472,15 @@
"vue": "^3.2.37"
}
},
+ "node_modules/vue-chartjs": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.2.0.tgz",
+ "integrity": "sha512-d3zpKmGZr2OWHQ1xmxBcAn5ShTG917+/UCLaSpaCDDqT0U7DBsvFzTs69ZnHCgKoXT55GZDW8YEj9Av+dlONLA==",
+ "peerDependencies": {
+ "chart.js": "^4.1.1",
+ "vue": "^3.0.0-0 || ^2.7.0"
+ }
+ },
"node_modules/vue-eslint-parser": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz",
diff --git a/package.json b/package.json
index c95ca6dbbc8fb..199fda86e174c 100644
--- a/package.json
+++ b/package.json
@@ -18,8 +18,12 @@
"add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2",
"asciinema-player": "3.6.2",
+ "chart.js": "4.3.0",
+ "chartjs-adapter-dayjs-4": "1.0.4",
+ "chartjs-plugin-zoom": "2.0.1",
"clippie": "4.0.6",
"css-loader": "6.8.1",
+ "dayjs": "1.11.10",
"dropzone": "6.0.0-beta.2",
"easymde": "2.18.0",
"esbuild-loader": "4.0.2",
@@ -46,6 +50,7 @@
"uint8-to-base64": "0.2.0",
"vue": "3.3.4",
"vue-bar-graph": "2.0.0",
+ "vue-chartjs": "5.2.0",
"vue-loader": "17.3.0",
"vue3-calendar-heatmap": "2.0.5",
"webpack": "5.88.2",
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 61658d213b365..e565607d714c2 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1234,6 +1234,7 @@ func Routes() *web.Route {
m.Get("/statuses", repo.GetCommitStatusesByRef)
}, context.ReferencesGitRepo())
}, reqRepoReader(unit.TypeCode))
+ m.Get("/contributors", context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode), repo.GetAllContributorsStats)
m.Group("/git", func() {
m.Group("/commits", func() {
m.Get("/{sha}", repo.GetSingleCommit)
diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go
index 2538bcdbc628d..529e164649f66 100644
--- a/routers/api/v1/repo/collaborators.go
+++ b/routers/api/v1/repo/collaborators.go
@@ -14,6 +14,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
repo_module "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
@@ -359,3 +360,54 @@ func GetAssignees(ctx *context.APIContext) {
}
ctx.JSON(http.StatusOK, convert.ToUsers(ctx, ctx.Doer, assignees))
}
+
+// GetAllContributorsStats retrieves a map of contributors along with their weekly commit statistics
+func GetAllContributorsStats(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/contributors repository repoGetAllContributorsStats
+ // ---
+ // summary: Get a map of all contributors along with their weekly commit statistics from a repository
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: owner
+ // in: path
+ // description: owner of the repo
+ // type: string
+ // required: true
+ // - name: repo
+ // in: path
+ // description: name of the repo
+ // type: string
+ // required: true
+ // - name: sha
+ // in: query
+ // description: SHA or branch to start listing commits from (usually 'master')
+ // type: string
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ContributorDataMap"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "409":
+ // "$ref": "#/responses/EmptyRepository"
+
+ if ctx.Repo.Repository.IsEmpty {
+ ctx.JSON(http.StatusConflict, api.APIError{
+ Message: "Git Repository is empty.",
+ URL: setting.API.SwaggerURL,
+ })
+ return
+ }
+
+ sha := ctx.FormString("sha")
+
+ if len(sha) == 0 {
+ sha = ctx.Repo.Repository.DefaultBranch
+ }
+
+ if contributorStats, err := repo_service.GetContributorStats(ctx, ctx.Repo.Repository, sha); err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetContributorStats", err)
+ } else {
+ ctx.JSON(http.StatusOK, &contributorStats)
+ }
+}
diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go
index 3e23aa4d5a5ac..a4e35b260f033 100644
--- a/routers/api/v1/swagger/repo.go
+++ b/routers/api/v1/swagger/repo.go
@@ -253,6 +253,13 @@ type swaggerCommitList struct {
Body []api.Commit `json:"body"`
}
+// ContributorDataMap
+// swagger:response ContributorDataMap
+type swaggerContributorDataMap struct {
+ // in: body
+ Body map[string]*api.ContributorData `json:"body"`
+}
+
// ChangedFileList
// swagger:response ChangedFileList
type swaggerChangedFileList struct {
diff --git a/routers/web/repo/activity.go b/routers/web/repo/activity.go
index 3d030edaca065..53742e1a29265 100644
--- a/routers/web/repo/activity.go
+++ b/routers/web/repo/activity.go
@@ -22,6 +22,9 @@ func Activity(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.activity")
ctx.Data["PageIsActivity"] = true
+ ctx.Data["PageIsPulse"] = true
+ ctx.Data["PageIsContributors"] = false
+
ctx.Data["Period"] = ctx.Params("period")
timeUntil := time.Now()
diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go
new file mode 100644
index 0000000000000..b09ca599c780e
--- /dev/null
+++ b/routers/web/repo/contributors.go
@@ -0,0 +1,40 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ contributors_service "code.gitea.io/gitea/services/repository"
+)
+
+const (
+ tplContributors base.TplName = "repo/activity"
+)
+
+// Contributors render the page to show repository contributors graph
+func Contributors(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.contributors")
+
+ ctx.Data["PageIsActivity"] = true
+ ctx.Data["PageIsPulse"] = false
+ ctx.Data["PageIsContributors"] = true
+
+ ctx.PageData["contributionType"] = "commits"
+
+ ctx.PageData["repoLink"] = ctx.Repo.RepoLink
+
+ ctx.HTML(http.StatusOK, tplContributors)
+}
+
+// ContributorsData renders JSON of contributors along with their weekly commit statistics
+func ContributorsData(ctx *context.Context) {
+ if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+ ctx.ServerError("GetContributorStats", err)
+ } else {
+ ctx.JSON(http.StatusOK, contributorStats)
+ }
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index f8b745fb10b55..0d3eb25828bb9 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1354,6 +1354,10 @@ func registerRoutes(m *web.Route) {
m.Group("/activity", func() {
m.Get("", repo.Activity)
m.Get("/{period}", repo.Activity)
+ m.Group("/contributors", func() {
+ m.Get("", repo.Contributors)
+ m.Get("/data", repo.ContributorsData)
+ })
}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases))
m.Group("/activity_author_data", func() {
diff --git a/services/repository/contributors_graph.go b/services/repository/contributors_graph.go
new file mode 100644
index 0000000000000..3ee388114eb58
--- /dev/null
+++ b/services/repository/contributors_graph.go
@@ -0,0 +1,114 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ api "code.gitea.io/gitea/modules/structs"
+ util "code.gitea.io/gitea/modules/util"
+)
+
+// CreateWeeks converts list of sundays to list of *api.WeekData
+func CreateWeeks(sundays []int64) []*api.WeekData {
+ var weeks []*api.WeekData
+ for _, week := range sundays {
+ weeks = append(weeks, &api.WeekData{
+ Week: week,
+ Additions: 0,
+ Deletions: 0,
+ Commits: 0,
+ },
+ )
+ }
+ return weeks
+}
+
+// GetContributorStats returns contributors stats for git commits for given revision or default branch
+func GetContributorStats(ctx context.Context, repo *repo_model.Repository, revision string) (map[string]*api.ContributorData, error) {
+ gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath())
+ if err != nil {
+ return nil, fmt.Errorf("OpenRepository: %w", err)
+ }
+ defer closer.Close()
+
+ if len(revision) == 0 {
+ revision = repo.DefaultBranch
+ }
+ extendedCommitStats, err := gitRepo.ExtendedCommitStats(revision)
+ if err != nil {
+ return nil, fmt.Errorf("ExtendedCommitStats: %w", err)
+ }
+
+ layout := "2006-01-02"
+ initialCommitDate := extendedCommitStats[0].Author.Date
+
+ startingSunday, _ := util.FindLastSundayBeforeDate(initialCommitDate)
+ endingSunday, _ := util.FindFirstSundayAfterDate(time.Now().Format(layout))
+
+ sundays, _ := util.ListSundaysBetween(startingSunday, endingSunday)
+
+ unknownUserAvatarLink := user_model.NewGhostUser().AvatarLink(ctx)
+ contributorsCommitStats := make(map[string]*api.ContributorData)
+ contributorsCommitStats["total"] = &api.ContributorData{
+ Name: "Total",
+ AvatarLink: unknownUserAvatarLink,
+ Weeks: CreateWeeks(sundays),
+ }
+ total := contributorsCommitStats["total"]
+
+ for _, v := range extendedCommitStats {
+ if len(v.Author.Email) == 0 {
+ continue
+ }
+ if _, ok := contributorsCommitStats[v.Author.Email]; !ok {
+ u, _ := user_model.GetUserByEmail(ctx, v.Author.Email)
+ if u == nil {
+ contributorsCommitStats[v.Author.Email] = &api.ContributorData{
+ Name: v.Author.Name,
+ AvatarLink: unknownUserAvatarLink,
+ Weeks: CreateWeeks(sundays),
+ }
+ } else {
+ contributorsCommitStats[v.Author.Email] = &api.ContributorData{
+ Name: u.DisplayName(),
+ Login: u.LowerName,
+ AvatarLink: u.AvatarLink(ctx),
+ HomeLink: u.HomeLink(),
+ Weeks: CreateWeeks(sundays),
+ }
+ }
+ }
+ // Update user statistics
+ user := contributorsCommitStats[v.Author.Email]
+ startingOfWeek, _ := util.FindLastSundayBeforeDate(v.Author.Date)
+
+ val, _ := time.Parse(layout, startingOfWeek)
+ startingSundayParsed, _ := time.Parse(layout, startingSunday)
+ idx := int(val.Sub(startingSundayParsed).Hours()/24) / 7
+
+ if idx >= 0 && idx < len(user.Weeks) {
+ user.Weeks[idx].Additions += v.Stats.Additions
+ user.Weeks[idx].Deletions += v.Stats.Deletions
+ user.Weeks[idx].Commits++
+ user.TotalCommits++
+
+ // Update overall statistics
+ total.Weeks[idx].Additions += v.Stats.Additions
+ total.Weeks[idx].Deletions += v.Stats.Deletions
+ total.Weeks[idx].Commits++
+ total.TotalCommits++
+ } else {
+ log.Warn("date range of the commit is not between starting date and ending date, skipping...")
+ }
+ }
+
+ return contributorsCommitStats, nil
+}
diff --git a/templates/repo/activity.tmpl b/templates/repo/activity.tmpl
index fe592c00005d6..960083d2fbd7f 100644
--- a/templates/repo/activity.tmpl
+++ b/templates/repo/activity.tmpl
@@ -1,235 +1,15 @@
{{template "base/head" .}}
{{template "repo/header" .}}
-
-
-
-
- {{if (or (.Permission.CanRead $.UnitTypeIssues) (.Permission.CanRead $.UnitTypePullRequests))}}
-
-
- {{if .Permission.CanRead $.UnitTypePullRequests}}
-
- {{if gt .Activity.ActivePRCount 0}}
-
- {{else}}
-
- {{end}}
- {{ctx.Locale.TrN .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n" .Activity.ActivePRCount | Safe}}
-
- {{end}}
- {{if .Permission.CanRead $.UnitTypeIssues}}
-
- {{if gt .Activity.ActiveIssueCount 0}}
-
- {{else}}
-
- {{end}}
- {{ctx.Locale.TrN .Activity.ActiveIssueCount "repo.activity.active_issues_count_1" "repo.activity.active_issues_count_n" .Activity.ActiveIssueCount | Safe}}
-
- {{end}}
+
+
+ {{template "repo/navbar" .}}
-
- {{if .Permission.CanRead $.UnitTypePullRequests}}
-
- {{svg "octicon-git-pull-request"}} {{.Activity.MergedPRCount}}
- {{ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.merged_prs_count_1" "repo.activity.merged_prs_count_n"}}
-
-
- {{svg "octicon-git-branch"}} {{.Activity.OpenedPRCount}}
- {{ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.opened_prs_count_1" "repo.activity.opened_prs_count_n"}}
-
- {{end}}
- {{if .Permission.CanRead $.UnitTypeIssues}}
-
- {{svg "octicon-issue-closed"}} {{.Activity.ClosedIssueCount}}
- {{ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.closed_issues_count_1" "repo.activity.closed_issues_count_n"}}
-
-
- {{svg "octicon-issue-opened"}} {{.Activity.OpenedIssueCount}}
- {{ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.new_issues_count_1" "repo.activity.new_issues_count_n"}}
-
- {{end}}
+
+ {{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}}
+ {{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}}
- {{end}}
-
- {{if .Permission.CanRead $.UnitTypeCode}}
- {{if eq .Activity.Code.CommitCountInAllBranches 0}}
-
-
-
- {{end}}
- {{if gt .Activity.Code.CommitCountInAllBranches 0}}
-
-
- {{ctx.Locale.Tr "repo.activity.git_stats_exclude_merges"}}
- {{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_author_1" "repo.activity.git_stats_author_n" .Activity.Code.AuthorCount}}
- {{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_pushed_1" "repo.activity.git_stats_pushed_n"}}
- {{ctx.Locale.TrN .Activity.Code.CommitCount "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCount}}
- {{ctx.Locale.Tr "repo.activity.git_stats_push_to_branch" .Repository.DefaultBranch}}
- {{ctx.Locale.TrN .Activity.Code.CommitCountInAllBranches "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCountInAllBranches}}
- {{ctx.Locale.Tr "repo.activity.git_stats_push_to_all_branches"}}
- {{ctx.Locale.Tr "repo.activity.git_stats_on_default_branch" .Repository.DefaultBranch}}
- {{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_file_1" "repo.activity.git_stats_file_n" .Activity.Code.ChangedFiles}}
- {{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_files_changed_1" "repo.activity.git_stats_files_changed_n"}}
- {{ctx.Locale.Tr "repo.activity.git_stats_additions"}}
- {{ctx.Locale.TrN .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n" .Activity.Code.Additions}}
- {{ctx.Locale.Tr "repo.activity.git_stats_and_deletions"}}
- {{ctx.Locale.TrN .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n" .Activity.Code.Deletions}}.
-
-
-
- {{end}}
- {{end}}
-
- {{if gt .Activity.PublishedReleaseCount 0}}
-
- {{svg "octicon-tag" 16 "gt-mr-3"}}
- {{ctx.Locale.Tr "repo.activity.title.releases_published_by"
- (ctx.Locale.TrN .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n" .Activity.PublishedReleaseCount)
- (ctx.Locale.TrN .Activity.PublishedReleaseAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.PublishedReleaseAuthorCount)
- }}
-
-
- {{range .Activity.PublishedReleases}}
-
- {{ctx.Locale.Tr "repo.activity.published_release_label"}}
- {{.TagName}}
- {{if not .IsTag}}
- {{.Title | RenderEmoji $.Context}}
- {{end}}
- {{TimeSinceUnix .CreatedUnix ctx.Locale}}
-
- {{end}}
-
- {{end}}
-
- {{if gt .Activity.MergedPRCount 0}}
-
- {{svg "octicon-git-pull-request" 16 "gt-mr-3"}}
- {{ctx.Locale.Tr "repo.activity.title.prs_merged_by"
- (ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.MergedPRCount)
- (ctx.Locale.TrN .Activity.MergedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.MergedPRAuthorCount)
- }}
-
-
- {{end}}
-
- {{if gt .Activity.OpenedPRCount 0}}
-
- {{svg "octicon-git-branch" 16 "gt-mr-3"}}
- {{ctx.Locale.Tr "repo.activity.title.prs_opened_by"
- (ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.OpenedPRCount)
- (ctx.Locale.TrN .Activity.OpenedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedPRAuthorCount)
- }}
-
-
- {{range .Activity.OpenedPRs}}
-
- {{ctx.Locale.Tr "repo.activity.opened_prs_label"}}
- #{{.Index}} {{.Issue.Title | RenderEmoji $.Context}}
- {{TimeSinceUnix .Issue.CreatedUnix ctx.Locale}}
-
- {{end}}
-
- {{end}}
-
- {{if gt .Activity.ClosedIssueCount 0}}
-
- {{svg "octicon-issue-closed" 16 "gt-mr-3"}}
- {{ctx.Locale.Tr "repo.activity.title.issues_closed_from"
- (ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.ClosedIssueCount)
- (ctx.Locale.TrN .Activity.ClosedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.ClosedIssueAuthorCount)
- }}
-
-
- {{range .Activity.ClosedIssues}}
-
- {{ctx.Locale.Tr "repo.activity.closed_issue_label"}}
- #{{.Index}} {{.Title | RenderEmoji $.Context}}
- {{TimeSinceUnix .ClosedUnix ctx.Locale}}
-
- {{end}}
-
- {{end}}
-
- {{if gt .Activity.OpenedIssueCount 0}}
-
- {{svg "octicon-issue-opened" 16 "gt-mr-3"}}
- {{ctx.Locale.Tr "repo.activity.title.issues_created_by"
- (ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.OpenedIssueCount)
- (ctx.Locale.TrN .Activity.OpenedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedIssueAuthorCount)
- }}
-
-
- {{range .Activity.OpenedIssues}}
-
- {{ctx.Locale.Tr "repo.activity.new_issue_label"}}
- #{{.Index}} {{.Title | RenderEmoji $.Context}}
- {{TimeSinceUnix .CreatedUnix ctx.Locale}}
-
- {{end}}
-
- {{end}}
-
- {{if gt .Activity.UnresolvedIssueCount 0}}
-
- {{svg "octicon-comment-discussion" 16 "gt-mr-3"}}
- {{ctx.Locale.TrN .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n" .Activity.UnresolvedIssueCount}}
-
-
- {{end}}
{{template "base/footer" .}}
+
diff --git a/templates/repo/contributors.tmpl b/templates/repo/contributors.tmpl
new file mode 100644
index 0000000000000..49a251c1f9692
--- /dev/null
+++ b/templates/repo/contributors.tmpl
@@ -0,0 +1,13 @@
+{{if .Permission.CanRead $.UnitTypeCode}}
+
+
+{{end}}
diff --git a/templates/repo/navbar.tmpl b/templates/repo/navbar.tmpl
new file mode 100644
index 0000000000000..d24372825631b
--- /dev/null
+++ b/templates/repo/navbar.tmpl
@@ -0,0 +1,8 @@
+
diff --git a/templates/repo/pulse.tmpl b/templates/repo/pulse.tmpl
new file mode 100644
index 0000000000000..75f49c2a74526
--- /dev/null
+++ b/templates/repo/pulse.tmpl
@@ -0,0 +1,227 @@
+
+
+{{if (or (.Permission.CanRead $.UnitTypeIssues) (.Permission.CanRead $.UnitTypePullRequests))}}
+
+
+ {{if .Permission.CanRead $.UnitTypePullRequests}}
+
+ {{if gt .Activity.ActivePRCount 0}}
+
+ {{else}}
+
+ {{end}}
+ {{ctx.Locale.TrN .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n" .Activity.ActivePRCount | Safe}}
+
+ {{end}}
+ {{if .Permission.CanRead $.UnitTypeIssues}}
+
+ {{if gt .Activity.ActiveIssueCount 0}}
+
+ {{else}}
+
+ {{end}}
+ {{ctx.Locale.TrN .Activity.ActiveIssueCount "repo.activity.active_issues_count_1" "repo.activity.active_issues_count_n" .Activity.ActiveIssueCount | Safe}}
+
+ {{end}}
+
+
+{{end}}
+
+{{if .Permission.CanRead $.UnitTypeCode}}
+ {{if eq .Activity.Code.CommitCountInAllBranches 0}}
+
+
+
+ {{end}}
+ {{if gt .Activity.Code.CommitCountInAllBranches 0}}
+
+
+ {{ctx.Locale.Tr "repo.activity.git_stats_exclude_merges"}}
+ {{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_author_1" "repo.activity.git_stats_author_n" .Activity.Code.AuthorCount}}
+ {{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_pushed_1" "repo.activity.git_stats_pushed_n"}}
+ {{ctx.Locale.TrN .Activity.Code.CommitCount "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCount}}
+ {{ctx.Locale.Tr "repo.activity.git_stats_push_to_branch" .Repository.DefaultBranch}}
+ {{ctx.Locale.TrN .Activity.Code.CommitCountInAllBranches "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCountInAllBranches}}
+ {{ctx.Locale.Tr "repo.activity.git_stats_push_to_all_branches"}}
+ {{ctx.Locale.Tr "repo.activity.git_stats_on_default_branch" .Repository.DefaultBranch}}
+ {{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_file_1" "repo.activity.git_stats_file_n" .Activity.Code.ChangedFiles}}
+ {{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_files_changed_1" "repo.activity.git_stats_files_changed_n"}}
+ {{ctx.Locale.Tr "repo.activity.git_stats_additions"}}
+ {{ctx.Locale.TrN .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n" .Activity.Code.Additions}}
+ {{ctx.Locale.Tr "repo.activity.git_stats_and_deletions"}}
+ {{ctx.Locale.TrN .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n" .Activity.Code.Deletions}}.
+
+
+
+ {{end}}
+{{end}}
+
+{{if gt .Activity.PublishedReleaseCount 0}}
+
+ {{svg "octicon-tag" 16 "gt-mr-3"}}
+ {{ctx.Locale.Tr "repo.activity.title.releases_published_by"
+ (ctx.Locale.TrN .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n" .Activity.PublishedReleaseCount)
+ (ctx.Locale.TrN .Activity.PublishedReleaseAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.PublishedReleaseAuthorCount)
+ }}
+
+
+ {{range .Activity.PublishedReleases}}
+
+ {{ctx.Locale.Tr "repo.activity.published_release_label"}}
+ {{.TagName}}
+ {{if not .IsTag}}
+ {{.Title | RenderEmoji $.Context}}
+ {{end}}
+ {{TimeSinceUnix .CreatedUnix ctx.Locale}}
+
+ {{end}}
+
+{{end}}
+
+{{if gt .Activity.MergedPRCount 0}}
+
+ {{svg "octicon-git-pull-request" 16 "gt-mr-3"}}
+ {{ctx.Locale.Tr "repo.activity.title.prs_merged_by"
+ (ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.MergedPRCount)
+ (ctx.Locale.TrN .Activity.MergedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.MergedPRAuthorCount)
+ }}
+
+
+{{end}}
+
+{{if gt .Activity.OpenedPRCount 0}}
+
+ {{svg "octicon-git-branch" 16 "gt-mr-3"}}
+ {{ctx.Locale.Tr "repo.activity.title.prs_opened_by"
+ (ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.OpenedPRCount)
+ (ctx.Locale.TrN .Activity.OpenedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedPRAuthorCount)
+ }}
+
+
+ {{range .Activity.OpenedPRs}}
+
+ {{ctx.Locale.Tr "repo.activity.opened_prs_label"}}
+ #{{.Index}} {{.Issue.Title | RenderEmoji $.Context}}
+ {{TimeSinceUnix .Issue.CreatedUnix ctx.Locale}}
+
+ {{end}}
+
+{{end}}
+
+{{if gt .Activity.ClosedIssueCount 0}}
+
+ {{svg "octicon-issue-closed" 16 "gt-mr-3"}}
+ {{ctx.Locale.Tr "repo.activity.title.issues_closed_from"
+ (ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.ClosedIssueCount)
+ (ctx.Locale.TrN .Activity.ClosedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.ClosedIssueAuthorCount)
+ }}
+
+
+ {{range .Activity.ClosedIssues}}
+
+ {{ctx.Locale.Tr "repo.activity.closed_issue_label"}}
+ #{{.Index}} {{.Title | RenderEmoji $.Context}}
+ {{TimeSinceUnix .ClosedUnix ctx.Locale}}
+
+ {{end}}
+
+{{end}}
+
+{{if gt .Activity.OpenedIssueCount 0}}
+
+ {{svg "octicon-issue-opened" 16 "gt-mr-3"}}
+ {{ctx.Locale.Tr "repo.activity.title.issues_created_by"
+ (ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.OpenedIssueCount)
+ (ctx.Locale.TrN .Activity.OpenedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedIssueAuthorCount)
+ }}
+
+
+ {{range .Activity.OpenedIssues}}
+
+ {{ctx.Locale.Tr "repo.activity.new_issue_label"}}
+ #{{.Index}} {{.Title | RenderEmoji $.Context}}
+ {{TimeSinceUnix .CreatedUnix ctx.Locale}}
+
+ {{end}}
+
+{{end}}
+
+{{if gt .Activity.UnresolvedIssueCount 0}}
+
+ {{svg "octicon-comment-discussion" 16 "gt-mr-3"}}
+ {{ctx.Locale.TrN .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n" .Activity.UnresolvedIssueCount}}
+
+
+{{end}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 75a45dc68ac56..24c4964597026 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -4847,6 +4847,51 @@
}
}
},
+ "/repos/{owner}/{repo}/contributors": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Get a map of all contributors along with their weekly commit statistics from a repository",
+ "operationId": "repoGetAllContributorsStats",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "SHA or branch to start listing commits from (usually 'master')",
+ "name": "sha",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/ContributorDataMap"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ },
+ "409": {
+ "$ref": "#/responses/EmptyRepository"
+ }
+ }
+ }
+ },
"/repos/{owner}/{repo}/diffpatch": {
"post": {
"consumes": [
@@ -17491,6 +17536,41 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "ContributorData": {
+ "description": "ContributorData represents statistical git commit count data",
+ "type": "object",
+ "properties": {
+ "avatar_link": {
+ "type": "string",
+ "x-go-name": "AvatarLink"
+ },
+ "home_link": {
+ "type": "string",
+ "x-go-name": "HomeLink"
+ },
+ "login": {
+ "type": "string",
+ "x-go-name": "Login"
+ },
+ "name": {
+ "type": "string",
+ "x-go-name": "Name"
+ },
+ "total_commits": {
+ "type": "integer",
+ "format": "int64",
+ "x-go-name": "TotalCommits"
+ },
+ "weeks": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/WeekData"
+ },
+ "x-go-name": "Weeks"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"CreateAccessTokenOption": {
"description": "CreateAccessTokenOption options when create access token",
"type": "object",
@@ -22955,6 +23035,32 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "WeekData": {
+ "type": "object",
+ "properties": {
+ "additions": {
+ "type": "integer",
+ "format": "int64",
+ "x-go-name": "Additions"
+ },
+ "commits": {
+ "type": "integer",
+ "format": "int64",
+ "x-go-name": "Commits"
+ },
+ "deletions": {
+ "type": "integer",
+ "format": "int64",
+ "x-go-name": "Deletions"
+ },
+ "week": {
+ "type": "integer",
+ "format": "int64",
+ "x-go-name": "Week"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"WikiCommit": {
"description": "WikiCommit page commit/revision",
"type": "object",
@@ -23267,6 +23373,15 @@
"$ref": "#/definitions/ContentsResponse"
}
},
+ "ContributorDataMap": {
+ "description": "ContributorDataMap",
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/definitions/ContributorData"
+ }
+ }
+ },
"CronList": {
"description": "CronList",
"schema": {
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
new file mode 100644
index 0000000000000..27738a8715c2d
--- /dev/null
+++ b/web_src/js/components/RepoContributors.vue
@@ -0,0 +1,385 @@
+
+
+
+
+
+
+
+
+
+ {{ locale.loadingInfo }}
+
+
+
+ {{ errorText }}
+
+
+
+
+
+
+
+
+
diff --git a/web_src/js/features/contributors.js b/web_src/js/features/contributors.js
new file mode 100644
index 0000000000000..66185ac315a19
--- /dev/null
+++ b/web_src/js/features/contributors.js
@@ -0,0 +1,28 @@
+import {createApp} from 'vue';
+
+export async function initRepoContributors() {
+ const el = document.getElementById('repo-contributors-chart');
+ if (!el) return;
+
+ const {default: RepoContributors} = await import(/* webpackChunkName: "contributors-graph" */'../components/RepoContributors.vue');
+ try {
+ const View = createApp(RepoContributors, {
+ locale: {
+ filterLabel: el.getAttribute('data-locale-filter-label'),
+ contributionType: {
+ commits: el.getAttribute('data-locale-contribution-type-commits'),
+ additions: el.getAttribute('data-locale-contribution-type-additions'),
+ deletions: el.getAttribute('data-locale-contribution-type-deletions'),
+ },
+
+ loadingTitle: el.getAttribute('data-locale-loading-title'),
+ loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
+ loadingInfo: el.getAttribute('data-locale-loading-info'),
+ }
+ });
+ View.mount(el);
+ } catch (err) {
+ console.error('RepoContributors failed to load', err);
+ el.textContent = el.getAttribute('data-locale-component-failed-to-load');
+ }
+}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 4713618506b0c..078f9fc9df415 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -83,6 +83,7 @@ import {initGiteaFomantic} from './modules/fomantic.js';
import {onDomReady} from './utils/dom.js';
import {initRepoIssueList} from './features/repo-issue-list.js';
import {initCommonIssueListQuickGoto} from './features/common-issue-list.js';
+import {initRepoContributors} from './features/contributors.js';
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
import {initDirAuto} from './modules/dirauto.js';
@@ -172,6 +173,7 @@ onDomReady(() => {
initRepoWikiForm();
initRepository();
initRepositoryActionView();
+ initRepoContributors();
initCommitStatuses();
initCaptcha();