~jan0sch/smederee

Showing details for patch 0f89a87dcde628984b93cf67ddfa0f5db878f5ca.
2022-07-15 (Fri), 1:51 PM - Jens Grassel - 0f89a87dcde628984b93cf67ddfa0f5db878f5ca

darcs: Add simple wrapper for executing darcs commands

- initialize
- log
- whatsnew
Summary of changes
6 files added
  • modules/darcs/src/it/resources/logback-test.xml
  • modules/darcs/src/main/resources/logback.xml
  • modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala
  • modules/darcs/src/test/resources/logback-test.xml
  • modules/darcs/src/test/scala/de/smederee/darcs/DarcsCommandsTest.scala
  • modules/darcs/src/test/scala/de/smederee/darcs/TestHelpers.scala
1 files modified with 32 lines added and 1 lines removed
  • build.sbt with 32 added and 1 removed lines
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
+}