~jan0sch/smederee
Showing details for patch 191d72b579b56ff4705471efd5db9ef946cbfbdf.
diff -rN -u old-smederee/build.sbt new-smederee/build.sbt --- old-smederee/build.sbt 2025-01-31 08:01:11.238738201 +0000 +++ new-smederee/build.sbt 2025-01-31 08:01:11.238738201 +0000 @@ -216,7 +216,7 @@ "cats.syntax.all._", "de.smederee.html._", "de.smederee.i18n._", - "de.smederee.security.CsrfToken", + "de.smederee.security.{ CsrfToken, UserId, Username }", "org.http4s.Uri" ) ) 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-01-31 08:01:11.238738201 +0000 +++ new-smederee/modules/hub/src/it/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala 2025-01-31 08:01:11.238738201 +0000 @@ -169,6 +169,51 @@ } } + test("findVcsRepositoryBranches must return all branches") { + (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 => + loadVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name) + ) + toFork <- vcsRepositories.drop(1).take(5).traverse { vcsRepository => + loadVcsRepositoryId(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)) => repo.findVcsRepositoryBranches(originalId).compile.toList + 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!") + foundForks.zip(vcsRepositories.drop(1).take(5)).map { tuple => + val ((ownerName, repoName), repo) = tuple + assertEquals(ownerName, repo.owner.name) + assertEquals(repoName, repo.name) + } + } + case _ => fail("Could not generate data samples!") + } + } + test("loadVcsRepositoryId must return the id of an existing repository") { (genValidAccount.sample, genValidVcsRepository.sample) match { case (Some(account), Some(repository)) => diff -rN -u old-smederee/modules/hub/src/main/resources/messages.properties new-smederee/modules/hub/src/main/resources/messages.properties --- old-smederee/modules/hub/src/main/resources/messages.properties 2025-01-31 08:01:11.238738201 +0000 +++ new-smederee/modules/hub/src/main/resources/messages.properties 2025-01-31 08:01:11.238738201 +0000 @@ -50,6 +50,8 @@ form.edit-repo.website.placeholder=https://example.com form.edit-repo.website.help=An optional URI pointing to the website of your project. form.fork.button.submit=Clone to your account. +form.fork.button.submit.not-validated=Please validate your account first, only validated users can create repositories. +form.fork.help=Please remember that darcs has a different nomenclature than for example git. Cloning this repository to your account will create a branch (equal to a git fork). form.label.create.button.submit=Create label form.label.colour=Colour form.label.colour.help=Pick a colour which will be used as background colour for the label. @@ -194,6 +196,8 @@ repositories.yours.column.name=Name repositories.yours.none-found=Looks like you don''t have any repositories created yet. +repository.branches.summary={0} branches exist for this repository. + repository.changes.patch.description=Showing details for patch {0}. repository.changes.patch.title.link=Show details for patch {0}. repository.changes.patch.summary.title=Summary of changes @@ -216,6 +220,7 @@ repository.labels.list.empty=There are no labels defined. repository.labels.list.title={0} labels. +repository.menu.branches=Branches ({0}) repository.menu.changes.next=Next repository.menu.changes=Changes repository.menu.delete=Delete @@ -239,7 +244,7 @@ repository.description.title=Summary: repository.description.forked-from=Forked from: -repository.overview.clone.fork=Create your personal fork. +repository.overview.clone.fork=Create your personal branch (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/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-01-31 08:01:11.238738201 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieVcsMetadataRepository.scala 2025-01-31 08:01:11.238738201 +0000 @@ -73,6 +73,19 @@ query.query[VcsRepository].option.transact(tx) } + override def findVcsRepositoryBranches(originalRepositoryId: Long): Stream[F, (Username, VcsRepositoryName)] = { + val query = sql"""SELECT + "accounts"."name" AS "owner_name", + "repos"."name" AS "repository_name" + FROM "hub"."forks" AS "forks" + JOIN "hub"."repositories" AS "repos" + ON "forks"."forked_repo" = "repos"."id" + JOIN "hub"."accounts" AS "accounts" + ON "repos"."owner" = "accounts"."uid" + WHERE "forks"."original_repo" = $originalRepositoryId""" + query.query[(Username, VcsRepositoryName)].stream.transact(tx) + } + override def findVcsRepositoryId(owner: VcsRepositoryOwner, name: VcsRepositoryName): F[Option[Long]] = { val nameFilter = fr"""name = $name""" val ownerFilter = fr"""owner = ${owner.uid}""" 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-01-31 08:01:11.238738201 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsMetadataRepository.scala 2025-01-31 08:01:11.238738201 +0000 @@ -68,6 +68,15 @@ */ def findVcsRepository(owner: VcsRepositoryOwner, name: VcsRepositoryName): F[Option[VcsRepository]] + /** Find all branches (created via the `fork` functionality) for the repository with the given id. + * + * @param originalRepositoryId + * The id from the original repository from which the branches (forks) were created. + * @return + * A stream of tuples holding the username of the branch owner and the branch (repository) names. + */ + def findVcsRepositoryBranches(originalRepositoryId: Long): Stream[F, (Username, VcsRepositoryName)] + /** Search for the internal database specific (auto generated) ID of the given owner / repository combination which * serves as a primary key for the database table. * 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-01-31 08:01:11.238738201 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala 2025-01-31 08:01:11.242738200 +0000 @@ -289,6 +289,57 @@ } } yield resp + /** Logic for rendering an overview of the existing branches of a repository. + * + * @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. + * @return + * An HTTP response containing the rendered HTML. + */ + private def doShowRepositoryBranches( + csrf: Option[CsrfToken] + )(user: Option[Account])(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Response[F]] = + for { + language <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en"))) + repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName) + repo = repoAndId.map(_._1) + actionBaseUri <- Sync[F].delay( + linkConfig.createFullUri( + Uri(path = + Uri.Path( + Vector(Uri.Path.Segment(s"~$repositoryOwnerName"), Uri.Path.Segment(repositoryName.toString)) + ) + ) + ) + ) + branches <- repoAndId.map(_._2) match { + case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList + case _ => Sync[F].delay(List.empty) + } + 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) => + Ok( + views.html.showRepositoryBranches(baseUri, lang = language)( + actionBaseUri, + csrf, + s"Smederee/~$repositoryOwnerName/$repositoryName".some, + user + )(repo, branches) + ) + } + } yield resp + /** Logic for rendering the content of a repository directory or file visible to the given user account. * * @param csrf @@ -333,6 +384,10 @@ ) ) ) + branches <- repoAndId.map(_._2) match { + case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList + case _ => Sync[F].delay(List.empty) + } 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( @@ -372,6 +427,7 @@ user )( repo, + vcsRepositoryBranches = branches, vcsRepositoryHistory = patches, vcsRepositoryParentFork = parentFork, vcsRepositoryReadme = readme, @@ -470,6 +526,10 @@ case true => Sync[F].pure(List.empty) } + branches <- repoAndId.map(_._2) match { + case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList + case _ => Sync[F].delay(List.empty) + } resp <- repo match { case None => NotFound("Repository not found!") @@ -487,7 +547,7 @@ Option(goBackUri), s"Smederee/~$repositoryOwnerName/$repositoryName".some, user - )(fileContent, listing, repositoryBaseUri, repo) + )(fileContent, listing, repositoryBaseUri, repo, branches) ) } } yield resp @@ -524,6 +584,10 @@ ) ) ) + branches <- repoAndId.map(_._2) match { + case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList + case _ => Sync[F].delay(List.empty) + } countChanges <- darcs.log(directory.toNIO)(repositoryName.toString)(Chain("--count")) numberOfChanges <- Sync[F].delay(countChanges.stdout.toList.mkString.trim.toInt) maxCount = 10 @@ -579,7 +643,7 @@ Option(goBackUri), s"Smederee - History of ~$repositoryOwnerName/$repositoryName".some, user - )(patches, next, repositoryBaseUri, repo) + )(patches, next, repositoryBaseUri, repo, branches) ) } } yield resp @@ -624,6 +688,10 @@ cleanedPatchDiff <- Sync[F].delay(darcsDiff.stdout.toList.mkString.split(patchCutMarker)(0)) patchDetails <- Sync[F].delay(cleanedPatchDiff) htmlPatchDetails <- Sync[F].delay(new UtilsAnsiHtml().convertAnsiToHtml(patchDetails)) + branches <- repoAndId.map(_._2) match { + case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList + case _ => Sync[F].delay(List.empty) + } actionBaseUri <- Sync[F].delay( linkConfig.createFullUri( Uri(path = @@ -641,7 +709,8 @@ .showRepositoryPatch(baseUri, lang = language)(actionBaseUri, csrf, patch.map(_.name.toString), user)( patch, htmlPatchDetails, - repo + repo, + branches ) ) } @@ -778,70 +847,81 @@ ) / "fork" as user => ar.req.decodeStrict[F, UrlForm] { urlForm => for { - csrf <- Sync[F].delay(ar.req.getCsrfToken) - repoAndId <- loadRepo(user.some)(repositoryOwnerName, repositoryName) - sourceRepository = repoAndId.map(_._1) - // Check if a repository with that name already exists for the user. - loadedTargetRepo <- vcsMetadataRepo.findVcsRepository(user.toVcsRepositoryOwner, repositoryName) - // If no repo exists we copy and adjust the source one, otherwise we return `None`. - 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) - ) - ) + csrf <- Sync[F].delay(ar.req.getCsrfToken) + language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en"))) + resp <- user.validatedEmail match { + case false => + Forbidden( + views.html.errors + .unvalidatedAccount(lang = language)(csrf, "Smederee - Account not validated!".some, user) ) - ) - ) - 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) => + case true => for { - _ <- Sync[F].delay( - Files.createDirectories( - Paths.get(darcsConfig.repositoriesDirectory.toString, repoMetadata.owner.name.toString) + repoAndId <- loadRepo(user.some)(repositoryOwnerName, repositoryName) + sourceRepository = repoAndId.map(_._1) + // Check if a repository with that name already exists for the user. + loadedTargetRepo <- vcsMetadataRepo.findVcsRepository(user.toVcsRepositoryOwner, repositoryName) + // If no repo exists we copy and adjust the source one, otherwise we return `None`. + 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 ) ) - darcsClone <- darcs.clone(sourceDirectory, targetDirectory)(Chain("--complete")) - createRepo <- - if (darcsClone.exitValue === 0) + output <- targetRepository match { + case None => Sync[F].pure(0) + case Some(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) + _ <- Sync[F].delay( + Files.createDirectories( + Paths.get(darcsConfig.repositoriesDirectory.toString, repoMetadata.owner.name.toString) + ) ) - 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 + darcsClone <- darcs.clone(sourceDirectory, targetDirectory)(Chain("--complete")) + createRepo <- + if (darcsClone.exitValue === 0) + 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 + } + resp <- SeeOther(Location(targetUri)) + } yield resp } - resp <- SeeOther(Location(targetUri)) } yield resp } } @@ -994,6 +1074,10 @@ ) repoAndId <- loadRepo(user.some)(repositoryOwnerName, repositoryName) repo = repoAndId.map(_._1) + branches <- repoAndId.map(_._2) match { + case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList + case _ => Sync[F].delay(List.empty) + } resp <- repo match { case None => NotFound() case Some(repo) => @@ -1042,7 +1126,8 @@ repoUri, Option(s"~$repositoryOwnerName/$repositoryName - edit"), user, - repo + repo, + branches )(formData, FormErrors.fromNec(errors)) ) case Validated.Valid(updatedVcsRepository) => @@ -1080,16 +1165,23 @@ private val showCreateRepositoryForm: AuthedRoutes[Account, F] = AuthedRoutes.of { case ar @ GET -> Root / "repo" / "create" as user => for { - csrf <- Sync[F].delay(ar.req.getCsrfToken) + csrf <- Sync[F].delay(ar.req.getCsrfToken) + language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en"))) resp <- user.validatedEmail match { case false => Forbidden( - views.html.errors.unvalidatedAccount()(csrf, "Smederee - Account not validated!".some, user) + views.html.errors + .unvalidatedAccount(lang = language)(csrf, "Smederee - Account not validated!".some, user) ) case true => Ok( views.html - .createRepository()(createRepoPath, csrf, "Smederee - Create a new repository".some, user)() + .createRepository(lang = language)( + createRepoPath, + csrf, + "Smederee - Create a new repository".some, + user + )() ) } } yield resp @@ -1103,6 +1195,10 @@ csrf <- Sync[F].delay(ar.req.getCsrfToken) repoAndId <- loadRepo(user.some)(repositoryOwnerName, repositoryName) repo = repoAndId.map(_._1) + branches <- repoAndId.map(_._2) match { + case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList + case _ => Sync[F].delay(List.empty) + } deleteAction <- Sync[F].delay( linkConfig.createFullUri( Uri(path = @@ -1138,7 +1234,8 @@ csrf, Option(s"~$repositoryOwnerName/$repositoryName - delete"), user, - repo + repo, + branches ) ) } @@ -1153,6 +1250,10 @@ csrf <- Sync[F].delay(ar.req.getCsrfToken) repoAndId <- loadRepo(user.some)(repositoryOwnerName, repositoryName) repo = repoAndId.map(_._1) + branches <- repoAndId.map(_._2) match { + case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList + case _ => Sync[F].delay(List.empty) + } editAction <- Sync[F].delay( linkConfig.createFullUri( Uri(path = @@ -1188,7 +1289,8 @@ repoUri, Option(s"~$repositoryOwnerName/$repositoryName - edit"), user, - repo + repo, + branches )(formData) ) case _ => NotFound() @@ -1212,6 +1314,26 @@ } yield resp } + private val showRepositoryBranches: AuthedRoutes[Account, F] = AuthedRoutes.of { + case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter( + repositoryName + ) / "branches" as user => + for { + csrf <- Sync[F].delay(ar.req.getCsrfToken) + resp <- doShowRepositoryBranches(csrf)(user.some)(repositoryOwnerName, repositoryName) + } yield resp + } + + private val showRepositoryBranchesForGuests: HttpRoutes[F] = HttpRoutes.of { + case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter( + repositoryName + ) / "branches" => + for { + csrf <- Sync[F].delay(req.getCsrfToken) + resp <- doShowRepositoryBranches(csrf)(None)(repositoryOwnerName, repositoryName) + } yield resp + } + private val showRepositoryOverview: AuthedRoutes[Account, F] = AuthedRoutes.of { case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter( repositoryName @@ -1296,13 +1418,13 @@ downloadDistribution <+> forkRepository <+> showAllRepositories <+> showRepositories <+> parseCreateRepositoryForm <+> parseDeleteRepositoryForm <+> parseEditRepositoryForm <+> showCreateRepositoryForm <+> showDeleteRepositoryForm <+> showEditRepositoryForm <+> - showRepositoryOverview <+> showRepositoryHistory <+> showRepositoryPatchDetails <+> - showRepositoryFiles + showRepositoryOverview <+> showRepositoryBranches <+> showRepositoryHistory <+> + showRepositoryPatchDetails <+> showRepositoryFiles val routes = cloneRepository <+> downloadDistributionForGuests <+> showAllRepositoriesForGuests <+> - showRepositoriesForGuests <+> showRepositoryOverviewForGuests <+> showRepositoryHistoryForGuests <+> - showRepositoryPatchDetailsForGuests <+> showRepositoryFilesForGuests + showRepositoriesForGuests <+> showRepositoryOverviewForGuests <+> showRepositoryBranchesForGuests <+> + showRepositoryHistoryForGuests <+> showRepositoryPatchDetailsForGuests <+> showRepositoryFilesForGuests } diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/deleteRepository.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/deleteRepository.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/deleteRepository.scala.html 2025-01-31 08:01:11.238738201 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/deleteRepository.scala.html 2025-01-31 08:01:11.242738200 +0000 @@ -7,7 +7,8 @@ csrf: Option[CsrfToken] = None, title: Option[String] = None, user: Account, - vcsRepository: VcsRepository + vcsRepository: VcsRepository, + vcsRepositoryBranches: List[(Username, VcsRepositoryName)] ) @main(baseUri, lang)()(csrf, title, user.some) { @defining(lang.toLocale) { implicit locale => @@ -16,7 +17,7 @@ <div class="pure-u-1"> <div class="l-box-left-right"> <h2><a href="@{baseUri.addSegment(s"~${vcsRepository.owner.name}")}">~@vcsRepository.owner.name</a>/@vcsRepository.name</h2> - @showRepositoryMenu(baseUri)(deleteAction.some, repositoryBaseUri, user.some, vcsRepository) + @showRepositoryMenu(baseUri)(deleteAction.some, vcsRepositoryBranches.size, repositoryBaseUri, user.some, vcsRepository) <div class="repo-summary-description"> @Messages("repository.delete.title") </div> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/editRepository.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/editRepository.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/editRepository.scala.html 2025-01-31 08:01:11.238738201 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/editRepository.scala.html 2025-01-31 08:01:11.242738200 +0000 @@ -10,7 +10,8 @@ repositoryBaseUri: Uri, title: Option[String] = None, user: Account, - vcsRepository: VcsRepository + vcsRepository: VcsRepository, + vcsRepositoryBranches: List[(Username, VcsRepositoryName)] )(formData: Map[String, String] = Map.empty, formErrors: FormErrors = FormErrors.empty ) @@ -21,7 +22,7 @@ <div class="pure-u-1"> <div class="l-box-left-right"> <h2><a href="@{baseUri.addSegment(s"~${vcsRepository.owner.name}")}">~@vcsRepository.owner.name</a>/@vcsRepository.name</h2> - @showRepositoryMenu(baseUri)(action.some, repositoryBaseUri, user.some, vcsRepository) + @showRepositoryMenu(baseUri)(action.some, vcsRepositoryBranches.size, repositoryBaseUri, user.some, vcsRepository) <div class="repo-summary-description"> @Messages("repository.edit.title") </div> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryBranches.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryBranches.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryBranches.scala.html 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryBranches.scala.html 2025-01-31 08:01:11.242738200 +0000 @@ -0,0 +1,45 @@ +@import de.smederee.hub._ + +@(baseUri: Uri, + lang: LanguageCode = LanguageCode("en") +)(actionBaseUri: Uri, + csrf: Option[CsrfToken] = None, + title: Option[String] = None, + user: Option[Account] +)(vcsRepository: VcsRepository, + vcsRepositoryBranches: List[(Username, VcsRepositoryName)] +) +@main(baseUri, lang)()(csrf, title, user) { +@defining(lang.toLocale) { implicit locale => + <div class="content"> + <div class="pure-g"> + <div class="pure-u-1"> + <div class="l-box-left-right"> + <h2><a href="@{baseUri.addSegment(s"~${vcsRepository.owner.name}")}">~@vcsRepository.owner.name</a>/@vcsRepository.name</h2> + @showRepositoryMenu(baseUri)(actionBaseUri.addSegment("branches").some, vcsRepositoryBranches.size, actionBaseUri, user, vcsRepository) + <div class="repo-summary-description"> + @Messages("repository.branches.summary", vcsRepositoryBranches.size) + </div> + </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"> + <ul> + @for(branch <- vcsRepositoryBranches) { + @defining(branch._1) { branchOwner => + @defining(branch._2) { branchName => + <li><a href="@{baseUri.addSegment(s"~${branchOwner}").addSegment(branchName.toString)}">~@branchOwner/@branchName</a></li> + } + } + } + </ul> + </div> + </div> + </div> + </div> +} +} diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html 2025-01-31 08:01:11.238738201 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html 2025-01-31 08:01:11.242738200 +0000 @@ -12,7 +12,8 @@ )(fileContent: List[String], listing: IndexedSeq[(os.RelPath, os.StatInfo)], repositoryBaseUri: Uri, - vcsRepository: VcsRepository + vcsRepository: VcsRepository, + vcsRepositoryBranches: List[(Username, VcsRepositoryName)], ) @main(baseUri, lang)()(csrf, title, user) { @defining(lang.toLocale) { implicit locale => @@ -21,7 +22,7 @@ <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> - @showRepositoryMenu(baseUri)(repositoryBaseUri.addSegment("files").some, repositoryBaseUri, user, vcsRepository) + @showRepositoryMenu(baseUri)(repositoryBaseUri.addSegment("files").some, vcsRepositoryBranches.size, repositoryBaseUri, user, vcsRepository) <div class="repo-summary-description"> <code>@{actionBaseUri.path.toString.replaceFirst("/files", "")}</code> </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-01-31 08:01:11.238738201 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHistory.scala.html 2025-01-31 08:01:11.242738200 +0000 @@ -10,7 +10,8 @@ )(history: List[VcsRepositoryPatchMetadata], nextEntry: Option[Int], repositoryBaseUri: Uri, - vcsRepository: VcsRepository + vcsRepository: VcsRepository, + vcsRepositoryBranches: List[(Username, VcsRepositoryName)], ) @main(baseUri, lang)()(csrf, title, user) { @defining(lang.toLocale) { implicit locale => @@ -19,7 +20,7 @@ <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> - @showRepositoryMenu(baseUri)(repositoryBaseUri.addSegment("history").some, repositoryBaseUri, user, vcsRepository) + @showRepositoryMenu(baseUri)(repositoryBaseUri.addSegment("history").some, vcsRepositoryBranches.size, repositoryBaseUri, user, vcsRepository) <div class="repo-summary-description"> @if(history.isEmpty) { @Messages("repository.changes.description.empty") diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryMenu.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryMenu.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryMenu.scala.html 2025-01-31 08:01:11.238738201 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryMenu.scala.html 2025-01-31 08:01:11.242738200 +0000 @@ -2,6 +2,7 @@ @(baseUri: Uri )(activeUri: Option[Uri], + branches: Int, repositoryBaseUri: Uri, user: Option[Account] = None, vcsRepository: VcsRepository @@ -17,6 +18,11 @@ @defining(repositoryBaseUri.addSegment("history")) { uri => <li class="pure-menu-item@if(activeUri.exists(_ === uri)){ pure-menu-active}else{}"><a class="pure-menu-link" href="@uri">@icon(baseUri)("list") @Messages("repository.menu.changes")</a></li> } + @if(branches > 0) { + @defining(repositoryBaseUri.addSegment("branches")) { uri => + <li class="pure-menu-item@if(activeUri.exists(_ === uri)){ pure-menu-active}else{}"><a class="pure-menu-link" href="@uri">@icon(baseUri)("git-branch") @Messages("repository.menu.branches", branches)</a></li> + } + } else {} @if(activeUri.exists(uri => uri === repositoryBaseUri || uri === repositoryBaseUri.addSegment("edit") || uri === repositoryBaseUri.addSegment("delete"))) { @if(user.exists(_.uid === vcsRepository.owner.uid)) { @defining(repositoryBaseUri.addSegment("edit")) { uri => 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-01-31 08:01:11.238738201 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html 2025-01-31 08:01:11.242738200 +0000 @@ -7,6 +7,7 @@ title: Option[String] = None, user: Option[Account] )(vcsRepository: VcsRepository, + vcsRepositoryBranches: List[(Username, VcsRepositoryName)], vcsRepositoryHistory: List[VcsRepositoryPatchMetadata], vcsRepositoryParentFork: Option[VcsRepository] = None, vcsRepositoryReadme: Option[String] = None, @@ -20,7 +21,7 @@ <div class="pure-u-1"> <div class="l-box-left-right"> <h2><a href="@{baseUri.addSegment(s"~${vcsRepository.owner.name}")}">~@vcsRepository.owner.name</a>/@vcsRepository.name</h2> - @showRepositoryMenu(baseUri)(actionBaseUri.some, actionBaseUri, user, vcsRepository) + @showRepositoryMenu(baseUri)(actionBaseUri.some, vcsRepositoryBranches.size, actionBaseUri, user, vcsRepository) <div class="repo-summary-description"> <strong>@Messages("repository.description.title")</strong> @vcsRepository.description </div> @@ -80,7 +81,8 @@ <fieldset> @csrfToken(csrf) <div class="pure-controls"> - <button type="submit" class="pure-button">@Messages("form.fork.button.submit")</button> + <button type="submit" class="pure-button" @if(user.exists(_.validatedEmail)){}else{title="@Messages("form.fork.button.submit.not-validated")" disabled=""}>@Messages("form.fork.button.submit")</button> + <small class="pure-form-message" id="fork.help">@Messages("form.fork.help")</small> </div> </fieldset> </form> 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 2025-01-31 08:01:11.238738201 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryPatch.scala.html 2025-01-31 08:01:11.242738200 +0000 @@ -8,7 +8,8 @@ user: Option[Account] )(patch: Option[VcsRepositoryPatchMetadata], patchDiff: String, - vcsRepository: VcsRepository + vcsRepository: VcsRepository, + vcsRepositoryBranches: List[(Username, VcsRepositoryName)], ) @main(baseUri, lang)()(csrf, title, user) { @defining(lang.toLocale) { implicit locale => @@ -17,7 +18,7 @@ <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> - @showRepositoryMenu(baseUri)(actionBaseUri.addSegment("history").some, actionBaseUri, user, vcsRepository) + @showRepositoryMenu(baseUri)(actionBaseUri.addSegment("history").some, vcsRepositoryBranches.size, actionBaseUri, user, vcsRepository) <div class="repo-summary-description"> @for(patch <- patch) { @Messages("repository.changes.patch.description", patch.hash.toString)