~wegtam/smederee

Showing details for patch 2ae3629344634ad68bd69633991b13c816d71255.
2024-09-27 (Fri), 10:51 AM - Jens Grassel - 2ae3629344634ad68bd69633991b13c816d71255

VCS: Extend repository statistics page.

- add data type for statistics
- move calculation to `VcsRepositoryStatistics`
- add translations
- remove parentheses around date in `formatDate`
- add more general statistics
- use 2 column layout for statistics page if possible
Summary of changes
1 files added
  • modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryStatistics.scala
5 files modified with 56 lines added and 11 lines removed
  • modules/hub/src/main/resources/messages.properties with 9 added and 0 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala with 4 added and 0 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala with 4 added and 7 removed lines
  • modules/hub/src/main/twirl/de/smederee/hub/views/format/formatDate.scala.html with 1 added and 1 removed lines
  • modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryStatistics.scala.html with 38 added and 3 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 15:57:06.523211987 +0000
+++ new-smederee/modules/hub/src/main/resources/messages.properties	2024-11-22 15:57:06.523211987 +0000
@@ -291,7 +291,16 @@
 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.average-weekly-code-lines=Average weekly code lines
+repository.statistics.average-weekly-code-lines.details={0} added, {1} removed
+repository.statistics.average-weekly-patch-count=Average weekly patch count
 repository.statistics.description=Some statistics on the repository. Details for programming languages are provided by tokei if available.
+repository.statistics.general-details=General details about the repository
+repository.statistics.latest-patch=Latest patch at
+repository.statistics.number-of-patch-authors=Number of patch authors
+repository.statistics.number-of-patches=Number of patches
+repository.statistics.oldest-patch=Oldest patch at
+repository.statistics.programming-language-details=Details about used programming languages
 repository.statistics.tokei.disabled=Please install and enable tokei to show detailled statistics.
 
 # User management / settings
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 15:57:06.523211987 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala	2024-11-22 15:57:06.523211987 +0000
@@ -866,19 +866,16 @@
                 case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
                 case _            => Sync[F].delay(List.empty)
             }
