~jan0sch/smederee

Showing details for patch c7a3dab1e84024471d31b11a58acf1798e06b7a7.
2022-09-17 (Sat), 8:25 AM - Jens Grassel - c7a3dab1e84024471d31b11a58acf1798e06b7a7

VCS: Add forking / cloning to own account.

Logged on users can fork a public repo to their own account.
The functionality is pretty basic:

1. Redirect to forkingUser/repoName afterwards.
2. Do nothing if forkingUser already has a repo with the same name.
3. Create forkingUser directory in repos dir to prevent errors.
4. Only show form for logged in users.
5. Do not show form for user owning the repo.
Summary of changes
5 files modified with 145 lines added and 1 lines removed
  • modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala with 25 added and 0 removed lines
  • modules/darcs/src/test/scala/de/smederee/darcs/DarcsCommandsTest.scala with 33 added and 0 removed lines
  • modules/hub/src/main/resources/messages_en.properties with 2 added and 0 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala with 68 added and 1 removed lines
  • modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html with 17 added and 0 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 09:48:36.768627283 +0000
+++ new-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala	2025-02-02 09:48:36.772627289 +0000
@@ -73,6 +73,31 @@
 final class DarcsCommands[F[_]: Sync](val darcsBinary: Path) {
   private val log = LoggerFactory.getLogger(getClass)
 
+  /** Clone a darcs repository from one directory into another one. This function is intended to be used for
+    * server side forking or repositories.
+    *
+    * @param sourceRepositoryPath
+    *   The path of the source repository.
+    * @param targetRespositoryPath
+    *   The path of the target repository.
+    * @param options
+    *   Additional options for the clone command.
+    * @return
+    *   The output of the darcs command.
+    */
+  def clone(sourceRepositoryPath: Path, targetRespositoryPath: Path)(
+      options: Chain[String]
+  ): F[DarcsCommandOutput] = {
+    val source = sourceRepositoryPath.toAbsolutePath
+    val target = targetRespositoryPath.toAbsolutePath
+    log.trace(s"Execute $darcsBinary clone $sourceRepositoryPath to $targetRespositoryPath with $options")
+    val darcsOptions    = List("clone") ::: options.toList ::: List(source.toString, target.toString)
+    val externalCommand = os.proc(darcsBinary.toString, darcsOptions)
+    for {
+      process <- Sync[F].delay(externalCommand.call(check = false))
+    } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
+  }
+
   /** 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.
     *
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	2025-02-02 09:48:36.768627283 +0000
+++ new-smederee/modules/darcs/src/test/scala/de/smederee/darcs/DarcsCommandsTest.scala	2025-02-02 09:48:36.772627289 +0000
@@ -37,6 +37,39 @@
 
   override def munitFixtures = List(workingDirectory)
 
+  test("darcs clone must create a proper copy") {
+    val cmd    = new DarcsCommands[IO](darcsBinary)
+    val source = Paths.get(workingDirectory().toString, "source")
+    val target = Paths.get(workingDirectory().toString, "target")
+    val test =
+      for {
+        init  <- cmd.initialize(workingDirectory())("source")(Chain.empty)
+        clone <- cmd.clone(source, target)(Chain.empty)
+      } yield (init, clone)
+    test.map { output =>
+      val (init, clone) = output
+      assert(init.exitValue === 0, "darcs init did not finish with exit code 0!")
+      assert(Files.exists(source.toAbsolutePath), "Source repository directory does not exist!")
+      assert(clone.exitValue === 0, "darcs clone did not finish with exit code 0!")
+      assert(Files.exists(target.toAbsolutePath), "Target repository directory does not exist!")
+    }
+  }
+
+  test("darcs clone must fail if the source does not exist") {
+    val cmd    = new DarcsCommands[IO](darcsBinary)
+    val source = Paths.get(workingDirectory().toString, "source-should-not-exist")
+    val target = Paths.get(workingDirectory().toString, "target-should-not-be-created")
+    val test =
+      for {
+        clone <- cmd.clone(source, target)(Chain.empty)
+      } yield clone
+    test.map { clone =>
+      assert(!Files.exists(source.toAbsolutePath), "Source repository directory must not exist!")
+      assert(clone.exitValue =!= 0, "darcs clone must not finish with exit code 0!")
+      assert(!Files.exists(target.toAbsolutePath), "Target repository directory must not exist!")
+    }
+  }
+
   test("darcs initialize must create a new repository") {
     val cmd               = new DarcsCommands[IO](darcsBinary)
     val repo              = "test-repository"
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-02 09:48:36.768627283 +0000
+++ new-smederee/modules/hub/src/main/resources/messages_en.properties	2025-02-02 09:48:36.772627289 +0000
@@ -25,6 +25,7 @@
 form.create-repo.website=Website
 form.create-repo.website.placeholder=https://example.com
 form.create-repo.website.help=An optional URI pointing to the website of your project.
+form.fork.button.submit=Clone to your account.
 form.login.button.submit=Login
 form.login.password=Password
 form.login.password.placeholder=Please enter your password here.
@@ -130,6 +131,7 @@
 repository.menu.website.tooltip=Click here to open the project website ({0}) in a new tab or window.
 repository.description.title=Summary:
 
+repository.overview.clone.fork=Create your personal fork.
 repository.overview.clone.title=Clone this repository
 repository.overview.clone.read-only=read-only
 repository.overview.clone.read-write=read-write
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 09:48:36.768627283 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala	2025-02-02 09:48:36.772627289 +0000
@@ -532,6 +532,73 @@
       } yield resp
   }
 
+  private val forkRepository: AuthedRoutes[Account, F] = AuthedRoutes.of {
+    case ar @ POST -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+          repositoryName
+        ) / "fork" as user =>
+      ar.req.decodeStrict[F, UrlForm] { urlForm =>
+        for {
+          csrf  <- Sync[F].delay(ar.req.getCsrfToken)
+          owner <- vcsMetadataRepo.findVcsRepositoryOwner(repositoryOwnerName)
+          loadedSourceRepo <- 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. ;-)
+          sourceRepository = loadedSourceRepo.filter(r => r.isPrivate === false)
+          // Check if a repository with that name already exists for the user.
+          loadedTargetRepo <- vcsMetadataRepo.findVcsRepository(user.toVcsRepositoryOwner, repositoryName)
+          targetRepository = loadedTargetRepo.fold(
+            sourceRepository.map(_.copy(owner = user.toVcsRepositoryOwner))
+          )(_ => None)
+          targetUri <- Sync[F].delay(
+            linkConfig.createFullUri(
+              Uri(path =
+                Uri.Path(
+                  Vector(
+                    Uri.Path.Segment(s"~${user.name.toString}"),
+                    Uri.Path.Segment(repositoryName.toString)
+                  )
+                )
+              )
+            )
+          )
+          sourceDirectory <- Sync[F].delay(
+            Paths.get(
+              darcsConfig.repositoriesDirectory.toString,
+              repositoryOwnerName.toString,
+              repositoryName.toString
+            )
+          )
+          targetDirectory <- Sync[F].delay(
+            Paths.get(
+              darcsConfig.repositoriesDirectory.toString,
+              user.name.toString,
+              repositoryName.toString
+            )
+          )
+          output <- targetRepository match {
+            case None => Sync[F].pure(0)
+            case Some(repoMetadata) =>
+              for {
+                _ <- Sync[F].delay(
+                  Files.createDirectories(
+                    Paths.get(darcsConfig.repositoriesDirectory.toString, repoMetadata.owner.name.toString)
+                  )
+                )
+                darcsClone <- darcs.clone(sourceDirectory, targetDirectory)(Chain("--complete"))
+                createRepo <-
+                  if (darcsClone.exitValue === 0)
+                    vcsMetadataRepo.createVcsRepository(repoMetadata)
+                  else
+                    Sync[F].pure(0) // Do not create DB entry if darcs init failed!
+              } yield createRepo
+          }
+          resp <- SeeOther(Location(targetUri))
+        } yield resp
+      }
+  }
+
   private val parseCreateRepositoryForm: AuthedRoutes[Account, F] = AuthedRoutes.of {
     case ar @ POST -> Root / "repo" / "create" as user =>
       ar.req.decodeStrict[F, UrlForm] { urlForm =>
@@ -725,7 +792,7 @@
   }
 
   val protectedRoutes =
-    showAllRepositories <+> showRepositories <+> parseCreateRepositoryForm <+> showCreateRepositoryForm <+> showRepositoryOverview <+> showRepositoryHistory <+> showRepositoryFiles
+    forkRepository <+> showAllRepositories <+> showRepositories <+> parseCreateRepositoryForm <+> showCreateRepositoryForm <+> showRepositoryOverview <+> showRepositoryHistory <+> showRepositoryFiles
 
   val routes =
     cloneRepository <+> showAllRepositoriesForGuests <+> showRepositoriesForGuests <+> showRepositoryOverviewForGuests <+> showRepositoryHistoryForGuests <+> showRepositoryFilesForGuests
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-02 09:48:36.768627283 +0000
+++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html	2025-02-02 09:48:36.772627289 +0000
@@ -51,6 +51,23 @@
                 </fieldset>
               </form>
             </dd>
+            @for(account <- user) {
+              @if(vcsRepository.owner === account.toVcsRepositoryOwner) {
+              <!-- Cloning/Forking a repo to ourself is disabled. -->
+              } else {
+              <dt>@Messages("repository.overview.clone.fork")</dt>
+              <dd>
+                <form action="@actionBaseUri.addSegment("fork")" method="POST" accept-charset="UTF-8" class="pure-form">
+                  <fieldset>
+                    @csrfToken(csrf)
+                    <div class="pure-controls">
+                      <button type="submit" class="pure-button">@Messages("form.fork.button.submit")</button>
+                    </div>
+                  </fieldset>
+                </form>
+              </dd>
+              }
+            }
           </dl>
         </div>
       </div>