~wegtam/smederee
Showing details for patch bc2512a72d2890be517a2a5d057ab38477a530c0.
diff -rN -u old-smederee/modules/hub/src/main/resources/messages.properties new-smederee/modules/hub/src/main/resources/messages.properties --- old-smederee/modules/hub/src/main/resources/messages.properties 2024-11-22 21:22:49.647586363 +0000 +++ new-smederee/modules/hub/src/main/resources/messages.properties 2024-11-22 21:22:49.651586371 +0000 @@ -260,6 +260,7 @@ repository.menu.labels=Labels repository.menu.milestones=Milestones repository.menu.overview=Overview +repository.menu.statistics=Statistics repository.menu.tickets=Tickets repository.menu.website=Website repository.menu.website.tooltip=Click here to open the project website ({0}) in a new tab or window. @@ -290,6 +291,9 @@ repository.overview.latest-changes.timestamp={0,date,yyyy-MM-dd (E)}, {0,time,short} repository.overview.rss-feed=RSS feed for this project. +repository.statistics.description=Some statistics on the repository. Details for programming languages are provided by tokei if available. +repository.statistics.tokei.disabled=Please install and enable tokei to show detailled statistics. + # User management / settings user.settings.account.delete.title=Delete your account user.settings.account.description=On this page you can manage your basic account settings and validate or delete your account. diff -rN -u old-smederee/modules/hub/src/main/resources/reference.conf new-smederee/modules/hub/src/main/resources/reference.conf --- old-smederee/modules/hub/src/main/resources/reference.conf 2024-11-22 21:22:49.651586371 +0000 +++ new-smederee/modules/hub/src/main/resources/reference.conf 2024-11-22 21:22:49.651586371 +0000 @@ -161,6 +161,17 @@ server-key-file = ${?SSH_SERVER_KEY} } + # Settings regarding the computation of statistics for repositories. + statistics { + # The path to the tokei binary executable. If not a full path (i.e. just + # `tokei`) it must be present on the `$PATH` of the environment under which + # the server is running. + # If tokei is not available then you MUST either comment it out or set it + # to `tokei = null`! + tokei = null + tokei = ${?SMEDEREE_TOKEI_EXECUTABLE} + } + # Signup / registration related settings. signup { enabled = true diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala 2024-11-22 21:22:49.651586371 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala 2024-11-22 21:22:49.651586371 +0000 @@ -187,6 +187,8 @@ * The configuration for the signup / registration feature. * @param ssh * Settings for the embedded SSH server component. + * @param statistics + * Settings regarding the computation of statistics for repositories. * @param tickets * Configuration regarding the integration with the hub service. */ @@ -203,6 +205,7 @@ external: ExternalUrlConfiguration, signup: SignupConfiguration, ssh: SshServerConfiguration, + statistics: StatisticsConfiguration, tickets: TicketIntegrationConfiguration ) @@ -230,7 +233,7 @@ ConfigReader.forProduct4("host", "path", "port", "scheme")(ExternalUrlConfiguration.apply) given ConfigReader[ServiceConfig] = - ConfigReader.forProduct13( + ConfigReader.forProduct14( "host", "port", "csrf-key-file", @@ -243,6 +246,7 @@ "external", "signup", "ssh", + "statistics", "ticket-integration" )(ServiceConfig.apply) } @@ -350,6 +354,16 @@ ConfigReader.forProduct2("executable", "repositories-directory")(DarcsConfiguration.apply) } +/** Settings regarding the computation of statistics for repositories. + */ +final case class StatisticsConfiguration(tokei: Option[Path]) + +object StatisticsConfiguration { + given Eq[StatisticsConfiguration] = Eq.fromUniversalEquals + + given ConfigReader[StatisticsConfiguration] = ConfigReader.forProduct1("tokei")(StatisticsConfiguration.apply) +} + /** Configuration for the signup feature. * * @param enabled diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala 2024-11-22 21:22:49.651586371 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala 2024-11-22 21:22:49.651586371 +0000 @@ -842,6 +842,83 @@ } } yield response + /** Render the statistics page for a vcs repository. + * + * @param csrf + * An optional CSRF-Token that shall be used. + * @param user + * An optional user account for whom the list of repositories shall be rendered. + * @param repositoryOwnerName + * The name of the user who owns the repository. + * @param repositoryName + * The actual name of the repository. + * @return + * An HTTP response containing the rendered HTML. + */ + def doShowRepositoryStatistics(csrf: Option[CsrfToken])( + user: Option[Account] + )(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Response[F]] = + for { + language <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en"))) + repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName) + repo = repoAndId.map(_._1) + branches <- repoAndId.map(_._2) match { + case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList + case _ => Sync[F].delay(List.empty) + } + repositoryDirectory <- Sync[F].delay( + os.Path( + Paths.get( + darcsConfig.repositoriesDirectory.toPath.toString, + repositoryOwnerName.toString, + repositoryName.toString + ) + ) + ) + stats <- Sync[F].delay( + configuration.statistics.tokei.map(tokei => + os.proc(tokei.toString).call(cwd = repositoryDirectory, check = false).out.text() + ) + ) + actionBaseUri <- Sync[F].delay( + linkConfig.createFullUri( + Uri(path = + Uri.Path( + Vector( + Uri.Path.Segment(s"~$repositoryOwnerName"), + Uri.Path.Segment(repositoryName.toString) + ) + ) + ) + ) + ) + repositoryBaseUri <- Sync[F].delay( + linkConfig.createFullUri( + Uri(path = + Uri.Path( + Vector( + Uri.Path.Segment(s"~$repositoryOwnerName"), + Uri.Path.Segment(repositoryName.toString) + ) + ) + ) + ) + ) + titleBase = genPageTitleBase(repositoryOwnerName)(repositoryName.some) + resp <- repo match { + case None => NotFound() + case Some(repo) => + Ok( + views.html.showRepositoryStatistics(baseUri, lang = language)( + actionBaseUri, + csrf = csrf, + title = titleBase.some, + user = user + )(repositoryBaseUri, stats, repo, branches) + ) + } + } yield resp + /** Get a diff for the requested patch and return the rendered response of it. * * @param csrf @@ -1719,18 +1796,38 @@ } yield resp } + private val showRepositoryStatistics: AuthedRoutes[Account, F] = AuthedRoutes.of { + case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter( + repositoryName + ) / "stats" as user => + for { + csrf <- Sync[F].delay(ar.req.getCsrfToken) + resp <- doShowRepositoryStatistics(csrf)(user.some)(repositoryOwnerName, repositoryName) + } yield resp + } + + private val showRepositoryStatisticsForGuests: HttpRoutes[F] = HttpRoutes.of { + case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter( + repositoryName + ) / "stats" => + for { + csrf <- Sync[F].delay(req.getCsrfToken) + resp <- doShowRepositoryStatistics(csrf)(None)(repositoryOwnerName, repositoryName) + } yield resp + } + val protectedRoutes = downloadDistribution <+> forkRepository <+> showAllRepositories <+> showRepositories <+> createRepository <+> deleteRepository <+> editRepository <+> showCreateRepositoryForm <+> showDeleteRepositoryForm <+> showEditRepositoryForm <+> showRepositoryOverview <+> showRepositoryBranches <+> showRepositoryHistory <+> - showRepositoryPatchDetails <+> showRepositoryFiles + showRepositoryStatistics <+> showRepositoryPatchDetails <+> showRepositoryFiles val routes = cloneRepository <+> downloadDistributionForGuests <+> showAllRepositoriesForGuests <+> showRepositoriesForGuests <+> showRepositoryOverviewForGuests <+> showRepositoryBranchesForGuests <+> - showRepositoryHistoryForGuests <+> showRepositoryPatchDetailsForGuests <+> showRepositoryFilesForGuests <+> - showRepositoryRssFeed + showRepositoryHistoryForGuests <+> showRepositoryStatisticsForGuests <+> showRepositoryPatchDetailsForGuests <+> + showRepositoryFilesForGuests <+> showRepositoryRssFeed } diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryMenu.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryMenu.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryMenu.scala.html 2024-11-22 21:22:49.651586371 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryMenu.scala.html 2024-11-22 21:22:49.651586371 +0000 @@ -28,6 +28,9 @@ @defining(repositoryBaseUri.addSegment("history")) { uri => <li class="pure-menu-item@if(activeUri.exists(_ === uri)){ pure-menu-active}else{}"><a class="pure-menu-link" href="@uri">@icon(baseUri)("list") @Messages("repository.menu.changes")</a></li> } + @defining(repositoryBaseUri.addSegment("stats")) { uri => + <li class="pure-menu-item@if(activeUri.exists(_ === uri)){ pure-menu-active}else{}"><a class="pure-menu-link" href="@uri">@icon(baseUri)("bar-chart-2") @Messages("repository.menu.statistics")</a></li> + } @if(branches > 0) { @defining(repositoryBaseUri.addSegment("branches")) { uri => <li class="pure-menu-item@if(activeUri.exists(_ === uri)){ pure-menu-active}else{}"><a class="pure-menu-link" href="@uri">@icon(baseUri)("git-branch") @Messages("repository.menu.branches", branches)</a></li> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryStatistics.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryStatistics.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryStatistics.scala.html 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryStatistics.scala.html 2024-11-22 21:22:49.651586371 +0000 @@ -0,0 +1,44 @@ +@import de.smederee.hub.* + +@( + baseUri: Uri, + lang: LanguageCode = LanguageCode("en") +)( + actionBaseUri: Uri, + csrf: Option[CsrfToken] = None, + goBackUri: Option[Uri] = None, + linkToTicketService: Option[Uri] = None, + title: Option[String] = None, + user: Option[Account] +)( + repositoryBaseUri: Uri, + stats: Option[String], + vcsRepository: VcsRepository, + vcsRepositoryBranches: List[(Username, VcsRepositoryName)] +) +@main(baseUri, lang)()(csrf, title, user) { +@defining(lang.toLocale) { implicit locale => + <div class="content"> + <div class="pure-g"> + <div class="pure-u-1-1 pure-u-md-1-1"> + <div class="l-box-left-right"> + <h2><a href="@{baseUri.addSegment(s"~${vcsRepository.owner.name}")}">~@vcsRepository.owner.name</a>/@vcsRepository.name</h2> + @showRepositoryMenu(baseUri, linkToTicketService)(repositoryBaseUri.addSegment("stats").some, vcsRepositoryBranches.size, repositoryBaseUri, user, vcsRepository) + <div class="repo-summary-description">@Messages("repository.statistics.description")</div> + </div> + </div> + </div> + </div> + <div class="content"> + <div class="pure-g"> + <div class="pure-u-1-1 pure-u-md-1-1"> + <div class="l-box"> + @defining(stats.getOrElse(Messages("repository.statistics.tokei.disabled"))) { output => + <pre><code>@output</code></pre> + } + </div> + </div> + </div> + </div> +} +} diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/config/ServiceConfigTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/config/ServiceConfigTest.scala --- old-smederee/modules/hub/src/test/scala/de/smederee/hub/config/ServiceConfigTest.scala 2024-11-22 21:22:49.651586371 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/config/ServiceConfigTest.scala 2024-11-22 21:22:49.655586381 +0000 @@ -60,6 +60,16 @@ } } + test("default configuration must have tokei statistics disabled") { + ConfigSource + .fromConfig(rawDefaultConfig()) + .at(s"${SmedereeHubConfig.location.toString}.service") + .load[ServiceConfig] match { + case Left(errors) => fail(errors.toList.mkString(", ")) + case Right(cfg) => assert(cfg.statistics.tokei.isEmpty) + } + } + test("default values for external linking must be setup for local development") { ConfigSource .fromConfig(rawDefaultConfig())