-            repositoryDirectory <- Sync[F].delay(
+            ownerDirectory <- Sync[F].delay(
                 os.Path(
                     Paths.get(
                         darcsConfig.repositoriesDirectory.toPath.toString,
-                        repositoryOwnerName.toString,
-                        repositoryName.toString
+                        repositoryOwnerName.toString
                     )
                 )
             )
-            stats <- Sync[F].delay(
-                configuration.statistics.tokei.map(tokei =>
-                    os.proc(tokei.toString).call(cwd = repositoryDirectory, check = false).out.text()
-                )
+            stats <- VcsRepositoryStatistics.calculateStatistics(configuration.statistics)(darcs)(ownerDirectory)(
+                repositoryName
             )
             actionBaseUri <- Sync[F].delay(
                 linkConfig.createFullUri(
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala	2024-11-22 15:57:06.523211987 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala	2024-11-22 15:57:06.523211987 +0000
@@ -365,6 +365,10 @@
       */
     def toInstant: Instant = timestamp.toInstant
 
+    def toLocalDate: LocalDate = timestamp.toLocalDate
+
+    def toLocalDateTime: LocalDateTime = timestamp.toLocalDateTime
+
     def toOffsetDateTime: OffsetDateTime = timestamp
 }
 
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryStatistics.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryStatistics.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryStatistics.scala	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryStatistics.scala	2024-11-22 15:57:06.523211987 +0000
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2022 Contributors as noted in the AUTHORS.md file
+ *
+ * Licensed under the EUPL
+ */
+
+package de.smederee.hub
+
+import java.time.temporal.ChronoField
+
+import cats.data.*
+import cats.effect.*
+import cats.kernel.Semigroup
+import cats.syntax.all.*
+import de.smederee.darcs.DarcsCommands
+import de.smederee.hub.config.StatisticsConfiguration
+import monocle.*
+import monocle.syntax.all.*
+
+/** Data class for statistics of a [[VcsRepository]].
+  *
+  * @param authors
+  *   A list of all patch authors who participated in the repository.
+  * @param languages
+  *   Detailed statistics about the programming languages used within the repository.
+  * @param numberOfPatches
+  *   The absolute number of patches in the repository.
+  * @param weeklyCodeFrequency
+  *   A list containing the code frequency (lines added and removed) per week since the first commit.
+  * @param weeklyCommitCount
+  *   A list containing the number of commits per week starting from the current week.
+  */
+final case class VcsRepositoryStatistics(
+    authors: List[VcsPatchAuthor],
+    languages: Option[String],
+    latestPatch: Option[VcsPatchTimestamp],
+    numberOfPatches: Int,
+    oldestPatch: Option[VcsPatchTimestamp],
+    weeklyCodeFrequency: List[VcsRepositoryStatisticsCodeFrequency],
+    weeklyCommitCount: List[Int]
+)
+
+object VcsRepositoryStatistics {
+
+    private val modifiedLines = Focus[VcsRepositoryPatchMetadata](_.summary.each.modified.each)
+
+    /** Calculate the statistics for a repository.
+      *
+      * @param configuration
+      *   The configuration for the statistics functionality.
+      * @param darcs
+      *   A class providing darcs VCS operations.
+      * @param ownerDirectory
+      *   The filesystem directory of the repository owner in which the repository is located.
+      * @param repositoryName
+      *   The name of the repository.
+      * @return
+      *   Several calculated statistics wrapped into a single object.
+      * @tparam F
+      *   A higher kinded type providing needed functionality, which is usually an IO monad like Async or Sync.
+      */
+    def calculateStatistics[F[_]: Async](configuration: StatisticsConfiguration)(darcs: DarcsCommands[F])(
+        ownerDirectory: os.Path
+    )(
+        repositoryName: VcsRepositoryName
+    ): F[VcsRepositoryStatistics] =
+        for {
+            repositoryDirectory <- Sync[F].delay(ownerDirectory / repositoryName.toString)
+            languages <- Sync[F].delay(
+                configuration.tokei.map(tokei =>
+                    os.proc(tokei.toString).call(cwd = repositoryDirectory, check = false).out.text()
+                )
+            )
+            countPatches    <- darcs.log(ownerDirectory.toNIO)(repositoryName.toString)(Chain("--count"))
+            numberOfPatches <- Sync[F].delay(countPatches.stdout.toList.mkString.trim.toInt)
+            vcsLog  <- darcs.log(ownerDirectory.toNIO)(repositoryName.toString)(Chain("--summary", "--xml-output"))
+            xmlLog  <- Sync[F].delay(scala.xml.XML.loadString(vcsLog.stdout.toList.mkString))
+            patches <- Sync[F].delay((xmlLog \ "patch").flatMap(VcsRepositoryPatchMetadata.fromDarcsXmlLog).toList)
+            latestPatch   = patches.headOption.map(_.timestamp)
+            oldestPatch   = patches.lastOption.map(_.timestamp)
+            authors       = patches.map(_.author).distinct
+            patchesByYear = patches.groupBy(_.timestamp.toLocalDate.getYear)
+            patchesByWeek = patchesByYear
+                .map { (year, patches) =>
+                    (
+                        year,
+                        patches
+                            .groupBy(_.timestamp.toLocalDate.get(ChronoField.ALIGNED_WEEK_OF_YEAR))
+                            .toList
+                            .sortBy(_._1)
+                    )
+                }
+                .toList
+                .sortBy(_._1) // Sort by year...
+                .reverse      // ...from the current year backwards.
+            weeklyCommitCount = patchesByWeek
+                .flatMap((_, patchesPerWeek) => patchesPerWeek.map((_, patches) => patches.size))
+            weeklyCodeFrequency = patchesByWeek.flatMap { (_, patchesPerWeek) =>
+                patchesPerWeek.map { (_, patches) =>
+                    val modified = patches.focus(_.each).andThen(modifiedLines).getAll
+                    modified
+                        .map(m => VcsRepositoryStatisticsCodeFrequency(m.added, m.removed))
+                        .fold(VcsRepositoryStatisticsCodeFrequency(0, 0))((a, b) => a |+| b)
+                }
+            }
+            stats = VcsRepositoryStatistics(
+                authors = authors,
+                languages = languages,
+                latestPatch = latestPatch,
+                numberOfPatches = numberOfPatches,
+                oldestPatch = oldestPatch,
+                weeklyCodeFrequency = weeklyCodeFrequency,
+                weeklyCommitCount = weeklyCommitCount
+            )
+        } yield stats
+}
+
+/** A data class for the "code frequency" the number of lines added and removed in a fixed period.
+  *
+  * @addedLines
+  *   The number of lines of code that were added.
+  * @removedLines
+  *   The number of lines of code that were removed.
+  */
+final case class VcsRepositoryStatisticsCodeFrequency(addedLines: Int, removedLines: Int)
+
+object VcsRepositoryStatisticsCodeFrequency {
+    given Semigroup[VcsRepositoryStatisticsCodeFrequency] =
+        new Semigroup[VcsRepositoryStatisticsCodeFrequency] {
+            override def combine(
+                x: VcsRepositoryStatisticsCodeFrequency,
+                y: VcsRepositoryStatisticsCodeFrequency
+            ): VcsRepositoryStatisticsCodeFrequency =
+                VcsRepositoryStatisticsCodeFrequency(
+                    addedLines = x.addedLines + y.addedLines,
+                    removedLines = x.removedLines + y.removedLines
+                )
+        }
+}
diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/format/formatDate.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/format/formatDate.scala.html
--- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/format/formatDate.scala.html	2024-11-22 15:57:06.523211987 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/format/formatDate.scala.html	2024-11-22 15:57:06.523211987 +0000
@@ -8,4 +8,4 @@
 )(
   implicit locale: Locale
 )
-(@DateTimeFormatter.ofLocalizedDate(style).withLocale(locale).format(date))
+@DateTimeFormatter.ofLocalizedDate(style).withLocale(locale).format(date)
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	2024-11-22 15:57:06.523211987 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryStatistics.scala.html	2024-11-22 15:57:06.523211987 +0000
@@ -1,4 +1,5 @@
 @import de.smederee.hub.*
+@import de.smederee.hub.views.html.format.*
 
 @(
   baseUri: Uri,
@@ -12,7 +13,7 @@
   user: Option[Account]
 )(
   repositoryBaseUri: Uri,
-  stats: Option[String],
+  stats: VcsRepositoryStatistics,
   vcsRepository: VcsRepository,
   vcsRepositoryBranches: List[(Username, VcsRepositoryName)]
 )
@@ -31,9 +32,43 @@
   </div>
   <div class="content">
     <div class="pure-g">
-      <div class="pure-u-1-1 pure-u-md-1-1">
+      <div class="pure-u-1 pure-u-lg-2-5">
+        <div class="l-box">
+          <h3>@Messages("repository.statistics.general-details")</h3>
+          <table class="pure-table pure-table-horizontal" style="text-align: left;">
+            <tbody>
+              <tr>
+                <th>@Messages("repository.statistics.latest-patch")</th>
+                <td>@for(date <- stats.latestPatch) { @formatDate(date.toLocalDate) }</td>
+              </tr>
+              <tr>
+                <th>@Messages("repository.statistics.oldest-patch")</th>
+                <td>@for(date <- stats.oldestPatch) { @formatDate(date.toLocalDate) }</td>
+              </tr>
+              <tr>
+                <th>@Messages("repository.statistics.number-of-patch-authors")</th>
+                <td>@stats.authors.size</td>
+              </tr>
+              <tr>
+                <th>@Messages("repository.statistics.number-of-patches")</th>
+                <td>@stats.numberOfPatches</td>
+              </tr>
+              <tr>
+                <th>@Messages("repository.statistics.average-weekly-patch-count")</th>
+                <td>@{stats.weeklyCommitCount.sum / stats.weeklyCommitCount.size}</td>
+              </tr>
+              <tr>
+                <th>@Messages("repository.statistics.average-weekly-code-lines")</th>
+                <td>@Messages("repository.statistics.average-weekly-code-lines.details", stats.weeklyCodeFrequency.map(_.addedLines).sum / stats.weeklyCodeFrequency.size, stats.weeklyCodeFrequency.map(_.removedLines).sum / stats.weeklyCodeFrequency.size)</td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+      <div class="pure-u-1 pure-u-lg-3-5">
         <div class="l-box">
-          @defining(stats.getOrElse(Messages("repository.statistics.tokei.disabled"))) { output =>
+          <h3>@Messages("repository.statistics.programming-language-details")</h3>
+          @defining(stats.languages.getOrElse(Messages("repository.statistics.tokei.disabled"))) { output =>
           <pre><code>@output</code></pre>
           }
         </div>