Skip to content

Commit d3982bc

Browse files
authored
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.
1 parent 0a426cc commit d3982bc

File tree

9 files changed

+233
-1
lines changed

9 files changed

+233
-1
lines changed

options/locale/locale_en-US.ini

+3-1
Original file line numberDiff line numberDiff line change
@@ -1915,8 +1915,9 @@ wiki.original_git_entry_tooltip = View original Git file instead of using friend
19151915

19161916
activity = Activity
19171917
activity.navbar.pulse = Pulse
1918-
activity.navbar.contributors = Contributors
19191918
activity.navbar.code_frequency = Code Frequency
1919+
activity.navbar.contributors = Contributors
1920+
activity.navbar.recent_commits = Recent Commits
19201921
activity.period.filter_label = Period:
19211922
activity.period.daily = 1 day
19221923
activity.period.halfweekly = 3 days
@@ -2597,6 +2598,7 @@ component_loading_info = This might take a bit…
25972598
component_failed_to_load = An unexpected error happened.
25982599
code_frequency.what = code frequency
25992600
contributors.what = contributions
2601+
recent_commits.what = recent commits
26002602
26012603
[org]
26022604
org_name_holder = Organization Name

routers/web/repo/recent_commits.go

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package repo
5+
6+
import (
7+
"errors"
8+
"net/http"
9+
10+
"code.gitea.io/gitea/modules/base"
11+
"code.gitea.io/gitea/modules/context"
12+
contributors_service "code.gitea.io/gitea/services/repository"
13+
)
14+
15+
const (
16+
tplRecentCommits base.TplName = "repo/activity"
17+
)
18+
19+
// RecentCommits renders the page to show recent commit frequency on repository
20+
func RecentCommits(ctx *context.Context) {
21+
ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.recent_commits")
22+
23+
ctx.Data["PageIsActivity"] = true
24+
ctx.Data["PageIsRecentCommits"] = true
25+
ctx.PageData["repoLink"] = ctx.Repo.RepoLink
26+
27+
ctx.HTML(http.StatusOK, tplRecentCommits)
28+
}
29+
30+
// RecentCommitsData returns JSON of recent commits data
31+
func RecentCommitsData(ctx *context.Context) {
32+
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
33+
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
34+
ctx.Status(http.StatusAccepted)
35+
return
36+
}
37+
ctx.ServerError("RecentCommitsData", err)
38+
} else {
39+
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
40+
}
41+
}

routers/web/web.go

+4
Original file line numberDiff line numberDiff line change
@@ -1402,6 +1402,10 @@ func registerRoutes(m *web.Route) {
14021402
m.Get("", repo.CodeFrequency)
14031403
m.Get("/data", repo.CodeFrequencyData)
14041404
})
1405+
m.Group("/recent-commits", func() {
1406+
m.Get("", repo.RecentCommits)
1407+
m.Get("/data", repo.RecentCommitsData)
1408+
})
14051409
}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases))
14061410

14071411
m.Group("/activity_author_data", func() {

templates/repo/activity.tmpl

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}}
1010
{{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}}
1111
{{if .PageIsCodeFrequency}}{{template "repo/code_frequency" .}}{{end}}
12+
{{if .PageIsRecentCommits}}{{template "repo/recent_commits" .}}{{end}}
1213
</div>
1314
</div>
1415
</div>

templates/repo/navbar.tmpl

