~wegtam/smederee
Showing details for patch 2ae3629344634ad68bd69633991b13c816d71255.
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>