Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1914,6 +1914,7 @@ 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.recent_commits = Recent Commits
activity.period.filter_label = Period:
activity.period.daily = 1 day
activity.period.halfweekly = 3 days
Expand Down Expand Up @@ -1989,6 +1990,12 @@ contributors.loading_title_failed = Could not load contributions
contributors.loading_info = This might take a bit…
contributors.component_failed_to_load = An unexpected error happened.

recent_commits = Recent Commits
recent_commits.loading_title = Loading recent commits...
recent_commits.loading_title_failed = Could not load recent commits
recent_commits.loading_info = This might take a bit…
recent_commits.component_failed_to_load = An unexpected error happened.

search = Search
search.search_repo = Search repository
search.type.tooltip = Search type
Expand Down
41 changes: 41 additions & 0 deletions routers/web/repo/recent_commits.go
Original file line number Diff line number Diff line change
@@ -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.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)
}
}
4 changes: 4 additions & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -1397,6 +1397,10 @@ func registerRoutes(m *web.Route) {
m.Get("", repo.Contributors)
m.Get("/data", repo.ContributorsData)
})
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() {
Expand Down
1 change: 1 addition & 0 deletions templates/repo/activity.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<div class="flex-container-main">
{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}}
{{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}}
{{if .PageIsRecentCommits}}{{template "repo/recent_commits" .}}{{end}}
</div>
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions templates/repo/navbar.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@
<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
{{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
</a>
<a class="{{if .PageIsRecentCommits}}active{{end}} item" href="{{.RepoLink}}/activity/recent-commits">
{{ctx.Locale.Tr "repo.activity.navbar.recent_commits"}}
</a>
</div>
9 changes: 9 additions & 0 deletions templates/repo/recent_commits.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{{if .Permission.CanRead $.UnitTypeCode}}
<div id="repo-recent-commits-chart"
data-locale-loading-title="{{ctx.Locale.Tr "repo.recent_commits.loading_title"}}"
data-locale-loading-title-failed="{{ctx.Locale.Tr "repo.recent_commits.loading_title_failed"}}"
data-locale-loading-info="{{ctx.Locale.Tr "repo.recent_commits.loading_info"}}"
data-locale-component-failed-to-load="{{ctx.Locale.Tr "repo.recent_commits.component_failed_to_load"}}"
>
</div>
{{end}}
183 changes: 183 additions & 0 deletions web_src/js/components/RepoRecentCommits.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<script>
import {SvgIcon} from '../svg.js';
import {
Chart,
Title,
Tooltip,
Legend,
BarElement,
CategoryScale,
LinearScale,
TimeScale,
PointElement,
LineElement,
Filler,
} from 'chart.js';
import {GET} from '../modules/fetch.js';
import zoomPlugin from 'chartjs-plugin-zoom';
import {Bar} from 'vue-chartjs';
import {
startDaysBetween,
firstStartDateAfterDate,
fillEmptyStartDaysWithZeroes,
} from '../utils/time.js';
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
import $ from 'jquery';

const {pageData} = window.config;

const colors = {
text: '--color-text',
border: '--color-secondary-alpha-60',
commits: '--color-primary-alpha-60',
additions: '--color-green',
deletions: '--color-red',
};

const styles = window.getComputedStyle(document.documentElement);
const getColor = (name) => styles.getPropertyValue(name).trim();

for (const [key, value] of Object.entries(colors)) {
colors[key] = getColor(value);
}

Chart.defaults.color = colors.text;
Chart.defaults.borderColor = colors.border;

Chart.register(
TimeScale,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
PointElement,
LineElement,
Filler,
zoomPlugin
);

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 new Promise((resolve) => setTimeout(resolve, 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: colors["commits"],
borderWidth: 0,
tension: 0.3,
},
],
};
},


getOptions() {
return {
responsive: true,
maintainAspectRatio: false,
animation: true,
plugins: {
legend: {
display: false,
},
},
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: 440px;
}
</style>
21 changes: 21 additions & 0 deletions web_src/js/features/recent-commits.js
Original file line number Diff line number Diff line change
@@ -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');
}
}
2 changes: 2 additions & 0 deletions web_src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import {onDomReady} from './utils/dom.js';
import {initRepoIssueList} from './features/repo-issue-list.js';
import {initCommonIssueListQuickGoto} from './features/common-issue-list.js';
import {initRepoContributors} from './features/contributors.js';
import {initRepoRecentCommits} from './features/recent-commits.js';
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
import {initDirAuto} from './modules/dirauto.js';

Expand Down Expand Up @@ -174,6 +175,7 @@ onDomReady(() => {
initRepository();
initRepositoryActionView();
initRepoContributors();
initRepoRecentCommits();

initCommitStatuses();
initCaptcha();
Expand Down