+3
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@
88
<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
99
{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
1010
</a>
11+
<a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
12+
{{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
13+
</a>
1114
</div>

templates/repo/recent_commits.tmpl

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{{if .Permission.CanRead $.UnitTypeCode}}
2+
<div id="repo-recent-commits-chart"
3+
data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.recent_commits.what")}}"
4+
data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.recent_commits.what")}}"
5+
data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}"
6+
data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}"
7+
>
8+
</div>
9+
{{end}}
+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<script>
2+
import {SvgIcon} from '../svg.js';
3+
import {
4+
Chart,
5+
Tooltip,
6+
BarElement,
7+
LinearScale,
8+
TimeScale,
9+
} from 'chart.js';
10+
import {GET} from '../modules/fetch.js';
11+
import {Bar} from 'vue-chartjs';
12+
import {
13+
startDaysBetween,
14+
firstStartDateAfterDate,
15+
fillEmptyStartDaysWithZeroes,
16+
} from '../utils/time.js';
17+
import {chartJsColors} from '../utils/color.js';
18+
import {sleep} from '../utils.js';
19+
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
20+
21+
const {pageData} = window.config;
22+
23+
Chart.defaults.color = chartJsColors.text;
24+
Chart.defaults.borderColor = chartJsColors.border;
25+
26+
Chart.register(
27+
TimeScale,
28+
LinearScale,
29+
BarElement,
30+
Tooltip,
31+
);
32+
33+
export default {
34+
components: {Bar, SvgIcon},
35+
props: {
36+
locale: {
37+
type: Object,
38+
required: true
39+
},
40+
},
41+
data: () => ({
42+
isLoading: false,
43+
errorText: '',
44+
repoLink: pageData.repoLink || [],
45+
data: [],
46+
}),
47+
mounted() {
48+
this.fetchGraphData();
49+
},
50+
methods: {
51+
async fetchGraphData() {
52+
this.isLoading = true;
53+
try {
54+
let response;
55+
do {
56+
response = await GET(`${this.repoLink}/activity/recent-commits/data`);
57+
if (response.status === 202) {
58+
await sleep(1000); // wait for 1 second before retrying
59+
}
60+
} while (response.status === 202);
61+
if (response.ok) {
62+
const data = await response.json();
63+
const start = Object.values(data)[0].week;
64+
const end = firstStartDateAfterDate(new Date());
65+
const startDays = startDaysBetween(new Date(start), new Date(end));
66+
this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52);
67+
this.errorText = '';
68+
} else {
69+
this.errorText = response.statusText;
70+
}
71+
} catch (err) {
72+
this.errorText = err.message;
73+
} finally {
74+
this.isLoading = false;
75+
}
76+
},
77+
78+
toGraphData(data) {
79+
return {
80+
datasets: [
81+
{
82+
data: data.map((i) => ({x: i.week, y: i.commits})),
83+
label: 'Commits',
84+
backgroundColor: chartJsColors['commits'],
85+
borderWidth: 0,
86+
tension: 0.3,
87+
},
88+
],
89+
};
90+
},
91+
92+
getOptions() {
93+
return {
94+
responsive: true,
95+
maintainAspectRatio: false,
96+
animation: true,
97+
scales: {
98+
x: {
99+
type: 'time',
100+
grid: {
101+
display: false,
102+
},
103+
time: {
104+
minUnit: 'week',
105+
},
106+
ticks: {
107+
maxRotation: 0,
108+
maxTicksLimit: 52
109+
},
110+
},
111+
y: {
112+
ticks: {
113+
maxTicksLimit: 6
114+
},
115+
},
116+
},
117+
};
118+
},
119+
},
120+
};
121+
</script>
122+
<template>
123+
<div>
124+
<div class="ui header gt-df gt-ac gt-sb">
125+
{{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "Number of commits in the past year" }}
126+
</div>
127+
<div class="gt-df ui segment main-graph">
128+
<div v-if="isLoading || errorText !== ''" class="gt-tc gt-m-auto">
129+
<div v-if="isLoading">
130+
<SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
131+
{{ locale.loadingInfo }}
132+
</div>
133+
<div v-else class="text red">
134+
<SvgIcon name="octicon-x-circle-fill"/>
135+
{{ errorText }}
136+
</div>
137+
</div>
138+
<Bar
139+
v-memo="data" v-if="data.length !== 0"
140+
:data="toGraphData(data)" :options="getOptions()"
141+
/>
142+
</div>
143+
</div>
144+
</template>
145+
<style scoped>
146+
.main-graph {
147+
height: 250px;
148+
}
149+
</style>

web_src/js/features/recent-commits.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {createApp} from 'vue';
2+
3+
export async function initRepoRecentCommits() {
4+
const el = document.getElementById('repo-recent-commits-chart');
5+
if (!el) return;
6+
7+
const {default: RepoRecentCommits} = await import(/* webpackChunkName: "recent-commits-graph" */'../components/RepoRecentCommits.vue');
8+
try {
9+
const View = createApp(RepoRecentCommits, {
10+
locale: {
11+
loadingTitle: el.getAttribute('data-locale-loading-title'),
12+
loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
13+
loadingInfo: el.getAttribute('data-locale-loading-info'),
14+
}
15+
});
16+
View.mount(el);
17+
} catch (err) {
18+
console.error('RepoRecentCommits failed to load', err);
19+
el.textContent = el.getAttribute('data-locale-component-failed-to-load');
20+
}
21+
}

web_src/js/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import {initRepoIssueList} from './features/repo-issue-list.js';
8585
import {initCommonIssueListQuickGoto} from './features/common-issue-list.js';
8686
import {initRepoContributors} from './features/contributors.js';
8787
import {initRepoCodeFrequency} from './features/code-frequency.js';
88+
import {initRepoRecentCommits} from './features/recent-commits.js';
8889
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
8990
import {initDirAuto} from './modules/dirauto.js';
9091

@@ -176,6 +177,7 @@ onDomReady(() => {
176177
initRepositoryActionView();
177178
initRepoContributors();
178179
initRepoCodeFrequency();
180+
initRepoRecentCommits();
179181

180182
initCommitStatuses();
181183
initCaptcha();

0 commit comments

Comments
 (0)