~jan0sch/smederee

Showing details for patch 22682cdf19557ed3f0733becc37bd149dbd23a5c.
2022-11-08 (Tue), 4:35 PM - Jens Grassel - 22682cdf19557ed3f0733becc37bd149dbd23a5c

VCS: Show patch details from history.

- add routes for showing patch details
- add `diff` command to `DarcsCommands`
- add forked `jansi` library as dependency for ansi to html conversion
- add templates, insert links
- adjust CSS
Summary of changes
1 files added
  • modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryPatch.scala.html
8 files modified with 146 lines added and 8 lines removed
  • build.sbt with 4 added and 0 removed lines
  • modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala with 23 added and 0 removed lines
  • modules/hub/src/main/resources/assets/css/main.css with 8 added and 0 removed lines
  • modules/hub/src/main/resources/messages_en.properties with 1 added and 0 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala with 99 added and 3 removed lines
  • modules/hub/src/main/twirl/de/smederee/hub/views/repositoryPatchMetadata.scala.html with 9 added and 3 removed lines
  • modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHistory.scala.html with 1 added and 1 removed lines
  • modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html with 1 added and 1 removed lines
diff -rN -u old-smederee/build.sbt new-smederee/build.sbt
--- old-smederee/build.sbt	2025-02-01 15:44:49.253733449 +0000
+++ new-smederee/build.sbt	2025-02-01 15:44:49.257733455 +0000
@@ -27,6 +27,7 @@
         "-Xfatal-warnings",
         "-Ykind-projector",
     ),
+    resolvers += "jitpack" at "https://jitpack.io", // for JANSI fork
     Compile / console / scalacOptions --= Seq("-Xfatal-warnings"),
     Test / console / scalacOptions --= Seq("-Xfatal-warnings"),
     Test / fork := true
@@ -177,6 +178,7 @@
         library.http4sEmberClient,
         library.http4sEmberServer,
         //library.http4sTwirl,
+        library.jansi,
         library.jclOverSlf4j, // Bridge Java Commons Logging to SLF4J.
         library.logback,
         library.osLib,
@@ -334,6 +336,7 @@
       val flyway          = "9.4.0"
       val http4s          = "1.0.0-M37"
       val ip4s            = "3.2.0"
+      val jansi           = "2.4.2"
       val jclOverSlf4j    = "1.7.36"
       val logback         = "1.2.11"
       val munit           = "0.7.29"
@@ -368,6 +371,7 @@
     val http4sEmberClient    = "org.http4s"                   %% "http4s-ember-client"    % Version.http4s
     //val http4sTwirl          = "org.http4s"                   %% "http4s-twirl"           % Version.http4s
     val ip4sCore             = "com.comcast"                  %% "ip4s-core"              % Version.ip4s
+    val jansi                = "com.github.Osiris-Team"       %  "jansi"                  % Version.jansi
     val jclOverSlf4j         = "org.slf4j"                    %  "jcl-over-slf4j"         % Version.jclOverSlf4j
     val logback              = "ch.qos.logback"               %  "logback-classic"        % Version.logback
     val munit                = "org.scalameta"                %% "munit"                  % Version.munit
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-01 15:44:49.253733449 +0000
+++ new-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala	2025-02-01 15:44:49.257733455 +0000
@@ -125,6 +125,29 @@
     } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
   }
 
