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.tags/v1.22.0-rc0
@@ -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 |
@@ -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) | |||
} | |||
} |
@@ -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() { |
@@ -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> |
@@ -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> |
@@ -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}} |
@@ -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> |
@@ -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'); | |||
} | |||
} |
@@ -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(); |