~wegtam/smederee

Showing details for patch bc2512a72d2890be517a2a5d057ab38477a530c0.
2024-09-24 (Tue), 11:28 AM - Jens Grassel - bc2512a72d2890be517a2a5d057ab38477a530c0

VCS: Add respository statistics page.

- add configuration section for statistics
- disable tokei by default
- add repository statistics page
- add code for computing and rendering statistics via tokei
Summary of changes
1 files added
  • modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryStatistics.scala.html
6 files modified with 143 lines added and 4 lines removed
  • modules/hub/src/main/resources/messages.properties with 4 added and 0 removed lines
  • modules/hub/src/main/resources/reference.conf with 11 added and 0 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala with 100 added and 3 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala with 15 added and 1 removed lines
  • modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryMenu.scala.html with 3 added and 0 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/config/ServiceConfigTest.scala with 10 added and 0 removed lines
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())