mirror of https://github.com/go-gitea/gitea.git
Browse Source
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.pull/29378/head
9 changed files with 233 additions and 1 deletions
@ -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) |
||||||
|
} |
||||||
|
} |
||||||
@ -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'); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue