~jan0sch/smederee

Showing details for patch a55e078d48f149d9ff07952745e4598094573a8c.
2022-08-14 (Sun), 2:27 PM - Jens Grassel - a55e078d48f149d9ff07952745e4598094573a8c

VCS: Loooots of changes

- more refactoring and moving things around
- split overview, files and history under sparate paths
- incomplete overview page for repo
- all should also work for guest users
Summary of changes
1 files added
  • modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html
4 files modified with 233 lines added and 52 lines removed
  • modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala with 25 added and 0 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala with 183 added and 48 removed lines
  • modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html with 8 added and 1 removed lines
  • modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHistory.scala.html with 17 added and 3 removed lines
diff -rN -u old-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala new-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala
--- old-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala	2025-02-02 16:49:24.958218841 +0000
+++ new-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala	2025-02-02 16:49:24.958218841 +0000
@@ -18,6 +18,31 @@
 import org.slf4j.LoggerFactory
 
 import scala.sys.process._
+import scala.util.matching.Regex
+
+opaque type DarcsHash = String
+object DarcsHash {
+  val Format: Regex = "^[a-f0-9]{40}$".r
+
+  /** Create an instance of DarcsHash from the given String type.
+    *
+    * @param source
+    *   An instance of type String which will be returned as a DarcsHash.
+    * @return
+    *   The appropriate instance of DarcsHash.
+    */
+  def apply(source: String): DarcsHash = source
+
+  /** Try to create an instance of DarcsHash from the given String.
+    *
+    * @param source
+    *   A String that should fulfil the requirements to be converted into a DarcsHash.
+    * @return
+    *   An option to the successfully converted DarcsHash.
+    */
+  def from(source: String): Option[DarcsHash] = Option(source).filter(s => Format.matches(s))
+
+}
 
 /** Wrapper for output generated by an external darcs command.
   *
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-02-02 16:49:24.958218841 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala	2025-02-02 16:49:24.958218841 +0000
@@ -21,6 +21,7 @@
 import de.smederee.hub.forms.types.FormErrors
 import org.http4s._
 import org.http4s.dsl.Http4sDsl
+import org.http4s.dsl.impl._
 import org.http4s.headers.Location
 import org.http4s.implicits._
 import org.http4s.twirl.TwirlInstances._
@@ -79,6 +80,55 @@
     *   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.
+    */
+  private def doShowRepositoryOverview(
+      csrf: Option[CsrfToken]
+  )(user: Option[Account])(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Response[F]] =
+    for {
+      owner <- vcsMetadataRepo.findVcsRepositoryOwner(repositoryOwnerName)
+      loadedRepo <- owner match {
+        case None        => Sync[F].pure(None)
+        case Some(owner) => vcsMetadataRepo.findVcsRepository(owner, repositoryName)
+      }
+      // TODO Replace with whatever we implement as proper permission model. ;-)
+      repo = user match {
+        case None => loadedRepo.filter(r => r.isPrivate === false)
+        case Some(user) =>
+          loadedRepo.filter(r => r.isPrivate === false || r.owner === user.toVcsRepositoryOwner)
+      }
+      actionBaseUri <- Sync[F].delay(
+        Uri(path =
+          Uri.Path.Root |+| Uri.Path(
+            Vector(Uri.Path.Segment(s"~$repositoryOwnerName"), Uri.Path.Segment(repositoryName.toString))
+          )
+        )
+      )
+      resp <- repo match {
+        case None => NotFound("Repository not found!")
+        case Some(repo) =>
+          Ok(
+            views.html.showRepositoryOverview()(
+              actionBaseUri,
+              csrf,
+              s"Smederee/~$repositoryOwnerName/$repositoryName".some,
+              user
+            )(repo.some)
+          )
+      }
+    } yield resp
+
+  /** Logic for rendering the content of a repository directory or file visible to the given user account.
+    *
+    * @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.
     * @param filePath
     *   An URI path which describes the path the the requested part of the repository (empty or `/` for the
     *   root directory of the repo).
@@ -150,9 +200,9 @@
           )
         )
       )
-      actionBaseUri <- Sync[F].delay(Uri(path = repositoryBaseUri.path |+| filePath))
+      actionBaseUri <- Sync[F].delay(Uri(path = repositoryBaseUri.path.addSegment("files") |+| filePath))
       goBackUri <- Sync[F].delay(
-        Uri(path = repositoryBaseUri.path |+| Uri.Path(filePath.segments.reverse.drop(1).reverse))
+        Uri(path = Uri.Path.Root |+| Uri.Path(actionBaseUri.path.segments.reverse.drop(1).reverse))
       )
       resp <-
         repo match {
@@ -165,7 +215,7 @@
               NotFound("File not found!")
             else
               Ok(
-                views.html.showRepository()(
+                views.html.showRepositoryFiles()(
                   actionBaseUri,
                   csrf,
                   Option(goBackUri),
@@ -176,6 +226,95 @@
         }
     } yield resp
 
+  /** Logic for showing the history (changes) for the requested 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.
+    * @param fromEntry
+    *   The optional number of the change from which the history shall be shown.
+    * @return
+    *   An HTTP response containing the rendered HTML.
+    */
+  def doShowRepositoryHistory(
+      csrf: Option[CsrfToken]
+  )(user: Option[Account])(repositoryOwnerName: Username, repositoryName: VcsRepositoryName)(
+      fromEntry: Option[Int]
+  ): F[Response[F]] =
+    for {
+      owner <- vcsMetadataRepo.findVcsRepositoryOwner(repositoryOwnerName)
+      loadedRepo <- owner match {
+        case None        => Sync[F].pure(None)
+        case Some(owner) => vcsMetadataRepo.findVcsRepository(owner, repositoryName)
+      }
+      // TODO Replace with whatever we implement as proper permission model. ;-)
+      repo = user match {
+        case None => loadedRepo.filter(r => r.isPrivate === false)
+        case Some(user) =>
+          loadedRepo.filter(r => r.isPrivate === false || r.owner === user.toVcsRepositoryOwner)
+      }
+      directory <- Sync[F].delay(
+        os.Path(
+          Paths.get(
+            config.repositoriesDirectory.toPath.toString,
+            repositoryOwnerName.toString
+          )
+        )
+      )
+      countChanges    <- darcs.log(directory.toNIO)(repositoryName.toString)(Chain("--count"))
+      numberOfChanges <- Sync[F].delay(countChanges.stdout.toList.mkString.trim.toInt)
+      maxCount = 10
+      from     = fromEntry.getOrElse(1)
+      to =
+        if (from + maxCount > numberOfChanges)
+          numberOfChanges
+        else
+          from + maxCount
+      next = if (to < numberOfChanges) Option(to + 1) else None
+      history <- darcs.log(directory.toNIO)(repositoryName.toString)(Chain(s"--index=$from-$to", "--summary"))
+      actionBaseUri <- Sync[F].delay(
+        Uri(path =
+          Uri.Path.Root |+| Uri.Path(
+            Vector(
+              Uri.Path.Segment(s"~$repositoryOwnerName"),
+              Uri.Path.Segment(repositoryName.toString)
+            )
+          )
+        )
+      )
+      goBackUri <- Sync[F].delay(
+        Uri(path =
+          Uri.Path.Root |+| Uri.Path(
+            Vector(
+              Uri.Path.Segment(s"~$repositoryOwnerName"),
+              Uri.Path.Segment(repositoryName.toString)
+            )
+          )
+        )
+      )
+      repositoryBaseUri <- Sync[F].delay(
+        Uri(path =
+          Uri.Path.Root |+| Uri.Path(
+            Vector(Uri.Path.Segment(s"~$repositoryOwnerName"), Uri.Path.Segment(repositoryName.toString))
+          )
+        )
+      )
+      resp <- Ok.apply(
+        views.html.showRepositoryHistory()(
+          actionBaseUri,
+          csrf,
+          Option(goBackUri),
+          s"Smederee - History of ~$repositoryOwnerName/$repositoryName".some,
+          user
+        )(history.stdout.toList.mkString("\n"), next, repositoryBaseUri, repositoryName)
+      )
+    } yield resp
+
   /** List walk the given directory at the first level and return all found files and directories and their
     * stats sorted by directory first and by name second. If the given path is _not_ a directory then no
     * traversal is done and an empty list is returned.
@@ -360,10 +499,30 @@
       } yield resp
   }
 
+  private val showRepositoryOverview: AuthedRoutes[Account, F] = AuthedRoutes.of {
+    case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+          repositoryName
+        ) as user =>
+      for {
+        csrf <- Sync[F].delay(ar.req.getCsrfToken)
+        resp <- doShowRepositoryOverview(csrf)(user.some)(repositoryOwnerName, repositoryName)
+      } yield resp
+  }
+
+  private val showRepositoryOverviewForGuests: HttpRoutes[F] = HttpRoutes.of {
+    case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+          repositoryName
+        ) =>
+      for {
+        csrf <- Sync[F].delay(req.getCsrfToken)
+        resp <- doShowRepositoryOverview(csrf)(None)(repositoryOwnerName, repositoryName)
+      } yield resp
+  }
+
   private val showRepositoryFiles: AuthedRoutes[Account, F] = AuthedRoutes.of {
     case ar @ GET -> UsernamePathParameter(repositoryOwnerName) /: VcsRepositoryNamePathParameter(
           repositoryName
-        ) /: filePath as user =>
+        ) /: "files" /: filePath as user =>
       for {
         csrf <- Sync[F].delay(ar.req.getCsrfToken)
         resp <- doShowRepositoryFiles(csrf)(user.some)(repositoryOwnerName, repositoryName)(filePath)
@@ -373,7 +532,7 @@
   private val showRepositoryFilesForGuests: HttpRoutes[F] = HttpRoutes.of {
     case req @ GET -> UsernamePathParameter(repositoryOwnerName) /: VcsRepositoryNamePathParameter(
           repositoryName
-        ) /: filePath =>
+        ) /: "files" /: filePath =>
       for {
         csrf <- Sync[F].delay(req.getCsrfToken)
         resp <- doShowRepositoryFiles(csrf)(None)(repositoryOwnerName, repositoryName)(filePath)
@@ -381,55 +540,31 @@
   }
 
   private val showRepositoryHistory: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(repositoryOwner) / VcsRepositoryNamePathParameter(
+    case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
           repositoryName
-        ) / "history" as user =>
+        ) / "history" :? HistoryFromQueryParameter(fromCount) as user =>
       for {
         csrf <- Sync[F].delay(ar.req.getCsrfToken)
-        directory <- Sync[F].delay(
-          os.Path(
-            Paths.get(
-              config.repositoriesDirectory.toPath.toString,
-              repositoryOwner.toString
-            )
-          )
-        )
-        history <- darcs.log(directory.toNIO)(repositoryName.toString)(Chain("--summary"))
-        actionBaseUri <- Sync[F].delay(
-          Uri(path =
-            Uri.Path.Root |+| Uri.Path(
-              Vector(
-                Uri.Path.Segment(s"~$repositoryOwner"),
-                Uri.Path.Segment(repositoryName.toString)
-              )
-            )
-          )
-        )
-        goBackUri <- Sync[F].delay(
-          Uri(path =
-            Uri.Path.Root |+| Uri.Path(
-              Vector(
-                Uri.Path.Segment(s"~$repositoryOwner"),
-                Uri.Path.Segment(repositoryName.toString)
-              )
-            )
-          )
-        )
-        resp <- Ok.apply(
-          views.html.showRepositoryHistory()(
-            actionBaseUri,
-            csrf,
-            Option(goBackUri),
-            s"Smederee - History of ~$repositoryOwner/$repositoryName".some,
-            user
-          )(history.stdout.toList.mkString("\n"), repositoryName)
-        )
+        resp <- doShowRepositoryHistory(csrf)(user.some)(repositoryOwnerName, repositoryName)(fromCount)
+      } yield resp
+  }
+
+  private val showRepositoryHistoryForGuests: HttpRoutes[F] = HttpRoutes.of {
+    case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+          repositoryName
+        ) / "history" :? HistoryFromQueryParameter(fromCount) =>
+      for {
+        csrf <- Sync[F].delay(req.getCsrfToken)
+        resp <- doShowRepositoryHistory(csrf)(None)(repositoryOwnerName, repositoryName)(fromCount)
       } yield resp
   }
 
   val protectedRoutes =
-    showAllRepositories <+> showRepositories <+> parseCreateRepositoryForm <+> showCreateRepositoryForm <+> showRepositoryHistory <+> showRepositoryFiles
+    showAllRepositories <+> showRepositories <+> parseCreateRepositoryForm <+> showCreateRepositoryForm <+> showRepositoryOverview <+> showRepositoryHistory <+> showRepositoryFiles
 
-  val routes = showAllRepositoriesForGuests <+> showRepositoryFilesForGuests
+  val routes =
+    showAllRepositoriesForGuests <+> showRepositoryOverviewForGuests <+> showRepositoryHistoryForGuests <+> showRepositoryFilesForGuests
 
 }
+
+object HistoryFromQueryParameter extends OptionalQueryParamDecoderMatcher[Int]("from")
diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html
--- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html	2025-02-02 16:49:24.958218841 +0000
@@ -0,0 +1,61 @@
+@(lang: LanguageCode = LanguageCode("en"), pathPrefix: Option[Uri] = None)(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, goBackUri: Option[Uri] = None, title: Option[String] = None, user: Option[Account])(fileContent: Option[String], listing: IndexedSeq[(os.RelPath, os.StatInfo)], repositoryBaseUri: Uri, repositoryName: VcsRepositoryName)
+@main(lang, pathPrefix)()(csrf, title, user) {
+@defining(lang.toLocale) { implicit locale =>
+  <div class="content">
+    <div class="pure-g">
+      <div class="l-box pure-u-1-1 pure-u-md-1-1">
+        <h2>@repositoryName</h2>
+        <nav class="pure-menu pure-menu-horizontal">
+          <ul class="pure-menu-list">
+            <li class="pure-menu-item"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(repositoryBaseUri)"><i class="fa-solid fa-eye"></i> Overview</a></li>
+            <li class="pure-menu-item pure-menu-active"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(repositoryBaseUri.addSegment("files"))"><i class="fa-solid fa-folder-tree"></i> Files</a></li>
+            <li class="pure-menu-item"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(repositoryBaseUri.addSegment("history"))"><i class="fa-solid fa-timeline"></i> Changes</a></li>
+          </ul>
+        </nav>
+      </div>
+    </div>
+  </div>
+  <div class="content">
+    <div class="pure-g">
+      <div class="l-box pure-u-1-1 pure-u-md-1-1">
+        @if(fileContent.isEmpty) {
+        <table class="pure-table pure-table-horizontal">
+          <thead>
+            <tr>
+              <th></th>
+              <th>Name</th>
+              <th>Size</th>
+              <th>Modified</th>
+            </tr>
+          </thead>
+          <tbody>
+            @for(link <- goBackUri) {
+            <tr>
+              <td><a href="@createFullPath(pathPrefix)(link)"><i class="fa-solid fa-angle-up"></i></a></td>
+              <td><a href="@createFullPath(pathPrefix)(link)">..</a></td>
+              <td></td>
+            </tr>
+            }
+            @for(entry <- listing) {
+            <tr>
+              <td>@if(entry._2.isDir) { <i class="fa-solid fa-folder"></i> } else { <i class="fa-solid fa-file"></i> }</td>
+              <td><a href="@createFullPath(pathPrefix)(actionBaseUri.addSegment(entry._1.last))">@{entry._1}</a></td>
+              <td>@{entry._2.size}</td>
+              <td>@{entry._2.mtime}</td>
+            </tr>
+            }
+          </tbody>
+        </table>
+        } else {
+          @for(link <- goBackUri) {
+            <a href="@createFullPath(pathPrefix)(link)"><i class="fa-solid fa-angle-up"></i></a>&nbsp;<a href="@createFullPath(pathPrefix)(link)">..</a>
+          }
+          @for(content <- fileContent) {
+            <pre class="repository-file-content"><code>@content</code></pre>
+          }
+        }
+      </div>
+    </div>
+  </div>
+}
+}
diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHistory.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHistory.scala.html
--- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHistory.scala.html	2025-02-02 16:49:24.958218841 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHistory.scala.html	2025-02-02 16:49:24.958218841 +0000
@@ -1,10 +1,17 @@
-@(lang: LanguageCode = LanguageCode("en"), pathPrefix: Option[Uri] = None)(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, goBackUri: Option[Uri] = None, title: Option[String] = None, user: Account)(history: String, repositoryName: VcsRepositoryName)
-@main(lang, pathPrefix)()(csrf, title, user.some) {
+@(lang: LanguageCode = LanguageCode("en"), pathPrefix: Option[Uri] = None)(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, goBackUri: Option[Uri] = None, title: Option[String] = None, user: Option[Account])(history: String, nextEntry: Option[Int], repositoryBaseUri: Uri, repositoryName: VcsRepositoryName)
+@main(lang, pathPrefix)()(csrf, title, user) {
 @defining(lang.toLocale) { implicit locale =>
   <div class="content">
     <div class="pure-g">
       <div class="l-box pure-u-1-1 pure-u-md-1-1">
-        <a class="" href="@createFullPath(pathPrefix)(actionBaseUri)"><i class="fa-solid fa-folder-tree"></i> Files</a>
+        <h2>@repositoryName</h2>
+        <nav class="pure-menu pure-menu-horizontal">
+          <ul class="pure-menu-list">
+            <li class="pure-menu-item"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(actionBaseUri)"><i class="fa-solid fa-eye"></i> Overview</a></li>
+            <li class="pure-menu-item"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(actionBaseUri.addSegment("files"))"><i class="fa-solid fa-folder-tree"></i> Files</a></li>
+            <li class="pure-menu-item pure-menu-active"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(actionBaseUri.addSegment("history"))"><i class="fa-solid fa-timeline"></i> Changes</a></li>
+          </ul>
+        </nav>
       </div>
     </div>
   </div>
@@ -13,6 +20,13 @@
       <div class="l-box pure-u-1-1 pure-u-md-1-1">
         <pre><code>@history</code></pre>
       </div>
+      <div class="l-box pure-u-1-1 pure-u-md-1-1">
+        <nav class="pure-menu pure-menu-horizontal">
+          <ul class="pure-menu-list">
+            <li class="pure-menu-item"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(repositoryBaseUri.addSegment("history").withOptionQueryParam("from", nextEntry))">Next <i class="fa-solid fa-angle-right"></i></a></li>
+          </ul>
+        </nav>
+      </div>
     </div>
   </div>
 }
diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html
--- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html	2025-02-02 16:49:24.958218841 +0000
@@ -0,0 +1,42 @@
+@(lang: LanguageCode = LanguageCode("en"), pathPrefix: Option[Uri] = None)(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, title: Option[String] = None, user: Option[Account])(vcsRepository: Option[VcsRepository])
+@main(lang, pathPrefix)()(csrf, title, user) {
+@defining(lang.toLocale) { implicit locale =>
+  @for(repo <- vcsRepository) {
+  <div class="content">
+    <div class="pure-g">
+      <div class="l-box pure-u-1-1 pure-u-md-1-1">
+        <h2>~@repo.owner.name/@repo.name</h2>
+        <nav class="pure-menu pure-menu-horizontal">
+          <ul class="pure-menu-list">
+            <li class="pure-menu-item pure-menu-active"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(actionBaseUri)"><i class="fa-solid fa-eye"></i> Overview</a></li>
+            <li class="pure-menu-item"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(actionBaseUri.addSegment("files"))"><i class="fa-solid fa-folder-tree"></i> Files</a></li>
+            <li class="pure-menu-item"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(actionBaseUri.addSegment("history"))"><i class="fa-solid fa-timeline"></i> Changes</a></li>
+          </ul>
+        </nav>
+      </div>
+    </div>
+  </div>
+  <div class="content">
+    <div class="pure-g">
+      <div class="l-box pure-u-1 pure-u-md-1-2 pure-u-lg-1-4">
+
+        <h3 class="content-subhead"><i class="fa-solid fa-file-code"></i>@Messages("landingpage.index.pitch.header.first")</h3>
+        <p>@Messages("landingpage.index.pitch.teaser.first")</p>
+      </div>
+      <div class="l-box pure-u-1 pure-u-md-1-2 pure-u-lg-1-4">
+        <h3 class="content-subhead"><i class="fa-solid fa-circle-nodes"></i>@Messages("landingpage.index.pitch.header.second")</h3>
+        <p>@Messages("landingpage.index.pitch.teaser.second")</p>
+      </div>
+      <div class="l-box pure-u-1 pure-u-md-1-2 pure-u-lg-1-4">
+        <h3 class="content-subhead"><i class="fa-solid fa-code-compare"></i>@Messages("landingpage.index.pitch.header.third")</h3>
+        <p>@Messages("landingpage.index.pitch.teaser.third")</p>
+      </div>
+      <div class="l-box pure-u-1 pure-u-md-1-2 pure-u-lg-1-4">
+        <h3 class="content-subhead"><i class="fa-solid fa-code-merge"></i>@Messages("landingpage.index.pitch.header.fourth")</h3>
+        <p>@Messages("landingpage.index.pitch.teaser.fourth")</p>
+      </div>
+    </div>
+  </div>
+  }
+}
+}
diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepository.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepository.scala.html
--- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepository.scala.html	2025-02-02 16:49:24.958218841 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepository.scala.html	1970-01-01 00:00:00.000000000 +0000
@@ -1,54 +0,0 @@
-@(lang: LanguageCode = LanguageCode("en"), pathPrefix: Option[Uri] = None)(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, goBackUri: Option[Uri] = None, title: Option[String] = None, user: Option[Account])(fileContent: Option[String], listing: IndexedSeq[(os.RelPath, os.StatInfo)], repositoryBaseUri: Uri, repositoryName: VcsRepositoryName)
-@main(lang, pathPrefix)()(csrf, title, user) {
-@defining(lang.toLocale) { implicit locale =>
-  <div class="content">
-    <div class="pure-g">
-      <div class="l-box pure-u-1-1 pure-u-md-1-1">
-        <a class="" href="@createFullPath(pathPrefix)(repositoryBaseUri.addSegment("history"))"><i class="fa-solid fa-timeline"></i> History</a>
-      </div>
-    </div>
-  </div>
-  <div class="content">
-    <div class="pure-g">
-      <div class="l-box pure-u-1-1 pure-u-md-1-1">
-        @if(fileContent.isEmpty) {
-        <table class="pure-table pure-table-horizontal">
-          <thead>
-            <tr>
-              <th></th>
-              <th>Name</th>
-              <th>Size</th>
-              <th>Modified</th>
-            </tr>
-          </thead>
-          <tbody>
-            @for(link <- goBackUri) {
-            <tr>
-              <td><a href="@createFullPath(pathPrefix)(link)"><i class="fa-solid fa-angle-up"></i></a></td>
-              <td><a href="@createFullPath(pathPrefix)(link)">..</a></td>
-              <td></td>
-            </tr>
-            }
-            @for(entry <- listing) {
-            <tr>
-              <td>@if(entry._2.isDir) { <i class="fa-solid fa-folder"></i> } else { <i class="fa-solid fa-file"></i> }</td>
-              <td><a href="@createFullPath(pathPrefix)(actionBaseUri.addSegment(entry._1.last))">@{entry._1}</a></td>
-              <td>@{entry._2.size}</td>
-              <td>@{entry._2.mtime}</td>
-            </tr>
-            }
-          </tbody>
-        </table>
-        } else {
-          @for(link <- goBackUri) {
-            <a href="@createFullPath(pathPrefix)(link)"><i class="fa-solid fa-angle-up"></i></a>&nbsp;<a href="@createFullPath(pathPrefix)(link)">..</a>
-          }
-          @for(content <- fileContent) {
-            <pre class="repository-file-content"><code>@content</code></pre>
-          }
-        }
-      </div>
-    </div>
-  </div>
-}
-}