~jan0sch/smederee

Showing details for patch ead7169dbb3b92689d41191bca1d632ba89b9054.
2025-04-20 (Sun), 2:43 PM - Jens Grassel - ead7169dbb3b92689d41191bca1d632ba89b9054

vcs: Add tab with repository health

- add tab to show repository health
- implement needed functionality
- `darcs repair --dry-run` is run on access
- add translation keys
Summary of changes
1 files added
  • modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHealth.scala.html
4 files modified with 152 lines added and 2 lines removed
  • modules/hub/src/main/resources/messages.properties with 4 added and 0 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala with 21 added and 0 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala with 123 added and 1 removed lines
  • modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryMenu.scala.html with 4 added and 1 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	2025-06-20 19:29:45.911645517 +0000
+++ new-smederee/modules/hub/src/main/resources/messages.properties	2025-06-20 19:29:45.911645517 +0000
@@ -18,6 +18,9 @@
 errors.internal-server-error.message=An error occured while processing the request.
 errors.repository.not-found.title=Not found
 errors.repository.not-found.message=No repository named "{0}" found for user or organisation "{1}"!
+errors.repository.health.error.output=Command output
+errors.repository.health.error=The repository health check returned a non-zero exit code which indicates an error!
+errors.repository.health.success=No errors were found.
 errors.user-or-organisation.not-found.title=Not found
 errors.user-or-organisation.not-found.message=No user or organisation named "{0}" could be found!
 
@@ -261,6 +264,7 @@
 repository.menu.delete=Delete
 repository.menu.edit=Edit
 repository.menu.files=Files
+repository.menu.health=Health
 repository.menu.labels=Labels
 repository.menu.milestones=Milestones
 repository.menu.overview=Overview
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	2025-06-20 19:29:45.911645517 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala	2025-06-20 19:29:45.911645517 +0000
@@ -8,6 +8,9 @@
 
 import java.io.IOException
 import java.nio.file.*
+import java.time.OffsetDateTime
+import java.time.ZoneId
+import java.time.ZoneOffset
 
 import cats.*
 import cats.data.*
@@ -661,6 +664,101 @@
                 }
         } yield resp
 