+  /** Return the diff of the specified patch from the given darcs repository.
+    *
+    * @param basePath
+    *   The base path under which the repository is located.
+    * @param repositoryName
+    *   The name of the repository.
+    * @param hash
+    *   The hash of the patch whose diff shall be generated.
+    * @param options
+    *   Additional options for the initialize command.
+    * @return
+    *   The output of the darcs command.
+    */
+  def diff(basePath: Path)(repositoryName: String)(hash: DarcsHash)(options: Chain[String]): F[DarcsCommandOutput] = {
+    log.trace(s"Execute $darcsBinary diff --hash $hash for $repositoryName with $options")
+    val repositoryDirectory = Paths.get(basePath.toString, repositoryName)
+    val darcsOptions        = List("diff", s"--hash=${hash.toString}") ::: options.toList
+    val externalCommand     = os.proc(darcsBinary.toString, darcsOptions)
+    for {
+      process <- Sync[F].delay(externalCommand.call(cwd = os.Path(repositoryDirectory), check = false))
+    } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
+  }
+
   /** Create a distribution archive from the given darcs repository.
     *
     * @param basePath
diff -rN -u old-smederee/modules/hub/src/main/resources/assets/css/main.css new-smederee/modules/hub/src/main/resources/assets/css/main.css
--- old-smederee/modules/hub/src/main/resources/assets/css/main.css	2025-02-01 15:44:49.253733449 +0000
+++ new-smederee/modules/hub/src/main/resources/assets/css/main.css	2025-02-01 15:44:49.257733455 +0000
@@ -244,6 +244,14 @@
   background: rgb(248, 248, 255) none repeat scroll 0% 0%;
 }
 
+pre.repository-patch-content {
+  background: rgb(248, 248, 255) none repeat scroll 0% 0%;
+  color: rgb(0, 0, 0);
+  display: block;
+  overflow-x: auto;
+  padding: 0.5em;
+}
+
 code {
   word-wrap: normal;
   background: none;
diff -rN -u old-smederee/modules/hub/src/main/resources/messages_en.properties new-smederee/modules/hub/src/main/resources/messages_en.properties
--- old-smederee/modules/hub/src/main/resources/messages_en.properties	2025-02-01 15:44:49.253733449 +0000
+++ new-smederee/modules/hub/src/main/resources/messages_en.properties	2025-02-01 15:44:49.257733455 +0000
@@ -167,6 +167,7 @@
 repositories.yours.column.name=Name
 repositories.yours.none-found=Looks like you don''t have any repositories created yet.
 
+repository.changes.patch.title.link=Show details for patch {0}.
 repository.changes.patch.summary.title=Summary of changes
 repository.changes.patch.summary.added={0} files added
 repository.changes.patch.summary.modified={0} files modified with {1} lines added and {2} lines removed
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-01 15:44:49.257733455 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala	2025-02-01 15:44:49.257733455 +0000
@@ -33,6 +33,7 @@
 import de.smederee.ssh._
 import org.commonmark.parser.Parser
 import org.commonmark.renderer.html.HtmlRenderer
+import org.fusesource.jansi.utils.UtilsAnsiHtml
 import org.http4s._
 import org.http4s.dsl.Http4sDsl
 import org.http4s.dsl.impl._
@@ -564,6 +565,74 @@
       }
     } yield resp
 
+  /** Get a diff for the requested patch and return the rendered response of it.
+    *
+    * @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 hash
+    *   The unique hash of the patch identifying it.
+    * @return
+    *   An HTTP response containing the rendered HTML.
+    */
+  def doShowRepositoryPatchDetails(csrf: Option[CsrfToken])(
+      user: Option[Account]
+  )(repositoryOwnerName: Username, repositoryName: VcsRepositoryName)(hash: DarcsHash): 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(
+            darcsConfig.repositoriesDirectory.toPath.toString,
+            repositoryOwnerName.toString
+          )
+        )
+      )
+      log <- darcs.log(directory.toNIO)(repositoryName.toString)(
+        Chain(s"--hash=${hash.toString}", "--summary", "--xml-output")
+      )
+      xmlLog <- Sync[F].delay(scala.xml.XML.loadString(log.stdout.toList.mkString))
+      patch  <- Sync[F].delay((xmlLog \ "patch").flatMap(VcsRepositoryPatchMetadata.fromDarcsXmlLog).toList.headOption)
+      darcsDiff        <- darcs.diff(directory.toNIO)(repositoryName.toString)(hash)(Chain("--no-pause-for-gui"))
+      patchDetails     <- Sync[F].delay(darcsDiff.stdout.toList.mkString)
+      htmlPatchDetails <- Sync[F].delay(new UtilsAnsiHtml().convertAnsiToHtml(patchDetails))
+      actionBaseUri <- Sync[F].delay(
+        linkConfig.createFullUri(
+          Uri(path =
+            Uri.Path(
+              Vector(Uri.Path.Segment(s"~$repositoryOwnerName"), Uri.Path.Segment(repositoryName.toString))
+            )
+          )
+        )
+      )
+      resp <- repo match {
+        case None => NotFound()
+        case Some(repo) =>
+          Ok(
+            views.html.showRepositoryPatch(baseUri)(actionBaseUri, csrf, patch.map(_.name.toString), user)(
+              patch,
+              htmlPatchDetails,
+              repo
+            )
+          )
+      }
+    } 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.
@@ -1159,17 +1228,44 @@
       } yield resp
   }
 
+  private val showRepositoryPatchDetails: AuthedRoutes[Account, F] = AuthedRoutes.of {
+    case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+          repositoryName
+        ) / "patch" / DarcsHashPathParameter(hash) as user =>
+      for {
+        csrf <- Sync[F].delay(ar.req.getCsrfToken)
+        resp <- doShowRepositoryPatchDetails(csrf)(user.some)(repositoryOwnerName, repositoryName)(hash)
+      } yield resp
+  }
+
+  private val showRepositoryPatchDetailsForGuests: HttpRoutes[F] = HttpRoutes.of {
+    case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+          repositoryName
+        ) / "patch" / DarcsHashPathParameter(hash) =>
+      for {
+        csrf <- Sync[F].delay(req.getCsrfToken)
+        resp <- doShowRepositoryPatchDetails(csrf)(None)(repositoryOwnerName, repositoryName)(hash)
+      } yield resp
+  }
+
   val protectedRoutes =
     downloadDistribution <+> forkRepository <+> showAllRepositories <+> showRepositories <+>
       parseCreateRepositoryForm <+> parseDeleteRepositoryForm <+> parseEditRepositoryForm <+>
       showCreateRepositoryForm <+> showDeleteRepositoryForm <+> showEditRepositoryForm <+>
