aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorŞahin Akkaya <sahin@sahinakkaya.dev>2024-02-24 13:22:51 +0300
committerGitHub <noreply@github.com>2024-02-24 10:22:51 +0000
commitd3982bcd814bac93e3cbce1c7eb749b17e413fbd (patch)
treeefac566be7e2d432017a58012954d77a159bce9f
parent0a426cc575734e5eff410d6a790f40473117f753 (diff)
downloadgitea-d3982bcd814bac93e3cbce1c7eb749b17e413fbd.tar.gz
gitea-d3982bcd814bac93e3cbce1c7eb749b17e413fbd.zip
Implement recent commits graph (#29210)
This is the implementation of Recent Commits page. This feature was mentioned on #18262. It adds another tab to Activity page called Recent Commits. Recent Commits tab shows number of commits since last year for the repository.
-rw-r--r--options/locale/locale_en-US.ini4
-rw-r--r--routers/web/repo/recent_commits.go41
-rw-r--r--routers/web/web.go4
-rw-r--r--templates/repo/activity.tmpl1
-rw-r--r--templates/repo/navbar.tmpl3
-rw-r--r--templates/repo/recent_commits.tmpl9
-rw-r--r--web_src/js/components/RepoRecentCommits.vue149
-rw-r--r--web_src/js/features/recent-commits.js21
-rw-r--r--web_src/js/index.js2
9 files changed, 233 insertions, 1 deletions
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 2c92f40a17..ff6a3f1b8e 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1915,8 +1915,9 @@ 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.navbar.contributors = Contributors
+activity.navbar.recent_commits = Recent Commits
activity.period.filter_label = Period:
activity.period.daily = 1 day
activity.period.halfweekly = 3 days
@@ -2597,6 +2598,7 @@ component_loading_info = This might take a bit…
component_failed_to_load = An unexpected error happened.
code_frequency.what = code frequency
contributors.what = contributions
+recent_commits.what = recent commits
[org]
org_name_holder = Organization Name
diff --git a/routers/web/repo/recent_commits.go b/routers/web/repo/recent_commits.go
new file mode 100644
index 0000000000..3507cb8752
--- /dev/null
+++ b/routers/web/repo/recent_commits.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 (
+ tplRecentCommits base.TplName = "repo/activity"
+)
+
+// RecentCommits renders the page to show recent commit frequency on repository
+func RecentCommits(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.recent_commits")
+
+ ctx.Data["PageIsActivity"] = true
+ ctx.Data["PageIsRecentCommits"] = true
+ ctx.PageData["repoLink"] = ctx.Repo.RepoLink
+
+ ctx.HTML(http.StatusOK, tplRecentCommits)
+}
+
+// RecentCommitsData returns JSON of recent commits data
+func RecentCommitsData(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("RecentCommitsData", err)
+ } else {
+ ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
+ }
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index a76b444e4f..8505417c88 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1402,6 +1402,10 @@ func registerRoutes(m *web.Route) {
m.Get("", repo.CodeFrequency)
m.Get("/data", repo.CodeFrequencyData)
})
+ m.Group("/recent-commits", func() {
+ m.Get("", repo.RecentCommits)
+ m.Get("/data", repo.RecentCommitsData)
+ })
}, 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 94f52b0e26..a19fb66261 100644
--- a/templates/repo/activity.tmpl
+++ b/templates/repo/activity.tmpl
@@ -9,6 +9,7 @@
{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}}
{{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}}
{{if .PageIsCodeFrequency}}{{template "repo/code_frequency" .}}{{end}}
+ {{if .PageIsRecentCommits}}{{template "repo/recent_commits" .}}{{end}}
</div>
</div>
</div>
diff --git a/templates/repo/navbar.tmpl b/templates/repo/navbar.tmpl
index aa5021e73a..b2471dc17e 100644
--- a/templates/repo/navbar.tmpl
+++ b/templates/repo/navbar.tmpl
@@ -8,4 +8,7 @@
<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
</a>
+ <a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
+ {{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
+ </a>
</div>
diff --git a/templates/repo/recent_commits.tmpl b/templates/repo/recent_commits.tmpl
new file mode 100644
index 0000000000..5c241d635c
--- /dev/null
+++ b/templates/repo/recent_commits.tmpl
@@ -0,0 +1,9 @@
+{{if .Permission.CanRead $.UnitTypeCode}}
+ <div id="repo-recent-commits-chart"
+ data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.recent_commits.what")}}"
+ data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.recent_commits.what")}}"
+ data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}"
+ data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}"
+ >
+ </div>
+{{end}}
diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue
new file mode 100644
index 0000000000..77697cd413
--- /dev/null
+++ b/web_src/js/components/RepoRecentCommits.vue
@@ -0,0 +1,149 @@
+<script>
+import {SvgIcon} from '../svg.js';
+import {
+ Chart,
+ Tooltip,
+ BarElement,
+ LinearScale,
+ TimeScale,
+} from 'chart.js';
+import {GET} from '../modules/fetch.js';
+import {Bar} from 'vue-chartjs';
+import {
+ startDaysBetween,
+ 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';
+
+const {pageData} = window.config;
+
+Chart.defaults.color = chartJsColors.text;
+Chart.defaults.borderColor = chartJsColors.border;
+
+Chart.register(
+ TimeScale,
+ LinearScale,
+ BarElement,
+ Tooltip,
+);
+
+export default {
+ components: {Bar, SvgIcon},
+ props: {
+ locale: {
+ type: Object,
+ required: true
+ },
+ },
+ data: () => ({
+ isLoading: false,
+ errorText: '',
+ repoLink: pageData.repoLink || [],
+ data: [],
+ }),
+ mounted() {
+ this.fetchGraphData();
+ },
+ methods: {
+ async fetchGraphData() {
+ this.isLoading = true;
+ try {
+ let response;
+ do {
+ response = await GET(`${this.repoLink}/activity/recent-commits/data`);
+ if (response.status === 202) {
+ await sleep(1000); // wait for 1 second before retrying
+ }
+ } while (response.status === 202);
+ if (response.ok) {
+ const data = await response.json();
+ const start = Object.values(data)[0].week;
+ const end = firstStartDateAfterDate(new Date());
+ const startDays = startDaysBetween(new Date(start), new Date(end));
+ this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52);
+ this.errorText = '';
+ } else {
+ this.errorText = response.statusText;
+ }
+ } catch (err) {
+ this.errorText = err.message;
+ } finally {
+ this.isLoading = false;
+ }
+ },
+
+ toGraphData(data) {
+ return {
+ datasets: [
+ {
+ data: data.map((i) => ({x: i.week, y: i.commits})),
+ label: 'Commits',
+ backgroundColor: chartJsColors['commits'],
+ borderWidth: 0,
+ tension: 0.3,
+ },
+ ],
+ };
+ },
+
+ getOptions() {
+ return {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: true,
+ scales: {
+ x: {
+ type: 'time',
+ grid: {
+ display: false,
+ },
+ time: {
+ minUnit: 'week',
+ },
+ ticks: {
+ maxRotation: 0,
+ maxTicksLimit: 52
+ },
+ },
+ y: {
+ ticks: {
+ maxTicksLimit: 6
+ },
+ },
+ },
+ };
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div class="ui header gt-df gt-ac gt-sb">
+ {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "Number of commits in the past year" }}
+ </div>
+ <div class="gt-df ui segment main-graph">
+ <div v-if="isLoading || errorText !== ''" class="gt-tc gt-m-auto">
+ <div v-if="isLoading">
+ <SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
+ {{ locale.loadingInfo }}
+ </div>
+ <div v-else class="text red">
+ <SvgIcon name="octicon-x-circle-fill"/>
+ {{ errorText }}
+ </div>
+ </div>
+ <Bar
+ v-memo="data" v-if="data.length !== 0"
+ :data="toGraphData(data)" :options="getOptions()"
+ />
+ </div>
+ </div>
+</template>
+<style scoped>
+.main-graph {
+ height: 250px;
+}
+</style>
diff --git a/web_src/js/features/recent-commits.js b/web_src/js/features/recent-commits.js
new file mode 100644
index 0000000000..ded10d39be
--- /dev/null
+++ b/web_src/js/features/recent-commits.js
@@ -0,0 +1,21 @@
+import {createApp} from 'vue';
+
+export async function initRepoRecentCommits() {
+ const el = document.getElementById('repo-recent-commits-chart');
+ if (!el) return;
+
+ const {default: RepoRecentCommits} = await import(/* webpackChunkName: "recent-commits-graph" */'../components/RepoRecentCommits.vue');
+ try {
+ const View = createApp(RepoRecentCommits, {
+ 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('RepoRecentCommits 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 d9cfff4084..b7f3ba99a0 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -85,6 +85,7 @@ 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 {initRepoRecentCommits} from './features/recent-commits.js';
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
import {initDirAuto} from './modules/dirauto.js';
@@ -176,6 +177,7 @@ onDomReady(() => {
initRepositoryActionView();
initRepoContributors();
initRepoCodeFrequency();
+ initRepoRecentCommits();
initCommitStatuses();
initCaptcha();