Skip to content

Commit 875f5ea

Browse files
Implement code frequency graph (#29191)
### Overview This is the implementation of Code Frequency page. This feature was mentioned on these issues: #18262, #7392. It adds another tab to Activity page called Code Frequency. Code Frequency tab shows additions and deletions over time since the repository existed. Before: <img width="1296" alt="image" src="https://github.com/go-gitea/gitea/assets/32161460/2603504f-aee7-4929-a8c4-fb3412a7a0f6"> After: <img width="1296" alt="image" src="https://github.com/go-gitea/gitea/assets/32161460/58c03721-729f-4536-a663-9f337f240963"> --- #### Features - See additions deletions over time since repository existed - Click on "Additions" or "Deletions" legend to show only one type of contribution - Use the same cache from Contributors page so that the loading of data will be fast once it is cached by visiting either one of the pages --------- Co-authored-by: Giteabot <[email protected]>
1 parent 6f6120d commit 875f5ea

File tree

13 files changed

+277
-32
lines changed

13 files changed

+277
-32
lines changed

options/locale/locale_en-US.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1919,6 +1919,7 @@ wiki.original_git_entry_tooltip = View original Git file instead of using friend
19191919
activity = Activity
19201920
activity.navbar.pulse = Pulse
19211921
activity.navbar.contributors = Contributors
1922+
activity.navbar.code_frequency = Code Frequency
19221923
activity.period.filter_label = Period:
19231924
activity.period.daily = 1 day
19241925
activity.period.halfweekly = 3 days
@@ -2597,6 +2598,7 @@ component_loading = Loading %s...
25972598
component_loading_failed = Could not load %s
25982599
component_loading_info = This might take a bit…
25992600
component_failed_to_load = An unexpected error happened.
2601+
code_frequency.what = code frequency
26002602
contributors.what = contributions
26012603
26022604
[org]

routers/web/repo/code_frequency.go

Lines changed: 41 additions & 0 deletions
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+
tplCodeFrequency base.TplName = "repo/activity"
17+
)
18+
19+
// CodeFrequency renders the page to show repository code frequency
20+
func CodeFrequency(ctx *context.Context) {
21+
ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.code_frequency")
22+
23+
ctx.Data["PageIsActivity"] = true
24+
ctx.Data["PageIsCodeFrequency"] = true
25+
ctx.PageData["repoLink"] = ctx.Repo.RepoLink
26+
27+
ctx.HTML(http.StatusOK, tplCodeFrequency)
28+
}
29+
30+
// CodeFrequencyData returns JSON of code frequency data
31+
func CodeFrequencyData(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("GetCodeFrequencyData", err)
38+
} else {
39+
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
40+
}
41+
}

routers/web/web.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,6 +1403,10 @@ func registerRoutes(m *web.Route) {
14031403
m.Get("", repo.Contributors)
14041404
m.Get("/data", repo.ContributorsData)
14051405
})
1406+
m.Group("/code-frequency", func() {
1407+
m.Get("", repo.CodeFrequency)
1408+
m.Get("/data", repo.CodeFrequencyData)
1409+
})
14061410
}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases))
14071411

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

services/repository/contributors_graph.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int
143143
PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
144144
_ = stdoutWriter.Close()
145145
scanner := bufio.NewScanner(stdoutReader)
146-
scanner.Split(bufio.ScanLines)
147146

148147
for scanner.Scan() {
149148
line := strings.TrimSpace(scanner.Text())
@@ -180,7 +179,6 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int
180179
}
181180
}
182181
commitStats.Total = commitStats.Additions + commitStats.Deletions
183-
scanner.Scan()
184182
scanner.Text() // empty line at the end
185183

186184
res := &ExtendedCommitStats{

templates/repo/activity.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<div class="flex-container-main">
99
{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}}
1010
{{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}}
11+
{{if .PageIsCodeFrequency}}{{template "repo/code_frequency" .}}{{end}}
1112
</div>
1213
</div>
1314
</div>

templates/repo/code_frequency.tmpl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{{if .Permission.CanRead $.UnitTypeCode}}
2+
<div id="repo-code-frequency-chart"
3+
data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.code_frequency.what")}}"
4+
data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.code_frequency.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}}

