~jan0sch/smederee
Showing details for patch a55e078d48f149d9ff07952745e4598094573a8c.
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 16:49:24.958218841 +0000 +++ new-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala 2025-02-02 16:49:24.958218841 +0000 @@ -18,6 +18,31 @@ import org.slf4j.LoggerFactory import scala.sys.process._ +import scala.util.matching.Regex + +opaque type DarcsHash = String +object DarcsHash { + val Format: Regex = "^[a-f0-9]{40}$".r + + /** Create an instance of DarcsHash from the given String type. + * + * @param source + * An instance of type String which will be returned as a DarcsHash. + * @return + * The appropriate instance of DarcsHash. + */ + def apply(source: String): DarcsHash = source + + /** Try to create an instance of DarcsHash from the given String. + * + * @param source + * A String that should fulfil the requirements to be converted into a DarcsHash. + * @return + * An option to the successfully converted DarcsHash. + */ + def from(source: String): Option[DarcsHash] = Option(source).filter(s => Format.matches(s)) + +} /** Wrapper for output generated by an external darcs command. * 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 16:49:24.958218841 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala 2025-02-02 16:49:24.958218841 +0000 @@ -21,6 +21,7 @@ import de.smederee.hub.forms.types.FormErrors import org.http4s._ import org.http4s.dsl.Http4sDsl +import org.http4s.dsl.impl._ import org.http4s.headers.Location import org.http4s.implicits._ import org.http4s.twirl.TwirlInstances._ @@ -79,6 +80,55 @@ * 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 doShowRepositoryOverview( + csrf: Option[CsrfToken] + )(user: Option[Account])(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Response[F]] = + for { + owner <- vcsMetadataRepo.findVcsRepositoryOwner(repositoryOwnerName) + loadedRepo <- 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. ;-) + repo = user match { + case None => loadedRepo.filter(r => r.isPrivate === false) + case Some(user) => + loadedRepo.filter(r => r.isPrivate === false || r.owner === user.toVcsRepositoryOwner) + } + actionBaseUri <- Sync[F].delay( + Uri(path = + Uri.Path.Root |+| Uri.Path( + Vector(Uri.Path.Segment(s"~$repositoryOwnerName"), Uri.Path.Segment(repositoryName.toString)) + ) + ) + ) + resp <- repo match { + case None => NotFound("Repository not found!") + case Some(repo) => + Ok( + views.html.showRepositoryOverview()( + actionBaseUri, + csrf, + s"Smederee/~$repositoryOwnerName/$repositoryName".some, + user + )(repo.some) + ) + } + } yield resp + + /** Logic for rendering the content of a repository directory or file visible to the given user account. + * + * @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. * @param filePath * An URI path which describes the path the the requested part of the repository (empty or `/` for the * root directory of the repo). @@ -150,9 +200,9 @@ ) ) ) - actionBaseUri <- Sync[F].delay(Uri(path = repositoryBaseUri.path |+| filePath)) + actionBaseUri <- Sync[F].delay(Uri(path = repositoryBaseUri.path.addSegment("files") |+| filePath)) goBackUri <- Sync[F].delay( - Uri(path = repositoryBaseUri.path |+| Uri.Path(filePath.segments.reverse.drop(1).reverse)) + Uri(path = Uri.Path.Root |+| Uri.Path(actionBaseUri.path.segments.reverse.drop(1).reverse)) ) resp <- repo match { @@ -165,7 +215,7 @@ NotFound("File not found!") else Ok( - views.html.showRepository()( + views.html.showRepositoryFiles()( actionBaseUri, csrf, Option(goBackUri), @@ -176,6 +226,95 @@ } } yield resp + /** Logic for showing the history (changes) for the requested 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. + * @param fromEntry + * The optional number of the change from which the history shall be shown. + * @return + * An HTTP response containing the rendered HTML. + */ + def doShowRepositoryHistory( + csrf: Option[CsrfToken] + )(user: Option[Account])(repositoryOwnerName: Username, repositoryName: VcsRepositoryName)( + fromEntry: Option[Int] + ): F[Response[F]] = + for { + owner <- vcsMetadataRepo.findVcsRepositoryOwner(repositoryOwnerName) + loadedRepo <- 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. ;-) + repo = user match { + case None => loadedRepo.filter(r => r.isPrivate === false) + case Some(user) => + loadedRepo.filter(r => r.isPrivate === false || r.owner === user.toVcsRepositoryOwner) + } + directory <- Sync[F].delay( + os.Path( + Paths.get( + config.repositoriesDirectory.toPath.toString, + repositoryOwnerName.toString + ) + ) + ) + countChanges <- darcs.log(directory.toNIO)(repositoryName.toString)(Chain("--count")) + numberOfChanges <- Sync[F].delay(countChanges.stdout.toList.mkString.trim.toInt) + maxCount = 10 + from = fromEntry.getOrElse(1) + to = + if (from + maxCount > numberOfChanges) + numberOfChanges + else + from + maxCount + next = if (to < numberOfChanges) Option(to + 1) else None + history <- darcs.log(directory.toNIO)(repositoryName.toString)(Chain(s"--index=$from-$to", "--summary")) + actionBaseUri <- Sync[F].delay( + Uri(path = + Uri.Path.Root |+| Uri.Path( + Vector( + Uri.Path.Segment(s"~$repositoryOwnerName"), + Uri.Path.Segment(repositoryName.toString) + ) + ) + ) + ) + goBackUri <- Sync[F].delay( + Uri(path = + Uri.Path.Root |+| Uri.Path( + Vector( + Uri.Path.Segment(s"~$repositoryOwnerName"), + Uri.Path.Segment(repositoryName.toString) + ) + ) + ) + ) + repositoryBaseUri <- Sync[F].delay( + Uri(path = + Uri.Path.Root |+| Uri.Path( + Vector(Uri.Path.Segment(s"~$repositoryOwnerName"), Uri.Path.Segment(repositoryName.toString)) + ) + ) + ) + resp <- Ok.apply( + views.html.showRepositoryHistory()( + actionBaseUri, + csrf, + Option(goBackUri), + s"Smederee - History of ~$repositoryOwnerName/$repositoryName".some, + user + )(history.stdout.toList.mkString("\n"), next, repositoryBaseUri, repositoryName) + ) + } yield resp + /** List walk the given directory at the first level and return all found files and directories and their * stats sorted by directory first and by name second. If the given path is _not_ a directory then no * traversal is done and an empty list is returned. @@ -360,10 +499,30 @@ } yield resp } + private val showRepositoryOverview: AuthedRoutes[Account, F] = AuthedRoutes.of { + case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter( + repositoryName + ) as user => + for { + csrf <- Sync[F].delay(ar.req.getCsrfToken) + resp <- doShowRepositoryOverview(csrf)(user.some)(repositoryOwnerName, repositoryName) + } yield resp + } + + private val showRepositoryOverviewForGuests: HttpRoutes[F] = HttpRoutes.of { + case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter( + repositoryName + ) => + for { + csrf <- Sync[F].delay(req.getCsrfToken) + resp <- doShowRepositoryOverview(csrf)(None)(repositoryOwnerName, repositoryName) + } yield resp + } + private val showRepositoryFiles: AuthedRoutes[Account, F] = AuthedRoutes.of { case ar @ GET -> UsernamePathParameter(repositoryOwnerName) /: VcsRepositoryNamePathParameter( repositoryName - ) /: filePath as user => + ) /: "files" /: filePath as user => for { csrf <- Sync[F].delay(ar.req.getCsrfToken) resp <- doShowRepositoryFiles(csrf)(user.some)(repositoryOwnerName, repositoryName)(filePath) @@ -373,7 +532,7 @@ private val showRepositoryFilesForGuests: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> UsernamePathParameter(repositoryOwnerName) /: VcsRepositoryNamePathParameter( repositoryName - ) /: filePath => + ) /: "files" /: filePath => for { csrf <- Sync[F].delay(req.getCsrfToken) resp <- doShowRepositoryFiles(csrf)(None)(repositoryOwnerName, repositoryName)(filePath) @@ -381,55 +540,31 @@ } private val showRepositoryHistory: AuthedRoutes[Account, F] = AuthedRoutes.of { - case ar @ GET -> Root / UsernamePathParameter(repositoryOwner) / VcsRepositoryNamePathParameter( + case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter( repositoryName - ) / "history" as user => + ) / "history" :? HistoryFromQueryParameter(fromCount) as user => for { csrf <- Sync[F].delay(ar.req.getCsrfToken) - directory <- Sync[F].delay( - os.Path( - Paths.get( - config.repositoriesDirectory.toPath.toString, - repositoryOwner.toString - ) - ) - ) - history <- darcs.log(directory.toNIO)(repositoryName.toString)(Chain("--summary")) - actionBaseUri <- Sync[F].delay( - Uri(path = - Uri.Path.Root |+| Uri.Path( - Vector( - Uri.Path.Segment(s"~$repositoryOwner"), - Uri.Path.Segment(repositoryName.toString) - ) - ) - ) - ) - goBackUri <- Sync[F].delay( - Uri(path = - Uri.Path.Root |+| Uri.Path( - Vector( - Uri.Path.Segment(s"~$repositoryOwner"), - Uri.Path.Segment(repositoryName.toString) - ) - ) - ) - ) - resp <- Ok.apply( - views.html.showRepositoryHistory()( - actionBaseUri, - csrf, - Option(goBackUri), - s"Smederee - History of ~$repositoryOwner/$repositoryName".some, - user - )(history.stdout.toList.mkString("\n"), repositoryName) - ) + resp <- doShowRepositoryHistory(csrf)(user.some)(repositoryOwnerName, repositoryName)(fromCount) + } yield resp + } + + private val showRepositoryHistoryForGuests: HttpRoutes[F] = HttpRoutes.of { + case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter( + repositoryName + ) / "history" :? HistoryFromQueryParameter(fromCount) => + for { + csrf <- Sync[F].delay(req.getCsrfToken) + resp <- doShowRepositoryHistory(csrf)(None)(repositoryOwnerName, repositoryName)(fromCount) } yield resp } val protectedRoutes = - showAllRepositories <+> showRepositories <+> parseCreateRepositoryForm <+> showCreateRepositoryForm <+> showRepositoryHistory <+> showRepositoryFiles + showAllRepositories <+> showRepositories <+> parseCreateRepositoryForm <+> showCreateRepositoryForm <+> showRepositoryOverview <+> showRepositoryHistory <+> showRepositoryFiles - val routes = showAllRepositoriesForGuests <+> showRepositoryFilesForGuests + val routes = + showAllRepositoriesForGuests <+> showRepositoryOverviewForGuests <+> showRepositoryHistoryForGuests <+> showRepositoryFilesForGuests } + +object HistoryFromQueryParameter extends OptionalQueryParamDecoderMatcher[Int]("from") 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 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html 2025-02-02 16:49:24.958218841 +0000 @@ -0,0 +1,61 @@ +@(lang: LanguageCode = LanguageCode("en"), pathPrefix: Option[Uri] = None)(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, goBackUri: Option[Uri] = None, title: Option[String] = None, user: Option[Account])(fileContent: Option[String], listing: IndexedSeq[(os.RelPath, os.StatInfo)], repositoryBaseUri: Uri, repositoryName: VcsRepositoryName) +@main(lang, pathPrefix)()(csrf, title, user) { +@defining(lang.toLocale) { implicit locale => + <div class="content"> + <div class="pure-g"> + <div class="l-box pure-u-1-1 pure-u-md-1-1"> + <h2>@repositoryName</h2> + <nav class="pure-menu pure-menu-horizontal"> + <ul class="pure-menu-list"> + <li class="pure-menu-item"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(repositoryBaseUri)"><i class="fa-solid fa-eye"></i> Overview</a></li> + <li class="pure-menu-item pure-menu-active"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(repositoryBaseUri.addSegment("files"))"><i class="fa-solid fa-folder-tree"></i> Files</a></li> + <li class="pure-menu-item"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(repositoryBaseUri.addSegment("history"))"><i class="fa-solid fa-timeline"></i> Changes</a></li> + </ul> + </nav> + </div> + </div> + </div> + <div class="content"> + <div class="pure-g"> + <div class="l-box pure-u-1-1 pure-u-md-1-1"> + @if(fileContent.isEmpty) { + <table class="pure-table pure-table-horizontal"> + <thead> + <tr> + <th></th> + <th>Name</th> + <th>Size</th> + <th>Modified</th> + </tr> + </thead> + <tbody> + @for(link <- goBackUri) { + <tr> + <td><a href="@createFullPath(pathPrefix)(link)"><i class="fa-solid fa-angle-up"></i></a></td> + <td><a href="@createFullPath(pathPrefix)(link)">..</a></td> + <td></td> + </tr> + } + @for(entry <- listing) { + <tr> + <td>@if(entry._2.isDir) { <i class="fa-solid fa-folder"></i> } else { <i class="fa-solid fa-file"></i> }</td> + <td><a href="@createFullPath(pathPrefix)(actionBaseUri.addSegment(entry._1.last))">@{entry._1}</a></td> + <td>@{entry._2.size}</td> + <td>@{entry._2.mtime}</td> + </tr> + } + </tbody> + </table> + } else { + @for(link <- goBackUri) { + <a href="@createFullPath(pathPrefix)(link)"><i class="fa-solid fa-angle-up"></i></a> <a href="@createFullPath(pathPrefix)(link)">..</a> + } + @for(content <- fileContent) { + <pre class="repository-file-content"><code>@content</code></pre> + } + } + </div> + </div> + </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-02-02 16:49:24.958218841 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHistory.scala.html 2025-02-02 16:49:24.958218841 +0000 @@ -1,10 +1,17 @@ -@(lang: LanguageCode = LanguageCode("en"), pathPrefix: Option[Uri] = None)(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, goBackUri: Option[Uri] = None, title: Option[String] = None, user: Account)(history: String, repositoryName: VcsRepositoryName) -@main(lang, pathPrefix)()(csrf, title, user.some) { +@(lang: LanguageCode = LanguageCode("en"), pathPrefix: Option[Uri] = None)(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, goBackUri: Option[Uri] = None, title: Option[String] = None, user: Option[Account])(history: String, nextEntry: Option[Int], repositoryBaseUri: Uri, repositoryName: VcsRepositoryName) +@main(lang, pathPrefix)()(csrf, title, user) { @defining(lang.toLocale) { implicit locale => <div class="content"> <div class="pure-g"> <div class="l-box pure-u-1-1 pure-u-md-1-1"> - <a class="" href="@createFullPath(pathPrefix)(actionBaseUri)"><i class="fa-solid fa-folder-tree"></i> Files</a> + <h2>@repositoryName</h2> + <nav class="pure-menu pure-menu-horizontal"> + <ul class="pure-menu-list"> + <li class="pure-menu-item"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(actionBaseUri)"><i class="fa-solid fa-eye"></i> Overview</a></li> + <li class="pure-menu-item"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(actionBaseUri.addSegment("files"))"><i class="fa-solid fa-folder-tree"></i> Files</a></li> + <li class="pure-menu-item pure-menu-active"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(actionBaseUri.addSegment("history"))"><i class="fa-solid fa-timeline"></i> Changes</a></li> + </ul> + </nav> </div> </div> </div> @@ -13,6 +20,13 @@ <div class="l-box pure-u-1-1 pure-u-md-1-1"> <pre><code>@history</code></pre> </div> + <div class="l-box pure-u-1-1 pure-u-md-1-1"> + <nav class="pure-menu pure-menu-horizontal"> + <ul class="pure-menu-list"> + <li class="pure-menu-item"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(repositoryBaseUri.addSegment("history").withOptionQueryParam("from", nextEntry))">Next <i class="fa-solid fa-angle-right"></i></a></li> + </ul> + </nav> + </div> </div> </div> } 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 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html 2025-02-02 16:49:24.958218841 +0000 @@ -0,0 +1,42 @@ +@(lang: LanguageCode = LanguageCode("en"), pathPrefix: Option[Uri] = None)(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, title: Option[String] = None, user: Option[Account])(vcsRepository: Option[VcsRepository]) +@main(lang, pathPrefix)()(csrf, title, user) { +@defining(lang.toLocale) { implicit locale => + @for(repo <- vcsRepository) { + <div class="content"> + <div class="pure-g"> + <div class="l-box pure-u-1-1 pure-u-md-1-1"> + <h2>~@repo.owner.name/@repo.name</h2> + <nav class="pure-menu pure-menu-horizontal"> + <ul class="pure-menu-list"> + <li class="pure-menu-item pure-menu-active"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(actionBaseUri)"><i class="fa-solid fa-eye"></i> Overview</a></li> + <li class="pure-menu-item"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(actionBaseUri.addSegment("files"))"><i class="fa-solid fa-folder-tree"></i> Files</a></li> + <li class="pure-menu-item"><a class="pure-menu-link" href="@createFullPath(pathPrefix)(actionBaseUri.addSegment("history"))"><i class="fa-solid fa-timeline"></i> Changes</a></li> + </ul> + </nav> + </div> + </div> + </div> + <div class="content"> + <div class="pure-g"> + <div class="l-box pure-u-1 pure-u-md-1-2 pure-u-lg-1-4"> + + <h3 class="content-subhead"><i class="fa-solid fa-file-code"></i>@Messages("landingpage.index.pitch.header.first")</h3> + <p>@Messages("landingpage.index.pitch.teaser.first")</p> + </div> + <div class="l-box pure-u-1 pure-u-md-1-2 pure-u-lg-1-4"> + <h3 class="content-subhead"><i class="fa-solid fa-circle-nodes"></i>@Messages("landingpage.index.pitch.header.second")</h3> + <p>@Messages("landingpage.index.pitch.teaser.second")</p> + </div> + <div class="l-box pure-u-1 pure-u-md-1-2 pure-u-lg-1-4"> + <h3 class="content-subhead"><i class="fa-solid fa-code-compare"></i>@Messages("landingpage.index.pitch.header.third")</h3> + <p>@Messages("landingpage.index.pitch.teaser.third")</p> + </div> + <div class="l-box pure-u-1 pure-u-md-1-2 pure-u-lg-1-4"> + <h3 class="content-subhead"><i class="fa-solid fa-code-merge"></i>@Messages("landingpage.index.pitch.header.fourth")</h3> + <p>@Messages("landingpage.index.pitch.teaser.fourth")</p> + </div> + </div> + </div> + } +} +} diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepository.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepository.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepository.scala.html 2025-02-02 16:49:24.958218841 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepository.scala.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,54 +0,0 @@ -@(lang: LanguageCode = LanguageCode("en"), pathPrefix: Option[Uri] = None)(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, goBackUri: Option[Uri] = None, title: Option[String] = None, user: Option[Account])(fileContent: Option[String], listing: IndexedSeq[(os.RelPath, os.StatInfo)], repositoryBaseUri: Uri, repositoryName: VcsRepositoryName) -@main(lang, pathPrefix)()(csrf, title, user) { -@defining(lang.toLocale) { implicit locale => - <div class="content"> - <div class="pure-g"> - <div class="l-box pure-u-1-1 pure-u-md-1-1"> - <a class="" href="@createFullPath(pathPrefix)(repositoryBaseUri.addSegment("history"))"><i class="fa-solid fa-timeline"></i> History</a> - </div> - </div> - </div> - <div class="content"> - <div class="pure-g"> - <div class="l-box pure-u-1-1 pure-u-md-1-1"> - @if(fileContent.isEmpty) { - <table class="pure-table pure-table-horizontal"> - <thead> - <tr> - <th></th> - <th>Name</th> - <th>Size</th> - <th>Modified</th> - </tr> - </thead> - <tbody> - @for(link <- goBackUri) { - <tr> - <td><a href="@createFullPath(pathPrefix)(link)"><i class="fa-solid fa-angle-up"></i></a></td> - <td><a href="@createFullPath(pathPrefix)(link)">..</a></td> - <td></td> - </tr> - } - @for(entry <- listing) { - <tr> - <td>@if(entry._2.isDir) { <i class="fa-solid fa-folder"></i> } else { <i class="fa-solid fa-file"></i> }</td> - <td><a href="@createFullPath(pathPrefix)(actionBaseUri.addSegment(entry._1.last))">@{entry._1}</a></td> - <td>@{entry._2.size}</td> - <td>@{entry._2.mtime}</td> - </tr> - } - </tbody> - </table> - } else { - @for(link <- goBackUri) { - <a href="@createFullPath(pathPrefix)(link)"><i class="fa-solid fa-angle-up"></i></a> <a href="@createFullPath(pathPrefix)(link)">..</a> - } - @for(content <- fileContent) { - <pre class="repository-file-content"><code>@content</code></pre> - } - } - </div> - </div> - </div> -} -}