~jan0sch/smederee
Showing details for patch 0f89a87dcde628984b93cf67ddfa0f5db878f5ca.
diff -rN -u old-smederee/build.sbt new-smederee/build.sbt --- old-smederee/build.sbt 2025-02-03 04:47:31.501455322 +0000 +++ new-smederee/build.sbt 2025-02-03 04:47:31.501455322 +0000 @@ -48,7 +48,38 @@ publish := {}, publishLocal := {} ) - .aggregate(email, hub, i18n, security, twirl) + .aggregate(darcs, email, hub, i18n, security, twirl) + +lazy val darcs = + project + .in(file("modules/darcs")) + .enablePlugins(AutomateHeaderPlugin) + .configs(IntegrationTest) + .settings(commonSettings) + .settings( + name := "darcs", + Defaults.itSettings, + headerSettings(IntegrationTest), + inConfig(IntegrationTest)(scalafmtSettings), + IntegrationTest / console / scalacOptions --= Seq("-Xfatal-warnings"), + IntegrationTest / fork := true, + IntegrationTest / parallelExecution := false, + libraryDependencies ++= Seq( + library.catsCore, + library.catsEffect, + library.logback, + library.munit % IntegrationTest, + library.munitCatsEffect % IntegrationTest, + library.munitDiscipline % IntegrationTest, + library.munitScalaCheck % IntegrationTest, + library.scalaCheck % IntegrationTest, + library.munit % Test, + library.munitCatsEffect % Test, + library.munitDiscipline % Test, + library.munitScalaCheck % Test, + library.scalaCheck % Test + ) + ) lazy val docs = project diff -rN -u old-smederee/modules/darcs/src/it/resources/logback-test.xml new-smederee/modules/darcs/src/it/resources/logback-test.xml --- old-smederee/modules/darcs/src/it/resources/logback-test.xml 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/darcs/src/it/resources/logback-test.xml 2025-02-03 04:47:31.501455322 +0000 @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration debug="false"> + <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> + <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> + <level>WARN</level> + </filter> + <encoder> + <pattern>%date %highlight(%-5level) %cyan(%logger{0}) - %msg%n</pattern> + </encoder> + </appender> + + <appender name="async-console" class="ch.qos.logback.classic.AsyncAppender"> + <appender-ref ref="console"/> + <queueSize>5000</queueSize> + <discardingThreshold>0</discardingThreshold> + </appender> + + <logger name="de.smederee.darcs" level="INFO" additivity="false"> + <appender-ref ref="console"/> + </logger> + + <root> + <appender-ref ref="console"/> + </root> +</configuration> diff -rN -u old-smederee/modules/darcs/src/main/resources/logback.xml new-smederee/modules/darcs/src/main/resources/logback.xml --- old-smederee/modules/darcs/src/main/resources/logback.xml 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/darcs/src/main/resources/logback.xml 2025-02-03 04:47:31.501455322 +0000 @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration debug="false"> + <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> + <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> + <level>INFO</level> + </filter> + <encoder> + <pattern>%date %highlight(%-5level) %cyan(%logger{0}) - %msg%n</pattern> + </encoder> + </appender> + + <appender name="async-console" class="ch.qos.logback.classic.AsyncAppender"> + <appender-ref ref="console"/> + <queueSize>5000</queueSize> + <discardingThreshold>0</discardingThreshold> + </appender> + + <logger name="de.smederee.darcs" level="INFO" additivity="false"> + <appender-ref ref="console"/> + </logger> + + <root> + <appender-ref ref="console"/> + </root> +</configuration> 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 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala 2025-02-03 04:47:31.501455322 +0000 @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022 Wegtam GmbH + * + * Business Source License 1.1 - See file LICENSE for details! + * + * Change Date: 2025-06-21 + * Change License: GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + */ + +package de.smederee.darcs + +import java.nio.file._ + +import cats._ +import cats.data._ +import cats.effect._ +import cats.syntax.all._ +import org.slf4j.LoggerFactory + +import scala.sys.process._ + +/** Wrapper for output generated by an external darcs command. + * + * @param exitValue + * The exit value return by the command (usually anything other than 0 indicates an error). + * @param stdout + * The standard output produced by the command. + * @param stderr + * The standard error output produced by the command. + */ +final case class DarcsCommandOutput(exitValue: Int, stdout: Chain[String], stderr: Chain[String]) + +/** Provide an interface to executing darcs commands via the external darcs binary provided. + * + * @param darcsBinary + * The path to the darcs binary which is invoked for commands. + * @tparam F + * A higher kinded type providing needed functionality, which is usually an IO monad like Async or Sync. + */ +final class DarcsCommands[F[_]: Sync](val darcsBinary: Path) { + private val log = LoggerFactory.getLogger(getClass) + + /** Initialize a darcs repository under the given base path with the provided name. This is done by running + * the external darcs binary with the appropriate parameters. + * + * @param basePath + * The base path under which the repository is located. + * @param repositoryName + * The name of the repository. + * @param options + * Additional options for the initialize command. + * @return + * The output of the darcs command. + */ + def initialize(basePath: Path)(repositoryName: String)(options: Chain[String]): F[DarcsCommandOutput] = { + log.trace(s"Execute $darcsBinary initialize $basePath/$repositoryName with $options") + var stdout = Chain.empty[String] + var stderr = Chain.empty[String] + val logger = ProcessLogger(line => stdout = stdout.append(line), line => stderr = stderr.append(line)) + val directory = Paths.get(basePath.toString, repositoryName) + val darcsOptions = List("initialize") ::: options.toList ::: List(directory.toString) + val externalCommand = Process(darcsBinary.toString, darcsOptions) + for { + exitValue <- Sync[F].delay(externalCommand.!(logger)) + } yield DarcsCommandOutput(exitValue, stdout, stderr) + } + + /** Run the darcs log command on the given repository and return the output. + * + * @param basePath + * The base path under which the repository is located. + * @param repositoryName + * The name of the repository. + * @param options + * Additional options for the log command. + * @return + * The output of the darcs command. + */ + def log(basePath: Path)(repositoryName: String)(options: Chain[String]): F[DarcsCommandOutput] = { + log.trace(s"Execute $darcsBinary log in $basePath/$repositoryName with $options") + var stdout = Chain.empty[String] + var stderr = Chain.empty[String] + val logger = ProcessLogger(line => stdout = stdout.append(line), line => stderr = stderr.append(line)) + val directory = Paths.get(basePath.toString, repositoryName) + val darcsOptions = List("log") ::: options.toList + val externalCommand = + Process("cd", List(directory.toString)).#&&(Process(darcsBinary.toString, darcsOptions)) + for { + exitValue <- Sync[F].delay(externalCommand.!(logger)) + } yield DarcsCommandOutput(exitValue, stdout, stderr) + } + + /** Run the darcs whatsnew command on the given repository and return the output. + * + * @param basePath + * The base path under which the repository is located. + * @param repositoryName + * The name of the repository. + * @param options + * Additional options for the log command. + * @return + * The output of the darcs command. + */ + def whatsnew(basePath: Path)(repositoryName: String)(options: Chain[String]): F[DarcsCommandOutput] = { + log.trace(s"Execute $darcsBinary whatsnew in $basePath/$repositoryName with $options") + var stdout = Chain.empty[String] + var stderr = Chain.empty[String] + val logger = ProcessLogger(line => stdout = stdout.append(line), line => stderr = stderr.append(line)) + val directory = Paths.get(basePath.toString, repositoryName) + val darcsOptions = List("whatsnew") ::: options.toList + val externalCommand = + Process("cd", List(directory.toString)).#&&(Process(darcsBinary.toString, darcsOptions)) + for { + exitValue <- Sync[F].delay(externalCommand.!(logger)) + } yield DarcsCommandOutput(exitValue, stdout, stderr) + } + +} diff -rN -u old-smederee/modules/darcs/src/test/resources/logback-test.xml new-smederee/modules/darcs/src/test/resources/logback-test.xml --- old-smederee/modules/darcs/src/test/resources/logback-test.xml 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/darcs/src/test/resources/logback-test.xml 2025-02-03 04:47:31.501455322 +0000 @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration debug="false"> + <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> + <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> + <level>WARN</level> + </filter> + <encoder> + <pattern>%date %highlight(%-5level) %cyan(%logger{0}) - %msg%n</pattern> + </encoder> + </appender> + + <appender name="async-console" class="ch.qos.logback.classic.AsyncAppender"> + <appender-ref ref="console"/> + <queueSize>5000</queueSize> + <discardingThreshold>0</discardingThreshold> + </appender> + + <logger name="de.smederee.darcs" level="INFO" additivity="false"> + <appender-ref ref="console"/> + </logger> + + <root> + <appender-ref ref="console"/> + </root> +</configuration> diff -rN -u old-smederee/modules/darcs/src/test/scala/de/smederee/darcs/DarcsCommandsTest.scala new-smederee/modules/darcs/src/test/scala/de/smederee/darcs/DarcsCommandsTest.scala --- old-smederee/modules/darcs/src/test/scala/de/smederee/darcs/DarcsCommandsTest.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/darcs/src/test/scala/de/smederee/darcs/DarcsCommandsTest.scala 2025-02-03 04:47:31.501455322 +0000 @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2022 Wegtam GmbH + * + * Business Source License 1.1 - See file LICENSE for details! + * + * Change Date: 2025-06-21 + * Change License: GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + */ + +package de.smederee.darcs + +import java.nio.file._ + +import cats.data._ +import cats.effect._ +import cats.syntax.all._ + +import munit._ + +final class DarcsCommandsTest extends CatsEffectSuite with TestHelpers { + val darcsBinary = Paths.get("darcs") + + val workingDirectory = ResourceSuiteLocalFixture( + "working-directory", + Resource.make(IO(Files.createTempDirectory("darcs-cmd-test-").toAbsolutePath()))(path => + IO(deleteDirectory(path)) + ) + ) + + override def munitFixtures = List(workingDirectory) + + test("darcs initialize must create a new repository") { + val cmd = new DarcsCommands[IO](darcsBinary) + val repo = "test-repository" + val expectedDirectory = Paths.get(workingDirectory().toString, repo) + val test = cmd.initialize(workingDirectory())(repo)(Chain.empty) + test.map { output => + assert(output.exitValue === 0, "Exit code of darcs command is expected to be 0!") + assert(Files.exists(expectedDirectory), "Expected directory does not exist!") + assert(Files.isDirectory(expectedDirectory), "Expected directory is not a directory!") + val darcsDirectory = Paths.get(expectedDirectory.toString, "_darcs") + assert(Files.exists(darcsDirectory), "The _darcs directory does not exist in the repository!") + } + } + + test("darcs initialize must create a new repository in an existing directory") { + val cmd = new DarcsCommands[IO](darcsBinary) + val repo = "test-repository-in-directory" + val expectedDirectory = Paths.get(workingDirectory().toString, repo) + val _ = Files.createDirectories(expectedDirectory) + val test = cmd.initialize(workingDirectory())(repo)(Chain.empty) + test.map { output => + assert(output.exitValue === 0, "Exit code of darcs command is expected to be 0!") + val darcsDirectory = Paths.get(expectedDirectory.toString, "_darcs") + assert(Files.exists(darcsDirectory), "The _darcs directory does not exist in the repository!") + } + } + + test("darcs initialize must fail if the repository already exists") { + val cmd = new DarcsCommands[IO](darcsBinary) + val repo = "test-repository-existing" + val expectedDirectory = Paths.get(workingDirectory().toString, repo) + val _ = Files.createDirectories(expectedDirectory) + val test = for { + _ <- cmd.initialize(workingDirectory())(repo)(Chain.empty) + output <- cmd.initialize(workingDirectory())(repo)(Chain.empty) + } yield output + test.map { output => + assert(output.exitValue =!= 0, "The initialize command is expected to fail here!") + assert(output.stderr.nonEmpty) + } + } +} diff -rN -u old-smederee/modules/darcs/src/test/scala/de/smederee/darcs/TestHelpers.scala new-smederee/modules/darcs/src/test/scala/de/smederee/darcs/TestHelpers.scala --- old-smederee/modules/darcs/src/test/scala/de/smederee/darcs/TestHelpers.scala 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/darcs/src/test/scala/de/smederee/darcs/TestHelpers.scala 2025-02-03 04:47:31.501455322 +0000 @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 Wegtam GmbH + * + * Business Source License 1.1 - See file LICENSE for details! + * + * Change Date: 2025-06-21 + * Change License: GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + */ + +package de.smederee.darcs + +import java.io.IOException +import java.nio.file._ +import java.nio.file.attribute.BasicFileAttributes + +import cats.syntax.all._ + +trait TestHelpers { + + /** Delete the given directory recursively. + * + * @param path + * The path on the filesystem to the directory that shall be deleted. + * @return + * `true` if the directory was deleted. + */ + protected def deleteDirectory(path: Path): Boolean = + if (path.toString.trim =!= "/") { + Files.walkFileTree( + path, + new FileVisitor[Path] { + override def visitFileFailed(file: Path, exc: IOException): FileVisitResult = + FileVisitResult.CONTINUE + + override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = { + Files.delete(file) + FileVisitResult.CONTINUE + } + + override def preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult = + FileVisitResult.CONTINUE + + override def postVisitDirectory(dir: Path, exc: IOException): FileVisitResult = { + Files.delete(dir) + FileVisitResult.CONTINUE + } + } + ) + Files.deleteIfExists(path) + } else false +}