+    /** Logic for performing a repository health check and show the results.
+      *
+      * TODO: Move the actual check into a cronjob and just display the results here.
+      *
+      * @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 doShowRepositoryHealth(
+        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)
+            directory <- Sync[F].delay(
+                os.Path(
+                    Paths.get(
+                        darcsConfig.repositoriesDirectory.toPath.toString,
+                        repositoryOwnerName.toString
+                    )
+                )
+            )
+            branches <- repoAndId.map(_._2) match {
+                case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
+                case _            => Sync[F].delay(List.empty)
+            }
+            check <- darcs.repair(directory.toNIO)(repositoryName.toString)(Chain("--dry-run"))
+            health = VcsRepositoryHealth(
+                command = "darcs repair --dry-run",
+                exitCode = check.exitValue,
+                stderr = check.stderr,
+                stdout = check.stdout,
+                createdAt = OffsetDateTime.now(ZoneId.of(ZoneOffset.UTC.getId))
+            )
+            actionBaseUri <- Sync[F].delay(
+                linkConfig.createFullUri(
+                    Uri(path =
+                        Uri.Path(
+                            Vector(
+                                Uri.Path.Segment(s"~$repositoryOwnerName"),
+                                Uri.Path.Segment(repositoryName.toString)
+                            )
+                        )
+                    )
+                )
+            )
+            goBackUri <- 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)
+                            )
+                        )
+                    )
+                )
+            )
+            pageTitle = genPageTitleBase(repositoryOwnerName)(repositoryName.some) |+| ": Health"
+            resp <- repo match {
+                case None => NotFound()
+                case Some(repo) =>
+                    Ok(
+                        views.html.showRepositoryHealth(baseUri, lang = language)(
+                            actionBaseUri,
+                            csrf,
+                            goBackUri.some,
+                            linkToTicketService,
+                            pageTitle.some,
+                            user
+                        )(health, repositoryBaseUri, repo, branches)
+                    )
+            }
+        } yield resp
+
     /** Logic for showing the history (changes) for the requested repository.
       *
       * @param csrf
@@ -1825,6 +1923,29 @@
             } yield resp
     }
 
+    private val showRepositoryHealth: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) / "health" as user =>
+            for {
+                csrf      <- Sync[F].delay(ar.req.getCsrfToken)
+                language  <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                repoAndId <- loadRepo(user.some)(repositoryOwnerName, repositoryName)
+                resp <- repoAndId.fold(
+                    NotFound(
+                        views.html.errors.repositoryNotFound(lang = language)(
+                            csrf = csrf,
+                            title = None,
+                            user.some
+                        )(
+                            repositoryOwnerName,
+                            repositoryName
+                        )
+                    )
+                )(_ => doShowRepositoryHealth(csrf)(user.some)(repositoryOwnerName, repositoryName))
+            } yield resp
+    }
+
     private val showRepositoryHistory: AuthedRoutes[Account, F] = AuthedRoutes.of {
         case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
                 repositoryName
@@ -1983,7 +2104,8 @@
             createRepository <+> deleteRepository <+> editRepository <+>
             showCreateRepositoryForm <+> showDeleteRepositoryForm <+> showEditRepositoryForm <+>
             showRepositoryOverview <+> showRepositoryBranches <+> showRepositoryHistory <+>
-            showRepositoryStatistics <+> showRepositoryPatchDetails <+> showRepositoryFiles
+            showRepositoryHealth <+> showRepositoryStatistics <+> showRepositoryPatchDetails <+>
+            showRepositoryFiles
 
     val routes =
         cloneRepository <+> downloadDistributionForGuests <+> showAllRepositoriesForGuests <+>
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	2025-06-20 19:29:45.911645517 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala	2025-06-20 19:29:45.911645517 +0000
@@ -540,6 +540,27 @@
     }
 }
 
+/** Container for repository health check run output.
+  *
+  * @param command
+  *   The command that was run on the repository.
+  * @param exitCode
+  *   The exit code of the command, usually 0 implies no errors.
+  * @param stderr
+  *   Saved output of the standard error channel of the command.
+  * @param stdout
+  *   Saved output of the standard output channel of the command.
+  * @param createdAt
+  *   The timestamp of when this entry was created which should be the last time the command was run.
+  */
+final case class VcsRepositoryHealth(
+    command: String,
+    exitCode: Int,
+    stderr: Chain[String],
+    stdout: Chain[String],
+    createdAt: OffsetDateTime
+)
+
 /** Data about a VCS respository.
   *
   * @param name
diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHealth.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHealth.scala.html
--- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHealth.scala.html	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHealth.scala.html	2025-06-20 19:29:45.911645517 +0000
@@ -0,0 +1,86 @@
+@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]
+)(
+  health: VcsRepositoryHealth,
+  repositoryBaseUri: Uri,
+  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("health").some, vcsRepositoryBranches.size, repositoryBaseUri, user, vcsRepository)
+          <div class="repo-summary-description">
+        </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">
+          @if(health.exitCode =!= 0) {
+            <p class="alert alert-error">
+              <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+              <span class="sr-only">@Messages("global.error"):</span>
+              @Messages("errors.repository.health.error")
+            </p>
+          } else {
+            <p class="alert alert-success">
+              @Messages("errors.repository.health.success")
+            </p>
+          }
+        </div>
+      </div>
+    </div>
+    <div class="pure-g">
+      <div class="pure-u-1-1 pure-u-md-1-1">
+        <div class="l-box">
+          <h4>@Messages("errors.repository.health.error.output")</h4>
+          <table class="pure-table">
+            <thead>
+            </thead>
+            <tbody class="repository-file-content">
+            @for(tuple <- health.stdout.iterator.zipWithIndex) {
+              @defining(tuple._2) { lineNumber =>
+                @defining(tuple._1) { line =>
+              <tr class="code-line">
+                <td class="code-line-number" id="L@lineNumber"><a href="#L@lineNumber">@lineNumber</a></td>
+                <td class="code-line"><code class="code-line">@line</code></td>
+              </tr>
+                }
+              }
+            }
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </div>
+    <div class="pure-g">
+      <div class="pure-u-1-1 pure-u-md-1-1">
+        <div class="l-box">
+          <pre>
+          @for(line <- health.stderr.iterator) {
+            @line
+          }
+          </pre>
+        </div>
+      </div>
+    </div>
+  </div>
+}
+}
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	2025-06-20 19:29:45.911645517 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryMenu.scala.html	2025-06-20 19:29:45.911645517 +0000
@@ -36,8 +36,11 @@
       <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>
       }
     } else {}
-    @if(activeUri.exists(uri => uri === repositoryBaseUri || uri === repositoryBaseUri.addSegment("edit") || uri === repositoryBaseUri.addSegment("delete"))) {
+    @if(activeUri.exists(uri => uri === repositoryBaseUri || uri === repositoryBaseUri.addSegment("edit") || uri === repositoryBaseUri.addSegment("delete") || uri === repositoryBaseUri.addSegment("health"))) {
       @if(user.exists(_.uid === vcsRepository.owner.uid)) {
+      @defining(repositoryBaseUri.addSegment("health")) { uri =>
+        <li class="pure-menu-item@if(activeUri.exists(_ === uri)){ pure-menu-active}else{}"><a class="pure-menu-link" href="@uri">@icon(baseUri)("activity") @Messages("repository.menu.health")</a></li>
+      }
       @defining(repositoryBaseUri.addSegment("edit")) { uri =>
         <li class="pure-menu-item@if(activeUri.exists(_ === uri)){ pure-menu-active}else{}"><a class="pure-menu-link" href="@uri">@icon(baseUri)("edit-2") @Messages("repository.menu.edit")</a></li>
       }