~jan0sch/smederee
Showing details for patch 22682cdf19557ed3f0733becc37bd149dbd23a5c.
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> +} +} +