~jan0sch/smederee
Showing details for patch c58485fe63ca711cd48b681ba40c149ca104b04b.
diff -rN -u old-smederee/CHANGELOG.md new-smederee/CHANGELOG.md --- old-smederee/CHANGELOG.md 2025-02-01 06:41:25.083311739 +0000 +++ new-smederee/CHANGELOG.md 2025-02-01 06:41:25.087311745 +0000 @@ -20,6 +20,11 @@ ## Unreleased +### Added + +- show parent repository from which the current one was forked on the + repository overview page + ### Changed - store the CSRF key on disk and load it if present diff -rN -u old-smederee/modules/hub/src/it/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala new-smederee/modules/hub/src/it/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala --- old-smederee/modules/hub/src/it/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala 2025-02-01 06:41:25.083311739 +0000 +++ new-smederee/modules/hub/src/it/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala 2025-02-01 06:41:25.087311745 +0000 @@ -28,6 +28,8 @@ import munit._ +import scala.collection.immutable.Queue + final class DoobieVcsMetadataRepositoryTest extends BaseSpec { /** Find the repository ID for the given owner and repository name. @@ -62,6 +64,32 @@ } yield account } + /** Find all forks of the original repository with the given ID. + * + * @param originalRepoId + * The unique ID of the original repo from which was forked. + * @return + * A list of ID pairs (original repository id, forked repository id) which may be empty. + */ + @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!") + protected def findForks(originalRepoId: Long): IO[Seq[(Long, Long)]] = + connectToDb(configuration).use { con => + for { + statement <- IO.delay( + con.prepareStatement("""SELECT original_repo, forked_repo FROM "forks" WHERE original_repo = ?""") + ) + _ <- IO.delay(statement.setLong(1, originalRepoId)) + result <- IO.delay(statement.executeQuery) + forks <- IO.delay { + var queue = Queue.empty[(Long, Long)] + while (result.next()) + queue = queue :+ (result.getLong("original_repo"), result.getLong("forked_repo")) + queue + } + _ <- IO.delay(statement.close()) + } yield forks.toList + } + override def beforeEach(context: BeforeEach): Unit = { val dbConfig = configuration.database val flyway: Flyway = @@ -79,6 +107,46 @@ val _ = flyway.clean() } + test("createFork must work correctly") { + (genValidAccounts.suchThat(_.size > 4).sample, genValidVcsRepositories.suchThat(_.size > 4).sample) match { + case (Some(accounts), Some(repositories)) => + val vcsRepositories = accounts.zip(repositories).map { tuple => + val (account, repo) = tuple + repo.copy(owner = account.toVcsRepositoryOwner) + } + val dbConfig = configuration.database + val tx = Transactor.fromDriverManager[IO](dbConfig.driver, dbConfig.url, dbConfig.user, dbConfig.pass) + val repo = new DoobieVcsMetadataRepository[IO](tx) + val test = for { + _ <- accounts.traverse(account => + createAccount(account, PasswordHash("I am not a password hash!"), None, None) + ) + written <- vcsRepositories.traverse(repo.createVcsRepository) + original <- vcsRepositories.headOption.traverse(vcsRepository => + findVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name) + ) + toFork <- vcsRepositories.drop(1).take(5).traverse { vcsRepository => + findVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name) + } + forked <- (original, toFork) match { + case (Some(Some(originalId)), forkIds) => forkIds.flatten.traverse(id => repo.createFork(originalId, id)) + case _ => IO.pure(List.empty) + } + foundForks <- original match { + case Some(Some(originalId)) => findForks(originalId) + case _ => IO.pure(List.empty) + } + } yield (written, forked, foundForks) + test.map { result => + val (written, forked, foundForks) = result + assert(written.sum === vcsRepositories.size, "Test repository data was not written to database!") + assert(forked.sum === vcsRepositories.drop(1).take(5).size, "Number of created forks does not match!") + assert(foundForks.size === vcsRepositories.drop(1).take(5).size, "Number of found forks does not match!") + } + case _ => fail("Could not generate data samples!") + } + } + test("createVcsRepository must create a repository entry") { (genValidAccount.sample, genValidVcsRepository.sample) match { case (Some(account), Some(repository)) => @@ -202,6 +270,45 @@ } case _ => fail("Could not generate data samples!") } + } + + test("findVcsRepositoryParentFork must return the parent repository if it exists") { + (genValidAccounts.suchThat(_.size > 4).sample, genValidVcsRepositories.suchThat(_.size > 4).sample) match { + case (Some(accounts), Some(repositories)) => + val vcsRepositories = accounts.zip(repositories).map { tuple => + val (account, repo) = tuple + repo.copy(owner = account.toVcsRepositoryOwner) + } + val dbConfig = configuration.database + val tx = Transactor.fromDriverManager[IO](dbConfig.driver, dbConfig.url, dbConfig.user, dbConfig.pass) + val repo = new DoobieVcsMetadataRepository[IO](tx) + val test = for { + _ <- accounts.traverse(account => + createAccount(account, PasswordHash("I am not a password hash!"), None, None) + ) + written <- vcsRepositories.traverse(repo.createVcsRepository) + original <- vcsRepositories.headOption.traverse(vcsRepository => + findVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name) + ) + toFork <- vcsRepositories.drop(1).take(5).traverse { vcsRepository => + findVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name) + } + forked <- (original, toFork) match { + case (Some(Some(originalId)), forkIds) => forkIds.flatten.traverse(id => repo.createFork(originalId, id)) + case _ => IO.pure(List.empty) + } + foundParents <- vcsRepositories.drop(1).take(5).traverse { vcsRepository => + repo.findVcsRepositoryParentFork(vcsRepository.owner, vcsRepository.name) + } + } yield (written, forked, vcsRepositories.headOption, foundParents) + test.map { result => + val (written, forked, original, foundParents) = result + assert(written.sum === vcsRepositories.size, "Test repository data was not written to database!") + assert(forked.sum === vcsRepositories.drop(1).take(5).size, "Number of created forks does not match!") + assert(foundParents.forall(_ === original), "Parent vcs repository not matching!") + } + case _ => fail("Could not generate data samples!") + } } test("listAllRepositories must return only public repositories for guest users") { diff -rN -u old-smederee/modules/hub/src/main/resources/db/migration/V3__fork_tables.sql new-smederee/modules/hub/src/main/resources/db/migration/V3__fork_tables.sql --- old-smederee/modules/hub/src/main/resources/db/migration/V3__fork_tables.sql 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/resources/db/migration/V3__fork_tables.sql 2025-02-01 06:41:25.087311745 +0000 @@ -0,0 +1,18 @@ +CREATE TABLE "forks" +( + "original_repo" BIGINT NOT NULL, + "forked_repo" BIGINT NOT NULL, + CONSTRAINT "forks_pk" PRIMARY KEY ("original_repo", "forked_repo"), + CONSTRAINT "forks_fk_original_repo" FOREIGN KEY ("original_repo") + REFERENCES "repositories" ("id") ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT "forks_fk_forked_repo" FOREIGN KEY ("forked_repo") + REFERENCES "repositories" ("id") ON UPDATE CASCADE ON DELETE CASCADE +) +WITH ( + OIDS=FALSE +); + +COMMENT ON TABLE "forks" IS 'Stores fork relationships between repositories.'; +COMMENT ON COLUMN "forks"."original_repo" IS 'The ID of the original repository from which was forked.'; +COMMENT ON COLUMN "forks"."forked_repo" IS 'The ID of the repository which is the fork.'; + 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 06:41:25.083311739 +0000 +++ new-smederee/modules/hub/src/main/resources/messages_en.properties 2025-02-01 06:41:25.087311745 +0000 @@ -183,6 +183,7 @@ repository.menu.website=Website repository.menu.website.tooltip=Click here to open the project website ({0}) in a new tab or window. repository.description.title=Summary: +repository.description.forked-from=Forked from: repository.overview.clone.fork=Create your personal fork. repository.overview.clone.title=Clone this repository diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieVcsMetadataRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieVcsMetadataRepository.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieVcsMetadataRepository.scala 2025-02-01 06:41:25.083311739 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieVcsMetadataRepository.scala 2025-02-01 06:41:25.087311745 +0000 @@ -41,6 +41,9 @@ private val selectRepositoryColumns = fr"""SELECT "repos".name AS name, "accounts".uid AS owner_id, "accounts".name AS owner_name, "repos".is_private AS is_private, "repos".description AS description, "repos".vcs_type AS vcs_type, "repos".website AS website FROM "repositories" AS "repos" JOIN "accounts" ON "repos".owner = "accounts".uid""" + override def createFork(source: Long, target: Long): F[Int] = + sql"""INSERT INTO "forks" (original_repo, forked_repo) VALUES ($source, $target)""".update.run.transact(tx) + override def createVcsRepository(repository: VcsRepository): F[Int] = sql"""INSERT INTO "repositories" (name, owner, is_private, description, vcs_type, website, created_at, updated_at) VALUES (${repository.name}, ${repository.owner.uid}, ${repository.isPrivate}, ${repository.description}, ${repository.vcsType}, ${repository.website}, NOW(), NOW())""".update.run .transact(tx) @@ -72,6 +75,15 @@ .option .transact(tx) + override def findVcsRepositoryParentFork( + owner: VcsRepositoryOwner, + name: VcsRepositoryName + ): F[Option[VcsRepository]] = { + val query = + selectRepositoryColumns ++ fr"""WHERE "repos".id = (SELECT original_repo FROM "forks" WHERE forked_repo = (SELECT id FROM "repositories" WHERE name = $name AND owner = ${owner.uid}))""" + query.query[VcsRepository].option.transact(tx) + } + override def listAllRepositories( requester: Option[Account] )(ordering: VcsMetadataRepositoriesOrdering): Stream[F, VcsRepository] = { diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsMetadataRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsMetadataRepository.scala --- old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsMetadataRepository.scala 2025-02-01 06:41:25.083311739 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsMetadataRepository.scala 2025-02-01 06:41:25.087311745 +0000 @@ -27,6 +27,17 @@ */ abstract class VcsMetadataRepository[F[_]] { + /** Create an entry in the forks table to save the relationship between to repositories. + * + * @param source + * The ID of the original repository from which was forked. + * @param target + * The ID of the forked repository. + * @return + * The number of affected database rows. + */ + def createFork(source: Long, target: Long): F[Int] + /** Create a database entry for the given vcs repository. * * @param repository @@ -77,6 +88,18 @@ */ def findVcsRepositoryOwner(name: Username): F[Option[VcsRepositoryOwner]] + /** Search for a possible parent repository (i.e. a repository forked from) of the one described by the given name and + * owner. + * + * @param owner + * Data about the owner of the repository containing information needed to query the database. + * @param name + * The repository name which must be unique in regard to the owner. + * @return + * An option to the successfully found parent vcs repository entry. + */ + def findVcsRepositoryParentFork(owner: VcsRepositoryOwner, name: VcsRepositoryName): F[Option[VcsRepository]] + /** Return a list of all repositories from all users. * * @param requester 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 06:41:25.083311739 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala 2025-02-01 06:41:25.087311745 +0000 @@ -311,8 +311,8 @@ ) ) ) - log <- darcs.log(directory.toNIO)(repositoryName.toString)(Chain(s"--max-count=2", "--xml-output")) - xmlLog <- Sync[F].delay(scala.xml.XML.loadString(log.stdout.toList.mkString)) + vcsLog <- darcs.log(directory.toNIO)(repositoryName.toString)(Chain(s"--max-count=2", "--xml-output")) + xmlLog <- Sync[F].delay(scala.xml.XML.loadString(vcsLog.stdout.toList.mkString)) patches <- Sync[F].delay((xmlLog \ "patch").flatMap(VcsRepositoryPatchMetadata.fromDarcsXmlLog).toList) readmeData <- repo.traverse(repo => doLoadReadme(repo)) readme <- readmeData match { @@ -327,6 +327,10 @@ case _ => Sync[F].delay(None) } readmeName = readmeData.flatMap(_._2) + parentFork <- repo match { + case None => Sync[F].pure(None) + case Some(repo) => vcsMetadataRepo.findVcsRepositoryParentFork(repo.owner, repo.name) + } resp <- repo match { case None => NotFound("Repository not found!") case Some(repo) => @@ -339,6 +343,7 @@ )( repo, vcsRepositoryHistory = patches, + vcsRepositoryParentFork = parentFork, vcsRepositoryReadme = readme, vcsRepositoryReadmeFilename = readmeName, vcsRepositorySshUri = sshUri @@ -515,10 +520,10 @@ else from + maxCount next = if (to < numberOfChanges) Option(to + 1) else None - log <- darcs.log(directory.toNIO)(repositoryName.toString)( + vcsLog <- darcs.log(directory.toNIO)(repositoryName.toString)( Chain(s"--index=$from-$to", "--summary", "--xml-output") ) - xmlLog <- Sync[F].delay(scala.xml.XML.loadString(log.stdout.toList.mkString)) + xmlLog <- Sync[F].delay(scala.xml.XML.loadString(vcsLog.stdout.toList.mkString)) patches <- Sync[F].delay((xmlLog \ "patch").flatMap(VcsRepositoryPatchMetadata.fromDarcsXmlLog).toList) actionBaseUri <- Sync[F].delay( linkConfig.createFullUri( @@ -603,10 +608,10 @@ ) ) ) - log <- darcs.log(directory.toNIO)(repositoryName.toString)( + vcsLog <- 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)) + xmlLog <- Sync[F].delay(scala.xml.XML.loadString(vcsLog.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")) patchCutMarker <- Sync[F].delay(s"patch ${hash.toString}") @@ -818,7 +823,18 @@ darcsClone <- darcs.clone(sourceDirectory, targetDirectory)(Chain("--complete")) createRepo <- if (darcsClone.exitValue === 0) - vcsMetadataRepo.createVcsRepository(repoMetadata) + for { + create <- vcsMetadataRepo.createVcsRepository(repoMetadata) + // Get IDs to create the fork entry in the database. + source <- sourceRepository.traverse(sourceRepo => + vcsMetadataRepo.findVcsRepositoryId(sourceRepo.owner, sourceRepo.name) + ) + target <- vcsMetadataRepo.findVcsRepositoryId(repoMetadata.owner, repoMetadata.name) + fork <- (source, target) match { + case (Some(Some(source)), Some(target)) => vcsMetadataRepo.createFork(source, target) + case _ => Sync[F].pure(0) + } + } yield create + fork else Sync[F].pure(0) // Do not create DB entry if darcs init failed! } yield createRepo 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 06:41:25.087311745 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html 2025-02-01 06:41:25.087311745 +0000 @@ -1,4 +1,16 @@ -@(baseUri: Uri, lang: LanguageCode = LanguageCode("en"))(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, title: Option[String] = None, user: Option[Account])(vcsRepository: VcsRepository, vcsRepositoryHistory: List[VcsRepositoryPatchMetadata], vcsRepositoryReadme: Option[String] = None, vcsRepositoryReadmeFilename: Option[String] = None, vcsRepositorySshUri: Option[String] = None) +@(baseUri: Uri, + lang: LanguageCode = LanguageCode("en") +)(actionBaseUri: Uri, + csrf: Option[CsrfToken] = None, + title: Option[String] = None, + user: Option[Account] +)(vcsRepository: VcsRepository, + vcsRepositoryHistory: List[VcsRepositoryPatchMetadata], + vcsRepositoryParentFork: Option[VcsRepository] = None, + vcsRepositoryReadme: Option[String] = None, + vcsRepositoryReadmeFilename: Option[String] = None, + vcsRepositorySshUri: Option[String] = None +) @main(baseUri, lang)()(csrf, title, user) { @defining(lang.toLocale) { implicit locale => <div class="content"> @@ -27,6 +39,11 @@ <strong>@Messages("repository.description.title")</strong> @description </div> } + @for(parentRepo <- vcsRepositoryParentFork) { + <div class="repo-summary-description"> + <i>@Messages("repository.description.forked-from") <a href="@{baseUri.addSegment(s"~${parentRepo.owner.name.toString}").addSegment(parentRepo.name.toString)}">~@{parentRepo.owner.name}/@{parentRepo.name}</a></i> + </div> + } </div> </div> </div>