diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 31dbabe874b85..b35672eac2e30 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1919,6 +1919,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
@@ -2597,6 +2598,7 @@ component_loading = Loading %s...
component_loading_failed = Could not load %s
component_loading_info = This might take a bit…
component_failed_to_load = An unexpected error happened.
+code_frequency.what = code frequency
contributors.what = contributions
[org]
diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go
new file mode 100644
index 0000000000000..48ade655b7b68
--- /dev/null
+++ b/routers/web/repo/code_frequency.go
@@ -0,0 +1,41 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "errors"
+ "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.activity.navbar.code_frequency")
+
+ 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.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+ if errors.Is(err, contributors_service.ErrAwaitGeneration) {
+ ctx.Status(http.StatusAccepted)
+ return
+ }
+ 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 77c8319f0675f..5e18aac67d7fe 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1403,6 +1403,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/services/repository/contributors_graph.go b/services/repository/contributors_graph.go
index 8421df8e3ae7e..7c9f535ae01a6 100644
--- a/services/repository/contributors_graph.go
+++ b/services/repository/contributors_graph.go
@@ -143,7 +143,6 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int
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())
@@ -180,7 +179,6 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int
}
}
commitStats.Total = commitStats.Additions + commitStats.Deletions
- scanner.Scan()
scanner.Text() // empty line at the end
res := &ExtendedCommitStats{
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..50ec1beb6b528
--- /dev/null
+++ b/templates/repo/code_frequency.tmpl
@@ -0,0 +1,9 @@
+{{if .Permission.CanRead $.UnitTypeCode}}
+
+
+{{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"}}
+
diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue
new file mode 100644
index 0000000000000..ad607a041a554
--- /dev/null
+++ b/web_src/js/components/RepoCodeFrequency.vue
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
+
+ {{ locale.loadingInfo }}
+
+
+
+ {{ errorText }}
+
+
+
+
+
+
+
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index fa1545b3dfb25..84fdcae1f61da 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -3,10 +3,7 @@ import {SvgIcon} from '../svg.js';
import {
Chart,
Title,
- Tooltip,
- Legend,
BarElement,
- CategoryScale,
LinearScale,
TimeScale,
PointElement,
@@ -21,27 +18,13 @@ import {
firstStartDateAfterDate,
fillEmptyStartDaysWithZeroes,
} from '../utils/time.js';
+import {chartJsColors} from '../utils/color.js';
+import {sleep} from '../utils.js';
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
import $ from 'jquery';
const {pageData} = window.config;
-const colors = {
- text: '--color-text',
- border: '--color-secondary-alpha-60',
- commits: '--color-primary-alpha-60',
- additions: '--color-green',
- deletions: '--color-red',
- title: '--color-secondary-dark-4',
-};
-
-const styles = window.getComputedStyle(document.documentElement);
-const getColor = (name) => styles.getPropertyValue(name).trim();
-
-for (const [key, value] of Object.entries(colors)) {
- colors[key] = getColor(value);
-}
-
const customEventListener = {
id: 'customEventListener',
afterEvent: (chart, args, opts) => {
@@ -54,17 +37,14 @@ const customEventListener = {
}
};
-Chart.defaults.color = colors.text;
-Chart.defaults.borderColor = colors.border;
+Chart.defaults.color = chartJsColors.text;
+Chart.defaults.borderColor = chartJsColors.border;
Chart.register(
TimeScale,
- CategoryScale,
LinearScale,
BarElement,
Title,
- Tooltip,
- Legend,
PointElement,
LineElement,
Filler,
@@ -122,7 +102,7 @@ export default {
do {
response = await GET(`${this.repoLink}/activity/contributors/data`);
if (response.status === 202) {
- await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for 1 second before retrying
+ await sleep(1000); // wait for 1 second before retrying
}
} while (response.status === 202);
if (response.ok) {
@@ -222,7 +202,7 @@ export default {
pointRadius: 0,
pointHitRadius: 0,
fill: 'start',
- backgroundColor: colors[this.type],
+ backgroundColor: chartJsColors[this.type],
borderWidth: 0,
tension: 0.3,
},
@@ -254,7 +234,6 @@ export default {
title: {
display: type === 'main',
text: 'drag: zoom, shift+drag: pan, double click: reset zoom',
- color: colors.title,
position: 'top',
align: 'center',
},
@@ -262,9 +241,6 @@ export default {
chartType: type,
instance: this,
},
- legend: {
- display: false,
- },
zoom: {
pan: {
enabled: true,
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 ddd435f05e82a..876e4291ee2ba 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -87,6 +87,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';
@@ -177,6 +178,7 @@ onDomReady(() => {
initRepository();
initRepositoryActionView();
initRepoContributors();
+ initRepoCodeFrequency();
initCommitStatuses();
initCaptcha();
diff --git a/web_src/js/utils.js b/web_src/js/utils.js
index c82e42d349045..3a2694335f0d0 100644
--- a/web_src/js/utils.js
+++ b/web_src/js/utils.js
@@ -139,3 +139,5 @@ export function parseDom(text, contentType) {
export function serializeXml(node) {
return xmlSerializer.serializeToString(node);
}
+
+export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
diff --git a/web_src/js/utils/color.js b/web_src/js/utils/color.js
index 5d9c4ca45dfd2..0ba6af49ee6e4 100644
--- a/web_src/js/utils/color.js
+++ b/web_src/js/utils/color.js
@@ -19,3 +19,17 @@ function getLuminance(r, g, b) {
export function useLightTextOnBackground(r, g, b) {
return getLuminance(r, g, b) < 0.453;
}
+
+function resolveColors(obj) {
+ const styles = window.getComputedStyle(document.documentElement);
+ const getColor = (name) => styles.getPropertyValue(name).trim();
+ return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)]));
+}
+
+export const chartJsColors = resolveColors({
+ text: '--color-text',
+ border: '--color-secondary-alpha-60',
+ commits: '--color-primary-alpha-60',
+ additions: '--color-green',
+ deletions: '--color-red',
+});