From df172cf253f9c3a3baf43eb756ff10eb5e62632a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=9Eahin=20Akkaya?= Date: Sun, 28 Jan 2024 15:03:30 +0300 Subject: [PATCH 01/17] Revert "Move types out from "modules/structs" to not pollute api" This reverts commit 0f61e592e00ef57ae8c748fbec709d4edeeab637. --- modules/structs/repo_collaborator.go | 17 ++++++++ modules/structs/repo_commit.go | 6 +++ routers/api/v1/swagger/repo.go | 7 ++++ services/repository/contributors_graph.go | 51 +++++++---------------- templates/swagger/v1_json.tmpl | 40 ++++++++++++++++++ 5 files changed, 84 insertions(+), 37 deletions(-) diff --git a/modules/structs/repo_collaborator.go b/modules/structs/repo_collaborator.go index 946a6ec7e78e0..a0c0b0cdd2b16 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 map[int64]*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/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/services/repository/contributors_graph.go b/services/repository/contributors_graph.go index 8421df8e3ae7e..675d346ab7493 100644 --- a/services/repository/contributors_graph.go +++ b/services/repository/contributors_graph.go @@ -37,29 +37,6 @@ var ( generateLock = sync.Map{} ) -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 map[int64]*WeekData `json:"weeks"` -} - -// ExtendedCommitStats contains information for commit stats with author data -type ExtendedCommitStats struct { - Author *api.CommitUser `json:"author"` - Stats *api.CommitStats `json:"stats"` -} - const layout = time.DateOnly func findLastSundayBeforeDate(dateStr string) (string, error) { @@ -79,7 +56,7 @@ func findLastSundayBeforeDate(dateStr string) (string, error) { } // GetContributorStats returns contributors stats for git commits for given revision or default branch -func GetContributorStats(ctx context.Context, cache cache.Cache, repo *repo_model.Repository, revision string) (map[string]*ContributorData, error) { +func GetContributorStats(ctx context.Context, cache cache.Cache, repo *repo_model.Repository, revision string) (map[string]*api.ContributorData, error) { // as GetContributorStats is resource intensive we cache the result cacheKey := fmt.Sprintf(contributorStatsCacheKey, repo.FullName(), revision) if !cache.IsExist(cacheKey) { @@ -108,7 +85,7 @@ func GetContributorStats(ctx context.Context, cache cache.Cache, repo *repo_mode switch v := cache.Get(cacheKey).(type) { case error: return nil, v - case map[string]*ContributorData: + case map[string]*api.ContributorData: return v, nil default: return nil, fmt.Errorf("unexpected type in cache detected") @@ -116,7 +93,7 @@ func GetContributorStats(ctx context.Context, cache cache.Cache, repo *repo_mode } // getExtendedCommitStats return the list of *ExtendedCommitStats for the given revision -func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int */) ([]*ExtendedCommitStats, error) { +func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int */) ([]*api.ExtendedCommitStats, error) { baseCommit, err := repo.GetCommit(revision) if err != nil { return nil, err @@ -134,7 +111,7 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int // AddOptionFormat("--max-count=%d", limit) gitCmd.AddDynamicArguments(baseCommit.ID.String()) - var extendedCommitStats []*ExtendedCommitStats + var extendedCommitStats []*api.ExtendedCommitStats stderr := new(strings.Builder) err = gitCmd.Run(&git.RunOpts{ Dir: repo.Path, @@ -183,7 +160,7 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int scanner.Scan() scanner.Text() // empty line at the end - res := &ExtendedCommitStats{ + res := &api.ExtendedCommitStats{ Author: &api.CommitUser{ Identity: api.Identity{ Name: authorName, @@ -236,10 +213,10 @@ func generateContributorStats(genDone chan struct{}, cache cache.Cache, cacheKey layout := time.DateOnly unknownUserAvatarLink := user_model.NewGhostUser().AvatarLinkWithSize(ctx, 0) - contributorsCommitStats := make(map[string]*ContributorData) - contributorsCommitStats["total"] = &ContributorData{ + contributorsCommitStats := make(map[string]*api.ContributorData) + contributorsCommitStats["total"] = &api.ContributorData{ Name: "Total", - Weeks: make(map[int64]*WeekData), + Weeks: make(map[int64]*api.WeekData), } total := contributorsCommitStats["total"] @@ -261,18 +238,18 @@ func generateContributorStats(genDone chan struct{}, cache cache.Cache, cacheKey if avatarLink == "" { avatarLink = unknownUserAvatarLink } - contributorsCommitStats[userEmail] = &ContributorData{ + contributorsCommitStats[userEmail] = &api.ContributorData{ Name: v.Author.Name, AvatarLink: avatarLink, - Weeks: make(map[int64]*WeekData), + Weeks: make(map[int64]*api.WeekData), } } else { - contributorsCommitStats[userEmail] = &ContributorData{ + contributorsCommitStats[userEmail] = &api.ContributorData{ Name: u.DisplayName(), Login: u.LowerName, AvatarLink: u.AvatarLinkWithSize(ctx, 0), HomeLink: u.HomeLink(), - Weeks: make(map[int64]*WeekData), + Weeks: make(map[int64]*api.WeekData), } } } @@ -284,7 +261,7 @@ func generateContributorStats(genDone chan struct{}, cache cache.Cache, cacheKey week := val.UnixMilli() if user.Weeks[week] == nil { - user.Weeks[week] = &WeekData{ + user.Weeks[week] = &api.WeekData{ Additions: 0, Deletions: 0, Commits: 0, @@ -292,7 +269,7 @@ func generateContributorStats(genDone chan struct{}, cache cache.Cache, cacheKey } } if total.Weeks[week] == nil { - total.Weeks[week] = &WeekData{ + total.Weeks[week] = &api.WeekData{ Additions: 0, Deletions: 0, Commits: 0, diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index a881afaf0ec63..a66ce03968f78 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -17585,6 +17585,37 @@ }, "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": { + "x-go-name": "Weeks" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "CreateAccessTokenOption": { "description": "CreateAccessTokenOption options when create access token", "type": "object", @@ -23400,6 +23431,15 @@ "$ref": "#/definitions/ContentsResponse" } }, + "ContributorDataMap": { + "description": "ContributorDataMap", + "schema": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ContributorData" + } + } + }, "CronList": { "description": "CronList", "schema": { From cdfa05787b6d8fe93652e3e476a2b25f9275ab13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=9Eahin=20Akkaya?= Date: Wed, 8 Nov 2023 13:55:38 +0300 Subject: [PATCH 02/17] Add initial code frequency page --- options/locale/locale_en-US.ini | 4 ++++ routers/web/repo/code_frequency.go | 35 ++++++++++++++++++++++++++++++ routers/web/web.go | 4 ++++ templates/repo/activity.tmpl | 1 + templates/repo/code_frequency.tmpl | 3 +++ templates/repo/navbar.tmpl | 3 +++ 6 files changed, 50 insertions(+) create mode 100644 routers/web/repo/code_frequency.go create mode 100644 templates/repo/code_frequency.tmpl diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 5f34bc4c1d745..56ee206b7aa49 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1914,6 +1914,7 @@ wiki.original_git_entry_tooltip = View original Git file instead of using friend activity = Activity activity.navbar.pulse = Pulse activity.navbar.contributors = Contributors +activity.navbar.code_frequency = Code Frequency activity.period.filter_label = Period: activity.period.daily = 1 day activity.period.halfweekly = 3 days @@ -1989,6 +1990,9 @@ 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. + +code_frequency = Code Frequency + search = Search search.search_repo = Search repository search.type.tooltip = Search type diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go new file mode 100644 index 0000000000000..66bef2df2fb3c --- /dev/null +++ b/routers/web/repo/code_frequency.go @@ -0,0 +1,35 @@ +// 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 ( + tplCodeFrequency base.TplName = "repo/activity" +) + +// CodeFrequency renders the page to show repository code frequency +func CodeFrequency(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("repo.code_frequency") + + ctx.Data["PageIsActivity"] = true + ctx.Data["PageIsCodeFrequency"] = true + + ctx.HTML(http.StatusOK, tplCodeFrequency) +} + +// CodeFrequencyData returns JSON of code frequency data +func CodeFrequencyData(ctx *context.Context) { + if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil { + ctx.ServerError("GetCodeFrequencyData", err) + } else { + ctx.JSON(http.StatusOK, contributorStats["total"].Weeks) + } +} diff --git a/routers/web/web.go b/routers/web/web.go index 0528b20328eb8..73851c8c8ed18 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1397,6 +1397,10 @@ func registerRoutes(m *web.Route) { m.Get("", repo.Contributors) m.Get("/data", repo.ContributorsData) }) + m.Group("/code-frequency", func() { + m.Get("", repo.CodeFrequency) + m.Get("/data", repo.CodeFrequencyData) + }) }, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases)) m.Group("/activity_author_data", func() { diff --git a/templates/repo/activity.tmpl b/templates/repo/activity.tmpl index 960083d2fbd7f..94f52b0e26386 100644 --- a/templates/repo/activity.tmpl +++ b/templates/repo/activity.tmpl @@ -8,6 +8,7 @@
{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}} {{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}} + {{if .PageIsCodeFrequency}}{{template "repo/code_frequency" .}}{{end}}
diff --git a/templates/repo/code_frequency.tmpl b/templates/repo/code_frequency.tmpl new file mode 100644 index 0000000000000..3d56a5709ff0e --- /dev/null +++ b/templates/repo/code_frequency.tmpl @@ -0,0 +1,3 @@ +{{if .Permission.CanRead $.UnitTypeCode}} +