templates/repo/navbar.tmpl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@
55
<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
66
{{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
77
</a>
8+
<a class="{{if .PageIsCodeFrequency}}active{{end}} item" href="{{.RepoLink}}/activity/code-frequency">
9+
{{ctx.Locale.Tr "repo.activity.navbar.code_frequency"}}
10+
</a>
811
</div>
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<script>
2+
import {SvgIcon} from '../svg.js';
3+
import {
4+
Chart,
5+
Legend,
6+
LinearScale,
7+
TimeScale,
8+
PointElement,
9+
LineElement,
10+
Filler,
11+
} from 'chart.js';
12+
import {GET} from '../modules/fetch.js';
13+
import {Line as ChartLine} from 'vue-chartjs';
14+
import {
15+
startDaysBetween,
16+
firstStartDateAfterDate,
17+
fillEmptyStartDaysWithZeroes,
18+
} from '../utils/time.js';
19+
import {chartJsColors} from '../utils/color.js';
20+
import {sleep} from '../utils.js';
21+
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
22+
23+
const {pageData} = window.config;
24+
25+
Chart.defaults.color = chartJsColors.text;
26+
Chart.defaults.borderColor = chartJsColors.border;
27+
28+
Chart.register(
29+
TimeScale,
30+
LinearScale,
31+
Legend,
32+
PointElement,
33+
LineElement,
34+
Filler,
35+
);
36+
37+
export default {
38+
components: {ChartLine, SvgIcon},
39+
props: {
40+
locale: {
41+
type: Object,
42+
required: true
43+
},
44+
},
45+
data: () => ({
46+
isLoading: false,
47+
errorText: '',
48+
repoLink: pageData.repoLink || [],
49+
data: [],
50+
}),
51+
mounted() {
52+
this.fetchGraphData();
53+
},
54+
methods: {
55+
async fetchGraphData() {
56+
this.isLoading = true;
57+
try {
58+
let response;
59+
do {
60+
response = await GET(`${this.repoLink}/activity/code-frequency/data`);
61+
if (response.status === 202) {
62+
await sleep(1000); // wait for 1 second before retrying
63+
}
64+
} while (response.status === 202);
65+
if (response.ok) {
66+
this.data = await response.json();
67+
const weekValues = Object.values(this.data);
68+
const start = weekValues[0].week;
69+
const end = firstStartDateAfterDate(new Date());
70+
const startDays = startDaysBetween(new Date(start), new Date(end));
71+
this.data = fillEmptyStartDaysWithZeroes(startDays, this.data);
72+
this.errorText = '';
73+
} else {
74+
this.errorText = response.statusText;
75+
}
76+
} catch (err) {
77+
this.errorText = err.message;
78+
} finally {
79+
this.isLoading = false;
80+
}
81+
},
82+
83+
toGraphData(data) {
84+
return {
85+
datasets: [
86+
{
87+
data: data.map((i) => ({x: i.week, y: i.additions})),
88+
pointRadius: 0,
89+
pointHitRadius: 0,
90+
fill: true,
91+
label: 'Additions',
92+
backgroundColor: chartJsColors['additions'],
93+
borderWidth: 0,
94+
tension: 0.3,
95+
},
96+
{
97+
data: data.map((i) => ({x: i.week, y: -i.deletions})),
98+
pointRadius: 0,
99+
pointHitRadius: 0,
100+
fill: true,
101+
label: 'Deletions',
102+
backgroundColor: chartJsColors['deletions'],
103+
borderWidth: 0,
104+
tension: 0.3,
105+
},
106+
],
107+
};
108+
},
109+
110+
getOptions() {
111+
return {
112+
responsive: true,
113+
maintainAspectRatio: false,
114+
animation: true,
115+
plugins: {
116+
legend: {
117+
display: true,
118+
},
119+
},
120+
scales: {
121+
x: {
122+
type: 'time',
123+
grid: {
124+
display: false,
125+
},
126+
time: {
127+
minUnit: 'month',
128+
},
129+
ticks: {
130+
maxRotation: 0,
131+
maxTicksLimit: 12
132+
},
133+
},
134+
y: {
135+
ticks: {
136+
maxTicksLimit: 6
137+
},
138+
},
139+
},
140+
};
141+
},
142+
},
143+
};
144+
</script>
145+
<template>
146+
<div>
147+
<div class="ui header gt-df gt-ac gt-sb">
148+
{{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: `Code frequency over the history of ${repoLink.slice(1)}` }}
149+
</div>
150+
<div class="gt-df ui segment main-graph">
151+
<div v-if="isLoading || errorText !== ''" class="gt-tc gt-m-auto">
152+
<div v-if="isLoading">
153+
<SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
154+
{{ locale.loadingInfo }}
155+
</div>
156+
<div v-else class="text red">
157+
<SvgIcon name="octicon-x-circle-fill"/>
158+
{{ errorText }}
159+
</div>
160+
</div>
161+
<ChartLine
162+
v-memo="data" v-if="data.length !== 0"
163+
:data="toGraphData(data)" :options="getOptions()"
164+
/>
165+
</div>
166+
</div>
167+
</template>
168+
<style scoped>
169+
.main-graph {
170+
height: 440px;
171+
}
172+
</style>

web_src/js/components/RepoContributors.vue

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import {SvgIcon} from '../svg.js';
33
import {
44
Chart,
55
Title,
6-
Tooltip,
7-
Legend,
86
BarElement,
9-
CategoryScale,
107
LinearScale,
118
TimeScale,
129
PointElement,
@@ -21,27 +18,13 @@ import {
2118
firstStartDateAfterDate,
2219
fillEmptyStartDaysWithZeroes,
2320
} from '../utils/time.js';
21+
import {chartJsColors} from '../utils/color.js';
22+
import {sleep} from '../utils.js';
2423
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
2524
import $ from 'jquery';
2625
2726
const {pageData} = window.config;
2827
29-
const colors = {
30-
text: '--color-text',
31-
border: '--color-secondary-alpha-60',
32-
commits: '--color-primary-alpha-60',
33-
additions: '--color-green',
34-
deletions: '--color-red',
35-
title: '--color-secondary-dark-4',
36-
};
37-
38-
const styles = window.getComputedStyle(document.documentElement);
39-
const getColor = (name) => styles.getPropertyValue(name).trim();
40-
41-
for (const [key, value] of Object.entries(colors)) {
42-
colors[key] = getColor(value);
43-
}
44-
4528
const customEventListener = {
4629
id: 'customEventListener',
4730
afterEvent: (chart, args, opts) => {
@@ -54,17 +37,14 @@ const customEventListener = {
5437
}
5538
};
5639
57-
Chart.defaults.color = colors.text;
58-
Chart.defaults.borderColor = colors.border;
40+
Chart.defaults.color = chartJsColors.text;
41+
Chart.defaults.borderColor = chartJsColors.border;
5942
6043
Chart.register(
6144
TimeScale,
62-
CategoryScale,
6345
LinearScale,
6446
BarElement,
6547
Title,
66-
Tooltip,
67-
Legend,
6848
PointElement,
6949
LineElement,
7050
Filler,
@@ -122,7 +102,7 @@ export default {
122102
do {
123103
response = await GET(`${this.repoLink}/activity/contributors/data`);
124104
if (response.status === 202) {
125-
await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for 1 second before retrying
105+
await sleep(1000); // wait for 1 second before retrying
126106
}
127107
} while (response.status === 202);
128108
if (response.ok) {
@@ -222,7 +202,7 @@ export default {
222202
pointRadius: 0,
223203
pointHitRadius: 0,
224204
fill: 'start',
225-
backgroundColor: colors[this.type],
205+
backgroundColor: chartJsColors[this.type],
226206
borderWidth: 0,
227207
tension: 0.3,
228208
},
@@ -254,17 +234,13 @@ export default {
254234
title: {
255235
display: type === 'main',
256236
text: 'drag: zoom, shift+drag: pan, double click: reset zoom',
257-
color: colors.title,
258237
position: 'top',
259238
align: 'center',
260239
},
261240
customEventListener: {
262241
chartType: type,
263242
instance: this,
264243
},
265-
legend: {
266-
display: false,
267-
},
268244
zoom: {
269245
pan: {
270246
enabled: true,

0 commit comments

Comments
 (0)