-      showRepositoryOverview <+> showRepositoryHistory <+> showRepositoryFiles
+      showRepositoryOverview <+> showRepositoryHistory <+> showRepositoryPatchDetails <+>
+      showRepositoryFiles
 
   val routes =
     cloneRepository <+> downloadDistributionForGuests <+> showAllRepositoriesForGuests <+>
       showRepositoriesForGuests <+> showRepositoryOverviewForGuests <+> showRepositoryHistoryForGuests <+>
-      showRepositoryFilesForGuests
+      showRepositoryPatchDetailsForGuests <+> showRepositoryFilesForGuests
+
+}
 
+/** A path parameter extractor to get the hash of a darcs patch for showing patch details.
+  */
+object DarcsHashPathParameter {
+  def unapply(str: String): Option[DarcsHash] = Option(str).flatMap(DarcsHash.from)
 }
 
 /** Extractor for an optional query parameter we use in our history route.
diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/repositoryPatchMetadata.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/repositoryPatchMetadata.scala.html
--- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/repositoryPatchMetadata.scala.html	2025-02-01 15:44:49.257733455 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/repositoryPatchMetadata.scala.html	2025-02-01 15:44:49.257733455 +0000
@@ -1,9 +1,15 @@
-@(patch: VcsRepositoryPatchMetadata)(implicit locale: java.util.Locale)
+@(actionBaseUri: Option[Uri], patch: VcsRepositoryPatchMetadata)(implicit locale: java.util.Locale)
 <div class="patch">
   <div class="patch-details">
-    <span class="timestamp">@Messages("repository.overview.latest-changes.timestamp", java.util.Date.from(patch.timestamp.toInstant))</span> - <span class="author">@patch.author.withoutEmail</span>
+    <span class="timestamp">@Messages("repository.overview.latest-changes.timestamp", java.util.Date.from(patch.timestamp.toInstant))</span> - <span class="author">@patch.author.withoutEmail</span> - <span class="patch-hash">@patch.hash</span>
   </div>
-  <h4 class="patch-name">@patch.name</h4>
+  @if(actionBaseUri.nonEmpty) {
+    @for(uri <- actionBaseUri) {
+      <h4 class="patch-name"><a href="@uri.addSegment("patch").addSegment(patch.hash.toString)" title="@Messages("repository.changes.patch.title.link", patch.hash)">@patch.name</a></h4>
+    }
+  } else {
+    <h4 class="patch-name">@patch.name</h4>
+  }
   <div class="patch-comments">
     <pre class="latest-changes"><code>@patch.comment</code></pre>
   </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-01 15:44:49.257733455 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHistory.scala.html	2025-02-01 15:44:49.257733455 +0000
@@ -25,7 +25,7 @@
       <div class="pure-u-1-1 pure-u-md-1-1">
         <div class="l-box">
           @for(patch <- history) {
-            @repositoryPatchMetadata(patch)
+            @repositoryPatchMetadata(Option(actionBaseUri), patch)
           }
         </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	2025-02-01 15:44:49.257733455 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html	2025-02-01 15:44:49.257733455 +0000
@@ -36,7 +36,7 @@
           <h3>@Messages("repository.overview.latest-changes")</h3>
           <div class="overview-latest-changes">
             @for(patch <- vcsRepositoryHistory) {
-              @repositoryPatchMetadata(patch)
+              @repositoryPatchMetadata(None, patch)
             }
           </div>
         </div>
diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryPatch.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryPatch.scala.html
--- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryPatch.scala.html	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryPatch.scala.html	2025-02-01 15:44:49.257733455 +0000
@@ -0,0 +1,41 @@
+@(baseUri: Uri, lang: LanguageCode = LanguageCode("en"))(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, title: Option[String] = None, user: Option[Account])(patch: Option[VcsRepositoryPatchMetadata], patchDiff: String, vcsRepository: VcsRepository)
+@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>
+          <nav class="pure-menu pure-menu-horizontal">
+            <ul class="pure-menu-list">
+              <li class="pure-menu-item"><a class="pure-menu-link" href="@actionBaseUri"><i class="fa-solid fa-eye"></i> @Messages("repository.menu.overview")</a></li>
+              <li class="pure-menu-item"><a class="pure-menu-link" href="@actionBaseUri.addSegment("files")"><i class="fa-solid fa-folder-tree"></i> @Messages("repository.menu.files")</a></li>
+              <li class="pure-menu-item pure-menu-active"><a class="pure-menu-link" href="@actionBaseUri.addSegment("history")"><i class="fa-solid fa-timeline"></i> @Messages("repository.menu.changes")</a></li>
+              @for(website <- vcsRepository.website) {
+              <li class="pure-menu-item"><a class="pure-menu-link" href="@website" target="_blank" title="@Messages("repository.menu.website.tooltip", website)"><i class="fa-solid fa-up-right-from-square"></i> @Messages("repository.menu.website")</a></li>
+              }
+            </ul>
+          </nav>
+        </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">
+          @for(patch <- patch) {
+            @repositoryPatchMetadata(None, patch)
+          }
+        </div>
+      </div>
+      <div class="pure-u-1-1 pure-u-md-1-1">
+        <div class="l-box">
+          <pre class="repository-patch-content">@Html(patchDiff)</pre>
+        </div>
+      </div>
+    </div>
+  </div>
+}
+}
+