mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	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.
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
							
								
								
									
										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) | ||||
| 	} | ||||
| } | ||||
| @@ -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> | ||||
|   | ||||
							
								
								
									
										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 {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(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Şahin Akkaya
					Şahin Akkaya