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. (cherry picked from commit d3982bcd814bac93e3cbce1c7eb749b17e413fbd)
This commit is contained in:
parent
1608ef0ce9
commit
428008ac19
9 changed files with 233 additions and 1 deletions
|
@ -1961,8 +1961,9 @@ wiki.original_git_entry_tooltip = View original Git file instead of using friend
|
||||||
|
|
||||||
activity = Activity
|
activity = Activity
|
||||||
activity.navbar.pulse = Pulse
|
activity.navbar.pulse = Pulse
|
||||||
activity.navbar.contributors = Contributors
|
|
||||||
activity.navbar.code_frequency = Code Frequency
|
activity.navbar.code_frequency = Code Frequency
|
||||||
|
activity.navbar.contributors = Contributors
|
||||||
|
activity.navbar.recent_commits = Recent Commits
|
||||||
activity.period.filter_label = Period:
|
activity.period.filter_label = Period:
|
||||||
activity.period.daily = 1 day
|
activity.period.daily = 1 day
|
||||||
activity.period.halfweekly = 3 days
|
activity.period.halfweekly = 3 days
|
||||||
|
@ -2659,6 +2660,7 @@ component_loading_info = This might take a bit…
|
||||||
component_failed_to_load = An unexpected error happened.
|
component_failed_to_load = An unexpected error happened.
|
||||||
code_frequency.what = code frequency
|
code_frequency.what = code frequency
|
||||||
contributors.what = contributions
|
contributors.what = contributions
|
||||||
|
recent_commits.what = recent commits
|
||||||
|
|
||||||
[org]
|
[org]
|
||||||
org_name_holder = Organization Name
|
org_name_holder = Organization Name
|
||||||
|
|
41
routers/web/repo/recent_commits.go
Normal file
41
routers/web/repo/recent_commits.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1452,6 +1452,10 @@ func registerRoutes(m *web.Route) {
|
||||||
m.Get("", repo.CodeFrequency)
|
m.Get("", repo.CodeFrequency)
|
||||||
m.Get("/data", repo.CodeFrequencyData)
|
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))
|
}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases))
|
||||||
|
|
||||||
m.Group("/activity_author_data", func() {
|
m.Group("/activity_author_data", func() {
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}}
|
{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}}
|
||||||
{{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}}
|
{{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}}
|
||||||
{{if .PageIsCodeFrequency}}{{template "repo/code_frequency" .}}{{end}}
|
{{if .PageIsCodeFrequency}}{{template "repo/code_frequency" .}}{{end}}
|
||||||
|
{{if .PageIsRecentCommits}}{{template "repo/recent_commits" .}}{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,4 +8,7 @@
|
||||||
<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
|
<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
|
||||||
{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
|
{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
|
||||||
</a>
|
</a>
|
||||||
|
<a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
|
||||||
|
{{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
9
templates/repo/recent_commits.tmpl
Normal file
9
templates/repo/recent_commits.tmpl
Normal file
|
@ -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}}
|
149
web_src/js/components/RepoRecentCommits.vue
Normal file
149
web_src/js/components/RepoRecentCommits.vue
Normal file
|
@ -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>
|
21
web_src/js/features/recent-commits.js
Normal file
21
web_src/js/features/recent-commits.js
Normal file
|
@ -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 {initCommonIssueListQuickGoto} from './features/common-issue-list.js';
|
||||||
import {initRepoContributors} from './features/contributors.js';
|
import {initRepoContributors} from './features/contributors.js';
|
||||||
import {initRepoCodeFrequency} from './features/code-frequency.js';
|
import {initRepoCodeFrequency} from './features/code-frequency.js';
|
||||||
|
import {initRepoRecentCommits} from './features/recent-commits.js';
|
||||||
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
|
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
|
||||||
import {initDirAuto} from './modules/dirauto.js';
|
import {initDirAuto} from './modules/dirauto.js';
|
||||||
|
|
||||||
|
@ -176,6 +177,7 @@ onDomReady(() => {
|
||||||
initRepositoryActionView();
|
initRepositoryActionView();
|
||||||
initRepoContributors();
|
initRepoContributors();
|
||||||
initRepoCodeFrequency();
|
initRepoCodeFrequency();
|
||||||
|
initRepoRecentCommits();
|
||||||
|
|
||||||
initCommitStatuses();
|
initCommitStatuses();
|
||||||
initCaptcha();
|
initCaptcha();
|
||||||
|
|
Loading…
Reference in a new issue