Code Frequency

+{{end}} diff --git a/templates/repo/navbar.tmpl b/templates/repo/navbar.tmpl index a9042ee30d0b6..aa5021e73a70c 100644 --- a/templates/repo/navbar.tmpl +++ b/templates/repo/navbar.tmpl @@ -5,4 +5,7 @@ {{ctx.Locale.Tr "repo.activity.navbar.contributors"}} + + {{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}} + From 214a18d132e6b34e2489cc0ce5cee2476fa4cf93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=9Eahin=20Akkaya?= Date: Wed, 8 Nov 2023 15:38:45 +0300 Subject: [PATCH 03/17] Implement code frequency feature --- options/locale/locale_en-US.ini | 4 + routers/web/repo/code_frequency.go | 3 +- templates/repo/code_frequency.tmpl | 8 +- web_src/js/components/RepoCodeFrequency.vue | 191 ++++++++++++++++++++ web_src/js/features/code-frequency.js | 21 +++ web_src/js/index.js | 2 + 6 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 web_src/js/components/RepoCodeFrequency.vue create mode 100644 web_src/js/features/code-frequency.js diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 56ee206b7aa49..a02bf2fa53220 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1992,6 +1992,10 @@ contributors.component_failed_to_load = An unexpected error happened. code_frequency = Code Frequency +code_frequency.loading_title = Loading code frequency... +code_frequency.loading_title_failed = Could not load code frequency +code_frequency.loading_info = This might take a bit… +code_frequency.component_failed_to_load = An unexpected error happened. search = Search search.search_repo = Search repository diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go index 66bef2df2fb3c..2a6c4e7805e34 100644 --- a/routers/web/repo/code_frequency.go +++ b/routers/web/repo/code_frequency.go @@ -21,13 +21,14 @@ func CodeFrequency(ctx *context.Context) { ctx.Data["PageIsActivity"] = true ctx.Data["PageIsCodeFrequency"] = true + ctx.PageData["repoLink"] = ctx.Repo.RepoLink ctx.HTML(http.StatusOK, tplCodeFrequency) } // CodeFrequencyData returns JSON of code frequency data func CodeFrequencyData(ctx *context.Context) { - if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil { + if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil { ctx.ServerError("GetCodeFrequencyData", err) } else { ctx.JSON(http.StatusOK, contributorStats["total"].Weeks) diff --git a/templates/repo/code_frequency.tmpl b/templates/repo/code_frequency.tmpl index 3d56a5709ff0e..166369cf21376 100644 --- a/templates/repo/code_frequency.tmpl +++ b/templates/repo/code_frequency.tmpl @@ -1,3 +1,9 @@ {{if .Permission.CanRead $.UnitTypeCode}} -

Code Frequency

+
+
{{end}} diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue new file mode 100644 index 0000000000000..0c3be91207649 --- /dev/null +++ b/web_src/js/components/RepoCodeFrequency.vue @@ -0,0 +1,191 @@ + + + diff --git a/web_src/js/features/code-frequency.js b/web_src/js/features/code-frequency.js new file mode 100644 index 0000000000000..103d82f6e33be --- /dev/null +++ b/web_src/js/features/code-frequency.js @@ -0,0 +1,21 @@ +import {createApp} from 'vue'; + +export async function initRepoCodeFrequency() { + const el = document.getElementById('repo-code-frequency-chart'); + if (!el) return; + + const {default: RepoCodeFrequency} = await import(/* webpackChunkName: "code-frequency-graph" */'../components/RepoCodeFrequency.vue'); + try { + const View = createApp(RepoCodeFrequency, { + locale: { + 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('RepoCodeFrequency 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 078f9fc9df415..4155b9596ae1a 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -84,6 +84,7 @@ 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 {initRepoCodeFrequency} from './features/code-frequency.js'; import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js'; import {initDirAuto} from './modules/dirauto.js'; @@ -174,6 +175,7 @@ onDomReady(() => { initRepository(); initRepositoryActionView(); initRepoContributors(); + initRepoCodeFrequency(); initCommitStatuses(); initCaptcha(); From c426ecdf80876e129aaa8ef1e55870b3255e4593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=9Eahin=20Akkaya?= Date: Wed, 8 Nov 2023 15:47:11 +0300 Subject: [PATCH 04/17] Fix linting errors --- web_src/js/components/RepoCodeFrequency.vue | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue index 0c3be91207649..eb2e0e1ea6869 100644 --- a/web_src/js/components/RepoCodeFrequency.vue +++ b/web_src/js/components/RepoCodeFrequency.vue @@ -22,7 +22,6 @@ import { fillEmptyStartDaysWithZeroes, } from '../utils/time.js'; import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; -import $ from 'jquery'; const {pageData} = window.config; @@ -106,8 +105,8 @@ export default { pointRadius: 0, pointHitRadius: 0, fill: true, - label: "Additions", - backgroundColor: colors["additions"], + label: 'Additions', + backgroundColor: colors['additions'], borderWidth: 0, tension: 0.3, }, @@ -116,8 +115,8 @@ export default { pointRadius: 0, pointHitRadius: 0, fill: true, - label: "Deletions", - backgroundColor: colors["deletions"], + label: 'Deletions', + backgroundColor: colors['deletions'], borderWidth: 0, tension: 0.3, }, @@ -125,7 +124,6 @@ export default { }; }, - getOptions() { return { responsive: true, @@ -160,11 +158,11 @@ export default { }, }, }; - +